js/publicApi.mjs

/**
 * publicApi.mjs
 * 對外公開的地圖操作方法(panTo、getMapObject 等)
 * 注意:popupPoint / popupFeatureById 因需要 Vue $refs 與 $nextTick,
 *       仍保留於 Vue 元件中,此處僅提取純邏輯計算。
 */
import get from 'lodash-es/get.js'
import cloneDeep from 'lodash-es/cloneDeep.js'
import isNumber from 'lodash-es/isNumber.js'
import each from 'lodash-es/each.js'
import size from 'lodash-es/size.js'
import isarr from 'wsemi/src/isarr.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import isnum from 'wsemi/src/isnum.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import cdbl from 'wsemi/src/cdbl.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import getCentroidMultiPolygon from 'w-gis/src/getCentroidMultiPolygon.mjs'


/**
 * 取得 map 物件(包裝 panTo 使其相容 Leaflet 的 [lat,lng] 格式)。
 * 以 Proxy 包裝而非改寫原 map 實例——就地替換 panTo 會污染共享實例,
 * 使元件內部以原生 [lng,lat] 呼叫的平移被再翻轉一次(座標顛倒)
 * @param {Object} map
 * @returns {Object|null}
 */
export function getMapObject(map) {
    if (!map) return null
    if (!map._wlfProxy) {
        let panToLatLng = (latLng, opts) => {
            if (isarr(latLng) && size(latLng) >= 2) map.panTo([latLng[1], latLng[0]], opts)
            else map.panTo(latLng, opts)
        }
        map._wlfProxy = new Proxy(map, {
            get(target, prop) {
                if (prop === 'panTo') return panToLatLng
                let v = Reflect.get(target, prop, target) //this 一律綁原 map, 避免 proxy receiver 干擾內部實作
                return typeof v === 'function' ? v.bind(target) : v
            },
        })
    }
    return map._wlfProxy
}


/**
 * 計算 panTo 偏移後的新中心座標
 * @param {Object} map - MapLibre 地圖實例
 * @param {Array} latLng - [lat, lng]
 * @param {Object} opt - { ratioHorizontal, ratioVertical, funLatLng }
 * @returns {Array|null} 新的 [lat, lng],或 null 表示不需移動
 */
export function calcPanToCenter(map, latLng, opt) {
    if (!isearr(latLng) || !map) return null
    let ratioHorizontal = get(opt, 'ratioHorizontal'); if (isnum(ratioHorizontal)) ratioHorizontal = cdbl(ratioHorizontal)
    let ratioVertical = get(opt, 'ratioVertical'); if (isnum(ratioVertical)) ratioVertical = cdbl(ratioVertical)
    let funLatLng = get(opt, 'funLatLng')

    let bds = map.getBounds()
    let ne = bds.getNorthEast(); let sw = bds.getSouthWest()
    let lngRange = ne.lng - sw.lng; let latRange = ne.lat - sw.lat
    let lngMin = latLng[1] - lngRange / 2; let lngMax = latLng[1] + lngRange / 2
    let latMin = latLng[0] - latRange / 2; let latMax = latLng[0] + latRange / 2

    let lngNew = null; let latNew = null
    if (isNumber(ratioHorizontal)) {
        let lngMaxNew = lngMax - lngRange * ratioHorizontal; lngNew = (lngMin + lngMaxNew) / 2
    }
    if (isNumber(ratioVertical)) {
        let latMinNew = latMin + latRange * ratioVertical; latNew = (latMinNew + latMax) / 2
    }

    let latLngNew = cloneDeep(latLng)
    if (isNumber(ratioVertical)) latLngNew[0] = latNew
    if (isNumber(ratioHorizontal)) latLngNew[1] = lngNew

    if (isfun(funLatLng)) {
        latLngNew = funLatLng(latLng, { bds, lngRange, latRange, lngMin, lngMax, latMin, latMax, ratioHorizontal, ratioVertical, latLngNew })
    }
    return latLngNew
}


/**
 * 計算折線的中間點座標
 * @param {Array} latLngs
 * @returns {Array|null} [lat, lng]
 */
export function calcPolylineCenter(latLngs) {
    if (!isearr(latLngs)) return null
    let seg = latLngs
    while (isearr(seg[0]) && isearr(seg[0][0])) seg = seg[0] //鑽到 [[lat,lng],...] 線層, 支援 2/3/4 層
    if (!isearr(seg)) return null
    let mid = seg[Math.floor(seg.length / 2)]
    if (!isarr(mid)) return null
    return [mid[0], mid[1]]
}


/**
 * 計算多邊形第一環重心座標
 * @param {Array} latLngs
 * @returns {Array|null} [lat, lng]
 */
export function calcPolygonCenter(latLngs) {
    if (!isearr(latLngs)) return null
    let pt = getCentroidMultiPolygon(latLngs) //正確處理 ringString/polygon/MultiPolygon(含 depth-4), 回傳同序 [lat,lng]
    if (!isearr(pt)) return null
    return pt
}


/**
 * 從 pointSets 中依 id 查找 point 資料
 * @param {Array} pointSets
 * @param {String} targetId
 * @returns {{ foundPt, foundPs, foundPsIndex }}
 */
export function findPointById(pointSets, targetId) {
    let foundPt = null; let foundPs = null; let foundPsIndex = -1
    each(pointSets, (ps, kps) => {
        each(ps.points, (pt) => {
            if (pt.id === targetId) {
                foundPt = pt; foundPs = ps; foundPsIndex = kps; return false
            }
        })
        if (foundPt) return false
    })
    return { foundPt, foundPs, foundPsIndex }
}


/**
 * 解析 feature id 並取得對應資料(折線/多邊形)
 * @param {String} id - 例如 'polylineSet-0' 或 'polygonSet-0'
 * @param {Array} polylineSets
 * @param {Array} polygonSets
 * @returns {{ center, featureData, ownerPath, type } | null}
 */
export function resolveFeatureById(id, polylineSets, polygonSets) {
    if (!isestr(id)) return null
    if (id.startsWith('polylineSet-')) {
        let idx = parseInt(id.replace('polylineSet-', ''), 10)
        let pls = get(polylineSets, idx, null); if (!pls) return null
        let center = calcPolylineCenter(get(pls, 'latLngs', []))
        if (!center) return null
        return { center, featureData: pls, ownerPath: `polylineSets.${idx}`, type: 'polyline' }
    }
    if (id.startsWith('polygonSet-')) {
        let idx = parseInt(id.replace('polygonSet-', ''), 10)
        let pg = get(polygonSets, idx, null); if (!pg) return null
        let center = calcPolygonCenter(get(pg, 'latLngs', []))
        if (!center) return null
        return { center, featureData: pg, ownerPath: `polygonSets.${idx}`, type: 'polygon' }
    }
    return null
}