js/layerVisibility.mjs

/**
 * layerVisibility.mjs
 * 圖層可見性管理:tracked 清理、items 列表生成、可見數量計算
 */
import each from 'lodash-es/each.js'
import filter from 'lodash-es/filter.js'
import sortBy from 'lodash-es/sortBy.js'
import cloneDeep from 'lodash-es/cloneDeep.js'
import isNumber from 'lodash-es/isNumber.js'


/**
 * 清除以指定前綴開頭的 layer 與 source
 * @param {Object} map - MapLibre 地圖實例
 * @param {String} prefix - 前綴字串
 * @param {Array} trackedLayerIds - 目前追蹤的 layer ID 陣列
 * @param {Array} trackedSourceIds - 目前追蹤的 source ID 陣列
 * @returns {{ layerIds: Array, sourceIds: Array }} 更新後的追蹤陣列
 */
export function clearTrackedByPrefix(map, prefix, trackedLayerIds, trackedSourceIds) {
    if (!map) return { layerIds: trackedLayerIds, sourceIds: trackedSourceIds }
    //先清委派 listeners: 呼叫端(如 cluster 設定變更)清掉後會以同 layerId 重建並重新註冊,
    //不清會逐次累積 → click 回呼被重複觸發
    offDelegatedListenersByLayer(map, (id) => id.startsWith(prefix))
    let rl = []; let rs = []
    each(trackedLayerIds, (id) => {
        id.startsWith(prefix) ? (map.getLayer(id) && map.removeLayer(id)) : rl.push(id)
    })
    each(trackedSourceIds, (id) => {
        id.startsWith(prefix) ? (map.getSource(id) && map.removeSource(id)) : rs.push(id)
    })
    return { layerIds: rl, sourceIds: rs }
}


/**
 * 反註冊委派 listeners 中綁定於符合條件 layer id 者(使註冊與圖層同壽命, 避免累積)。
 * 注意: 依賴 maplibre 私有結構 map._delegatedListeners, 升級 maplibre 時須確認仍相容——
 * 故全 codebase 僅此一份實作, 各清理路徑共用。
 * @param {Object} map - MapLibre 地圖實例
 * @param {Function} matchLayerId - (layerId) => Boolean
 */
export function offDelegatedListenersByLayer(map, matchLayerId) {
    let dl = map._delegatedListeners
    if (!dl || !map.off) return
    each(Object.keys(dl), (type) => {
        each([...(dl[type] || [])], (entry) => {
            if (entry && entry.listener && filter(entry.layers || [], matchLayerId).length > 0) {
                try {
                    map.off(type, entry.layers, entry.listener)
                }
                catch (e) {
                    /* ignore stale listener cleanup failures */
                }
            }
        })
    })
}


/**
 * 建立統一的圖層列表(供圖層顯隱面板使用)
 * @param {Array} imageSets
 * @param {Array} pointSets
 * @param {Array} polylineSets
 * @param {Array} polygonSets
 * @param {Array} geojsonSets
 * @param {Array} contourSets
 * @returns {Array} items - [{ name, msg, order, visible, updatePath }]
 */
export function buildItemsList(imageSets, pointSets, polylineSets, polygonSets, geojsonSets, contourSets) {
    let items = []
    let add = (n, m, o, v, p) => {
        items.push({ name: n, msg: m, order: o, visible: v, updatePath: p })
    }
    each(imageSets, (v, k) => {
        add(v.title, v.msg, v.order, v.visible, `imageSets.${k}.visible`)
    })
    each(pointSets, (v, k) => {
        add(v.title, v.msg, v.order, v.visible, `pointSets.${k}.visible`)
    })
    each(polylineSets, (v, k) => {
        add(v.title, v.msg, v.order, v.visible, `polylineSets.${k}.visible`)
    })
    each(polygonSets, (v, k) => {
        add(v.title, v.msg, v.order, v.visible, `polygonSets.${k}.visible`)
    })
    each(geojsonSets, (v, k) => {
        add(v.title, v.msg, v.order, v.visible, `geojsonSets.${k}.visible`)
    })
    each(contourSets, (v, k) => {
        add(v.title, v.msg, v.order, v.visible, `contourSets.${k}.visible`)
    })
    let ord = sortBy(filter(items, (v) => isNumber(v.order)), 'order')
    let rest = filter(items, (v) => !isNumber(v.order))
    return cloneDeep([...ord, ...rest])
}


/**
 * 計算可見圖層數量(純函式)
 * @param {Array} arr - 含 visible 屬性的陣列
 * @returns {Number}
 */
export function countVisible(arr) {
    let i = 0
    each(arr, (v) => {
        if (v.visible) i++
    })
    return i
}