|
|
@@ -1,27 +1,22 @@
|
|
|
package com.grkj.iscs_mars.view.widget
|
|
|
|
|
|
-import android.graphics.Bitmap
|
|
|
-import android.graphics.Canvas
|
|
|
-import android.graphics.Color
|
|
|
-import android.graphics.Matrix
|
|
|
-import android.graphics.Paint
|
|
|
-import android.graphics.PointF
|
|
|
+import android.graphics.*
|
|
|
import android.os.SystemClock
|
|
|
import android.view.MotionEvent
|
|
|
import androidx.core.content.ContextCompat
|
|
|
import com.grkj.iscs_mars.MyApplication
|
|
|
import com.grkj.iscs_mars.R
|
|
|
import com.grkj.iscs_mars.modbus.DockBean
|
|
|
-import com.grkj.iscs_mars.modbus.ModBusController
|
|
|
-import com.grkj.iscs_mars.util.log.LogUtil
|
|
|
import com.onlylemi.mapview.library.MapView
|
|
|
import com.onlylemi.mapview.library.layer.MapBaseLayer
|
|
|
-import com.sik.sikcore.SIKCore
|
|
|
import kotlin.math.abs
|
|
|
import kotlin.math.sin
|
|
|
|
|
|
/**
|
|
|
- * 自定义开关层级(按点位缓存状态版本,变了才刷新)
|
|
|
+ * 自定义开关层(保持对外 API 兼容):
|
|
|
+ * - 点位坐标恒定使用“后端坐标系”(0..backendW, 0..backendH)
|
|
|
+ * - 点位尺寸:以 BASE_SWITCH_SIZE 为基准,受 setRatio() 与 currentZoom 共同影响
|
|
|
+ * - 文本:根据圆直径自适应,左右各保留 ~3px
|
|
|
*/
|
|
|
class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
mapView: MapView?,
|
|
|
@@ -30,14 +25,18 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
|
|
|
// ===== 数据结构 =====
|
|
|
data class IsolationPoint(
|
|
|
- var pos: PointF, // 可能是中心 or 左上角
|
|
|
- var entityName: String,
|
|
|
+ var pos: PointF, // 后端坐标系的“中心点”
|
|
|
+ var motorCode: String?,
|
|
|
+ var pointName: String?,
|
|
|
+ var motorName: String?,
|
|
|
+ var motorType: String?,
|
|
|
+ var pointId: Long?,
|
|
|
var icon: Bitmap?,
|
|
|
val entityId: Long,
|
|
|
- var pointSerialNumber: String?, // 仅用于和 switch.idx 对表
|
|
|
+ var pointSerialNumber: String?,
|
|
|
var isSelected: Boolean,
|
|
|
var pointNfc: String?,
|
|
|
- var status: Int = STATUS_UNKNOWN // 见常量
|
|
|
+ var status: Int = STATUS_UNKNOWN
|
|
|
)
|
|
|
|
|
|
companion object {
|
|
|
@@ -47,32 +46,20 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
const val STATUS_UNKNOWN = 3
|
|
|
}
|
|
|
|
|
|
- // ===== 配置开关:你的 pos 是否已经是中心点? =====
|
|
|
- private val POS_IS_CENTER = true // 若上游 pos 就是中心,设 true;若是左上角,改 false
|
|
|
+ // ===== 配置:你的 pos 是否已经是中心点? =====
|
|
|
+ private val POS_IS_CENTER = true
|
|
|
|
|
|
- // ===== 同步&选择 =====
|
|
|
+ // ===== 同步 & 选择 =====
|
|
|
private val dataLock = Any()
|
|
|
|
|
|
- @Volatile
|
|
|
- private var selectedEntityIdForKeep: Long? = null
|
|
|
+ @Volatile private var selectedEntityIdForKeep: Long? = null
|
|
|
+ @Volatile private var selectedNameForKeep: String? = null
|
|
|
|
|
|
- @Volatile
|
|
|
- private var selectedNameForKeep: String? = null
|
|
|
-
|
|
|
- // ===== switch 数据快照(仅存原始列表,不在 draw 里查表)=====
|
|
|
- @Volatile
|
|
|
- private var hasAlarmFlag: Boolean = false
|
|
|
-
|
|
|
- // token 防过期回调
|
|
|
- @Volatile
|
|
|
- private var selectToken: Long = 0L
|
|
|
- private inline fun withLiveToken(token: Long, block: () -> Unit) {
|
|
|
- if (token == selectToken) block()
|
|
|
- }
|
|
|
+ @Volatile private var selectToken: Long = 0L
|
|
|
+ private inline fun withLiveToken(token: Long, block: () -> Unit) { if (token == selectToken) block() }
|
|
|
|
|
|
// ===== 动画参数 =====
|
|
|
- @Volatile
|
|
|
- var inDraw: Boolean = false
|
|
|
+ @Volatile var inDraw: Boolean = false
|
|
|
private set
|
|
|
|
|
|
private val breathePeriod = 1200f
|
|
|
@@ -94,16 +81,19 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ // 点击/缩放动画
|
|
|
private val longPressCenterDurationMs = 280L
|
|
|
private val clickZoomDurationMs = 220L
|
|
|
private val clickCenterDurationMs = 220L
|
|
|
private val desiredSelectScale = 2.0f
|
|
|
|
|
|
- // 尺寸
|
|
|
+ // ===== 尺寸(与原版兼容)=====
|
|
|
+ private val BASE_SWITCH_SIZE = 16f
|
|
|
+ private val BASE_TEXT_SIZE = 12f
|
|
|
+
|
|
|
+ // 仍然保留 setRatio 语义
|
|
|
private var ratio: Float = 1f
|
|
|
- private val BASE_SWITCH_SIZE = 18f
|
|
|
- private val BASE_TEXT_SIZE = 14f
|
|
|
- private var switchSize: Float = BASE_SWITCH_SIZE
|
|
|
+ private var switchSize: Float = BASE_SWITCH_SIZE // 仅作为屏幕像素基准(dp->px后)
|
|
|
private var textSize: Float = BASE_TEXT_SIZE
|
|
|
|
|
|
// 绘制状态
|
|
|
@@ -116,30 +106,13 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
private val appCtx get() = MyApplication.instance!!.applicationContext
|
|
|
private val colOn by lazy { ContextCompat.getColor(appCtx, R.color.common_switch_enable) }
|
|
|
private val colOff by lazy { ContextCompat.getColor(appCtx, R.color.common_switch_disable) }
|
|
|
- private val colRed by lazy {
|
|
|
- runCatching {
|
|
|
- ContextCompat.getColor(
|
|
|
- appCtx,
|
|
|
- R.color.red_500
|
|
|
- )
|
|
|
- }.getOrDefault(Color.parseColor("#EF4444"))
|
|
|
- }
|
|
|
- private val colOrange by lazy {
|
|
|
- runCatching {
|
|
|
- ContextCompat.getColor(
|
|
|
- appCtx,
|
|
|
- R.color.orange_500
|
|
|
- )
|
|
|
- }.getOrDefault(Color.parseColor("#F97316"))
|
|
|
- }
|
|
|
+ private val colRed by lazy { runCatching { ContextCompat.getColor(appCtx, R.color.red_500) }.getOrDefault(Color.parseColor("#EF4444")) }
|
|
|
+ private val colOrange by lazy { runCatching { ContextCompat.getColor(appCtx, R.color.orange_500) }.getOrDefault(Color.parseColor("#F97316")) }
|
|
|
private val colSelectRing = Color.argb(180, 66, 133, 244)
|
|
|
|
|
|
// 长按探测
|
|
|
- private val longPressTimeoutMs: Long =
|
|
|
- android.view.ViewConfiguration.getLongPressTimeout().toLong()
|
|
|
- private val touchSlop: Float = android.view.ViewConfiguration.get(
|
|
|
- mapView?.context ?: SIKCore.getApplication()
|
|
|
- ).scaledTouchSlop * 1.5f
|
|
|
+ private val longPressTimeoutMs: Long = android.view.ViewConfiguration.getLongPressTimeout().toLong()
|
|
|
+ private val touchSlop: Float = android.view.ViewConfiguration.get(mapView?.context ?: appCtx).scaledTouchSlop * 1.5f
|
|
|
private val touchSlopSq: Float = touchSlop * touchSlop
|
|
|
private var pendingLongPress = false
|
|
|
private var longPressTriggered = false
|
|
|
@@ -157,163 +130,93 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
val c = centerOf(p)
|
|
|
mapView?.postDelayed({
|
|
|
val screen = mapToScreen(c.x, c.y)
|
|
|
- onLongPressListener?.invoke(p, screen.x, screen.y - switchSize / 2 - 4, c.x, c.y)
|
|
|
+ onLongPressListener?.invoke(p, screen.x, screen.y - getSwitchR() / 2f - 4, c.x, c.y)
|
|
|
}, longPressCenterDurationMs)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- var onLongPressListener: ((point: IsolationPoint, screenX: Float, screenY: Float, mapX: Float, mapY: Float) -> Unit)? =
|
|
|
- null
|
|
|
+ // ===== 兼容原版:对外回调 =====
|
|
|
+ var onLongPressListener: ((point: IsolationPoint, screenX: Float, screenY: Float, mapX: Float, mapY: Float) -> Unit)? = null
|
|
|
|
|
|
init {
|
|
|
paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL_AND_STROKE }
|
|
|
- setRatio(1f)
|
|
|
+ setRatio(1f) // 初始化
|
|
|
startAnimation()
|
|
|
}
|
|
|
|
|
|
- // ================= 外部接口 =================
|
|
|
+ // ================= 外部接口(保持兼容) =================
|
|
|
|
|
|
- /** 点位列表:深拷贝 pos;可选保留当前选中(按 entityId→name) */
|
|
|
- // ==== 精准 Diff 的 submitPoints ====
|
|
|
+ /** 点位列表:精准 Diff(坐标=后端坐标,不乘比例);可选保留当前选中 */
|
|
|
fun submitPoints(points: List<IsolationPoint>, keepSelection: Boolean = true) {
|
|
|
- // 快照避免外部列表后续被改动
|
|
|
val incoming = if (points === stationList) points.map { deepCopyItem(it) } else points
|
|
|
|
|
|
val keepEid = if (keepSelection) selectedEntityIdForKeep else null
|
|
|
val keepName = if (keepSelection) selectedNameForKeep else null
|
|
|
|
|
|
- // 记录变化以做局部刷新
|
|
|
val changedCenters = ArrayList<PointF>(8)
|
|
|
- val changedRadiusPx = 72 // 以图标 ~64px 给点冗余
|
|
|
+ val changedRadiusPx = 84
|
|
|
|
|
|
synchronized(dataLock) {
|
|
|
- // 1) 建索引(现有 & 新数据)
|
|
|
val oldById = HashMap<Long, IsolationPoint>(stationList.size)
|
|
|
stationList.forEach { oldById[it.entityId] = it }
|
|
|
|
|
|
val seenIds = HashSet<Long>(incoming.size)
|
|
|
val nextList = ArrayList<IsolationPoint>(incoming.size)
|
|
|
|
|
|
- // 2) 逐项处理:更新/新增
|
|
|
for (src in incoming) {
|
|
|
val id = src.entityId
|
|
|
seenIds.add(id)
|
|
|
val dst = oldById[id]
|
|
|
|
|
|
if (dst == null) {
|
|
|
- // 新增:深拷贝一次,避免后续被外部改动影响
|
|
|
val add = deepCopyItem(src).apply {
|
|
|
isSelected = when {
|
|
|
!keepSelection -> false
|
|
|
keepEid != null -> entityId == keepEid
|
|
|
- keepName != null -> entityName == keepName
|
|
|
+ keepName != null -> motorCode == keepName
|
|
|
else -> false
|
|
|
}
|
|
|
}
|
|
|
nextList.add(add)
|
|
|
changedCenters.add(add.pos)
|
|
|
} else {
|
|
|
- // 更新:字段级 copy,尽量复用对象引用,避免动画/缓存抖动
|
|
|
var dirty = false
|
|
|
+ if (!dst.pos.approxEq(src.pos)) { dst.pos = PointF(src.pos.x, src.pos.y); dirty = true }
|
|
|
+ if (dst.status != src.status) { dst.status = src.status; dirty = true }
|
|
|
+ if (dst.motorCode != src.motorCode) { dst.motorCode = src.motorCode; dirty = true }
|
|
|
+ if (dst.pointNfc != src.pointNfc) { dst.pointNfc = src.pointNfc; dirty = true }
|
|
|
+ if (dst.pointSerialNumber != src.pointSerialNumber) { dst.pointSerialNumber = src.pointSerialNumber; dirty = true }
|
|
|
+ if (src.icon != null && dst.icon !== src.icon) { dst.icon = src.icon; dirty = true }
|
|
|
|
|
|
- if (!dst.pos.approxEq(src.pos)) {
|
|
|
- dst.pos = PointF(src.pos.x, src.pos.y) // 新对象,避免外部持有同一引用
|
|
|
- dirty = true
|
|
|
- }
|
|
|
- if (dst.status != src.status) {
|
|
|
- dst.status = src.status; dirty = true
|
|
|
- }
|
|
|
- if (dst.entityName != src.entityName) {
|
|
|
- dst.entityName = src.entityName; dirty = true
|
|
|
- }
|
|
|
- if (dst.pointNfc != src.pointNfc) {
|
|
|
- dst.pointNfc = src.pointNfc; dirty = true
|
|
|
- }
|
|
|
- if (dst.pointSerialNumber != src.pointSerialNumber) {
|
|
|
- dst.pointSerialNumber = src.pointSerialNumber; dirty = true
|
|
|
- }
|
|
|
- // icon:如果外部异步回填,这里一般不动;若你也带了新 icon:
|
|
|
- if (src.icon != null && dst.icon !== src.icon) {
|
|
|
- dst.icon = src.icon; dirty = true
|
|
|
- }
|
|
|
-
|
|
|
- // 选中保持 or 重置
|
|
|
dst.isSelected = when {
|
|
|
!keepSelection -> false
|
|
|
keepEid != null -> dst.entityId == keepEid
|
|
|
- keepName != null -> dst.entityName == keepName
|
|
|
+ keepName != null -> dst.motorCode == keepName
|
|
|
else -> false
|
|
|
}
|
|
|
-
|
|
|
if (dirty) changedCenters.add(dst.pos)
|
|
|
- nextList.add(dst) // 复用原对象引用
|
|
|
+ nextList.add(dst)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // 3) 删除:老数据里有、新数据里没有的
|
|
|
- if (seenIds.size != oldById.size) {
|
|
|
- // 直接按 nextList(新顺序)重建 stationList 更简单干净
|
|
|
- // 被删除的对象引用不再放入 nextList 即可
|
|
|
- }
|
|
|
-
|
|
|
- // 4) 最小代价重建列表(保持新顺序,但复用对象)
|
|
|
stationList.clear()
|
|
|
stationList.addAll(nextList)
|
|
|
}
|
|
|
|
|
|
- // 5) 局部刷新(尽可能小范围)
|
|
|
- if (changedCenters.isNotEmpty()) {
|
|
|
- refreshRegionsOrThrottle(changedCenters, changedRadiusPx)
|
|
|
- } else {
|
|
|
- // 没有变化也触发一次轻微节流刷新,避免偶发不同步
|
|
|
- throttleInvalidate()
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
-// ==== 辅助方法 ====
|
|
|
-
|
|
|
- // PointF 近似比较,避免浮点误差导致的伪变更
|
|
|
- private fun PointF.approxEq(other: PointF, eps: Float = 0.1f): Boolean {
|
|
|
- return kotlin.math.abs(x - other.x) <= eps && kotlin.math.abs(y - other.y) <= eps
|
|
|
+ if (changedCenters.isNotEmpty()) refreshRegionsOrThrottle(changedCenters, changedRadiusPx)
|
|
|
+ else throttleInvalidate()
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 优先做局部刷新;如果你的 MapView 没有暴露 invalidateRect,就回退到节流整层刷新。
|
|
|
- * 这里假设 mapView/invalidateRect 可用;否则把反射去掉,直接 throttleInvalidate()
|
|
|
- */
|
|
|
- private fun refreshRegionsOrThrottle(centers: List<PointF>, radiusPx: Int) {
|
|
|
- val mv = mapView ?: return throttleInvalidate()
|
|
|
-
|
|
|
- var didPartial = false
|
|
|
- runCatching {
|
|
|
- val m = mv.javaClass.getMethod(
|
|
|
- "invalidateRect", Int::class.java, Int::class.java, Int::class.java, Int::class.java
|
|
|
- )
|
|
|
- for (c in centers) {
|
|
|
- val l = (c.x - radiusPx).toInt()
|
|
|
- val t = (c.y - radiusPx).toInt()
|
|
|
- val r = (c.x + radiusPx).toInt()
|
|
|
- val b = (c.y + radiusPx).toInt()
|
|
|
- m.invoke(mv, l, t, r, b)
|
|
|
- }
|
|
|
- didPartial = true
|
|
|
- }.onFailure {
|
|
|
- // ignore
|
|
|
- }
|
|
|
-
|
|
|
- if (!didPartial) throttleInvalidate()
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
- /** 比例(基准) */
|
|
|
+ /** 比例(基准)。兼容原有 API:影响圆与文字的“基准尺寸”,不影响坐标 */
|
|
|
fun setRatio(ratio: Float) {
|
|
|
- this.ratio = ratio
|
|
|
- switchSize = BASE_SWITCH_SIZE * ratio
|
|
|
- textSize = BASE_TEXT_SIZE * ratio
|
|
|
+ this.ratio = ratio.coerceAtLeast(0.1f)
|
|
|
+ switchSize = BASE_SWITCH_SIZE * this.ratio // 屏幕像素基准
|
|
|
+ textSize = BASE_TEXT_SIZE * this.ratio // (文本的 min/max 可用它当基准去推)
|
|
|
throttleInvalidate()
|
|
|
}
|
|
|
|
|
|
- /** 按唯一 name 选中并定位(若 name 唯一) */
|
|
|
+
|
|
|
+ /** 按 entityId 选中并定位(保留原 API) */
|
|
|
fun selectPoint(
|
|
|
entityId: Long?,
|
|
|
animated: Boolean = true,
|
|
|
@@ -326,12 +229,8 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
val token = ++selectToken
|
|
|
|
|
|
val eid = target.entityId
|
|
|
- val name = target.entityName
|
|
|
- synchronized(dataLock) {
|
|
|
- stationList.forEach {
|
|
|
- it.isSelected = it.entityId == entityId
|
|
|
- }
|
|
|
- }
|
|
|
+ val name = target.motorCode
|
|
|
+ synchronized(dataLock) { stationList.forEach { it.isSelected = it.entityId == entityId } }
|
|
|
selectedEntityIdForKeep = eid
|
|
|
selectedNameForKeep = name
|
|
|
throttleInvalidate()
|
|
|
@@ -344,7 +243,7 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
return true
|
|
|
}
|
|
|
|
|
|
- /** 局部刷新(点在屏幕上时) */
|
|
|
+ /** 局部刷新(点在屏幕上时)——保留原 API */
|
|
|
fun refreshIfVisible(point: PointF, margin: Float = 0f) {
|
|
|
if (inDraw) return
|
|
|
val w = mapView.width
|
|
|
@@ -352,8 +251,7 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
if (w == 0 || h == 0) return
|
|
|
val pts = floatArrayOf(point.x, point.y)
|
|
|
mapView.currentMatrix.mapPoints(pts)
|
|
|
- val x = pts[0];
|
|
|
- val y = pts[1]
|
|
|
+ val x = pts[0]; val y = pts[1]
|
|
|
if (x + margin >= 0 && x - margin <= w && y + margin >= 0 && y - margin <= h) {
|
|
|
throttleInvalidate()
|
|
|
}
|
|
|
@@ -378,28 +276,20 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
MotionEvent.ACTION_MOVE -> {
|
|
|
lastScreenX = event.x; lastScreenY = event.y
|
|
|
if (pendingLongPress && !longPressTriggered) {
|
|
|
val dx = event.x - downScreenX
|
|
|
val dy = event.y - downScreenY
|
|
|
- if (dx * dx + dy * dy > touchSlopSq) {
|
|
|
+ if (dx*dx + dy*dy > touchSlopSq) {
|
|
|
pendingLongPress = false
|
|
|
mapView.removeCallbacks(longPressRunnable)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
MotionEvent.ACTION_UP -> {
|
|
|
mapView.removeCallbacks(longPressRunnable)
|
|
|
- if (longPressTriggered) {
|
|
|
- pendingLongPress = false
|
|
|
- longPressTriggered = false
|
|
|
- currentPressed = null
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
+ if (longPressTriggered) { pendingLongPress = false; longPressTriggered = false; currentPressed = null; return }
|
|
|
val mp = screenToMap(event.x, event.y) ?: return
|
|
|
val hit = hitTest(mp.x, mp.y) ?: return
|
|
|
val token = ++selectToken
|
|
|
@@ -407,36 +297,23 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
val targetScale = desiredSelectScale
|
|
|
val targetCenter = centerOf(hit)
|
|
|
val eid = hit.entityId
|
|
|
- val name = hit.entityName
|
|
|
+ val name = hit.motorCode
|
|
|
|
|
|
- // ① 先拉缩放(以当前中心为 pivot)
|
|
|
- currentMapCenter()?.let { cur ->
|
|
|
- mapView?.animateCenterOnPoint(cur.x, cur.y, clickZoomDurationMs, targetScale)
|
|
|
- }
|
|
|
- // ② 再居中到目标(保持 targetScale)
|
|
|
+ currentMapCenter()?.let { cur -> mapView?.animateCenterOnPoint(cur.x, cur.y, clickZoomDurationMs, targetScale) }
|
|
|
mapView?.postDelayed({
|
|
|
withLiveToken(token) {
|
|
|
- mapView?.animateCenterOnPoint(
|
|
|
- targetCenter.x,
|
|
|
- targetCenter.y,
|
|
|
- clickCenterDurationMs,
|
|
|
- targetScale
|
|
|
- )
|
|
|
+ mapView?.animateCenterOnPoint(targetCenter.x, targetCenter.y, clickCenterDurationMs, targetScale)
|
|
|
}
|
|
|
}, clickZoomDurationMs)
|
|
|
- // ③ 完成后只选中目标
|
|
|
mapView?.postDelayed({
|
|
|
withLiveToken(token) {
|
|
|
- synchronized(dataLock) {
|
|
|
- stationList.replaceAll { it.copy(isSelected = (it.entityId == eid) || (it.entityName == name)) }
|
|
|
- }
|
|
|
+ synchronized(dataLock) { stationList.replaceAll { it.copy(isSelected = (it.entityId == eid) || (it.motorCode == name)) } }
|
|
|
selectedEntityIdForKeep = eid
|
|
|
selectedNameForKeep = name
|
|
|
throttleInvalidate()
|
|
|
}
|
|
|
}, clickZoomDurationMs + clickCenterDurationMs)
|
|
|
}
|
|
|
-
|
|
|
MotionEvent.ACTION_CANCEL -> {
|
|
|
pendingLongPress = false
|
|
|
longPressTriggered = false
|
|
|
@@ -447,106 +324,109 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
}
|
|
|
|
|
|
// ================= 绘制 =================
|
|
|
- override fun draw(
|
|
|
- canvas: Canvas,
|
|
|
- currentMatrix: Matrix,
|
|
|
- currentZoom: Float,
|
|
|
- currentRotateDegrees: Float
|
|
|
- ) {
|
|
|
- if (!isVisible) return
|
|
|
- if (inDraw) return
|
|
|
+ override fun draw(canvas: Canvas, currentMatrix: Matrix, currentZoom: Float, currentRotateDegrees: Float) {
|
|
|
+ if (!isVisible || inDraw) return
|
|
|
inDraw = true
|
|
|
-
|
|
|
this.currentZoom = currentZoom
|
|
|
- this.currentDegree = 360 - currentRotateDegrees
|
|
|
|
|
|
try {
|
|
|
canvas.save()
|
|
|
- try {
|
|
|
- canvas.concat(currentMatrix)
|
|
|
-
|
|
|
- val points = synchronized(dataLock) { stationList.toList() }
|
|
|
- val switches = runCatching { ModBusController.getSwitchData() }.getOrNull()
|
|
|
- for (p in points) {
|
|
|
- val c = centerOf(p)
|
|
|
-
|
|
|
- // 选中外环
|
|
|
- if (p.isSelected) {
|
|
|
- paint.style = Paint.Style.STROKE
|
|
|
- paint.strokeWidth = 4f
|
|
|
- paint.color = colSelectRing
|
|
|
- canvas.drawCircle(c.x, c.y, switchSize * 1.5f, paint)
|
|
|
- }
|
|
|
-
|
|
|
- // 主体:直接用 p.status(由 setSwitchData 决定)
|
|
|
- paint.style = Paint.Style.FILL_AND_STROKE
|
|
|
- var status =
|
|
|
- p.pointSerialNumber?.let { parseStatus(switches?.find { it.idx.toString() == p.pointSerialNumber }) }
|
|
|
- ?: p.status
|
|
|
-// if (p.entityName=="E-45"){
|
|
|
-// status = STATUS_ALARM
|
|
|
-// }
|
|
|
-// val status = Random.nextInt(3)
|
|
|
- when (status) {
|
|
|
- STATUS_ON -> {
|
|
|
- paint.color = colOn; paint.alpha = 255
|
|
|
- canvas.drawCircle(c.x, c.y, switchSize, paint)
|
|
|
- }
|
|
|
-
|
|
|
- STATUS_OFF -> {
|
|
|
- paint.color = colOff; paint.alpha = 255
|
|
|
- canvas.drawCircle(c.x, c.y, switchSize, paint)
|
|
|
- }
|
|
|
-
|
|
|
- STATUS_ALARM -> {
|
|
|
- val t = pulsePhase
|
|
|
- val color = if (t < 0.5f) colRed else colOrange
|
|
|
- val a = (160 + 95 * abs(sin(2f * Math.PI.toFloat() * t))).toInt()
|
|
|
- paint.color = color; paint.alpha = a
|
|
|
- val r = switchSize * (1.0f + 0.10f * sin(2f * Math.PI.toFloat() * t))
|
|
|
- canvas.drawCircle(c.x, c.y, r, paint)
|
|
|
- paint.alpha = (a * 0.35f).toInt()
|
|
|
- canvas.drawCircle(c.x, c.y, r * 1.25f, paint)
|
|
|
- paint.alpha = 255
|
|
|
- }
|
|
|
+ canvas.concat(currentMatrix) // 地图照常缩放/旋转
|
|
|
+
|
|
|
+ val points = synchronized(dataLock) { stationList.toList() }
|
|
|
+
|
|
|
+ // 屏幕上希望看到的半径/描边/文字大小(像素):
|
|
|
+ val screenR = switchSize // e.g. 30px
|
|
|
+ val screenStroke = 4f
|
|
|
+ val screenTextMin = 8f
|
|
|
+ val screenTextMax = 22f
|
|
|
+
|
|
|
+ // 由于 canvas 已被 currentMatrix 放大了 zoom 倍,
|
|
|
+ // 画到“地图坐标”层面的尺寸需要 /zoom 才能抵消缩放,获得恒定屏幕像素。
|
|
|
+ val mapR = screenR / currentZoom
|
|
|
+ val mapStroke = screenStroke / currentZoom
|
|
|
+
|
|
|
+ for (p in points) {
|
|
|
+ val c = centerOf(p)
|
|
|
+
|
|
|
+ // 选中环
|
|
|
+ if (p.isSelected) {
|
|
|
+ paint.style = Paint.Style.STROKE
|
|
|
+ paint.strokeWidth = mapStroke
|
|
|
+ paint.color = colSelectRing
|
|
|
+ canvas.drawCircle(c.x, c.y, mapR, paint)
|
|
|
+ }
|
|
|
|
|
|
- else -> { // STATUS_UNKNOWN
|
|
|
- paint.color = Color.DKGRAY; paint.alpha = 200
|
|
|
- canvas.drawCircle(c.x, c.y, switchSize, paint)
|
|
|
- paint.alpha = 255
|
|
|
- }
|
|
|
+ // 主体圆
|
|
|
+ paint.style = Paint.Style.FILL_AND_STROKE
|
|
|
+ when (p.status) {
|
|
|
+ STATUS_ON -> { paint.color = colOn; paint.alpha = 255; canvas.drawCircle(c.x, c.y, mapR, paint) }
|
|
|
+ STATUS_OFF -> { paint.color = colOff; paint.alpha = 255; canvas.drawCircle(c.x, c.y, mapR, paint) }
|
|
|
+ STATUS_ALARM -> {
|
|
|
+ val t = pulsePhase
|
|
|
+ val color = if (t < 0.5f) colRed else colOrange
|
|
|
+ val a = (160 + 95 * kotlin.math.abs(kotlin.math.sin(2f * Math.PI.toFloat() * t))).toInt()
|
|
|
+ paint.color = color; paint.alpha = a
|
|
|
+ val r = mapR * (1.0f + 0.10f * kotlin.math.sin(2f * Math.PI.toFloat() * t))
|
|
|
+ canvas.drawCircle(c.x, c.y, r, paint)
|
|
|
+ paint.alpha = (a * 0.35f).toInt()
|
|
|
+ canvas.drawCircle(c.x, c.y, r * 1.25f, paint)
|
|
|
+ paint.alpha = 255
|
|
|
}
|
|
|
+ else -> { paint.color = Color.DKGRAY; paint.alpha = 200; canvas.drawCircle(c.x, c.y, mapR, paint); paint.alpha = 255 }
|
|
|
+ }
|
|
|
|
|
|
- // 文本
|
|
|
- if (currentZoom >= TEXT_VISIBLE_ZOOM_THRESHOLD) {
|
|
|
- paint.style = Paint.Style.FILL
|
|
|
- paint.strokeWidth = 1f
|
|
|
- paint.color = Color.WHITE
|
|
|
- paint.textSize = textSize
|
|
|
- val text = p.entityName
|
|
|
- val textW = paint.measureText(text)
|
|
|
- val fm = paint.fontMetrics
|
|
|
- val textH = fm.bottom - fm.top
|
|
|
- canvas.drawText(
|
|
|
- text,
|
|
|
- c.x - textW / 2f,
|
|
|
- c.y + (textH / 2 - fm.bottom),
|
|
|
- paint
|
|
|
- )
|
|
|
- }
|
|
|
+ // 文本:先算希望的屏幕宽度,再 /zoom 得到地图层文字 size
|
|
|
+ val text = p.motorCode ?: ""
|
|
|
+ if (text.isNotEmpty()) {
|
|
|
+ paint.style = Paint.Style.FILL
|
|
|
+ paint.strokeWidth = mapStroke
|
|
|
+ paint.color = Color.WHITE
|
|
|
+
|
|
|
+ val maxTextWidthScreen = (screenR * 2f) - 6f // 屏幕像素
|
|
|
+ // 先在“屏幕像素语义”下求字号
|
|
|
+ val screenTextSize = fitTextSizeScreen(paint, text, maxTextWidthScreen, screenTextMin, screenTextMax)
|
|
|
+ val mapTextSize = screenTextSize / currentZoom
|
|
|
+
|
|
|
+ paint.textSize = mapTextSize
|
|
|
+ val textW = paint.measureText(text)
|
|
|
+ val fm = paint.fontMetrics
|
|
|
+ val textH = fm.bottom - fm.top
|
|
|
+ canvas.drawText(text, c.x - textW / 2f, c.y + (textH / 2 - fm.bottom), paint)
|
|
|
}
|
|
|
- } finally {
|
|
|
- canvas.restore()
|
|
|
}
|
|
|
} finally {
|
|
|
+ canvas.restore()
|
|
|
inDraw = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ private fun fitTextSizeScreen(p: Paint, text: String, maxWidthScreenPx: Float, minSp: Float, maxSp: Float): Float {
|
|
|
+ if (text.isEmpty()) return minSp
|
|
|
+ // 在“屏幕像素语义”下先估字号,再回到 draw() 里除以 zoom 赋给 paint.textSize
|
|
|
+ p.textSize = maxSp
|
|
|
+ var w = p.measureText(text)
|
|
|
+ if (w <= maxWidthScreenPx) return maxSp
|
|
|
+ val est = (maxWidthScreenPx / w) * maxSp
|
|
|
+ val clamped = est.coerceIn(minSp, maxSp)
|
|
|
+ p.textSize = clamped
|
|
|
+ w = p.measureText(text)
|
|
|
+ if (w > maxWidthScreenPx) return (clamped * 0.95f).coerceAtLeast(minSp)
|
|
|
+ return clamped
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
// ================= 工具 =================
|
|
|
+ private fun getSwitchR(): Float {
|
|
|
+ // 保持原有 ratio 语义 + 轻微随 zoom 增益
|
|
|
+ val zoomScale = 0.9f + 0.1f * currentZoom.coerceIn(1f, 2.5f)
|
|
|
+ return switchSize * zoomScale // switchSize = BASE_SWITCH_SIZE * ratio
|
|
|
+ }
|
|
|
+
|
|
|
private fun centerOf(p: IsolationPoint): PointF {
|
|
|
return if (POS_IS_CENTER) PointF(p.pos.x, p.pos.y)
|
|
|
- else PointF(p.pos.x + switchSize / 2f, p.pos.y + switchSize / 2f)
|
|
|
+ else PointF(p.pos.x + getSwitchR() / 2f, p.pos.y + getSwitchR() / 2f)
|
|
|
}
|
|
|
|
|
|
private fun screenToMap(x: Float, y: Float): PointF? {
|
|
|
@@ -573,8 +453,10 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
}
|
|
|
|
|
|
private fun hitTest(mapX: Float, mapY: Float): IsolationPoint? {
|
|
|
- val hitR = switchSize * 1.2f
|
|
|
+ val screenR = switchSize * 1.2f
|
|
|
+ val hitR = screenR / currentZoom // 转回地图坐标
|
|
|
val hitR2 = hitR * hitR
|
|
|
+
|
|
|
val snapshot = synchronized(dataLock) { stationList.toList() }
|
|
|
var hit: IsolationPoint? = null
|
|
|
var bestD2 = Float.MAX_VALUE
|
|
|
@@ -582,25 +464,34 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
val c = centerOf(p)
|
|
|
val dx = mapX - c.x
|
|
|
val dy = mapY - c.y
|
|
|
- val d2 = dx * dx + dy * dy
|
|
|
- if (d2 <= hitR2 && d2 < bestD2) {
|
|
|
- bestD2 = d2
|
|
|
- hit = p
|
|
|
- }
|
|
|
+ val d2 = dx*dx + dy*dy
|
|
|
+ if (d2 <= hitR2 && d2 < bestD2) { bestD2 = d2; hit = p }
|
|
|
}
|
|
|
return hit
|
|
|
}
|
|
|
|
|
|
- private fun parseStatus(bean: DockBean.SwitchBean?): Int {
|
|
|
- if (bean == null) return STATUS_UNKNOWN
|
|
|
- if (isAlarm(bean)) return STATUS_ALARM
|
|
|
- return if (bean.enabled) STATUS_ON else STATUS_OFF
|
|
|
+
|
|
|
+ private fun PointF.approxEq(other: PointF, eps: Float = 0.1f): Boolean {
|
|
|
+ return kotlin.math.abs(x - other.x) <= eps && kotlin.math.abs(y - other.y) <= eps
|
|
|
}
|
|
|
|
|
|
- /** 告警判断:按你业务改 */
|
|
|
- private fun isAlarm(bean: DockBean.SwitchBean?): Boolean {
|
|
|
- // TODO: return bean?.type == 2 || bean?.statusCode == ALARM
|
|
|
- return false
|
|
|
+ /** 优先做局部刷新;MapView 若无 invalidateRect 则回退整层刷新 */
|
|
|
+ private fun refreshRegionsOrThrottle(centers: List<PointF>, radiusPx: Int) {
|
|
|
+ val mv = mapView ?: return throttleInvalidate()
|
|
|
+ var didPartial = false
|
|
|
+ runCatching {
|
|
|
+ val m = mv.javaClass.getMethod("invalidateRect", Int::class.java, Int::class.java, Int::class.java, Int::class.java)
|
|
|
+ for (c in centers) {
|
|
|
+ val l = (c.x - radiusPx).toInt()
|
|
|
+ val t = (c.y - radiusPx).toInt()
|
|
|
+ val r = (c.x + radiusPx).toInt()
|
|
|
+ val b = (c.y + radiusPx).toInt()
|
|
|
+ m.invoke(mv, l, t, r, b)
|
|
|
+ }
|
|
|
+ didPartial = true
|
|
|
+ }.onFailure { /* ignore */ }
|
|
|
+
|
|
|
+ if (!didPartial) throttleInvalidate()
|
|
|
}
|
|
|
|
|
|
private fun throttleInvalidate() {
|
|
|
@@ -618,6 +509,30 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
mapView.removeCallbacks(refreshRunnable)
|
|
|
}
|
|
|
|
|
|
+ // —— 与原版兼容的占位:如果你项目里还调用了它们 ——
|
|
|
+ private fun parseStatus(bean: DockBean.SwitchBean?): Int {
|
|
|
+ if (bean == null) return STATUS_UNKNOWN
|
|
|
+ if (isAlarm(bean)) return STATUS_ALARM
|
|
|
+ return if (bean.enabled) STATUS_ON else STATUS_OFF
|
|
|
+ }
|
|
|
+ private fun isAlarm(bean: DockBean.SwitchBean?): Boolean {
|
|
|
+ // TODO: 按业务判断
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
private fun deepCopyPoint(p: PointF) = PointF(p.x, p.y)
|
|
|
private fun deepCopyItem(it: IsolationPoint) = it.copy(pos = deepCopyPoint(it.pos))
|
|
|
+
|
|
|
+ private fun fitTextSize(p: Paint, text: String, maxWidth: Float, minSp: Float = 8f, maxSp: Float = 22f): Float {
|
|
|
+ if (text.isEmpty()) return minSp
|
|
|
+ p.textSize = maxSp
|
|
|
+ var w = p.measureText(text)
|
|
|
+ if (w <= maxWidth) return maxSp
|
|
|
+ val est = (maxWidth / w) * maxSp
|
|
|
+ val clamped = est.coerceIn(minSp, maxSp)
|
|
|
+ p.textSize = clamped
|
|
|
+ w = p.measureText(text)
|
|
|
+ if (w > maxWidth) p.textSize = (clamped * 0.95f).coerceAtLeast(minSp)
|
|
|
+ return p.textSize
|
|
|
+ }
|
|
|
}
|