瀏覽代碼

refactor(更新) :
- 重构地图与点位加载逻辑,统一封装到 `StepPresenter.mapDataHandleForStations` 方法中
- 优化地图加载流程:采用先下载至文件再分步解码(低清预览、高清原图)的策略,以减少内存占用
- 统一处理后端坐标系到地图视图绘制坐标的转换
- `CustomStationLayer` 性能与功能优化
- 新增 `submitPoints` 方法,通过差分对比高效更新点位数据
- 引入局部刷新与绘制节流机制,提升渲染性能
- 优化图标绘制逻辑,支持居中裁剪铺满和拉伸填充两种模式
- `BitmapUtil` 工具类功能增强
- 新增 `loadBitmapSmall` 方法,用于异步加载指定尺寸的小图标
- `loadBitmapFromFile` 方法增加 EXIF 方向自动修正和背景色填充功能
- 更新获取地图信息的接口 (`LOTO_MAP`) 及相应的数据模型 (`LotoMapRespVO`)
- 调整取锁逻辑,增加根据 `hardwareId` 过滤已注册锁具的功能
- 将 ArcSoft 激活方式修改为离线模式
- 调整部分 Modbus 和业务日志的输出级别 (Info -> Debug)

周文健 1 月之前
父節點
當前提交
e9f5c021c8

+ 6 - 4
app/src/main/java/com/grkj/iscs_mars/BusinessManager.kt

@@ -139,7 +139,7 @@ object BusinessManager {
      */
     fun initMsgEventBus() {
         mEventBus.observeForever {
-            LogUtil.i("msgEvent : $it")
+            LogUtil.d("msgEvent : $it")
             when (it.code) {
                 // loading消息
                 MSG_EVENT_LOADING -> {
@@ -238,7 +238,7 @@ object BusinessManager {
                             )
                         }
                         NetApi.updateSwitchList(switchListReqVOS) {
-                            LogUtil.i("开关更新完成")
+                            LogUtil.d("开关更新完成")
                         }
                     }
                 }
@@ -260,7 +260,7 @@ object BusinessManager {
                     if (BleSendDispatcher.canConnect()) {
                         LogUtil.i("蓝牙连接-发送队列可以连接")
                         mac?.let {
-                            connectExistsKey(listOf(it))
+//                            connectExistsKey(listOf(it))
                         }
                     }
                 } else {
@@ -381,7 +381,7 @@ object BusinessManager {
      * 5、蓝牙数据通讯
      */
     private fun deviceStatusHandle(res: Any) {
-        LogUtil.i("硬件状态:${(res as List<ByteArray>).map { it.toHexStrings() }}")
+        LogUtil.d("硬件状态:${(res as List<ByteArray>).map { it.toHexStrings() }}")
         if (res.isEmpty() || res.any { it.isEmpty() }) {
             var tipStr = CommonUtils.getStr(R.string.no_response_board_exists) + " : "
             val addressList = mutableListOf<String>()
@@ -829,6 +829,8 @@ object BusinessManager {
                 val lockMap = withContext(Dispatchers.Default) {
                     ModBusController.getLocks(
                         needLockCount,
+                        locksPage?.records?.filter { it.hardwareId != null && it.hardwareId== SPUtils.getCabinetId() }
+                            ?.mapNotNull { it.lockNfc } ?: mutableListOf(),
                         slotsPage?.records?.filter {
                             it.slotType == slotTypeList.find { d -> d.dictLabel == "锁" }?.dictValue && it.status == slotStatusList.find { d -> d.dictLabel == "异常" }?.dictValue
                         }?.toMutableList() ?: mutableListOf(),

+ 6 - 6
app/src/main/java/com/grkj/iscs_mars/modbus/DockBean.kt

@@ -60,7 +60,7 @@ class DockBean(
                     val isLeftCharging = (byteArray[4].toInt() shr 1) and 0x1 == 1
                     val rightHasKey = (byteArray[3].toInt() shr 0) and 0x1 == 1
                     val isRightCharging = (byteArray[3].toInt() shr 1) and 0x1 == 1
-                    LogUtil.i("钥匙刷新状态 : $leftHasKey - $isLeftCharging - $rightHasKey - $isRightCharging")
+                    LogUtil.d("钥匙刷新状态 : $leftHasKey - $isLeftCharging - $rightHasKey - $isRightCharging")
                     if (getKeyList().isEmpty()) {
                         deviceList.add(
                             KeyBean(
@@ -144,7 +144,7 @@ class DockBean(
                         }
                     }
 
-                    LogUtil.i("锁具刷新状态 : $changeList")
+                    LogUtil.d("锁具刷新状态 : $changeList")
                     return DockBean(
                         addr,
                         dockConfig.find { it.address == addr }?.row?.toInt() ?: 0,
@@ -243,7 +243,7 @@ class DockBean(
 
                 DOCK_TYPE_COLLECT -> {
                     val working = (byteArray[4].toInt() shr 0) and 0x1 == 1
-                    LogUtil.i("开关量采集板是否工作 : $working")
+                    LogUtil.d("开关量采集板是否工作 : $working")
                     return DockBean(
                         addr,
                         dockConfig.find { it.address == addr }?.row?.toInt() ?: 0,
@@ -310,7 +310,7 @@ class DockBean(
                         getLockList()[i].lockEnabled = tempList[i]
                     }
 
-                    LogUtil.i("锁具刷新状态 : $changeList")
+                    LogUtil.d("锁具刷新状态 : $changeList")
                     return DockBean(
                         addr,
                         dockConfig.find { it.address == addr }?.row?.toInt() ?: 0,
@@ -338,7 +338,7 @@ class DockBean(
                         getLockList()[i].lockEnabled = tempList[i]
                     }
 
-                    LogUtil.i("电磁锁具刷新状态 : $changeList")
+                    LogUtil.d("电磁锁具刷新状态 : $changeList")
                     return DockBean(
                         addr,
                         dockConfig.find { it.address == addr }?.row?.toInt() ?: 0,
@@ -475,7 +475,7 @@ class DockBean(
                         getLockList()[getLockList().size - 2 + i].lockEnabled = tempList[i]
                     }
 
-                    LogUtil.i("锁具刷新状态 : $changeList")
+                    LogUtil.d("锁具刷新状态 : $changeList")
                     return DockBean(
                         addr,
                         dockConfig.find { it.address == addr }?.row?.toInt() ?: 0,

+ 13 - 13
app/src/main/java/com/grkj/iscs_mars/modbus/ModBusController.kt

@@ -95,7 +95,7 @@ object ModBusController {
             ?.repeatSendToAll(MBFrame.READ_STATUS, {
                 interruptReadStatus
             }, { res ->
-                LogUtil.i("****************************************************************************")
+                LogUtil.d("****************************************************************************")
                 // 过滤非空的数据,重置slaveCount
                 // 不再使用slaveCount,改用地址池
                 for (l in listeners) {
@@ -296,7 +296,7 @@ object ModBusController {
     fun updateAllBuckleStatus(done: () -> Unit) {
         val remaining = AtomicInteger(2)
         modBusManager?.sendToAll(MBFrame.READ_BUCKLE_STATUS) { res ->
-            LogUtil.i("****************************************************************************")
+            LogUtil.d("****************************************************************************")
             // 过滤非空的数据,重置slaveCount
             // 不再使用slaveCount,改用地址池
             lockBuckleStatus(res)
@@ -306,7 +306,7 @@ object ModBusController {
             }
         }
         modBusManager?.sendToAll(MBFrame.READ_LOCK_BUCKLE_EXTRA_STATUS) { res ->
-            LogUtil.i("****************************************************************************")
+            LogUtil.d("****************************************************************************")
             // 过滤非空的数据,重置slaveCount
             // 不再使用slaveCount,改用地址池
             lockBuckleExtraStatus(res)
@@ -323,7 +323,7 @@ object ModBusController {
     fun updateSwitchStatus(done: () -> Unit) {
         modBusManager?.mSlaveAddressList?.find { it == (0xA1).toByte() }?.let {
             modBusManager?.sendTo(it, MBFrame.READ_BUCKLE_STATUS) { res ->
-                LogUtil.i("****************************************************************************")
+                LogUtil.d("****************************************************************************")
                 // 过滤非空的数据,重置slaveCount
                 // 不再使用slaveCount,改用地址池
                 switchStatus(res, done)
@@ -335,7 +335,7 @@ object ModBusController {
      * 第9,10锁位卡扣状态
      */
     private fun lockBuckleExtraStatus(res: Any) {
-        LogUtil.i("硬件状态:${(res as List<ByteArray>).map { it.toHexStrings() }}")
+        LogUtil.d("硬件状态:${(res as List<ByteArray>).map { it.toHexStrings() }}")
         if (res.isEmpty() || res.any { it.isEmpty() }) {
             var tipStr = CommonUtils.getStr(R.string.no_response_board_exists) + " : "
             val addressList = mutableListOf<String>()
@@ -369,7 +369,7 @@ object ModBusController {
      * 开关量更新
      */
     fun switchStatus(res: Any, done: () -> Unit) {
-        LogUtil.i("开关板:${(res as ByteArray).toHexStrings()}")
+        LogUtil.d("开关板:${(res as ByteArray).toHexStrings()}")
         if (res.isEmpty()) {
             var tipStr = CommonUtils.getStr(R.string.no_response_board_exists) + " : "
             val addressList = mutableListOf<String>()
@@ -389,7 +389,7 @@ object ModBusController {
      * 第1-8锁位卡扣状态和钥匙
      */
     private fun lockBuckleStatus(res: Any) {
-        LogUtil.i("硬件状态:${(res as List<ByteArray>).map { it.toHexStrings() }}")
+        LogUtil.d("硬件状态:${(res as List<ByteArray>).map { it.toHexStrings() }}")
         if (res.isEmpty() || res.any { it.isEmpty() }) {
             var tipStr = CommonUtils.getStr(R.string.no_response_board_exists) + " : "
             val addressList = mutableListOf<String>()
@@ -1196,6 +1196,7 @@ object ModBusController {
      */
     fun getLocks(
         needLockCount: Int,
+        registerLockRfid: List<String>,
         exceptionSlots: MutableList<CabinetSlotsRecord>,
         exceptionLocks: MutableList<String>
     ): MutableMap<Byte, MutableList<DockBean.LockBean>> {
@@ -1211,12 +1212,11 @@ object ModBusController {
         LogUtil.i("异常锁仓位:${exceptionSlots.joinToString(",") { "${it.row},${it.col}" }}")
         for (lockDockIndex in lockDockList.indices) {
             if (provideCount >= needLockCount) break
-            val validLocks =
-                lockDockList[lockDockIndex].getLockList().filter { it.rfid !in exceptionLocks }
-                    .filter {
-                        it.isExist && it.idx !in exceptionSlots.filter { it.row?.toInt() == (lockDockIndex + 2) }
-                            .map { (it.col?.toInt() ?: 1) - 1 }
-                    }
+            val validLocks = lockDockList[lockDockIndex].getLockList()
+                .filter { it.rfid !in exceptionLocks && it.rfid in registerLockRfid }.filter {
+                    it.isExist && it.idx !in exceptionSlots.filter { it.row?.toInt() == (lockDockIndex + 2) }
+                        .map { (it.col?.toInt() ?: 1) - 1 }
+                }
             val toTake = (needLockCount - provideCount).coerceAtMost(validLocks.size)
             if (toTake > 0) {
                 map[lockDockList[lockDockIndex].addr] = validLocks.take(toTake).toMutableList()

+ 2 - 1
app/src/main/java/com/grkj/iscs_mars/model/UrlConsts.kt

@@ -175,7 +175,8 @@ object UrlConsts {
     /**
      * 获取电柜map解析数据
      */
-    const val LOTO_MAP = "/iscs/station/selectLotoMapById"
+//    const val LOTO_MAP = "/iscs/station/selectLotoMapById"
+    const val LOTO_MAP = "/iscs/station/selectIsLotoStationById"
 
     /**
      * 取消作业票

+ 2 - 0
app/src/main/java/com/grkj/iscs_mars/model/vo/lock/LockPageRespVO.kt

@@ -11,6 +11,8 @@ data class LockPageItem(
     val lockId: String?,
     val lockNfc: String?,
     val lockName: String?,
+    val hardwareId: String?,
+    val hardwareName: String?,
     val exStatus: String?,
     val exRemark: String?
 )

+ 13 - 35
app/src/main/java/com/grkj/iscs_mars/model/vo/ticket/LotoMapRespVO.kt

@@ -1,45 +1,23 @@
 package com.grkj.iscs_mars.model.vo.ticket
 
 data class LotoMapRespVO(
-    val row: Int?,
-
-    val col: Int?,
-
-    val pointId: Long?,
-
-    val pointName: String?,
-
-    val remark: String?,
-
-    val prePointId: Long?,
-
-    val pointType: String?,
-
-    val pointTypeName: String?,
-
-    val powerType: String?,
-
-    val powerTypeName: String?,
-
-    val state: Boolean?,
-
-    val pointIcon: String?,
-
-    val pointPicture: String?,
-
-    val mapImg: String?,
-
-    val x: String?,
-
-    val y: String?,
+    val id: Long?,                      // JSON里是字符串数字,解析时转 Long?
+    val mapId: Long?,
+    val mapName: String?,
+    val mapType: String?,
 
     val entityId: Long?,
-
     val entityName: String?,
 
-    val id: Long?,
+    val x: String?,                     // 后端给的是字符串坐标,保持 String?
+    val y: String?,
 
-    val mapId: Long?,
+    val delFlag: String?,
+    val pointIcon: String?,
+    val pointPicture: String?,
+    val pointNfc: String?,
+    val pointSerialNumber: String?,
 
-    val mapType: String?
+    val switchStatus: String?,
+    val switchLastUpdateTime: String?
 )

+ 8 - 8
app/src/main/java/com/grkj/iscs_mars/util/ArcSoftUtil.kt

@@ -62,10 +62,10 @@ object ArcSoftUtil {
     fun checkActiveStatus(context: Context) {
         val configJson = try {
             val arcSoftLicenseFile =
-                File("${FileUtil.getRootFolder(context)?.absolutePath}${FileUtil.CONFIG_DIR}${File.separator}${configFileName}")
+                File("${FileUtil.getRootFolder(context,5)?.absolutePath}${FileUtil.CONFIG_DIR}${File.separator}${configFileName}")
             val arcSoftActiveOfflineFile =
-                File("${FileUtil.getRootFolder(context)?.absolutePath}${FileUtil.CONFIG_DIR}${File.separator}${offlineActiveFileName}")
-            File("${FileUtil.getRootFolder(context)?.absolutePath}${FileUtil.CONFIG_DIR}").apply {
+                File("${FileUtil.getRootFolder(context,5)?.absolutePath}${FileUtil.CONFIG_DIR}${File.separator}${offlineActiveFileName}")
+            File("${FileUtil.getRootFolder(context,5)?.absolutePath}${FileUtil.CONFIG_DIR}").apply {
                 if (!exists()) {
                     mkdirs()
                 }
@@ -80,7 +80,7 @@ object ArcSoftUtil {
                     Constants.APP_ID,
                     Constants.SDK_KEY,
                     Constants.ACTIVE_KEY,
-                    true,
+                    false,
                     arcSoftActiveOfflineFile.absolutePath
                 ).toJson()
                 arcSoftLicenseFile.writeText(defaultJson)
@@ -88,11 +88,11 @@ object ArcSoftUtil {
             }
         } catch (e: Exception) {
             val arcSoftLicenseFile =
-                File("${FileUtil.getRootFolder(context)?.absolutePath}${FileUtil.CONFIG_DIR}${File.separator}${configFileName}")
+                File("${FileUtil.getRootFolder(context,5)?.absolutePath}${FileUtil.CONFIG_DIR}${File.separator}${configFileName}")
             val arcSoftActiveOfflineFile =
-                File("${FileUtil.getRootFolder(context)?.absolutePath}${FileUtil.CONFIG_DIR}${File.separator}${offlineActiveFileName}")
+                File("${FileUtil.getRootFolder(context,5)?.absolutePath}${FileUtil.CONFIG_DIR}${File.separator}${offlineActiveFileName}")
             LogUtil.i("获取文件报错,写一份")
-            File("${FileUtil.getRootFolder(context)?.absolutePath}${FileUtil.CONFIG_DIR}").apply {
+            File("${FileUtil.getRootFolder(context,5)?.absolutePath}${FileUtil.CONFIG_DIR}").apply {
                 if (!exists()) {
                     mkdirs()
                 }
@@ -102,7 +102,7 @@ object ArcSoftUtil {
                 Constants.APP_ID,
                 Constants.SDK_KEY,
                 Constants.ACTIVE_KEY,
-                true,
+                false,
                 arcSoftActiveOfflineFile.absolutePath
             ).toJson()
             arcSoftLicenseFile.writeText(defaultJson)

+ 93 - 9
app/src/main/java/com/grkj/iscs_mars/util/BitmapUtil.kt

@@ -26,12 +26,15 @@ import com.bumptech.glide.request.target.Target
 import com.bumptech.glide.request.transition.Transition
 import com.grkj.iscs_mars.util.log.LogUtil
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.isActive
 import kotlinx.coroutines.withContext
 import java.io.ByteArrayOutputStream
 import java.io.File
 import java.io.FileInputStream
 import java.io.FileOutputStream
 import java.io.IOException
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
 import kotlin.math.max
 import kotlin.math.roundToInt
 
@@ -167,6 +170,51 @@ object BitmapUtil {
             })
     }
 
+    /**
+     * 4‑A:**非阻塞回调版** —— UI 层用这个最方便
+     */
+    suspend fun loadBitmapSmall(
+        ctx: Context,
+        url: String?,
+        reqW: Int,
+        reqH: Int,
+        placeholder: Int? = null
+    ): Bitmap? {
+        if (url.isNullOrEmpty()) {
+            return null
+        }
+        smallCache[url]?.let { return it }
+        val bitmap = suspendCoroutine { cont ->
+            Glide.with(ctx)
+                .asBitmap()
+                .load(url)
+                .override(reqW, reqH)
+                .format(DecodeFormat.PREFER_RGB_565)
+                .diskCacheStrategy(DiskCacheStrategy.ALL)
+                .apply { placeholder?.let { placeholder(it) } }
+                .into(object : CustomTarget<Bitmap>() {
+                    override fun onResourceReady(
+                        resource: Bitmap,
+                        transition: Transition<in Bitmap>?
+                    ) {
+                        smallCache.put(url, resource)
+                        if (cont.context.isActive) {
+                            cont.resume(resource)
+                        }
+                    }
+
+                    override fun onLoadFailed(errorDrawable: Drawable?) {
+                        if (cont.context.isActive) {
+                            cont.resume(null)
+                        }
+                    }
+
+                    override fun onLoadCleared(placeholder: Drawable?) {}
+                })
+        }
+        return bitmap
+    }
+
     /**
      * 4‑B:**挂起函数版** —— 需要在协程里同步拿 Bitmap 可用
      */
@@ -353,17 +401,22 @@ object BitmapUtil {
             when (ExifInterface(path).getAttributeInt(
                 ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL
             )) {
-                ExifInterface.ORIENTATION_ROTATE_90  -> 90
+                ExifInterface.ORIENTATION_ROTATE_90 -> 90
                 ExifInterface.ORIENTATION_ROTATE_180 -> 180
                 ExifInterface.ORIENTATION_ROTATE_270 -> 270
                 else -> 0
             }
-        } catch (_: Throwable) { 0 }
+        } catch (_: Throwable) {
+            0
+        }
 
         // 1) 读原始尺寸
         val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true }
-        try { FileInputStream(file).use { BitmapFactory.decodeStream(it, null, bounds) } }
-        catch (_: Throwable) { return null }
+        try {
+            FileInputStream(file).use { BitmapFactory.decodeStream(it, null, bounds) }
+        } catch (_: Throwable) {
+            return null
+        }
         var sw = bounds.outWidth
         var sh = bounds.outHeight
         if (sw <= 0 || sh <= 0) return null
@@ -391,13 +444,16 @@ object BitmapUtil {
             inDither = true
             inSampleSize = inSample
         }
-        var bmp = try { FileInputStream(file).use { BitmapFactory.decodeStream(it, null, opts) } }
-        catch (_: Throwable) { null }
+        var bmp = try {
+            FileInputStream(file).use { BitmapFactory.decodeStream(it, null, opts) }
+        } catch (_: Throwable) {
+            null
+        }
         if (bmp == null) return null
 
         // 3) EXIF 方向修正
         if (fixOrientation) {
-            val deg = readExifRotation(file.absolutePath)
+            val deg = readExifRotation(file)
             if (deg != 0) {
                 val m = Matrix().apply { postRotate(deg.toFloat()) }
                 Bitmap.createBitmap(bmp, 0, 0, bmp.width, bmp.height, m, true)?.let { r ->
@@ -414,8 +470,10 @@ object BitmapUtil {
 
         val (drawW, drawH) = if (stretchToFill) {
             // 非等比拉伸:独立的 x/y 缩放;allowUpscale=false 时只缩不放
-            val sx = if (allowUpscale) outWidth / sw.toFloat() else minOf(1f, outWidth / sw.toFloat())
-            val sy = if (allowUpscale) outHeight / sh.toFloat() else minOf(1f, outHeight / sh.toFloat())
+            val sx =
+                if (allowUpscale) outWidth / sw.toFloat() else minOf(1f, outWidth / sw.toFloat())
+            val sy =
+                if (allowUpscale) outHeight / sh.toFloat() else minOf(1f, outHeight / sh.toFloat())
             maxOf(1, (sw * sx).toInt()) to maxOf(1, (sh * sy).toInt())
         } else {
             // 等比 contain:不裁剪,左上贴,留空
@@ -436,4 +494,30 @@ object BitmapUtil {
         return out
     }
 
+    private fun isJpeg(file: File): Boolean {
+        FileInputStream(file).use {
+            val b1 = it.read()
+            val b2 = it.read()
+            // JPEG 魔数 0xFF 0xD8
+            return b1 == 0xFF && b2 == 0xD8
+        }
+    }
+
+    private fun readExifRotation(file: File): Int {
+        return try {
+            if (!isJpeg(file)) return 0
+            val exif = ExifInterface(file)
+            when (exif.getAttributeInt(
+                ExifInterface.TAG_ORIENTATION,
+                ExifInterface.ORIENTATION_NORMAL
+            )) {
+                ExifInterface.ORIENTATION_ROTATE_90 -> 90
+                ExifInterface.ORIENTATION_ROTATE_180 -> 180
+                ExifInterface.ORIENTATION_ROTATE_270 -> 270
+                else -> 0
+            }
+        } catch (_: Throwable) {
+            0
+        }
+    }
 }

+ 3 - 3
app/src/main/java/com/grkj/iscs_mars/util/NetApi.kt

@@ -711,7 +711,7 @@ object NetApi {
     /**
      * 获取电柜map解析数据
      */
-    fun getLotoMapData(lotoId: Long, callBack: (MutableList<LotoMapRespVO>?) -> Unit) {
+    fun getLotoMapData(lotoId: Long, callBack: (LotoMapRespVO?) -> Unit) {
         NetHttpManager.getInstance().doRequestNet(
             UrlConsts.LOTO_MAP,
             false,
@@ -1353,7 +1353,7 @@ object NetApi {
             false,
             mapOf(
                 "current" to 1,
-                "size" to 50
+                "size" to -1
             ),
             { res, _, _ ->
                 res?.let {
@@ -1415,7 +1415,7 @@ object NetApi {
             false,
             mapOf(
                 "current" to 1,
-                "size" to 50
+                "size" to -1
             ),
             { res, _, _ ->
                 res?.let {

+ 105 - 108
app/src/main/java/com/grkj/iscs_mars/view/fragment/StepFragment.kt

@@ -1,6 +1,5 @@
 package com.grkj.iscs_mars.view.fragment
 
-import android.graphics.PointF
 import android.view.GestureDetector
 import android.view.MotionEvent
 import android.view.View
@@ -19,7 +18,6 @@ import com.grkj.iscs_mars.model.vo.machinery.MachineryDetailRespVO
 import com.grkj.iscs_mars.model.vo.ticket.LotoMapRespVO
 import com.grkj.iscs_mars.model.vo.ticket.StepDetailRespVO
 import com.grkj.iscs_mars.model.vo.ticket.TicketDetailRespVO
-import com.grkj.iscs_mars.util.BitmapUtil
 import com.grkj.iscs_mars.util.SPUtils
 import com.grkj.iscs_mars.util.ToastUtils
 import com.grkj.iscs_mars.util.log.LogUtil
@@ -29,6 +27,8 @@ import com.grkj.iscs_mars.view.iview.IStepView
 import com.grkj.iscs_mars.view.presenter.StepPresenter
 import com.grkj.iscs_mars.view.widget.CustomStationLayer
 import com.onlylemi.mapview.library.MapViewListener
+import com.sik.sikcore.extension.isNullOrEmpty
+import com.sik.sikcore.thread.ThreadUtils
 import com.zhy.adapter.recyclerview.CommonAdapter
 import com.zhy.adapter.recyclerview.base.ViewHolder
 
@@ -39,7 +39,7 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
     BaseMvpFragment<IStepView, StepPresenter, FragmentStepBinding>() {
 
     private lateinit var mStepList: MutableList<StepBO>
-    private var mLotoList = mutableListOf<LotoMapRespVO>()
+    private var mLotoData: LotoMapRespVO? = null
     private var mChangePage: PageChangeBO? = null
     private var mMachineryDetail: MachineryDetailRespVO? = null
     private var mStep: Int = 0
@@ -181,19 +181,12 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
 
 
     private fun initMap() {
-        gestureDetector = GestureDetector(requireContext(), object :
-            GestureDetector.SimpleOnGestureListener() {
+        gestureDetector = GestureDetector(requireContext(), object : GestureDetector.SimpleOnGestureListener() {
             override fun onDoubleTap(e: MotionEvent): Boolean {
                 mBinding?.mapview?.currentRotateDegrees = 0f
                 return super.onDoubleTap(e)
             }
-
-            override fun onFling(
-                e1: MotionEvent?,
-                e2: MotionEvent,
-                velocityX: Float,
-                velocityY: Float
-            ): Boolean {
+            override fun onFling(e1: MotionEvent?, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
                 mBinding?.mapview?.currentRotateDegrees = 0f
                 return super.onFling(e1, e2, velocityX, velocityY)
             }
@@ -203,27 +196,21 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
             gestureDetector.onTouchEvent(event)
             false
         }
+
+        // ★★★ 修正这里:== null,且创建后 setRatio 再 addLayer ★★★
+        if (stationLayer == null) {
+            stationLayer = CustomStationLayer(mBinding?.mapview, mStationList)
+            stationLayer?.setRatio(1f) // 没这句 bgBitmap 为 null,draw 不会画
+            mBinding?.mapview?.addLayer(stationLayer)
+        }
+
         mBinding?.mapview?.setMapViewListener(object : MapViewListener {
             override fun onMapLoadSuccess() {
-                // 等底图先呈现一帧
                 mBinding?.mapview?.post {
-                    if (stationLayer != null) {
-                        mBinding?.mapview?.currentRotateDegrees = 0f
-                        return@post
-                    }
-                    stationLayer = CustomStationLayer(mBinding?.mapview, mStationList)
-                    stationLayer?.setMarkIsClickListener(object :
-                        CustomStationLayer.MarkIsClickListener {
-                        override fun markIsClick(index: Int, btnIndex: Int) {
-//                        ToastUtils.tip(mPointList[index].name + " is selected, btnIndex is " + btnIndex)
-                        }
-                    })
-                    mBinding?.mapview?.addLayer(stationLayer)
-                    stationLayer?.setRatio(mapRatio)
-                    mBinding?.mapview?.setMinZoom(mBinding?.mapview?.currentZoom ?: 0f)
+                    mBinding?.mapview?.currentRotateDegrees = 0f
+                    mBinding?.mapview?.refresh()
                 }
             }
-
             override fun onMapLoadFail() {
                 ToastUtils.tip("onMapLoadFail")
             }
@@ -281,90 +268,100 @@ class StepFragment(val goBack: () -> Unit, val changePage: (PageChangeBO) -> Uni
             pageChangeBO.machineryId!!, {
                 mMachineryDetail = it
                 Glide.with(this).load(it?.machineryImg).into(mBinding?.ivMachinery!!)
-            }) { itList ->
-            itList?.let {
-                mLotoList.clear()
-                mLotoList.addAll(it)
-            } ?: mLotoList.clear()
-
-            if (mLotoList.isNotEmpty()) {
-                mLotoList[0].mapId?.let { itId ->
-                    presenter?.getMapInfo(itId) { itMapInfo ->
-                        // 如果没有图 URL,直接返回
-                        val imageUrl = itMapInfo?.imageUrl ?: return@getMapInfo
+            }) {
+            mLotoData = it
 
-                        BitmapUtil.loadBitmapFromUrl(requireContext(), imageUrl) { mapBmp ->
-                            if (mapBmp == null) {
-                                LogUtil.e("Map pic is null")
-                                return@loadBitmapFromUrl
-                            }
-
-                            // 清空旧点
-                            mStationList.clear()
-
-                            // 1 格 对应的像素
-                            val cellPx = 50f
-                            // 后端给的“逻辑”子图原始尺寸(像素)
-                            val backendW = itMapInfo.width!!.toFloat()
-                            val backendH = itMapInfo.height!!.toFloat()
-                            // 实际下载回来的 Bitmap 尺寸(像素)
-                            val actualW = mapBmp.width.toFloat()
-                            val actualH = mapBmp.height.toFloat()
-                            // 计算缩放比例
-                            val ratioX = actualW / backendW
-                            val ratioY = actualH / backendH
-                            mapRatio = ratioX
-                            // 子图在全局坐标系里的左上角偏移(像素)
-                            val offsetX = itMapInfo.x!!.toFloat()
-                            val offsetY = itMapInfo.y!!.toFloat()
-                            // 图标请求尺寸:逻辑 45px * 缩放比
-                            val iconReqPx = (45f * ratioX).toInt().coerceAtLeast(1)
-
-                            itMapInfo.pointList?.forEach { pt ->
-                                // 1) 格数 → 全局像素
-                                val globalX = pt.x!!.toFloat() * cellPx
-                                val globalY = pt.y!!.toFloat() * cellPx
-                                // 2) 全局像素 - 子图偏移 = 子图内像素
-                                val localX = globalX - offsetX
-                                val localY = globalY - offsetY
-                                // 3) 再乘缩放比,得到真实 Bitmap 上的像素坐标
-                                val finalX = localX * ratioX
-                                val finalY = localY * ratioY
-                                // 异步加载点位图标,固定请求尺寸
-                                BitmapUtil.loadBitmapFromUrl(
-                                    requireContext(),
-                                    pt.pointIcon!!,
-                                    reqWidth = iconReqPx,
-                                    reqHeight = iconReqPx
-                                ) { bmpIcon ->
-                                    val icon = bmpIcon ?: BitmapUtil.getResizedBitmapFromMipmap(
-                                        requireContext(),
-                                        R.mipmap.ticket_type_placeholder,
-                                        iconReqPx,
-                                        iconReqPx
-                                    )
-
-                                    mStationList.add(
-                                        CustomStationLayer.IsolationPoint(
-                                            PointF(finalX, finalY),
-                                            pt.entityName!!,
-                                            icon,
-                                            pt.entityId!!.toLong(),
-                                            pt.pointSerialNumber,
-                                            mMachineryDetail?.pointIdList?.contains(pt.entityId) == true
-                                        )
-                                    )
-
-                                    // 全部点都加载完后,设置给 layer 并绘制
-                                    if (mStationList.size == itMapInfo.pointList.size) {
-                                        if (stationLayer?.inDraw == true) {
-                                            return@loadBitmapFromUrl
-                                        }
-                                        mBinding?.mapview?.loadMap(mapBmp)
-                                    }
+            if (!mLotoData.isNullOrEmpty()) {
+                mLotoData?.mapId?.let { itId ->
+                    presenter?.getMapInfo(itId) { itMapInfo ->
+                        ThreadUtils.runOnIO {
+                            presenter?.mapDataHandleForStations(
+                                requireContext(),
+                                itMapInfo,
+                                mMachineryDetail?.pointIdList?:mutableListOf(),
+                                { mBinding?.mapview },
+                                stationLayer
+                            ) { mapBmp ->
+                                ThreadUtils.runOnMain {
+                                    mBinding?.mapview?.loadMap(mapBmp)
                                 }
                             }
                         }
+//                        // 如果没有图 URL,直接返回
+//                        val imageUrl = itMapInfo?.imageUrl ?: return@getMapInfo
+//
+//                        BitmapUtil.loadBitmapFromUrl(requireContext(), imageUrl) { mapBmp ->
+//                            if (mapBmp == null) {
+//                                LogUtil.e("Map pic is null")
+//                                return@loadBitmapFromUrl
+//                            }
+//
+//                            // 清空旧点
+//                            mStationList.clear()
+//
+//                            // 1 格 对应的像素
+//                            val cellPx = 50f
+//                            // 后端给的“逻辑”子图原始尺寸(像素)
+//                            val backendW = itMapInfo.width!!.toFloat()
+//                            val backendH = itMapInfo.height!!.toFloat()
+//                            // 实际下载回来的 Bitmap 尺寸(像素)
+//                            val actualW = mapBmp.width.toFloat()
+//                            val actualH = mapBmp.height.toFloat()
+//                            // 计算缩放比例
+//                            val ratioX = actualW / backendW
+//                            val ratioY = actualH / backendH
+//                            mapRatio = ratioX
+//                            // 子图在全局坐标系里的左上角偏移(像素)
+//                            val offsetX = itMapInfo.x!!.toFloat()
+//                            val offsetY = itMapInfo.y!!.toFloat()
+//                            // 图标请求尺寸:逻辑 45px * 缩放比
+//                            val iconReqPx = (45f * ratioX).toInt().coerceAtLeast(1)
+//
+//                            itMapInfo.pointList?.forEach { pt ->
+//                                // 1) 格数 → 全局像素
+//                                val globalX = pt.x!!.toFloat() * cellPx
+//                                val globalY = pt.y!!.toFloat() * cellPx
+//                                // 2) 全局像素 - 子图偏移 = 子图内像素
+//                                val localX = globalX - offsetX
+//                                val localY = globalY - offsetY
+//                                // 3) 再乘缩放比,得到真实 Bitmap 上的像素坐标
+//                                val finalX = localX * ratioX
+//                                val finalY = localY * ratioY
+//                                // 异步加载点位图标,固定请求尺寸
+//                                BitmapUtil.loadBitmapFromUrl(
+//                                    requireContext(),
+//                                    pt.pointIcon!!,
+//                                    reqWidth = iconReqPx,
+//                                    reqHeight = iconReqPx
+//                                ) { bmpIcon ->
+//                                    val icon = bmpIcon ?: BitmapUtil.getResizedBitmapFromMipmap(
+//                                        requireContext(),
+//                                        R.mipmap.ticket_type_placeholder,
+//                                        iconReqPx,
+//                                        iconReqPx
+//                                    )
+//
+//                                    mStationList.add(
+//                                        CustomStationLayer.IsolationPoint(
+//                                            PointF(finalX, finalY),
+//                                            pt.entityName!!,
+//                                            icon,
+//                                            pt.entityId!!.toLong(),
+//                                            pt.pointSerialNumber,
+//                                            mMachineryDetail?.pointIdList?.contains(pt.entityId) == true
+//                                        )
+//                                    )
+//
+//                                    // 全部点都加载完后,设置给 layer 并绘制
+//                                    if (mStationList.size == itMapInfo.pointList.size) {
+//                                        if (stationLayer?.inDraw == true) {
+//                                            return@loadBitmapFromUrl
+//                                        }
+//                                        mBinding?.mapview?.loadMap(mapBmp)
+//                                    }
+//                                }
+//                            }
+//                        }
                     }
                 }
             }

+ 121 - 2
app/src/main/java/com/grkj/iscs_mars/view/presenter/StepPresenter.kt

@@ -1,6 +1,7 @@
 package com.grkj.iscs_mars.view.presenter
 
 import android.content.Context
+import android.graphics.Bitmap
 import com.grkj.iscs_mars.BusinessManager
 import com.grkj.iscs_mars.R
 import com.grkj.iscs_mars.modbus.DockBean
@@ -13,14 +14,19 @@ import com.grkj.iscs_mars.model.vo.map.MapInfoRespVO
 import com.grkj.iscs_mars.model.vo.ticket.LotoMapRespVO
 import com.grkj.iscs_mars.model.vo.ticket.StepDetailRespVO
 import com.grkj.iscs_mars.model.vo.ticket.TicketDetailRespVO
+import com.grkj.iscs_mars.util.BitmapUtil
 import com.grkj.iscs_mars.util.Executor
 import com.grkj.iscs_mars.util.NetApi
 import com.grkj.iscs_mars.util.ToastUtils
+import com.grkj.iscs_mars.util.log.LogUtil
 import com.grkj.iscs_mars.view.base.BasePresenter
 import com.grkj.iscs_mars.view.iview.IStepView
 import com.grkj.iscs_mars.view.step_mode.IStepMode
+import com.grkj.iscs_mars.view.widget.CustomStationLayer
+import com.onlylemi.mapview.library.MapView
 import com.sik.sikcore.thread.ThreadUtils
 import kotlinx.coroutines.delay
+import kotlin.coroutines.suspendCoroutine
 
 class StepPresenter : BasePresenter<IStepView>() {
 
@@ -35,12 +41,12 @@ class StepPresenter : BasePresenter<IStepView>() {
     fun getMachineryDetail(
         machineryId: Long,
         callBack: (MachineryDetailRespVO?) -> Unit,
-        mapCallBack: (MutableList<LotoMapRespVO>?) -> Unit
+        mapCallBack: (LotoMapRespVO?) -> Unit
     ) {
         NetApi.getMachineryDetail(machineryId) {
             NetApi.getLotoMapData(it?.lotoId!!) {
                 Executor.runOnMain {
-                    mapCallBack(it?.sortedWith(compareBy({ it.row }, { it.col }))?.toMutableList())
+                    mapCallBack(it)
                 }
             }
             Executor.runOnMain {
@@ -190,4 +196,117 @@ class StepPresenter : BasePresenter<IStepView>() {
             BusinessManager.checkMyTodoForHandleKey()
         }
     }
+
+    /**
+     * 把后端地图与点位数据装配到 MapView / StationLayer
+     *
+     * 约定:
+     * - 后端坐标单位是“格”(cell),cellPx 表示 1 格对应的像素(后端坐标系)
+     * - 点位的 pos 存在“世界坐标系”(后端大图坐标,非屏幕坐标)
+     * - 最终绘制由 MapView.currentMatrix 负责(缩放/旋转/平移),点击命中建议在屏幕坐标判断
+     */
+    suspend fun mapDataHandleForStations(
+        context: Context,
+        itMapInfo: MapInfoRespVO?,
+        selectedPointId: List<Long>,
+        mapViewRef: () -> MapView?,
+        stationLayer: CustomStationLayer?,         // 你的站点图层(含 submitPoints/refreshIfVisible 等)
+        cellPx: Float = 50f,                       // 和你旧代码保持一致的格尺寸
+        yAxisFlip: Boolean = false,                // 后端若以左下为原点则传 true
+        iconReqPx: Int = 64,                       // 小图标请求尺寸(像素)
+        onPreview: (Bitmap) -> Unit = {}           // 低清预览回调,可用于先占位
+    ) {
+        val imageUrl = itMapInfo?.imageUrl ?: run {
+            LogUtil.e("Map: empty imageUrl"); return
+        }
+
+        // 1) 先把原图下载到本地文件(避免一上来就全量 decode 占内存)
+        val tempImageFile = BitmapUtil.downloadToFile(context, imageUrl) ?: run {
+            LogUtil.e("Map download failed → $imageUrl"); return
+        }
+
+        // 2) 读取后端坐标系尺寸(世界坐标系宽高)
+        val backendW = itMapInfo.width?.toFloat() ?: run {
+            LogUtil.e("Map: empty width"); return
+        }
+        val backendH = itMapInfo.height?.toFloat() ?: run {
+            LogUtil.e("Map: empty height"); return
+        }
+
+        // 3) 先解个“低清预览”,尺寸按后端世界大小来,背景上个底色
+        val preview = BitmapUtil.loadBitmapFromFile(
+            tempImageFile,
+            outWidth = backendW.toInt(),
+            outHeight = backendH.toInt(),
+            backgroundColor = context.getColor(R.color.color_map_base)
+        )
+        preview?.also(onPreview)
+
+        // 4) 计算子图在世界坐标系中的偏移(把“局部子图”搬到“世界大图”)
+        val offX = itMapInfo.x?.toFloatOrNull() ?: 0f
+        val offY = itMapInfo.y?.toFloatOrNull() ?: 0f
+
+        // 5) 组装点位(世界坐标)
+        val points = mutableListOf<CustomStationLayer.IsolationPoint>()
+        val byEntityId = mutableMapOf<Long, CustomStationLayer.IsolationPoint>()
+
+        itMapInfo.pointList?.asSequence()
+            ?.filter { pt -> pt.x != null && pt.y != null && pt.entityId != null }
+            ?.forEach { pt ->
+                // 5.1 “格坐标”→“局部像素”
+                val gridX = pt.x!!.toFloat() * cellPx
+                val gridY = pt.y!!.toFloat() * cellPx
+
+                // ✅ 统一在“子图本地坐标”里画(MapView 用的也是这张子图)
+                val worldTopY = if (yAxisFlip) backendH - gridY else gridY
+                val drawX = gridX - offX          // ← 关键:扣掉子图 X 偏移
+                val drawY = worldTopY - offY      // ← 关键:先翻转再扣 Y 偏移
+
+                val p = CustomStationLayer.IsolationPoint(
+                    pos = android.graphics.PointF(drawX, drawY),
+                    entityName = pt.entityName ?: "",
+                    icon = null, // 图标异步回填
+                    iconUrl = pt.pointIcon,
+                    entityId = pt.entityId ?: 0L,
+                    pointSerialNumber = pt.pointSerialNumber,
+                    isSelected = selectedPointId.contains(pt.entityId) // 也可以按你的业务传:mMachineryDetail?.pointIdList?.contains(...)
+                )
+                points += p
+                byEntityId[p.entityId] = p
+            }
+        points.groupBy { it.iconUrl }.forEach {
+            // 5.4 异步加载小图标(固定请求尺寸,足够清晰且内存友好)
+            val url = it.key
+            if (!url.isNullOrBlank()) {
+                val bmp = BitmapUtil.loadBitmapSmall(
+                    ctx = context,
+                    url = url,
+                    reqW = iconReqPx,
+                    reqH = iconReqPx
+                )
+                val icon = bmp ?: BitmapUtil.getResizedBitmapFromMipmap(
+                    context, R.mipmap.ticket_type_placeholder, iconReqPx, iconReqPx
+                )
+                it.value.forEach {
+                    it.icon = icon
+                }
+            }
+        }
+        // 6) 提交点位(一次性)
+        stationLayer?.submitPoints(points)
+
+        // 7) 最终把原图真正解码并塞给 MapView(高分图)
+        //    如果担心大图过大,可以用 BitmapUtil 提供的 inJustDecodeBounds / 采样率做内存控制
+        val full = BitmapUtil.loadBitmapFromFile(
+            tempImageFile,
+            itMapInfo.width.toInt(),
+            itMapInfo.height.toInt()
+        )
+        if (full == null) {
+            LogUtil.e("Map decode failed for full bitmap.")
+            return
+        }
+        mapViewRef()?.loadMap(full)
+    }
+
 }

+ 239 - 52
app/src/main/java/com/grkj/iscs_mars/view/widget/CustomStationLayer.kt

@@ -9,17 +9,30 @@ import android.graphics.PointF
 import android.util.Pair
 import android.view.MotionEvent
 import com.grkj.iscs_mars.R
-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 kotlinx.coroutines.Runnable
 import kotlin.math.cos
 import kotlin.math.sin
 
 class CustomStationLayer @JvmOverloads constructor(
-    mapView: MapView?, private var pointList: List<IsolationPoint> = mutableListOf()
+    mapView: MapView?, private var pointList: MutableList<IsolationPoint> = mutableListOf()
 ) : MapBaseLayer(mapView) {
+
+    // ===== 配置:你当前实现把位图画在“左上角”,保持兼容 =====
+    private val POS_IS_TOP_LEFT = true
+
+    private val ICON_TOP_OFFSET_PX = 0f   // 例如想再向上 6px 就写 6f
+
+    // ===== 同步 & 选择保持 =====
+    private val dataLock = Any()
+
+    // ===== 绘制与节流 =====
+    @Volatile
     var inDraw: Boolean = false
+        private set
+
     private var listener: MarkIsClickListener? = null
     private var radiusMark = 0f
     private lateinit var paint: Paint
@@ -30,15 +43,20 @@ class CustomStationLayer @JvmOverloads constructor(
     private var ratio: Float = 1f
     private var switchSize: Float = 1f
 
+    private val FRAME_INTERVAL_MS = 32L
+    private val invalidateRunnable = Runnable {
+        mapView?.refresh()
+    }
+
     init {
         initLayer()
     }
 
     private fun initLayer() {
         radiusMark = setValue(6.0f)
-        paint = Paint()
-        paint.isAntiAlias = true
-        paint.style = Paint.Style.FILL_AND_STROKE
+        paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
+            style = Paint.Style.FILL_AND_STROKE
+        }
     }
 
     fun setRatio(ratio: Float) {
@@ -57,83 +75,120 @@ class CustomStationLayer @JvmOverloads constructor(
             (78 * ratio).toInt()
         )!!
         switchSize = setValue(4 * ratio)
+        throttleInvalidate()
     }
 
     override fun onTouch(event: MotionEvent) {
-
+        // 你原来未实现点击,这里保持空实现,避免改变现有交互。
+        // 如果要做命中,可按 mapToScreen -> 在屏幕坐标圈选,或直接在 draw 使用的同一锚点上做距离判断。
     }
 
     override fun draw(
         canvas: Canvas, currentMatrix: Matrix, currentZoom: Float, currentRotateDegrees: Float
     ) {
         if (!isVisible) return
-        if (inDraw) {
-            return
-        }
+        if (inDraw) return
         inDraw = true
         this.currentZoom = currentZoom
         currentDegree = 360 - currentRotateDegrees
+
         try {
             canvas.save()
             try {
-                // 把 mapView 本身的缩放/平移/旋转一次性 concat 到 Canvas
                 canvas.concat(currentMatrix)
-                val tempPointList = pointList.toList()
+
+                val tempPointList = synchronized(dataLock) { pointList.toList() }
+
+                // 只画可见区域,避免全量遍历导致多余 overdraw
                 val inv = Matrix().apply { currentMatrix.invert(this) }
-                val screen = android.graphics.RectF(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat())
-                inv.mapRect(screen) // screen 现在是“图内坐标系下”的可见区域
+                val screen =
+                    android.graphics.RectF(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat())
+                inv.mapRect(screen) // 图内坐标下的可见区域
 
                 for (point in tempPointList) {
-                    // point.pos.x/y 已经是「图内像素坐标」
-                    val x = point.pos.x
-                    val y = point.pos.y
-
+                    val c = centerOf(point) // 当前锚点(左上或中心)
                     val bw = bgBitmap?.width ?: 0
                     val bh = bgBitmap?.height ?: 0
-                    if (!screen.intersects(x, y, x + bw, y + bh)) continue  // ← 屏外跳过
-                    // 先画背景(它会被 currentMatrix 自动缩放)
-                    bgBitmap?.let {bg->
-                        canvas.drawBitmap(
-                            bg, x, y, paint
-                        )
+
+                    // 左上锚点:c 就是左上;中心锚点:换算左上绘制位
+                    val drawX = if (POS_IS_TOP_LEFT) c.x else (c.x - bw / 2f)
+                    val drawY = if (POS_IS_TOP_LEFT) c.y else (c.y - bh / 2f)
+
+                    if (!screen.intersects(drawX, drawY, drawX + bw, drawY + bh)) continue
+
+                    bgBitmap?.let { bg ->
+                        canvas.drawBitmap(bg, drawX, drawY, paint)
+
                         point.icon?.let { raw ->
-                            // 背景里要放图标的“内容槽”大小(自己按 bg 的留白算,这里举例居中正方形)
-                            val slot = minOf(bg.width, bg.height) * 0.6f
-                            val w = slot.toInt()
-                            val h = w
-                            val icon = getScaledIcon(raw, w, h)   // ← 关键:先缩放再画
-                            val dx = x + (bg.width - w) / 2f
-                            val dy = y + (bg.height - h) / 2f
                             paint.isFilterBitmap = true
                             paint.isDither = true
-                            canvas.drawBitmap(icon, dx, dy, paint)
+                            paint.alpha = 255
+
+                            // 如果想彻底禁用 density 影响,也可以加一行(可选):
+                            raw.density = Bitmap.DENSITY_NONE
+
+                            // 设定图标的“槽位”大小:宽占满背景,高留 70%(下边还能放字)
+                            val slotW = bg.width * 0.8f       // 想全宽就是 1.0f
+                            val left = drawX + (bg.width - slotW) / 2f
+                            val top = drawY + (bg.width - slotW) / 2f
+                            val dst = android.graphics.RectF(left, top, left + slotW, top + slotW)
+
+                            // —— 两种模式,二选一 ——
+                            val USE_COVER = true  // true=铺满裁剪更饱满;false=拉伸到槽位(可能变形)
+
+                            if (!USE_COVER) {
+                                // 拉伸到槽位(简单直接)
+                                canvas.drawBitmap(raw, null, dst, paint)
+                            } else {
+                                // 居中裁剪成“封面模式”(不变形,铺满槽位)
+                                val src = run {
+                                    val sw = raw.width.toFloat();
+                                    val sh = raw.height.toFloat()
+                                    val dw = dst.width();
+                                    val dh = dst.height()
+                                    val s = maxOf(dw / sw, dh / sh) // 放大倍数
+                                    val cw = dw / s;
+                                    val ch = dh / s // 需要的源裁剪宽高
+                                    val cx = sw / 2f;
+                                    val cy = sh / 2f
+                                    android.graphics.Rect(
+                                        (cx - cw / 2f).toInt().coerceAtLeast(0),
+                                        (cy - ch / 2f).toInt().coerceAtLeast(0),
+                                        (cx + cw / 2f).toInt().coerceAtMost(raw.width),
+                                        (cy + ch / 2f).toInt().coerceAtMost(raw.height)
+                                    )
+                                }
+                                canvas.drawBitmap(raw, src, dst, paint)
+                            }
                         }
-                        // 然后画文字
+
+                        // 文本
                         paint.style = Paint.Style.FILL
                         paint.strokeWidth = 1f
                         paint.color = Color.RED
-                        paint.textSize = radiusMark * ratio  // 这里是「图内」的文字大小,后面会跟着缩放
+                        paint.textSize = radiusMark * ratio
                         val textW = paint.measureText(point.entityName)
                         canvas.drawText(
                             point.entityName,
-                            x + (bg.width - textW) / 2,
-                            y + (bg.height - radiusMark / 2 - bg.height / 10),
+                            drawX + (bg.width - textW) / 2,
+                            drawY + (bg.height - radiusMark / 2 - bg.height / 10),
                             paint
                         )
 
-                        // 如果选中,再叠加一个标记
+                        // ===== 选中覆盖:放在最上层;确保不透明 =====
                         if (point.isSelected) {
-                            coverBitmap?.let {
-                                canvas.drawBitmap(
-                                    it, x, y, paint
-                                )
+                            coverBitmap?.let { cover ->
+                                paint.alpha = 255
+                                canvas.drawBitmap(cover, drawX, drawY, paint)
                             }
-                            val checkW = paint.measureText("√")
+                            val check = "√"
+                            val checkW = paint.measureText(check)
                             paint.color = Color.WHITE
+                            paint.alpha = 255
                             canvas.drawText(
-                                "√",
-                                x + (bg.width - checkW) / 2,
-                                y + (bg.height / 2 + radiusMark / 2),
+                                check,
+                                drawX + (bg.width - checkW) / 2f,
+                                drawY + (bg.height / 2f + radiusMark / 2f),
                                 paint
                             )
                         }
@@ -147,6 +202,141 @@ class CustomStationLayer @JvmOverloads constructor(
         }
     }
 
+    // ===================== 新增:对外 API(与 CustomSwitchStationLayer 对齐) =====================
+
+    /**
+     * 点位列表:精准 Diff(pos 已是世界坐标);可选保留当前选中
+     */
+    fun submitPoints(points: List<IsolationPoint>, keepSelection: Boolean = true) {
+        val incoming = if (points === pointList) points.map { deepCopyItem(it) } else points
+
+        val changedCenters = ArrayList<PointF>(8)
+        val changedRadiusPx = 120 // 局部刷新半径(像素,按位图尺寸留富余)
+
+        synchronized(dataLock) {
+            val oldById = HashMap<Long, IsolationPoint>(pointList.size)
+            pointList.forEach { oldById[it.entityId] = it }
+
+            val nextList = ArrayList<IsolationPoint>(incoming.size)
+
+            for (src in incoming) {
+                val id = src.entityId
+                val dst = oldById[id]
+                if (dst == null) {
+                    val add = deepCopyItem(src).copy(
+                        // 选中保持
+                        isSelected = src.isSelected
+                    )
+                    nextList.add(add)
+                    changedCenters.add(centerOf(add))
+                } else {
+                    var dirty = false
+                    if (!dst.pos.approxEq(src.pos)) {
+                        dst.pos = deepCopyPoint(src.pos); dirty = true
+                    }
+                    if (dst.entityName != src.entityName) {
+                        dst.entityName = src.entityName; dirty = true
+                    }
+                    if (src.icon != null && dst.icon !== src.icon) {
+                        dst.icon = src.icon; dirty = true
+                    }
+                    // 保持选中
+                    dst.isSelected = dst.isSelected
+                    if (dirty) changedCenters.add(centerOf(dst))
+                    nextList.add(dst)
+                }
+            }
+
+            // 删除的点位不再加入 nextList,自然会被移除
+            pointList.clear()
+            pointList.addAll(nextList)
+        }
+
+        if (changedCenters.isNotEmpty()) refreshRegionsOrThrottle(changedCenters, changedRadiusPx)
+        else throttleInvalidate()
+    }
+
+    /**
+     * 更新图标
+     */
+    fun updateIcon(entityId: Long, bmp: Bitmap?) {
+        synchronized(dataLock) {
+            val item = pointList.firstOrNull { it.entityId == entityId } ?: return
+            item.icon = bmp
+            // 局部刷新就行(pos 是图内坐标)
+            refreshIfVisible(item.pos, margin = 120f)
+        }
+    }
+
+    private var refreshIfVisibleRunnable: Runnable? = null
+
+
+    /** 局部刷新(点在屏幕上时) */
+    fun refreshIfVisible(point: PointF, margin: Float = 0f) {
+        if (inDraw) {
+            refreshIfVisibleRunnable?.let {
+                mapView.removeCallbacks(it)
+            }
+            refreshIfVisibleRunnable = Runnable { refreshIfVisible(point, margin) }
+            mapView.postDelayed(refreshIfVisibleRunnable, 1000)
+            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) {
+            throttleInvalidate()
+        }
+    }
+
+    // ===================== 工具 & 刷新 =====================
+
+    private fun throttleInvalidate() {
+        mapView?.postDelayed(invalidateRunnable, FRAME_INTERVAL_MS)
+    }
+
+    /** 优先局部刷新;若宿主没有 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
+            )
+            centers.forEach { c ->
+                val r = radiusPx
+                val l = (c.x - r).toInt()
+                val t = (c.y - r).toInt()
+                val rr = (c.x + r).toInt()
+                val b = (c.y + r).toInt()
+                m.invoke(mv, l, t, rr, b)
+            }
+            didPartial = true
+        }.onFailure { /* ignore */ }
+        if (!didPartial) throttleInvalidate()
+    }
+
+    private fun centerOf(p: IsolationPoint): PointF {
+        if (POS_IS_TOP_LEFT) return PointF(p.pos.x, p.pos.y)
+        val bw = bgBitmap?.width ?: 0
+        val bh = bgBitmap?.height ?: 0
+        return PointF(p.pos.x + bw / 2f, p.pos.y + bh / 2f)
+    }
+
+    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 deepCopyPoint(p: PointF) = PointF(p.x, p.y)
+    private fun deepCopyItem(it: IsolationPoint) = it.copy(pos = deepCopyPoint(it.pos))
+
+    // ===================== 你的原有代码 =====================
+
     private val iconCache = object : android.util.LruCache<String, Bitmap>(12 * 1024 * 1024) {
         override fun sizeOf(key: String, value: Bitmap) = value.byteCount
     }
@@ -163,13 +353,9 @@ class CustomStationLayer @JvmOverloads constructor(
     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())
     }
 
@@ -182,11 +368,12 @@ class CustomStationLayer @JvmOverloads constructor(
     }
 
     data class IsolationPoint(
-        val pos: PointF,
-        val entityName: String,
-        val icon: Bitmap?,
+        var pos: PointF,
+        var entityName: String,
+        var icon: Bitmap?,
+        var iconUrl: String?,
         val entityId: Long,
         val pointSerialNumber: String?,
-        val isSelected: Boolean,
+        var isSelected: Boolean,
     )
 }