|
@@ -13,80 +13,134 @@ import com.grkj.iscs_mars.MyApplication
|
|
|
import com.grkj.iscs_mars.R
|
|
import com.grkj.iscs_mars.R
|
|
|
import com.grkj.iscs_mars.modbus.DockBean
|
|
import com.grkj.iscs_mars.modbus.DockBean
|
|
|
import com.grkj.iscs_mars.modbus.ModBusController
|
|
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.MapView
|
|
|
import com.onlylemi.mapview.library.layer.MapBaseLayer
|
|
import com.onlylemi.mapview.library.layer.MapBaseLayer
|
|
|
import com.sik.sikcore.SIKCore
|
|
import com.sik.sikcore.SIKCore
|
|
|
-import com.sik.sikcore.thread.ThreadUtils
|
|
|
|
|
import kotlin.math.abs
|
|
import kotlin.math.abs
|
|
|
import kotlin.math.sin
|
|
import kotlin.math.sin
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 自定义开关层级
|
|
|
|
|
|
|
+ * 自定义开关层级(按点位缓存状态版本,变了才刷新)
|
|
|
*/
|
|
*/
|
|
|
class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
- mapView: MapView?, private var stationList: MutableList<IsolationPoint> = mutableListOf()
|
|
|
|
|
|
|
+ mapView: MapView?,
|
|
|
|
|
+ private var stationList: MutableList<IsolationPoint> = mutableListOf()
|
|
|
) : MapBaseLayer(mapView) {
|
|
) : MapBaseLayer(mapView) {
|
|
|
|
|
|
|
|
|
|
+ // ===== 数据结构 =====
|
|
|
|
|
+ data class IsolationPoint(
|
|
|
|
|
+ var pos: PointF, // 可能是中心 or 左上角
|
|
|
|
|
+ var entityName: String,
|
|
|
|
|
+ var icon: Bitmap?,
|
|
|
|
|
+ val entityId: Long,
|
|
|
|
|
+ var pointSerialNumber: String?, // 仅用于和 switch.idx 对表
|
|
|
|
|
+ var isSelected: Boolean,
|
|
|
|
|
+ var pointNfc: String?,
|
|
|
|
|
+ var status: Int = STATUS_UNKNOWN // 见常量
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ companion object {
|
|
|
|
|
+ const val STATUS_OFF = 0
|
|
|
|
|
+ const val STATUS_ON = 1
|
|
|
|
|
+ const val STATUS_ALARM = 2
|
|
|
|
|
+ const val STATUS_UNKNOWN = 3
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 配置开关:你的 pos 是否已经是中心点? =====
|
|
|
|
|
+ private val POS_IS_CENTER = true // 若上游 pos 就是中心,设 true;若是左上角,改 false
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 同步&选择 =====
|
|
|
|
|
+ private val dataLock = Any()
|
|
|
|
|
+
|
|
|
@Volatile
|
|
@Volatile
|
|
|
- var inDraw: Boolean = false
|
|
|
|
|
- private set
|
|
|
|
|
|
|
+ private var selectedEntityIdForKeep: Long? = null
|
|
|
|
|
|
|
|
- // ---- 动画参数 ----
|
|
|
|
|
- private val breathePeriod = 1200f // ALARM 脉冲周期(ms)
|
|
|
|
|
- private val FRAME_INTERVAL = 32L // ~30fps
|
|
|
|
|
- private var pulsePhase: Float = 0f // 0..1 脉冲相位
|
|
|
|
|
|
|
+ @Volatile
|
|
|
|
|
+ private var selectedNameForKeep: String? = null
|
|
|
|
|
|
|
|
- // —— 动画时长配置 ——
|
|
|
|
|
- private val longPressCenterDurationMs = 280L
|
|
|
|
|
- private val clickZoomDurationMs = 220L
|
|
|
|
|
- private val clickCenterDurationMs = 220L
|
|
|
|
|
|
|
+ // ===== switch 数据快照(仅存原始列表,不在 draw 里查表)=====
|
|
|
|
|
+ @Volatile
|
|
|
|
|
+ private var hasAlarmFlag: Boolean = false
|
|
|
|
|
|
|
|
- // 点击后希望的目标缩放(你想要多大就设多少)
|
|
|
|
|
- private val desiredSelectScale = 2.0f
|
|
|
|
|
|
|
+ // token 防过期回调
|
|
|
|
|
+ @Volatile
|
|
|
|
|
+ private var selectToken: Long = 0L
|
|
|
|
|
+ private inline fun withLiveToken(token: Long, block: () -> Unit) {
|
|
|
|
|
+ if (token == selectToken) block()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ===== 动画参数 =====
|
|
|
|
|
+ @Volatile
|
|
|
|
|
+ var inDraw: Boolean = false
|
|
|
|
|
+ private set
|
|
|
|
|
|
|
|
- // 旧的 alpha 给 ON 态呼吸用的;现在只给 ALARM 脉冲使用
|
|
|
|
|
- private var alpha = 255
|
|
|
|
|
|
|
+ private val breathePeriod = 1200f
|
|
|
|
|
+ private var pulsePhase: Float = 0f
|
|
|
|
|
+
|
|
|
|
|
+ private val FRAME_INTERVAL_MS = 32L
|
|
|
|
|
+ private var nextInvalidateScheduled = false
|
|
|
|
|
+ private val invalidateRunnable = Runnable {
|
|
|
|
|
+ nextInvalidateScheduled = false
|
|
|
|
|
+ mapView?.refresh()
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
private val refreshRunnable: Runnable = object : Runnable {
|
|
private val refreshRunnable: Runnable = object : Runnable {
|
|
|
override fun run() {
|
|
override fun run() {
|
|
|
val now = SystemClock.uptimeMillis()
|
|
val now = SystemClock.uptimeMillis()
|
|
|
- val phase = ((now % breathePeriod).toFloat()) / breathePeriod
|
|
|
|
|
- pulsePhase = phase
|
|
|
|
|
- alpha = (((sin(phase * 2 * Math.PI) + 1) / 2f) * (255 - 50) + 50).toInt()
|
|
|
|
|
-
|
|
|
|
|
- val points = synchronized(stationList) { stationList.toList() }
|
|
|
|
|
- val switches = runCatching { ModBusController.getSwitchData() }.getOrNull()
|
|
|
|
|
-
|
|
|
|
|
- if (hasAnyAlarm(points, switches)) {
|
|
|
|
|
- mapView?.refresh()
|
|
|
|
|
- } else {
|
|
|
|
|
- mapView?.refresh()
|
|
|
|
|
- }
|
|
|
|
|
- mapView?.postDelayed(this, FRAME_INTERVAL) // 🔑 用 this 引用自己
|
|
|
|
|
|
|
+ pulsePhase = ((now % breathePeriod).toFloat()) / breathePeriod
|
|
|
|
|
+ throttleInvalidate()
|
|
|
|
|
+ mapView?.postDelayed(this, FRAME_INTERVAL_MS)
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private lateinit var paint: Paint
|
|
|
|
|
- private var currentZoom = 0f
|
|
|
|
|
- private var currentDegree = 0f
|
|
|
|
|
|
|
+ private val longPressCenterDurationMs = 280L
|
|
|
|
|
+ private val clickZoomDurationMs = 220L
|
|
|
|
|
+ private val clickCenterDurationMs = 220L
|
|
|
|
|
+ private val desiredSelectScale = 2.0f
|
|
|
|
|
+
|
|
|
|
|
+ // 尺寸
|
|
|
private var ratio: Float = 1f
|
|
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 textSize: Float = BASE_TEXT_SIZE
|
|
|
|
|
|
|
|
- // 尺寸:注意你把 switchSize 当作「半径」来使用
|
|
|
|
|
- private var switchSize: Float = 2f
|
|
|
|
|
- private var textSize: Float = 1f
|
|
|
|
|
- private var lastKnownZoom: Float = 1f
|
|
|
|
|
|
|
+ // 绘制状态
|
|
|
|
|
+ private lateinit var paint: Paint
|
|
|
|
|
+ private var currentZoom = 1f
|
|
|
|
|
+ private var currentDegree = 0f
|
|
|
|
|
+ private val TEXT_VISIBLE_ZOOM_THRESHOLD = 1f
|
|
|
|
|
+
|
|
|
|
|
+ // 颜色
|
|
|
|
|
+ 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 colSelectRing = Color.argb(180, 66, 133, 244)
|
|
|
|
|
|
|
|
// 长按探测
|
|
// 长按探测
|
|
|
private val longPressTimeoutMs: Long =
|
|
private val longPressTimeoutMs: Long =
|
|
|
android.view.ViewConfiguration.getLongPressTimeout().toLong()
|
|
android.view.ViewConfiguration.getLongPressTimeout().toLong()
|
|
|
-
|
|
|
|
|
- // 建议更宽容一些(x1.5):
|
|
|
|
|
- private val touchSlop: Float =
|
|
|
|
|
- android.view.ViewConfiguration.get(
|
|
|
|
|
- mapView?.context ?: SIKCore.getApplication()
|
|
|
|
|
- ).scaledTouchSlop * 1.5f
|
|
|
|
|
|
|
+ private val touchSlop: Float = android.view.ViewConfiguration.get(
|
|
|
|
|
+ mapView?.context ?: SIKCore.getApplication()
|
|
|
|
|
+ ).scaledTouchSlop * 1.5f
|
|
|
private val touchSlopSq: Float = touchSlop * touchSlop
|
|
private val touchSlopSq: Float = touchSlop * touchSlop
|
|
|
-
|
|
|
|
|
private var pendingLongPress = false
|
|
private var pendingLongPress = false
|
|
|
private var longPressTriggered = false
|
|
private var longPressTriggered = false
|
|
|
private var downScreenX = 0f
|
|
private var downScreenX = 0f
|
|
@@ -98,149 +152,235 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
private val longPressRunnable = Runnable {
|
|
private val longPressRunnable = Runnable {
|
|
|
if (pendingLongPress && currentPressed != null) {
|
|
if (pendingLongPress && currentPressed != null) {
|
|
|
longPressTriggered = true
|
|
longPressTriggered = true
|
|
|
- pendingLongPress = false // ✅ 关键:触发后不再允许被 MOVE 取消
|
|
|
|
|
-
|
|
|
|
|
|
|
+ pendingLongPress = false
|
|
|
val p = currentPressed!!
|
|
val p = currentPressed!!
|
|
|
- val centerMapX = p.pos.x + switchSize / 2f
|
|
|
|
|
- val centerMapY = p.pos.y + switchSize / 2f
|
|
|
|
|
-
|
|
|
|
|
|
|
+ val c = centerOf(p)
|
|
|
mapView?.postDelayed({
|
|
mapView?.postDelayed({
|
|
|
- val screen = mapToScreen(centerMapX, centerMapY)
|
|
|
|
|
- onLongPressListener?.invoke(
|
|
|
|
|
- p,
|
|
|
|
|
- screen.x,
|
|
|
|
|
- screen.y - switchSize / 2 - 4,
|
|
|
|
|
- centerMapX,
|
|
|
|
|
- centerMapY
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ val screen = mapToScreen(c.x, c.y)
|
|
|
|
|
+ onLongPressListener?.invoke(p, screen.x, screen.y - switchSize / 2 - 4, c.x, c.y)
|
|
|
}, longPressCenterDurationMs)
|
|
}, 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 {
|
|
init {
|
|
|
- initLayer()
|
|
|
|
|
|
|
+ paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL_AND_STROKE }
|
|
|
|
|
+ setRatio(1f)
|
|
|
|
|
+ startAnimation()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private fun initLayer() {
|
|
|
|
|
- paint = Paint()
|
|
|
|
|
- paint.isAntiAlias = true
|
|
|
|
|
- paint.style = Paint.Style.FILL_AND_STROKE
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // ================= 外部接口 =================
|
|
|
|
|
+
|
|
|
|
|
+ /** 点位列表:深拷贝 pos;可选保留当前选中(按 entityId→name) */
|
|
|
|
|
+ // ==== 精准 Diff 的 submitPoints ====
|
|
|
|
|
+ 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 给点冗余
|
|
|
|
|
+
|
|
|
|
|
+ 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
|
|
|
|
|
+ else -> false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ nextList.add(add)
|
|
|
|
|
+ changedCenters.add(add.pos)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 更新:字段级 copy,尽量复用对象引用,避免动画/缓存抖动
|
|
|
|
|
+ var dirty = false
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * Presenter 计算好所有点后调用此方法;线程安全,可任何线程调用。
|
|
|
|
|
- */
|
|
|
|
|
- fun submitPoints(points: List<IsolationPoint>) {
|
|
|
|
|
- synchronized(stationList) {
|
|
|
|
|
|
|
+ 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
|
|
|
|
|
+ else -> false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (dirty) changedCenters.add(dst.pos)
|
|
|
|
|
+ nextList.add(dst) // 复用原对象引用
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 3) 删除:老数据里有、新数据里没有的
|
|
|
|
|
+ if (seenIds.size != oldById.size) {
|
|
|
|
|
+ // 直接按 nextList(新顺序)重建 stationList 更简单干净
|
|
|
|
|
+ // 被删除的对象引用不再放入 nextList 即可
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 4) 最小代价重建列表(保持新顺序,但复用对象)
|
|
|
stationList.clear()
|
|
stationList.clear()
|
|
|
- stationList.addAll(points)
|
|
|
|
|
|
|
+ stationList.addAll(nextList)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 5) 局部刷新(尽可能小范围)
|
|
|
|
|
+ if (changedCenters.isNotEmpty()) {
|
|
|
|
|
+ refreshRegionsOrThrottle(changedCenters, changedRadiusPx)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 没有变化也触发一次轻微节流刷新,避免偶发不同步
|
|
|
|
|
+ throttleInvalidate()
|
|
|
}
|
|
}
|
|
|
- mapView.postInvalidate()
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 若给定点位在屏幕可视区域内,则执行一次重绘;否则忽略。
|
|
|
|
|
- */
|
|
|
|
|
- fun refreshIfVisible(point: PointF, margin: Float = 0f) {
|
|
|
|
|
- if (inDraw) return
|
|
|
|
|
- val w = mapView.width
|
|
|
|
|
- val h = mapView.height
|
|
|
|
|
- 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]
|
|
|
|
|
- if (x + margin >= 0 && x - margin <= w && y + margin >= 0 && y - margin <= h) {
|
|
|
|
|
- mapView.postInvalidate()
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // 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
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 全局比例设置(叠乘版)
|
|
|
|
|
- * 如果不想叠乘(反复调用越来越大/小),改为基准写法:
|
|
|
|
|
- * switchSize = setValue(BASE_SWITCH_SIZE * ratio)
|
|
|
|
|
- * textSize = switchSize
|
|
|
|
|
|
|
+ * 优先做局部刷新;如果你的 MapView 没有暴露 invalidateRect,就回退到节流整层刷新。
|
|
|
|
|
+ * 这里假设 mapView/invalidateRect 可用;否则把反射去掉,直接 throttleInvalidate()
|
|
|
*/
|
|
*/
|
|
|
- fun setRatio(ratio: Float) {
|
|
|
|
|
- this.ratio = ratio
|
|
|
|
|
- switchSize = setValue(4 * ratio * switchSize)
|
|
|
|
|
- textSize = switchSize * 0.8f
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ 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
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- fun startAnimation() {
|
|
|
|
|
- mapView.removeCallbacks(refreshRunnable)
|
|
|
|
|
- mapView.postDelayed(refreshRunnable, FRAME_INTERVAL)
|
|
|
|
|
|
|
+ if (!didPartial) throttleInvalidate()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- fun stopAnimation() {
|
|
|
|
|
- mapView.removeCallbacks(refreshRunnable)
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- // —— 坐标互转工具 ——
|
|
|
|
|
- private fun screenToMap(x: Float, y: Float): PointF? {
|
|
|
|
|
- val inv = Matrix()
|
|
|
|
|
- val m = Matrix(mapView.currentMatrix)
|
|
|
|
|
- if (!m.invert(inv)) return null
|
|
|
|
|
- val pt = floatArrayOf(x, y)
|
|
|
|
|
- inv.mapPoints(pt)
|
|
|
|
|
- return PointF(pt[0], pt[1])
|
|
|
|
|
|
|
+ /** 比例(基准) */
|
|
|
|
|
+ fun setRatio(ratio: Float) {
|
|
|
|
|
+ this.ratio = ratio
|
|
|
|
|
+ switchSize = BASE_SWITCH_SIZE * ratio
|
|
|
|
|
+ textSize = BASE_TEXT_SIZE * ratio
|
|
|
|
|
+ throttleInvalidate()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- private fun mapToScreen(mx: Float, my: Float): PointF {
|
|
|
|
|
- val m = Matrix(mapView.currentMatrix)
|
|
|
|
|
- val pt = floatArrayOf(mx, my)
|
|
|
|
|
- m.mapPoints(pt)
|
|
|
|
|
- return PointF(pt[0], pt[1])
|
|
|
|
|
|
|
+ /** 按唯一 name 选中并定位(若 name 唯一) */
|
|
|
|
|
+ fun selectPoint(
|
|
|
|
|
+ entityId: Long?,
|
|
|
|
|
+ animated: Boolean = true,
|
|
|
|
|
+ durationMs: Long = 300L,
|
|
|
|
|
+ scale: Float = 2f
|
|
|
|
|
+ ): Boolean {
|
|
|
|
|
+ if (entityId == null) return false
|
|
|
|
|
+ val snapshot = synchronized(dataLock) { stationList.toList() }
|
|
|
|
|
+ val target = snapshot.firstOrNull { it.entityId == entityId } ?: return false
|
|
|
|
|
+ val token = ++selectToken
|
|
|
|
|
+
|
|
|
|
|
+ val eid = target.entityId
|
|
|
|
|
+ val name = target.entityName
|
|
|
|
|
+ synchronized(dataLock) {
|
|
|
|
|
+ stationList.forEach {
|
|
|
|
|
+ it.isSelected = it.entityId == entityId
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ selectedEntityIdForKeep = eid
|
|
|
|
|
+ selectedNameForKeep = name
|
|
|
|
|
+ throttleInvalidate()
|
|
|
|
|
+
|
|
|
|
|
+ val c = centerOf(target)
|
|
|
|
|
+ mapView?.let { mv ->
|
|
|
|
|
+ if (animated) mv.animateCenterOnPoint(c.x, c.y, durationMs, scale)
|
|
|
|
|
+ else mv.mapCenterWithPoint(c.x, c.y)
|
|
|
|
|
+ }
|
|
|
|
|
+ return true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 取“当前屏幕中心”所对应的图内坐标,用来实现只缩放不平移 */
|
|
|
|
|
- private fun currentMapCenter(): PointF? {
|
|
|
|
|
|
|
+ /** 局部刷新(点在屏幕上时) */
|
|
|
|
|
+ fun refreshIfVisible(point: PointF, margin: Float = 0f) {
|
|
|
|
|
+ if (inDraw) return
|
|
|
val w = mapView.width
|
|
val w = mapView.width
|
|
|
val h = mapView.height
|
|
val h = mapView.height
|
|
|
- if (w == 0 || h == 0) return null
|
|
|
|
|
- return screenToMap(w * 0.5f, h * 0.5f)
|
|
|
|
|
|
|
+ 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]
|
|
|
|
|
+ if (x + margin >= 0 && x - margin <= w && y + margin >= 0 && y - margin <= h) {
|
|
|
|
|
+ throttleInvalidate()
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
|
|
+ // ================= 触摸 =================
|
|
|
override fun onTouch(event: MotionEvent) {
|
|
override fun onTouch(event: MotionEvent) {
|
|
|
when (event.actionMasked) {
|
|
when (event.actionMasked) {
|
|
|
MotionEvent.ACTION_DOWN -> {
|
|
MotionEvent.ACTION_DOWN -> {
|
|
|
- downScreenX = event.x
|
|
|
|
|
- downScreenY = event.y
|
|
|
|
|
- lastScreenX = event.x
|
|
|
|
|
- lastScreenY = event.y
|
|
|
|
|
-
|
|
|
|
|
|
|
+ downScreenX = event.x; downScreenY = event.y
|
|
|
|
|
+ lastScreenX = event.x; lastScreenY = event.y
|
|
|
longPressTriggered = false
|
|
longPressTriggered = false
|
|
|
pendingLongPress = false
|
|
pendingLongPress = false
|
|
|
currentPressed = null
|
|
currentPressed = null
|
|
|
-
|
|
|
|
|
- val mapPt = screenToMap(event.x, event.y)
|
|
|
|
|
- if (mapPt != null) {
|
|
|
|
|
- val hit = hitTest(mapPt.x, mapPt.y)
|
|
|
|
|
- if (hit != null) {
|
|
|
|
|
|
|
+ screenToMap(event.x, event.y)?.let { mp ->
|
|
|
|
|
+ hitTest(mp.x, mp.y)?.let { hit ->
|
|
|
currentPressed = hit
|
|
currentPressed = hit
|
|
|
pendingLongPress = true
|
|
pendingLongPress = true
|
|
|
- mapView.parent?.requestDisallowInterceptTouchEvent(true) // ✅ 防父布局拦截
|
|
|
|
|
|
|
+ mapView.parent?.requestDisallowInterceptTouchEvent(true)
|
|
|
mapView.removeCallbacks(longPressRunnable)
|
|
mapView.removeCallbacks(longPressRunnable)
|
|
|
mapView.postDelayed(longPressRunnable, longPressTimeoutMs)
|
|
mapView.postDelayed(longPressRunnable, longPressTimeoutMs)
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
MotionEvent.ACTION_MOVE -> {
|
|
MotionEvent.ACTION_MOVE -> {
|
|
|
- lastScreenX = event.x
|
|
|
|
|
- lastScreenY = event.y
|
|
|
|
|
|
|
+ lastScreenX = event.x; lastScreenY = event.y
|
|
|
if (pendingLongPress && !longPressTriggered) {
|
|
if (pendingLongPress && !longPressTriggered) {
|
|
|
val dx = event.x - downScreenX
|
|
val dx = event.x - downScreenX
|
|
|
val dy = event.y - downScreenY
|
|
val dy = event.y - downScreenY
|
|
@@ -252,10 +392,7 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
MotionEvent.ACTION_UP -> {
|
|
MotionEvent.ACTION_UP -> {
|
|
|
- // 取消长按
|
|
|
|
|
mapView.removeCallbacks(longPressRunnable)
|
|
mapView.removeCallbacks(longPressRunnable)
|
|
|
-
|
|
|
|
|
- // 如果刚才已触发长按,吞掉点击
|
|
|
|
|
if (longPressTriggered) {
|
|
if (longPressTriggered) {
|
|
|
pendingLongPress = false
|
|
pendingLongPress = false
|
|
|
longPressTriggered = false
|
|
longPressTriggered = false
|
|
@@ -263,44 +400,39 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // —— 点击:先放大,再居中,再标记(保证最终居中)——
|
|
|
|
|
- val mapPt = screenToMap(event.x, event.y) ?: return
|
|
|
|
|
- val hit = hitTest(mapPt.x, mapPt.y) ?: return
|
|
|
|
|
|
|
+ val mp = screenToMap(event.x, event.y) ?: return
|
|
|
|
|
+ val hit = hitTest(mp.x, mp.y) ?: return
|
|
|
|
|
+ val token = ++selectToken
|
|
|
|
|
|
|
|
val targetScale = desiredSelectScale
|
|
val targetScale = desiredSelectScale
|
|
|
- val targetMapX = hit.pos.x + switchSize / 2f
|
|
|
|
|
- val targetMapY = hit.pos.y + switchSize / 2f
|
|
|
|
|
-
|
|
|
|
|
- // ① 只缩放:以当前屏幕中心为 pivot,把 scale 动到 targetScale
|
|
|
|
|
- val curCenter = currentMapCenter()
|
|
|
|
|
- if (curCenter != null) {
|
|
|
|
|
- mapView?.animateCenterOnPoint(
|
|
|
|
|
- curCenter.x,
|
|
|
|
|
- curCenter.y,
|
|
|
|
|
- clickZoomDurationMs,
|
|
|
|
|
- targetScale
|
|
|
|
|
- )
|
|
|
|
|
- } else {
|
|
|
|
|
- // 万一拿不到中心,就直接在下一步里统一做
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ val targetCenter = centerOf(hit)
|
|
|
|
|
+ val eid = hit.entityId
|
|
|
|
|
+ val name = hit.entityName
|
|
|
|
|
|
|
|
- // ② 再居中到目标点(保持 targetScale),确保最后居中
|
|
|
|
|
|
|
+ // ① 先拉缩放(以当前中心为 pivot)
|
|
|
|
|
+ currentMapCenter()?.let { cur ->
|
|
|
|
|
+ mapView?.animateCenterOnPoint(cur.x, cur.y, clickZoomDurationMs, targetScale)
|
|
|
|
|
+ }
|
|
|
|
|
+ // ② 再居中到目标(保持 targetScale)
|
|
|
mapView?.postDelayed({
|
|
mapView?.postDelayed({
|
|
|
- mapView?.animateCenterOnPoint(
|
|
|
|
|
- targetMapX,
|
|
|
|
|
- targetMapY,
|
|
|
|
|
- clickCenterDurationMs,
|
|
|
|
|
- targetScale
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ withLiveToken(token) {
|
|
|
|
|
+ mapView?.animateCenterOnPoint(
|
|
|
|
|
+ targetCenter.x,
|
|
|
|
|
+ targetCenter.y,
|
|
|
|
|
+ clickCenterDurationMs,
|
|
|
|
|
+ targetScale
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
}, clickZoomDurationMs)
|
|
}, clickZoomDurationMs)
|
|
|
-
|
|
|
|
|
- // ③ 动画完成后再 selectPoint(此时位置稳定)
|
|
|
|
|
|
|
+ // ③ 完成后只选中目标
|
|
|
mapView?.postDelayed({
|
|
mapView?.postDelayed({
|
|
|
- hit.pointSerialNumber?.let { serial ->
|
|
|
|
|
- // 这里 selectPoint 内部再调用 animate 的 scale/center 就会乱,所以用“保持现状”:
|
|
|
|
|
- // 直接标记 + 刷新,不再让 selectPoint 改缩放/中心(你可以加个参数控制)
|
|
|
|
|
- stationList.replaceAll { it.copy(isSelected = (it.pointSerialNumber == serial)) }
|
|
|
|
|
- mapView?.postInvalidate()
|
|
|
|
|
|
|
+ withLiveToken(token) {
|
|
|
|
|
+ synchronized(dataLock) {
|
|
|
|
|
+ stationList.replaceAll { it.copy(isSelected = (it.entityId == eid) || (it.entityName == name)) }
|
|
|
|
|
+ }
|
|
|
|
|
+ selectedEntityIdForKeep = eid
|
|
|
|
|
+ selectedNameForKeep = name
|
|
|
|
|
+ throttleInvalidate()
|
|
|
}
|
|
}
|
|
|
}, clickZoomDurationMs + clickCenterDurationMs)
|
|
}, clickZoomDurationMs + clickCenterDurationMs)
|
|
|
}
|
|
}
|
|
@@ -314,176 +446,94 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
- private fun hitTest(mapX: Float, mapY: Float): IsolationPoint? {
|
|
|
|
|
- val hitR = switchSize * 1.2f
|
|
|
|
|
- val hitR2 = hitR * hitR
|
|
|
|
|
- val snapshot = synchronized(stationList) { stationList.toList() }
|
|
|
|
|
- var hit: IsolationPoint? = null
|
|
|
|
|
- var bestD2 = Float.MAX_VALUE
|
|
|
|
|
- for (p in snapshot) {
|
|
|
|
|
- val dx = mapX - p.pos.x
|
|
|
|
|
- val dy = mapY - p.pos.y
|
|
|
|
|
- val d2 = dx * dx + dy * dy
|
|
|
|
|
- if (d2 <= hitR2 && d2 < bestD2) {
|
|
|
|
|
- bestD2 = d2
|
|
|
|
|
- hit = p
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return hit
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
|
|
+ // ================= 绘制 =================
|
|
|
override fun draw(
|
|
override fun draw(
|
|
|
- canvas: Canvas, currentMatrix: Matrix, currentZoom: Float, currentRotateDegrees: Float
|
|
|
|
|
|
|
+ canvas: Canvas,
|
|
|
|
|
+ currentMatrix: Matrix,
|
|
|
|
|
+ currentZoom: Float,
|
|
|
|
|
+ currentRotateDegrees: Float
|
|
|
) {
|
|
) {
|
|
|
if (!isVisible) return
|
|
if (!isVisible) return
|
|
|
if (inDraw) return
|
|
if (inDraw) return
|
|
|
inDraw = true
|
|
inDraw = true
|
|
|
|
|
|
|
|
this.currentZoom = currentZoom
|
|
this.currentZoom = currentZoom
|
|
|
- lastKnownZoom = currentZoom
|
|
|
|
|
- currentDegree = 360 - currentRotateDegrees
|
|
|
|
|
|
|
+ this.currentDegree = 360 - currentRotateDegrees
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
canvas.save()
|
|
canvas.save()
|
|
|
try {
|
|
try {
|
|
|
- // 合并地图矩阵
|
|
|
|
|
canvas.concat(currentMatrix)
|
|
canvas.concat(currentMatrix)
|
|
|
|
|
|
|
|
- val switchData = ModBusController.getSwitchData()
|
|
|
|
|
- val tempPointList = synchronized(stationList) { stationList.toList() }
|
|
|
|
|
|
|
+ val points = synchronized(dataLock) { stationList.toList() }
|
|
|
|
|
+ val switches = runCatching { ModBusController.getSwitchData() }.getOrNull()
|
|
|
|
|
+ for (p in points) {
|
|
|
|
|
+ val c = centerOf(p)
|
|
|
|
|
|
|
|
- // 如果存在 ALARM,则确保动画驱动起来
|
|
|
|
|
- if (hasAnyAlarm(tempPointList, switchData)) {
|
|
|
|
|
- startAnimation()
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- tempPointList.forEach { point ->
|
|
|
|
|
- val bean = switchData.find { it.idx == point.pointSerialNumber?.toInt() }
|
|
|
|
|
- val status = parseStatus(bean)
|
|
|
|
|
-
|
|
|
|
|
- val x = point.pos.x
|
|
|
|
|
- val y = point.pos.y
|
|
|
|
|
-
|
|
|
|
|
- // ---- 选中态:蓝色外环(想要回红框,把这段改回去即可)----
|
|
|
|
|
- if (point.isSelected) {
|
|
|
|
|
|
|
+ // 选中外环
|
|
|
|
|
+ if (p.isSelected) {
|
|
|
paint.style = Paint.Style.STROKE
|
|
paint.style = Paint.Style.STROKE
|
|
|
paint.strokeWidth = 4f
|
|
paint.strokeWidth = 4f
|
|
|
- paint.color = Color.argb(180, 66, 133, 244) // #4285F4 带透明
|
|
|
|
|
- canvas.drawCircle(
|
|
|
|
|
- x + switchSize / 2,
|
|
|
|
|
- y + switchSize / 2,
|
|
|
|
|
- switchSize * 1.5f,
|
|
|
|
|
- paint
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ paint.color = colSelectRing
|
|
|
|
|
+ canvas.drawCircle(c.x, c.y, switchSize * 1.5f, paint)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ---- 本体圆:按状态绘制 ----
|
|
|
|
|
|
|
+ // 主体:直接用 p.status(由 setSwitchData 决定)
|
|
|
paint.style = Paint.Style.FILL_AND_STROKE
|
|
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) {
|
|
when (status) {
|
|
|
- SwitchStatus.ON -> {
|
|
|
|
|
- stationList.firstOrNull { it.entityId == point.entityId }?.status = 1
|
|
|
|
|
- paint.color = ContextCompat.getColor(
|
|
|
|
|
- MyApplication.instance!!.applicationContext,
|
|
|
|
|
- R.color.common_switch_enable
|
|
|
|
|
- )
|
|
|
|
|
- paint.alpha = 255
|
|
|
|
|
- canvas.drawCircle(
|
|
|
|
|
- x + switchSize / 2,
|
|
|
|
|
- y + switchSize / 2,
|
|
|
|
|
- switchSize,
|
|
|
|
|
- paint
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ STATUS_ON -> {
|
|
|
|
|
+ paint.color = colOn; paint.alpha = 255
|
|
|
|
|
+ canvas.drawCircle(c.x, c.y, switchSize, paint)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- SwitchStatus.OFF -> {
|
|
|
|
|
- stationList.firstOrNull { it.entityId == point.entityId }?.status = 0
|
|
|
|
|
- paint.color = ContextCompat.getColor(
|
|
|
|
|
- MyApplication.instance!!.applicationContext,
|
|
|
|
|
- R.color.common_switch_disable
|
|
|
|
|
- )
|
|
|
|
|
- paint.alpha = 255
|
|
|
|
|
- canvas.drawCircle(
|
|
|
|
|
- x + switchSize / 2,
|
|
|
|
|
- y + switchSize / 2,
|
|
|
|
|
- switchSize,
|
|
|
|
|
- paint
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ STATUS_OFF -> {
|
|
|
|
|
+ paint.color = colOff; paint.alpha = 255
|
|
|
|
|
+ canvas.drawCircle(c.x, c.y, switchSize, paint)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- SwitchStatus.ALARM -> {
|
|
|
|
|
- stationList.firstOrNull { it.entityId == point.entityId }?.status = 2
|
|
|
|
|
- // 脉冲:颜色红/橙跳变 + alpha 呼吸 + 半径轻微伸缩 + 外光晕
|
|
|
|
|
|
|
+ STATUS_ALARM -> {
|
|
|
val t = pulsePhase
|
|
val t = pulsePhase
|
|
|
- val useRed = t < 0.5f
|
|
|
|
|
- val color = if (useRed) {
|
|
|
|
|
- // 如果你没有这些资源色,可以换成 Color.RED / 自定义颜色
|
|
|
|
|
- runCatching {
|
|
|
|
|
- ContextCompat.getColor(
|
|
|
|
|
- MyApplication.instance!!.applicationContext,
|
|
|
|
|
- R.color.red_500
|
|
|
|
|
- )
|
|
|
|
|
- }.getOrDefault(Color.parseColor("#EF4444"))
|
|
|
|
|
- } else {
|
|
|
|
|
- runCatching {
|
|
|
|
|
- ContextCompat.getColor(
|
|
|
|
|
- MyApplication.instance!!.applicationContext,
|
|
|
|
|
- R.color.orange_500
|
|
|
|
|
- )
|
|
|
|
|
- }.getOrDefault(Color.parseColor("#F97316"))
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- val alphaPulse =
|
|
|
|
|
- (160 + 95 * abs(sin(2f * Math.PI.toFloat() * t))).toInt()
|
|
|
|
|
- paint.color = color
|
|
|
|
|
- paint.alpha = alphaPulse
|
|
|
|
|
-
|
|
|
|
|
|
|
+ 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))
|
|
val r = switchSize * (1.0f + 0.10f * sin(2f * Math.PI.toFloat() * t))
|
|
|
- canvas.drawCircle(
|
|
|
|
|
- x + switchSize / 2,
|
|
|
|
|
- y + switchSize / 2,
|
|
|
|
|
- r,
|
|
|
|
|
- paint
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- paint.alpha = (alphaPulse * 0.35f).toInt()
|
|
|
|
|
- canvas.drawCircle(
|
|
|
|
|
- x + switchSize / 2,
|
|
|
|
|
- y + switchSize / 2,
|
|
|
|
|
- r * 1.25f,
|
|
|
|
|
- paint
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
|
|
+ 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
|
|
paint.alpha = 255
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- SwitchStatus.UNKNOWN -> {
|
|
|
|
|
- // 未知:深灰
|
|
|
|
|
- paint.color = Color.DKGRAY
|
|
|
|
|
- paint.alpha = 200
|
|
|
|
|
- canvas.drawCircle(
|
|
|
|
|
- x + switchSize / 2,
|
|
|
|
|
- y + switchSize / 2,
|
|
|
|
|
- switchSize,
|
|
|
|
|
- paint
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ else -> { // STATUS_UNKNOWN
|
|
|
|
|
+ paint.color = Color.DKGRAY; paint.alpha = 200
|
|
|
|
|
+ canvas.drawCircle(c.x, c.y, switchSize, paint)
|
|
|
paint.alpha = 255
|
|
paint.alpha = 255
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ---- 文本(和你原来一致)----
|
|
|
|
|
- paint.style = Paint.Style.FILL
|
|
|
|
|
- paint.strokeWidth = 1f
|
|
|
|
|
- paint.color = Color.WHITE
|
|
|
|
|
- paint.textSize = textSize
|
|
|
|
|
- val text = point.pointSerialNumber ?: ""
|
|
|
|
|
- val textW = paint.measureText(text)
|
|
|
|
|
- val fm = paint.fontMetrics
|
|
|
|
|
- val textH = fm.bottom - fm.top
|
|
|
|
|
- val textX = x + (switchSize - textW) / 2
|
|
|
|
|
- val textY = y + switchSize / 2 + (textH / 2 - fm.bottom)
|
|
|
|
|
- canvas.drawText(text, textX, textY, paint)
|
|
|
|
|
|
|
+ // 文本
|
|
|
|
|
+ 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
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
} finally {
|
|
} finally {
|
|
|
canvas.restore()
|
|
canvas.restore()
|
|
@@ -493,89 +543,81 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 根据序号查找点位的「图内坐标」。
|
|
|
|
|
- */
|
|
|
|
|
- fun getPointPosition(serial: String): PointF? {
|
|
|
|
|
- return stationList.find { it.pointSerialNumber == serial }?.pos
|
|
|
|
|
|
|
+ // ================= 工具 =================
|
|
|
|
|
+ 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)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 选中一个点位:标记 isSelected 并定位到视图中心。
|
|
|
|
|
- */
|
|
|
|
|
- fun selectPoint(
|
|
|
|
|
- serial: String,
|
|
|
|
|
- animated: Boolean = true,
|
|
|
|
|
- durationMs: Long = 300L,
|
|
|
|
|
- scale: Float = 2f
|
|
|
|
|
- ): Boolean {
|
|
|
|
|
- val point = stationList.find { it.pointSerialNumber == serial } ?: return false
|
|
|
|
|
- point.isSelected = true
|
|
|
|
|
- ThreadUtils.runOnMainDelayed(3000) {
|
|
|
|
|
- point.isSelected = false
|
|
|
|
|
- stationList.replaceAll { it.copy(isSelected = false) }
|
|
|
|
|
- mapView?.postInvalidate()
|
|
|
|
|
- }
|
|
|
|
|
- stationList.replaceAll { it.copy(isSelected = (it.pointSerialNumber == serial)) }
|
|
|
|
|
- mapView?.postInvalidate()
|
|
|
|
|
|
|
+ private fun screenToMap(x: Float, y: Float): PointF? {
|
|
|
|
|
+ val inv = Matrix()
|
|
|
|
|
+ val m = Matrix(mapView.currentMatrix)
|
|
|
|
|
+ if (!m.invert(inv)) return null
|
|
|
|
|
+ val pt = floatArrayOf(x, y)
|
|
|
|
|
+ inv.mapPoints(pt)
|
|
|
|
|
+ return PointF(pt[0], pt[1])
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- mapView?.let { mv ->
|
|
|
|
|
- if (animated) {
|
|
|
|
|
- mv.animateCenterOnPoint(point.pos.x, point.pos.y, durationMs, scale)
|
|
|
|
|
- } else {
|
|
|
|
|
- mv.mapCenterWithPoint(point.pos.x, point.pos.y)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- return true
|
|
|
|
|
|
|
+ private fun mapToScreen(mx: Float, my: Float): PointF {
|
|
|
|
|
+ val m = Matrix(mapView.currentMatrix)
|
|
|
|
|
+ val pt = floatArrayOf(mx, my)
|
|
|
|
|
+ m.mapPoints(pt)
|
|
|
|
|
+ return PointF(pt[0], pt[1])
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ====== 状态解析 & 动画触发判定 ======
|
|
|
|
|
|
|
+ private fun currentMapCenter(): PointF? {
|
|
|
|
|
+ val w = mapView.width
|
|
|
|
|
+ val h = mapView.height
|
|
|
|
|
+ if (w == 0 || h == 0) return null
|
|
|
|
|
+ return screenToMap(w * 0.5f, h * 0.5f)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- /** 预留:设备状态 */
|
|
|
|
|
- private enum class SwitchStatus { ON, OFF, ALARM, UNKNOWN }
|
|
|
|
|
|
|
+ private fun hitTest(mapX: Float, mapY: Float): IsolationPoint? {
|
|
|
|
|
+ val hitR = switchSize * 1.2f
|
|
|
|
|
+ val hitR2 = hitR * hitR
|
|
|
|
|
+ val snapshot = synchronized(dataLock) { stationList.toList() }
|
|
|
|
|
+ var hit: IsolationPoint? = null
|
|
|
|
|
+ var bestD2 = Float.MAX_VALUE
|
|
|
|
|
+ for (p in snapshot) {
|
|
|
|
|
+ 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
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return hit
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 统一解析设备状态:未来 SwitchBean 加字段时,只改这里。
|
|
|
|
|
- * 现在:优先看 isAlarm(bean),否则用 enabled 判定;bean==null 记为 UNKNOWN。
|
|
|
|
|
- */
|
|
|
|
|
- private fun parseStatus(bean: DockBean.SwitchBean?): SwitchStatus {
|
|
|
|
|
- if (bean == null) return SwitchStatus.UNKNOWN
|
|
|
|
|
- if (isAlarm(bean)) return SwitchStatus.ALARM
|
|
|
|
|
- return if (bean.enabled) SwitchStatus.ON else SwitchStatus.OFF
|
|
|
|
|
|
|
+ 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
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /**
|
|
|
|
|
- * 是否告警(预留钩子):将来你在 SwitchBean 里加 type/statusCode 等,改这里即可。
|
|
|
|
|
- * 当前默认:不告警。
|
|
|
|
|
- */
|
|
|
|
|
|
|
+ /** 告警判断:按你业务改 */
|
|
|
private fun isAlarm(bean: DockBean.SwitchBean?): Boolean {
|
|
private fun isAlarm(bean: DockBean.SwitchBean?): Boolean {
|
|
|
- // TODO: 未来形如:
|
|
|
|
|
- // return bean?.type == 2 || bean?.statusCode == ALARM
|
|
|
|
|
|
|
+ // TODO: return bean?.type == 2 || bean?.statusCode == ALARM
|
|
|
return false
|
|
return false
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- /** 是否存在任意 ALARM 点位,用于决定是否跑动画帧 */
|
|
|
|
|
- private fun hasAnyAlarm(
|
|
|
|
|
- points: List<IsolationPoint>,
|
|
|
|
|
- switches: List<DockBean.SwitchBean>?
|
|
|
|
|
- ): Boolean {
|
|
|
|
|
- if (switches == null) return false
|
|
|
|
|
- for (p in points) {
|
|
|
|
|
- val b = switches.find { it.idx == p.pointSerialNumber?.toInt() }
|
|
|
|
|
- if (isAlarm(b)) return true
|
|
|
|
|
- }
|
|
|
|
|
- return false
|
|
|
|
|
|
|
+ private fun throttleInvalidate() {
|
|
|
|
|
+ if (nextInvalidateScheduled) return
|
|
|
|
|
+ nextInvalidateScheduled = true
|
|
|
|
|
+ mapView?.postDelayed(invalidateRunnable, FRAME_INTERVAL_MS)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // ====== 数据结构(保持你的原样)======
|
|
|
|
|
|
|
+ private fun startAnimation() {
|
|
|
|
|
+ mapView.removeCallbacks(refreshRunnable)
|
|
|
|
|
+ mapView.postDelayed(refreshRunnable, FRAME_INTERVAL_MS)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- data class IsolationPoint(
|
|
|
|
|
- val pos: PointF,
|
|
|
|
|
- val entityName: String,
|
|
|
|
|
- var icon: Bitmap?,
|
|
|
|
|
- val entityId: Long,
|
|
|
|
|
- val pointSerialNumber: String?,
|
|
|
|
|
- var isSelected: Boolean,
|
|
|
|
|
- var status: Int = 0
|
|
|
|
|
- )
|
|
|
|
|
|
|
+ private fun stopAnimation() {
|
|
|
|
|
+ mapView.removeCallbacks(refreshRunnable)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun deepCopyPoint(p: PointF) = PointF(p.x, p.y)
|
|
|
|
|
+ private fun deepCopyItem(it: IsolationPoint) = it.copy(pos = deepCopyPoint(it.pos))
|
|
|
}
|
|
}
|