|
|
@@ -16,13 +16,19 @@ import com.grkj.iscs_mars.modbus.ModBusController
|
|
|
import com.grkj.iscs_mars.util.BitmapUtil
|
|
|
import com.onlylemi.mapview.library.MapView
|
|
|
import com.onlylemi.mapview.library.layer.MapBaseLayer
|
|
|
+import com.sik.sikcore.thread.ThreadUtils
|
|
|
import kotlin.math.cos
|
|
|
import kotlin.math.sin
|
|
|
|
|
|
+/**
|
|
|
+ * 自定义开关层级
|
|
|
+ */
|
|
|
class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
- mapView: MapView?, private var pointList: List<IsolationPoint> = mutableListOf()
|
|
|
+ mapView: MapView?, private var stationList: MutableList<IsolationPoint> = mutableListOf()
|
|
|
) : MapBaseLayer(mapView) {
|
|
|
- var inDraw = false
|
|
|
+ @Volatile
|
|
|
+ var inDraw: Boolean = false
|
|
|
+ private set
|
|
|
|
|
|
// 呼吸灯周期(毫秒)
|
|
|
private val breathePeriod = 1200f
|
|
|
@@ -40,46 +46,64 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
startAnimation()
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
- private var listener: MarkIsClickListener? = null
|
|
|
- private var radiusMark = 0f
|
|
|
- private var isClickMark: Boolean = false
|
|
|
- private var num: Int = -1
|
|
|
private lateinit var paint: Paint
|
|
|
- private var btnIndex: Int = -1
|
|
|
private var currentZoom = 0f
|
|
|
private var currentDegree = 0f
|
|
|
- private var bgBitmap: Bitmap? = null
|
|
|
- private var coverBitmap: Bitmap? = null
|
|
|
private var ratio: Float = 1f
|
|
|
private var switchSize: Float = 1f
|
|
|
+ private var textSize: Float = 1f
|
|
|
|
|
|
init {
|
|
|
initLayer()
|
|
|
}
|
|
|
|
|
|
private fun initLayer() {
|
|
|
- radiusMark = setValue(6.0f)
|
|
|
paint = Paint()
|
|
|
paint.isAntiAlias = true
|
|
|
paint.style = Paint.Style.FILL_AND_STROKE
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Presenter 计算好所有点后调用此方法;线程安全,可任何线程调用。
|
|
|
+ */
|
|
|
+ fun submitPoints(points: List<IsolationPoint>) {
|
|
|
+ synchronized(stationList) {
|
|
|
+ stationList.clear()
|
|
|
+ stationList.addAll(points)
|
|
|
+ }
|
|
|
+ mapView.postInvalidate() // 触发重绘
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 若给定点位在屏幕可视区域内,则执行一次重绘;否则忽略。
|
|
|
+ * 可在异步加载小图标完成后调用,避免整图层频繁刷新。
|
|
|
+ */
|
|
|
+ fun refreshIfVisible(point: PointF, margin: Float = 0f) {
|
|
|
+ // 如果当前仍在 draw() 里,直接返回,等那一帧完成即可
|
|
|
+ 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;如项目中名称不同请自行调整
|
|
|
+ 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()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
fun setRatio(ratio: Float) {
|
|
|
this.ratio = ratio
|
|
|
- bgBitmap = BitmapUtil.getResizedBitmapFromDrawable(
|
|
|
- mapView.context,
|
|
|
- R.drawable.red_stroke_bg,
|
|
|
- (50 * ratio).toInt(),
|
|
|
- (78 * ratio).toInt()
|
|
|
- )!!
|
|
|
- coverBitmap = BitmapUtil.getResizedBitmapFromDrawable(
|
|
|
- mapView.context,
|
|
|
- R.drawable.map_item_cover_bg,
|
|
|
- (50 * ratio).toInt(),
|
|
|
- (78 * ratio).toInt()
|
|
|
- )!!
|
|
|
switchSize = setValue(4 * ratio)
|
|
|
+ textSize = switchSize
|
|
|
}
|
|
|
|
|
|
fun startAnimation() {
|
|
|
@@ -114,7 +138,7 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
// 把 mapView 本身的缩放/平移/旋转一次性 concat 到 Canvas
|
|
|
canvas.concat(currentMatrix)
|
|
|
val switchData = ModBusController.getSwitchData()
|
|
|
- val tempPointList = pointList.toList()
|
|
|
+ val tempPointList = stationList.toList()
|
|
|
tempPointList.forEach { point ->
|
|
|
val switchStatus = switchData
|
|
|
.find { it.idx == point.pointSerialNumber?.toInt() }?.enabled
|
|
|
@@ -122,108 +146,125 @@ class CustomSwitchStationLayer @JvmOverloads constructor(
|
|
|
val x = point.pos.x
|
|
|
val y = point.pos.y
|
|
|
// 先画背景(它会被 currentMatrix 自动缩放)
|
|
|
- bgBitmap?.let {
|
|
|
- canvas.drawBitmap(
|
|
|
- it, x, y, paint
|
|
|
- )
|
|
|
- paint.alpha = 255
|
|
|
- if (switchStatus != null) {
|
|
|
- // 再画 icon
|
|
|
- if (switchStatus) {
|
|
|
- paint.color = ContextCompat.getColor(
|
|
|
- MyApplication.instance?.applicationContext!!,
|
|
|
- R.color.common_switch_enable
|
|
|
- )
|
|
|
- paint.alpha = alpha
|
|
|
- canvas.drawCircle(
|
|
|
- x + (it.width - switchSize) / 2 + switchSize / 2,
|
|
|
- y + (it.width - switchSize) / 2 + switchSize / 2,
|
|
|
- switchSize, paint
|
|
|
- )
|
|
|
- paint.alpha = 255
|
|
|
- } else {
|
|
|
- paint.color = ContextCompat.getColor(
|
|
|
- MyApplication.instance?.applicationContext!!,
|
|
|
- R.color.common_switch_disable
|
|
|
- )
|
|
|
- canvas.drawCircle(
|
|
|
- x + (it.width - switchSize) / 2 + switchSize / 2,
|
|
|
- y + (it.width - switchSize) / 2 + switchSize / 2,
|
|
|
- switchSize, paint
|
|
|
- )
|
|
|
- }
|
|
|
-// point.icon?.let { icon ->
|
|
|
-// canvas.drawBitmap(
|
|
|
-// icon,
|
|
|
-// x + (it.width - icon.width) / 2,
|
|
|
-// y + (it.width - icon.width) / 2,
|
|
|
-// paint
|
|
|
-// )
|
|
|
-// }
|
|
|
- }
|
|
|
- // 然后画文字
|
|
|
- paint.style = Paint.Style.FILL
|
|
|
- paint.strokeWidth = 1f
|
|
|
- paint.color = Color.RED
|
|
|
- paint.textSize = radiusMark * ratio // 这里是「图内」的文字大小,后面会跟着缩放
|
|
|
- val textW = paint.measureText(point.entityName)
|
|
|
- canvas.drawText(
|
|
|
- point.entityName,
|
|
|
- x + (it.width - textW) / 2,
|
|
|
- y + (it.height - radiusMark / 2 - it.height / 10),
|
|
|
- paint
|
|
|
- )
|
|
|
-
|
|
|
- // 如果选中,再叠加一个标记
|
|
|
+ paint.alpha = 255
|
|
|
+ if (switchStatus != null) {
|
|
|
if (point.isSelected) {
|
|
|
- coverBitmap?.let {
|
|
|
- canvas.drawBitmap(
|
|
|
- it, x, y, paint
|
|
|
- )
|
|
|
- }
|
|
|
- val checkW = paint.measureText("√")
|
|
|
- paint.color = Color.WHITE
|
|
|
- canvas.drawText(
|
|
|
- "√",
|
|
|
- x + (it.width - checkW) / 2,
|
|
|
- y + (it.height / 2 + radiusMark / 2),
|
|
|
+ paint.color = Color.RED
|
|
|
+ paint.strokeWidth = 2f
|
|
|
+ paint.style = Paint.Style.STROKE
|
|
|
+ canvas.drawRect(
|
|
|
+ x - switchSize / 2 - paint.strokeWidth,
|
|
|
+ y - switchSize / 2 - paint.strokeWidth,
|
|
|
+ x + switchSize * 1.5f + paint.strokeWidth,
|
|
|
+ y + switchSize * 1.5f + paint.strokeWidth,
|
|
|
paint
|
|
|
)
|
|
|
}
|
|
|
+ paint.style = Paint.Style.FILL_AND_STROKE
|
|
|
+ // 再画 icon
|
|
|
+ if (switchStatus) {
|
|
|
+ paint.color = ContextCompat.getColor(
|
|
|
+ MyApplication.instance?.applicationContext!!,
|
|
|
+ R.color.common_switch_enable
|
|
|
+ )
|
|
|
+ paint.alpha = alpha
|
|
|
+ canvas.drawCircle(
|
|
|
+ x + switchSize / 2,
|
|
|
+ y + switchSize / 2,
|
|
|
+ switchSize, paint
|
|
|
+ )
|
|
|
+ paint.alpha = 255
|
|
|
+ } else {
|
|
|
+ paint.color = ContextCompat.getColor(
|
|
|
+ MyApplication.instance?.applicationContext!!,
|
|
|
+ R.color.common_switch_disable
|
|
|
+ )
|
|
|
+ canvas.drawCircle(
|
|
|
+ x + switchSize / 2,
|
|
|
+ y + switchSize / 2,
|
|
|
+ switchSize, paint
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
+ // 然后画文字
|
|
|
+ paint.style = Paint.Style.FILL
|
|
|
+ paint.strokeWidth = 1f
|
|
|
+ paint.color = Color.WHITE
|
|
|
+ paint.textSize = textSize // 这里是「图内」的文字大小,后面会跟着缩放
|
|
|
+ val textW = paint.measureText(point.pointSerialNumber ?: "")
|
|
|
+ // 计算文本宽度和偏移
|
|
|
+ val fontMetrics = paint.fontMetrics
|
|
|
+ val textHeight = fontMetrics.bottom - fontMetrics.top
|
|
|
+
|
|
|
+ // 计算绘制文本的起点(左下角)
|
|
|
+ val textX = x + (switchSize - textW) / 2
|
|
|
+ val textY = y + switchSize / 2 + (textHeight / 2 - fontMetrics.bottom)
|
|
|
+ canvas.drawText(
|
|
|
+ "${point.pointSerialNumber}",
|
|
|
+ textX,
|
|
|
+ textY,
|
|
|
+ paint
|
|
|
+ )
|
|
|
}
|
|
|
|
|
|
canvas.restore()
|
|
|
inDraw = false
|
|
|
}
|
|
|
|
|
|
- private fun rotatePoint(
|
|
|
- oriX: Float, oriY: Float, desX: Float, desY: Float, rotateDegrees: Float
|
|
|
- ): Pair<Float, Float> {
|
|
|
- // 将度数转换为弧度
|
|
|
- val theta = Math.toRadians(rotateDegrees.toDouble())
|
|
|
-
|
|
|
- // 计算旋转后的坐标
|
|
|
- val newX = (oriX - desX) * cos(theta) - (oriY - desY) * sin(theta) + desX
|
|
|
- val newY = (oriX - desX) * sin(theta) + ((oriY - desY) * cos(theta)) + desY
|
|
|
-
|
|
|
- return Pair(newX.toFloat(), newY.toFloat())
|
|
|
+ /**
|
|
|
+ * 根据序号查找点位的「图内坐标」。
|
|
|
+ * @return 对应的 PointF(x,y),找不到返回 null。
|
|
|
+ */
|
|
|
+ fun getPointPosition(serial: String): PointF? {
|
|
|
+ return stationList
|
|
|
+ .find { it.pointSerialNumber == serial }
|
|
|
+ ?.pos
|
|
|
}
|
|
|
|
|
|
- fun setMarkIsClickListener(listener: MarkIsClickListener?) {
|
|
|
- this.listener = listener
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * 选中一个点位:标记 isSelected 并定位到视图中心。
|
|
|
+ * @param serial 要选中的序号
|
|
|
+ * @param animated 是否平滑动画,false 则瞬间定位
|
|
|
+ * @param durationMs 动画时长(仅 animated = true 时生效)
|
|
|
+ * @return 是否成功找到了并处理
|
|
|
+ */
|
|
|
+ fun selectPoint(
|
|
|
+ serial: String,
|
|
|
+ animated: Boolean = true,
|
|
|
+ durationMs: Long = 300L
|
|
|
+ ): 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()
|
|
|
+ }
|
|
|
+ // 1. 更新状态(如果你要改变外观,比如高亮,记得在 draw() 里用 isSelected)
|
|
|
+ stationList.replaceAll {
|
|
|
+ it.copy(isSelected = (it.pointSerialNumber == serial))
|
|
|
+ }
|
|
|
+ mapView?.postInvalidate()
|
|
|
|
|
|
- interface MarkIsClickListener {
|
|
|
- fun markIsClick(index: Int, btnIndex: Int)
|
|
|
+ // 2. 定位:调用 MapView 的 API
|
|
|
+ mapView?.let { mv ->
|
|
|
+ if (animated) {
|
|
|
+ mv.animateCenterOnPoint(point.pos.x, point.pos.y, durationMs)
|
|
|
+ } else {
|
|
|
+ mv.mapCenterWithPoint(point.pos.x, point.pos.y)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return true
|
|
|
}
|
|
|
|
|
|
data class IsolationPoint(
|
|
|
val pos: PointF,
|
|
|
val entityName: String,
|
|
|
- val icon: Bitmap?,
|
|
|
+ var icon: Bitmap?,
|
|
|
val entityId: Long,
|
|
|
val pointSerialNumber: String?,
|
|
|
- val isSelected: Boolean,
|
|
|
+ var isSelected: Boolean,
|
|
|
)
|
|
|
}
|