splitAndProcGeoJSON.mjs

import get from 'lodash-es/get.js'
import each from 'lodash-es/each.js'
import map from 'lodash-es/map.js'
import isEqual from 'lodash-es/isEqual.js'
import size from 'lodash-es/size.js'
import cloneDeep from 'lodash-es/cloneDeep.js'
import isearr from 'wsemi/src/isearr.mjs'
import isobj from 'wsemi/src/isobj.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import flattenMultiPolygon from './flattenMultiPolygon.mjs'


/**
 * 建立空的分類結果物件
 *
 * @returns {Object} 回傳包含 points、lines、polygons 三個空 FeatureCollection 的物件
 */
function createEmptyResult() {
    return {
        points: { type: 'FeatureCollection', features: [] },
        lines: { type: 'FeatureCollection', features: [] },
        polygons: { type: 'FeatureCollection', features: [] },
    }
}


/**
 * 修復 Polygon ring 閉合(首尾座標相同)
 *
 * @param {Array} ring 輸入 ring 座標陣列
 * @returns {Array} 回傳已閉合的 ring 座標陣列
 */
function ensureRingClosed(ring) {
    if (!isearr(ring)) {
        return ring
    }
    let n = size(ring)
    if (n < 2) {
        return ring
    }
    let first = ring[0]
    let last = ring[n - 1]
    if (!isEqual(first, last)) {
        ring = [...ring, first]
    }
    return ring
}


/**
 * 修復 Polygon 的所有 ring 閉合
 *
 * @param {Array} coordinates Polygon 的 coordinates(二維陣列,每個元素為一個 ring)
 * @returns {Array} 回傳已修復閉合的 coordinates
 */
function fixClosePolygonCoords(coordinates) {
    return map(coordinates, (ring) => ensureRingClosed(ring))
}


/**
 * 修復 MultiPolygon 的所有 ring 閉合
 *
 * @param {Array} coordinates MultiPolygon 的 coordinates(三維陣列)
 * @returns {Array} 回傳已修復閉合的 coordinates
 */
function fixCloseMultiPolygonCoords(coordinates) {
    return map(coordinates, (polygon) => fixClosePolygonCoords(polygon))
}


/**
 * 將單一 Feature 依幾何類型分類到結果物件中
 *
 * @param {Object} feature 輸入 GeoJSON Feature
 * @param {Object} result 輸入分類結果物件(包含 points、lines、polygons)
 */
function classifyFeature(feature, result) {

    // 安全取得 geometry
    let geometry = get(feature, 'geometry')
    if (!iseobj(geometry)) {
        return
    }

    let type = get(geometry, 'type', '')
    let properties = get(feature, 'properties', {}) || {}
    let id = get(feature, 'id')

    // GeometryCollection:遞迴拆解為多個獨立 Feature,各自繼承原始 properties
    if (type === 'GeometryCollection') {
        let geometries = get(geometry, 'geometries', [])
        each(geometries, (subGeom) => {
            let subFeature = {
                type: 'Feature',
                properties: cloneDeep(properties),
                geometry: cloneDeep(subGeom),
            }
            if (id !== undefined) {
                subFeature.id = id
            }
            classifyFeature(subFeature, result)
        })
        return
    }

    // 建立輸出 Feature(深拷貝以避免汙染原始資料)
    let outFeature = {
        type: 'Feature',
        properties: cloneDeep(properties),
        geometry: cloneDeep(geometry),
    }
    if (id !== undefined) {
        outFeature.id = id
    }

    // 依幾何類型分類
    switch (type) {

    case 'Point':
    case 'MultiPoint':
        result.points.features.push(outFeature)
        break

    case 'LineString':
    case 'MultiLineString':
        result.lines.features.push(outFeature)
        break

    case 'Polygon':
        // 修復 ring 閉合
        outFeature.geometry.coordinates = fixClosePolygonCoords(outFeature.geometry.coordinates)
        result.polygons.features.push(outFeature)
        break

    case 'MultiPolygon':
        // 修復 ring 閉合
        outFeature.geometry.coordinates = fixCloseMultiPolygonCoords(outFeature.geometry.coordinates)
        result.polygons.features.push(outFeature)
        break

    default:
        // 未知幾何類型跳過
        break
    }
}


/**
 * 將 GeoJSON 資料拆分成依幾何類型分類的多個 FeatureCollection
 *
 * 此函數的設計目的是針對 MapLibre GL JS 的 layer type 限制,
 * 將混合不同幾何類型的 GeoJSON 資料預先拆分成:
 *   - points:包含 Point / MultiPoint 的 FeatureCollection
 *   - lines:包含 LineString / MultiLineString 的 FeatureCollection
 *   - polygons:包含 Polygon / MultiPolygon 的 FeatureCollection
 *
 * 額外處理:
 *   1. GeometryCollection 會被遞迴拆解成獨立的 Feature,properties 繼承父 Feature
 *   2. Polygon / MultiPolygon 的 ring 自動修復閉合(首尾座標相同)
 *   3. 支援多種輸入格式:FeatureCollection、Feature、裸 Geometry、JSON 字串
 *   4. 深拷貝輸出,不汙染原始資料
 *   5. 空輸入 / 無效輸入回傳空的分類結果
 *
 * @param {Object|String|null} geoIn 輸入 GeoJSON 資料,可為 FeatureCollection、Feature、裸 Geometry 物件或 JSON 字串
 * @returns {Object} 回傳分類結果物件,結構為 { points: FeatureCollection, lines: FeatureCollection, polygons: FeatureCollection }
 *
 * @example
 *
 * // 混合類型 FeatureCollection
 * let input = {
 *     type: 'FeatureCollection',
 *     features: [
 *         { type: 'Feature', properties: { name: 'pt' }, geometry: { type: 'Point', coordinates: [121, 25] } },
 *         { type: 'Feature', properties: { name: 'ls' }, geometry: { type: 'LineString', coordinates: [[121, 25], [122, 26]] } },
 *         { type: 'Feature', properties: { name: 'pg' }, geometry: { type: 'Polygon', coordinates: [[[0, 0], [4, 0], [4, 4], [0, 4], [0, 0]]] } },
 *     ],
 * }
 * let result = splitGeoJSON(input)
 * console.log(result.points.features.length)   // 1
 * console.log(result.lines.features.length)    // 1
 * console.log(result.polygons.features.length) // 1
 *
 */
function splitGeoJSON(geoIn) {

    // 建立空的分類結果
    let result = createEmptyResult()

    // 空值 / 未定義 / 無效類型直接回傳空結果
    if (geoIn === null || geoIn === undefined) {
        return result
    }

    // 支援 JSON 字串輸入
    if (isestr(geoIn)) {
        try {
            geoIn = JSON.parse(geoIn)
        }
        catch (e) {
            return result
        }
    }

    // 非物件直接回傳空結果
    if (!isobj(geoIn)) {
        return result
    }

    let type = get(geoIn, 'type', '')

    // 輸入為 FeatureCollection
    if (type === 'FeatureCollection') {
        let features = get(geoIn, 'features', [])
        if (!isearr(features)) {
            return result
        }
        each(features, (feature) => {
            classifyFeature(feature, result)
        })
        return result
    }

    // 輸入為 Feature
    if (type === 'Feature') {
        classifyFeature(geoIn, result)
        return result
    }

    // 輸入為裸 Geometry(Point、LineString、Polygon 等)
    let geometryTypes = [
        'Point', 'MultiPoint',
        'LineString', 'MultiLineString',
        'Polygon', 'MultiPolygon',
        'GeometryCollection',
    ]
    if (geometryTypes.indexOf(type) >= 0) {
        // 包裝成 Feature 後分類
        let feature = {
            type: 'Feature',
            properties: {},
            geometry: geoIn,
        }
        classifyFeature(feature, result)
        return result
    }

    // 不認得的類型,回傳空結果
    return result
}


/**
 * 處理特殊 Polygon 數據,讓 MapLibre GL JS 可正確渲染
 *
 * 此函數的設計目的是處理 splitGeoJSON 回傳的 polygons FeatureCollection 中,
 * 含有「多層套疊 ring」的 Polygon / MultiPolygon 數據。
 *
 * Leaflet 利用 SVG 的 evenodd fill rule 可直接繪製多層套疊的 ring,
 * 但 MapLibre GL JS 僅依賴 winding order(外環 CCW、洞環 CW)來決定填色/挖洞。
 * 因此,必須將多層套疊結構轉換為符合 RFC 7946 的標準 MultiPolygon 格式。
 *
 * 處理邏輯:
 *   1. 遍歷所有 Feature
 *   2. 對於 Polygon 類型:
 *      - ring 數量 <= 2 時,使用 flattenMultiPolygon 修正 winding order
 *      - ring 數量 > 2 時(可能為多層套疊),使用 flattenMultiPolygon 搭配
 *        supposeType='ringStrings' 模式,透過 XOR 運算將套疊結構轉為標準 MultiPolygon
 *   3. 對於 MultiPolygon 類型:
 *      - 將每個子 polygon 各自透過 flattenMultiPolygon 處理後合併
 *   4. 處理完畢後,若 MultiPolygon 只含一個 polygon,降級為 Polygon 類型
 *   5. 深拷貝輸出,不汙染原始資料
 *   6. 保留所有 properties
 *
 * @param {Object|null} polygonsFC 輸入 polygons FeatureCollection(來自 splitGeoJSON 的 polygons 欄位)
 * @returns {Object} 回傳處理後的 FeatureCollection
 *
 * @example
 *
 * // 三層套疊 Polygon
 * let input = {
 *     type: 'FeatureCollection',
 *     features: [{
 *         type: 'Feature',
 *         properties: { name: '三層' },
 *         geometry: {
 *             type: 'Polygon',
 *             coordinates: [
 *                 [[0, 0], [20, 0], [20, 20], [0, 20], [0, 0]],
 *                 [[4, 4], [16, 4], [16, 16], [4, 16], [4, 4]],
 *                 [[8, 8], [12, 8], [12, 12], [8, 12], [8, 8]],
 *             ],
 *         },
 *     }],
 * }
 * let result = procSpecPolygon(input)
 * console.log(result.features[0].geometry.type) // 'MultiPolygon'
 *
 */
function procSpecPolygon(polygonsFC) {

    // 建立空結果
    let emptyResult = { type: 'FeatureCollection', features: [] }

    // 無效輸入
    if (polygonsFC === null || polygonsFC === undefined) {
        return emptyResult
    }
    if (!iseobj(polygonsFC)) {
        return emptyResult
    }

    let features = get(polygonsFC, 'features')
    if (!isearr(features)) {
        return { type: 'FeatureCollection', features: [] }
    }

    let outFeatures = []

    each(features, (feature) => {

        // 深拷貝避免汙染原始資料
        let feat = cloneDeep(feature)

        let geometry = get(feat, 'geometry')
        if (!iseobj(geometry)) {
            // 無 geometry 的 Feature 直接保留
            outFeatures.push(feat)
            return
        }

        let type = get(geometry, 'type', '')
        let coordinates = get(geometry, 'coordinates', [])

        if (type === 'Polygon') {
            // 處理 Polygon
            let processed = processPolygonCoords(coordinates)
            feat.geometry = processed
        }
        else if (type === 'MultiPolygon') {
            // 處理 MultiPolygon:每個子 polygon 各自處理後合併
            let allPolygons = []
            each(coordinates, (polygonCoords) => {
                let processed = processPolygonCoords(polygonCoords)
                if (processed.type === 'MultiPolygon') {
                    each(processed.coordinates, (pg) => {
                        allPolygons.push(pg)
                    })
                }
                else if (processed.type === 'Polygon') {
                    allPolygons.push(processed.coordinates)
                }
            })

            // 依結果數量決定類型
            if (size(allPolygons) === 1) {
                feat.geometry = { type: 'Polygon', coordinates: allPolygons[0] }
            }
            else if (size(allPolygons) > 1) {
                feat.geometry = { type: 'MultiPolygon', coordinates: allPolygons }
            }
        }

        outFeatures.push(feat)
    })

    return { type: 'FeatureCollection', features: outFeatures }
}


/**
 * 處理單一 Polygon 的 coordinates
 *
 * @param {Array} coordinates Polygon 的 coordinates(二維陣列,每個元素為一個 ring)
 * @returns {Object} 回傳 GeoJSON Geometry 物件(Polygon 或 MultiPolygon)
 */
function processPolygonCoords(coordinates) {

    if (!isearr(coordinates)) {
        return { type: 'Polygon', coordinates }
    }

    let ringCount = size(coordinates)

    if (ringCount === 0) {
        return { type: 'Polygon', coordinates }
    }

    // 使用 flattenMultiPolygon 處理
    // ring 數量 > 2 時可能為多層套疊(Leaflet evenodd 風格)
    // 使用 ringStrings 模式讓 polybooljs 做 XOR 運算
    let supposeType = ringCount > 2 ? 'ringStrings' : 'ringStrings'
    let pgsResult

    try {
        pgsResult = flattenMultiPolygon(coordinates, { supposeType })
    }
    catch (e) {
        // fallback:若 flattenMultiPolygon 失敗,原樣回傳
        return { type: 'Polygon', coordinates }
    }

    // flattenMultiPolygon 回傳的是 MultiPolygon coordinates(depth=3)
    if (!isearr(pgsResult)) {
        return { type: 'Polygon', coordinates }
    }

    let pgCount = size(pgsResult)
    if (pgCount === 0) {
        return { type: 'Polygon', coordinates }
    }
    else if (pgCount === 1) {
        return { type: 'Polygon', coordinates: pgsResult[0] }
    }
    else {
        return { type: 'MultiPolygon', coordinates: pgsResult }
    }
}


/**
 * 將任意 GeoJSON 資料拆分為依幾何類型分類的多個 FeatureCollection,
 * 並對其中的 Polygon 數據進行特殊處理(多層套疊 ring 轉換為標準 MultiPolygon)
 *
 * 此函數結合了 splitGeoJSON 與 procSpecPolygon 的功能:
 *   1. splitGeoJSON:將混合幾何類型的 GeoJSON 拆分為 points / lines / polygons
 *   2. procSpecPolygon:將 polygons 中含有多層套疊 ring 的 Polygon/MultiPolygon
 *      轉換為符合 RFC 7946 規範的標準格式,確保 winding order 正確
 *
 * 此函數設計為方便日後 MapLibre GL JS 渲染使用的一站式前處理函數。
 *
 * @param {Object|String|null} geoIn 輸入 GeoJSON 資料,可為 FeatureCollection、Feature、裸 Geometry 物件或 JSON 字串
 * @returns {Object} 回傳分類結果物件,結構為 { points: FeatureCollection, lines: FeatureCollection, polygons: FeatureCollection }
 *
 * @example
 *
 * let input = {
 *     type: 'FeatureCollection',
 *     features: [
 *         { type: 'Feature', properties: { name: 'pt' }, geometry: { type: 'Point', coordinates: [121, 25] } },
 *         { type: 'Feature', properties: { name: 'ls' }, geometry: { type: 'LineString', coordinates: [[121, 25], [122, 26]] } },
 *         {
 *             type: 'Feature',
 *             properties: { name: '三層' },
 *             geometry: {
 *                 type: 'Polygon',
 *                 coordinates: [
 *                     [[0, 0], [20, 0], [20, 20], [0, 20], [0, 0]],
 *                     [[4, 4], [16, 4], [16, 16], [4, 16], [4, 4]],
 *                     [[8, 8], [12, 8], [12, 12], [8, 12], [8, 8]],
 *                 ],
 *             },
 *         },
 *     ],
 * }
 * let result = splitAndProcGeoJSON(input)
 * console.log(result.points.features.length)   // 1
 * console.log(result.lines.features.length)    // 1
 * console.log(result.polygons.features.length) // 1(polygon 已被處理為 MultiPolygon)
 *
 */
function splitAndProcGeoJSON(geoIn) {

    // 步驟一:使用 splitGeoJSON 拆分幾何類型
    let splitted = splitGeoJSON(geoIn)

    // 步驟二:使用 procSpecPolygon 處理 polygon 數據
    let processedPolygons = procSpecPolygon(splitted.polygons)

    // 回傳結果,points 與 lines 不需特殊處理
    return {
        points: splitted.points,
        lines: splitted.lines,
        polygons: processedPolygons,
    }
}


export default splitAndProcGeoJSON