// @unocss-ignore

import type { Ref } from 'vue'
import type mapboxgl from 'mapbox-gl'
import { toRaw } from 'vue'
import { appendScript, appendStylesheet } from '@/utils/dom'

// Mapbox accessToken
export const accessToken = 'pk.eyJ1IjoiaWZ6bSIsImEiOiJjamswc3M1a2gwYWEwM3Zxa3ZsMjh3djkwIn0.h93-XtdRdtoC1KbVv_ZIow'

// 天地图密钥
export const tk = 'aa0ccd36f2dbb86dbb16cbf63f0034a6'

// 自定义图层 host 注意：renderjs里面不能引用带有 import.env 的任何文件，否则会编译报错
// const host = 'https://hntq.yiring.com/gis'
const host = 'https://foxgis.app.yiring.com'

// 构建天地图图层地址
export function buildTdtTileUrl(id: string,layer:string) {
    return `http://t0.tianditu.gov.cn/${id}/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=${layer}&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=${tk}`
    return `${host}/api/tdt/cache/${id}/{z}/{y}/{x}/${tk}?v=2022`
}

/**
 * 地图默认中心点
 */
export const DEFAULT_MAP_CENTER: [number, number] = [113.01798803033034, 28.098790979279613]
/**
 * 地图默认缩放级别
 */
export const DEFAULT_MAP_ZOOM = 11.5
/**
 * 地图样式
 */
export const defaultStyle: mapboxgl.Style = {
    version: 8,
    zoom: DEFAULT_MAP_ZOOM,
    center: [...DEFAULT_MAP_CENTER],
    sprite: `${host}/api/sprites/drainage/sprite`,
    glyphs: `${host}/api/fonts/{fontstack}/{range}.pbf`,
    sources: {
        'wms-vec_w-source': {
            type: 'raster',
            tiles: [buildTdtTileUrl('vec_w','vec')],
            tileSize: 256,
            maxzoom: 18,
        },
        'wms-cva_w-source': {
            type: 'raster',
            tiles: [buildTdtTileUrl('cva_w','cva')],
            tileSize: 256,
            maxzoom: 18,
        },
        // 'wms-cia_w-source': {
        //     type: 'raster',
        //     tiles: [buildTdtTileUrl('cia', tk)],
        //     tileSize: 256,
        //     maxzoom: 18,
        // },
        'wms-img_w-source': {
            type: 'raster',
            tiles: [buildTdtTileUrl('img_w','img')],
            tileSize: 256,
            maxzoom: 18,
        },
    },
    layers: [
        // 占位图层
        {
            id: 'background',
            type: 'background',
            layout: { visibility: 'visible' },
            paint: { 'background-color': '#EFF3FA' },
        },
        // 默认图层 - 标注底图
        {
            id: 'wms-vec_w-layer',
            type: 'raster',
            source: 'wms-vec_w-source',
            layout: { visibility: 'visible' },
        },
        {
            id: 'wms-cva_w-layer',
            type: 'raster',
            source: 'wms-cva_w-source',
            layout: { visibility: 'visible' },
        },
        // // 默认图层 - 影像底图
        // {
        //     id: 'wms-img_w-layer',
        //     type: 'raster',
        //     source: 'wms-img_w-source',
        //     layout: { visibility: 'visible' },
        // },
        // 天空图层
        {
            id: 'sky',
            type: 'sky',
            paint: {
                'sky-type': 'gradient',
                'sky-gradient': [
                    'interpolate',
                    ['linear'],
                    ['sky-radial-progress'],
                    0.8,
                    'rgba(135, 206, 235, 1.0)',
                    1,
                    'rgba(0,0,0,0.1)',
                ],
                'sky-gradient-center': [0, 0],
                'sky-gradient-radius': 90,
                'sky-opacity': ['interpolate', ['exponential', 0.1], ['zoom'], 5, 0, 22, 1],
            },
        },
        // 色斑渲染占位图层
        {
            id: 'fill-placeholder',
            type: 'background',
            layout: { visibility: 'none' },
        },

        // 遮罩占位图层
        {
            id: 'mask-placeholder',
            type: 'background',
            layout: { visibility: 'none' },
        },

        // 数值渲染占位图层
        {
            id: 'symbol-placeholder',
            type: 'background',
            layout: { visibility: 'none' },
        },
    ],
}

export function buildTileJsonURL(tileset: string) {
    return `${host}/api/tilesets/${tileset}/tilejson`
}

export function buildJsonAssetURL(tileset: string) {
    return `${host}/api/assets/json/${tileset}.json`
}

interface LayerOption {
    label: string
    value: string
    image: string
    active?: boolean
}

/**
 * 图层控件
 * 目前仅支持天地图图层
 */
class LayerControl implements mapboxgl.IControl {
    map: mapboxgl.Map | undefined
    container: HTMLElement | undefined
    tk: string
    layers?: LayerOption[]

    constructor(options: Recordable) {
        this.tk = options.tk
        this.layers = options.layers
    }

    onAdd(map: mapboxgl.Map) {
        this.map = map
        this.container = document.createElement('div')
        this.container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group mapbox-control'

        const layersWrapHtml: string[] = []
        if (this.layers) {
            layersWrapHtml.push('<div class="mapboxgl-ctrl-layer-wrap">')
            this.layers.forEach((layer) => {
                layersWrapHtml.push(`
                    <div class="mapboxgl-ctrl-layer ${layer.active ? 'active' : ''}" data-layer-ids="${layer.value}">
                        <img src="${layer.image}" alt="${layer.label}" />
                        <span>${layer.label}</span>
                    </div>
                `)

                for (const id of layer.value.split(',')) {
                    map.addLayer(
                        {
                            id: `t_${id}`,
                            type: 'raster',
                            source: {
                                type: 'raster',
                                tiles: [buildTdtTileUrl(id, this.tk)],
                                tileSize: 256,
                                maxzoom: 18,
                            },
                            layout: { visibility: layer.active ? 'visible' : 'none' },
                        },
                        'sky',
                    )
                }
            })
            layersWrapHtml.push('</div>')
        }

        this.container.innerHTML = `
            <button type="button" class="mapboxgl-ctrl-layer-button" title="地图样式选择">
                <span class="mapboxgl-ctrl-icon" aria-hidden="true" title="Layer"></span>
                ${layersWrapHtml.join('')}
            </button>
        `
        this.container.addEventListener('click', (e) => {
            const target = e.target as HTMLElement
            const parent = target.parentElement as HTMLElement
            if (parent.classList.contains('mapboxgl-ctrl-layer')) {
                const ids = parent.getAttribute('data-layer-ids') as string
                const list = parent.parentElement?.children as HTMLCollection
                for (let i = 0; i < list.length; i++) {
                    const item = list[i] as HTMLElement
                    if (item.classList.contains('mapboxgl-ctrl-layer')) {
                        if (item.getAttribute('data-layer-ids') === ids) {
                            item.classList.toggle('active')
                        } else {
                            item.classList.remove('active')
                        }
                    }
                }

                this.layers?.forEach((layer) => {
                    for (const id of layer.value.split(',')) {
                        map.setLayoutProperty(`t_${id}`, 'visibility', 'none')
                    }
                })
                if (parent.classList.contains('active')) {
                    for (const id of ids.split(',')) {
                        map.setLayoutProperty(`t_${id}`, 'visibility', 'visible')
                    }
                }
                return
            }

            this.container?.querySelector('button')?.classList.toggle('-active')
        })

        return this.container
    }

    onRemove() {
        this.container?.parentNode?.removeChild(this.container)
        this.map = undefined
    }
}

/**
 * 经纬度坐标拾取控件
 */
class GeolocateInfoControl implements mapboxgl.IControl {
    map: mapboxgl.Map | undefined
    container: HTMLElement | undefined

    toText(lngLat: mapboxgl.LngLat) {
        return `经度：${lngLat.lng.toFixed(6)}, 纬度：${lngLat.lat.toFixed(6)}`
    }

    onAdd(map: mapboxgl.Map) {
        this.map = map
        this.container = document.createElement('div')
        this.container.className = 'mapboxgl-ctrl mapboxgl-ctrl-lnglat'
        this.container.textContent = this.toText(map.getCenter())

        map.on('mousemove', (e) => {
            const element = this.container as HTMLElement
            element.textContent = this.toText(e.lngLat)
        })

        return this.container
    }

    onRemove() {
        this.container?.parentNode?.removeChild(this.container)
        this.map = undefined
    }
}

/**
 * 地图复位控件
 */
class ResetControl implements mapboxgl.IControl {
    map: mapboxgl.Map | undefined
    container: HTMLElement | undefined
    options: mapboxgl.FlyToOptions

    constructor(options: mapboxgl.FlyToOptions) {
        this.options = options
    }

    onAdd(map: mapboxgl.Map) {
        this.map = map
        this.container = document.createElement('div')
        this.container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group mapbox-control'
        this.container.innerHTML = `
            <button type="button" class="mapboxgl-ctrl-reset-button mapboxgl-ctrl-reset" title="地图复位">
                <span class="mapboxgl-ctrl-icon" aria-hidden="true" title="Reset"></span>
            </button>
        `

        this.container.addEventListener('click', () => {
            // alert('map: ' + JSON.stringify(this.map.getCenter()))
            this.map?.flyTo(this.options)
        })

        return this.container
    }

    onRemove() {
        this.container?.parentNode?.removeChild(this.container)
        this.map = undefined
    }
}

/**
 * 测量控件
 */
class MeasureControl implements mapboxgl.IControl {
    map: mapboxgl.Map | undefined
    container: HTMLElement | undefined

    onAdd(map: mapboxgl.Map) {
        this.map = map
        this.container = document.createElement('div')
        this.container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group mapbox-control'
        this.container.innerHTML = `
            <button type="button" class="mapboxgl-ctrl-reset-button" title="面积计算">
                <span class="iconify" data-icon="carbon:area-custom" data-width="24" data-height="24"></span>
            </button>
        `

        this.container.addEventListener('click', () => {
            this.container?.querySelector('button')?.classList.toggle('-active')
            console.log('TODO: 测量面积功能暂未实现, 敬请期待...')
        })

        return this.container
    }

    onRemove() {
        this.container?.parentNode?.removeChild(this.container)
        this.map = undefined
    }
}

/**
 * 风场控件
 */
export class WindyControl implements mapboxgl.IControl {
    map: mapboxgl.Map | undefined
    container: HTMLElement | undefined
    onOpen: () => Promise<void>
    onClose: Function

    constructor(options: Recordable) {
        this.onOpen = options.onOpen
        this.onClose = options.onClose
    }

    onAdd(map: mapboxgl.Map) {
        this.map = map
        this.container = document.createElement('div')
        this.container.className = 'mapboxgl-ctrl mapboxgl-ctrl-group mapbox-control'
        this.container.innerHTML = `
            <button type="button" class="mapboxgl-ctrl-reset-button" title="风场">
                <span class="iconify" data-icon="ph:wind-light" data-width="24" data-height="24"></span>
            </button>
        `

        this.container.addEventListener('click', () => {
            const button = this.container?.querySelector('button')
            if (!button?.classList.contains('-active')) {
                this.onOpen().then(() => {
                    button?.classList.add('-active')
                })
            } else {
                this.onClose()
                button?.classList.remove('-active')
            }
        })

        return this.container
    }

    onRemove() {
        this.container?.parentNode?.removeChild(this.container)
        this.map = undefined
    }
}

export interface MapOptions {
    control?: MapControlOptions
    style?: mapboxgl.Style | Recordable
}

export interface MapControlOptions {
    // 导航控件
    navigation?: {
        zoom?: boolean
        compass?: boolean
    }
    // 图层控制
    layer?:
    | boolean
    | {
        tk: string
        layers: LayerOption[]
    }
    // 重置控件
    reset?: boolean | mapboxgl.FlyToOptions
    // 全屏控件
    fullscreen?: boolean
    // 比例尺
    scale?: boolean
    // 经纬度拾取
    info?: boolean
    // 定位控件
    geolocate?: { initial?: boolean; zoom?: number; fn?: any }
    // 面积测算控件
    measure?: boolean
    // 距离测算控件
    ruler?: boolean
    // 风场控件
    windy?: boolean
    // 绘制控件
    draw?: boolean
}

/**
 * 加载地图控件
 * @param map 地图实例
 * @param options 控件配置
 */
export function loadMapControl(mapboxgl: any, map: mapboxgl.Map, options?: MapControlOptions) {
    if (!options) {
        return
    }

    // 导航控件
    if (options.navigation) {
        map.addControl(
            new mapboxgl.NavigationControl({
                showCompass: options.navigation.compass,
                showZoom: options.navigation.zoom,
            }),
        )
    }

    // 全屏控件
    if (options.fullscreen) {
        map.addControl(new mapboxgl.FullscreenControl())
    }

    // 地图复位控件
    if (options.reset) {
        if (typeof options.reset === 'boolean') {
            map.addControl(
                new ResetControl({
                    zoom: map.getZoom(),
                    pitch: map.getPitch(),
                    bearing: map.getBearing(),
                    center: map.getCenter(),
                }),
            )
        } else {
            map.addControl(new ResetControl(options.reset))
        }
    }

    // 图层选择控件
    if (options.layer) {
        if (typeof options.layer === 'boolean') {
            map.addControl(
                new LayerControl({
                    tk,
                    layers: [
                        {
                            label: '影像',
                            value: 'img,cia',
                            image: `${host}/api/assets/images/img_w.png`,
                        },
                        {
                            label: '矢量',
                            value: 'vec,cva',
                            image: `${host}/api/assets/images/vec_w.png`,
                            active: true,
                        },
                        {
                            label: '地形',
                            value: 'ter,cta',
                            image: `${host}/api/assets/images/ter_w.png`,
                        },
                    ],
                }),
            )
        } else {
            map.addControl(new LayerControl(options.layer))
        }
    }

    // 面积测量控件
    if (options.measure) {
        map.addControl(new MeasureControl())
    }

    // 经纬度信息控件
    if (options.info) {
        map.addControl(new GeolocateInfoControl(), 'bottom-left')
    }

    // 比例尺控件
    if (options.scale) {
        map.addControl(new mapboxgl.ScaleControl(), 'bottom-left')
    }

    // 定位
    if (options.geolocate) {
        const geolocate = new mapboxgl.GeolocateControl({
            fitBoundsOptions: {
                maxZoom: options.geolocate.zoom || 15,
                duration: 0,
            },
            positionOptions: {
                enableHighAccuracy: true,
                timeout: 30000,
            },
            trackUserLocation: false,
            showUserHeading: true,
        })
        // #ifdef APP-PLUS
        geolocate.geolocation = options.geolocate
        // #endif
        map.addControl(geolocate, 'bottom-right')

        // 初始化定位
        if (options.geolocate.initial) {
            // 由于定位控件初始化需要时间，所以延迟 0.5 秒后再触发定位
            setTimeout(() => {
                geolocate.trigger()
            }, 500)
        }
    }
}

export function toPointGeoJSON(
    data?: ({ lon: number; lat: number } & Recordable)[],
): GeoJSON.FeatureCollection<GeoJSON.Geometry, GeoJSON.GeoJsonProperties> {
    return {
        type: 'FeatureCollection',
        features: !data
            ? []
            : data.map((item) => {
                return {
                    type: 'Feature',
                    properties: {
                        ...item,
                    },
                    geometry: {
                        type: 'Point',
                        coordinates: [item.lon, item.lat],
                    },
                }
            }),
    }
}

export function createPopupHtml(entry: Recordable, mapping: Recordable) {
    const htmls: string[] = ['<div class="mapboxgl-custom-popup">']

    for (const key in entry) {
        const lable = mapping[key]
        if (!lable) {
            continue
        }

        const value = entry[key]
        htmls.push(`
            <div class="popup-row">
                <span class="popup-title">${lable}：</span>
                ${value}
            </div>
        `)
    }

    htmls.push('</div>')
    return htmls.join('')
}

/**
 * 加载地图依赖库
 */
export function loadMapboxLibs() {
    const id = 'mapbox-gl'
    const version = '2.15.0'
    const resource = `static/js/mapbox-gl-js/${version}/${id}`
    return Promise.all([
        appendScript(id, `${resource}.js`),
        appendStylesheet(id, `${resource}.css`),
        appendStylesheet(id, `${resource}.extra.css`),
    ])
}

export const getMapInstance = (ref: Ref<any>): mapboxgl.Map => toRaw(ref.value?.map) as mapboxgl.Map
export function getGeoJSONSource(map: mapboxgl.Map, sourceName: string): mapboxgl.GeoJSONSource {
    return map.getSource(sourceName) as mapboxgl.GeoJSONSource
}
export function getImageSource(map: mapboxgl.Map, sourceName: string): mapboxgl.ImageSource {
    return map.getSource(sourceName) as mapboxgl.ImageSource
}

export const EmptyImage =
    'data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQImWNgYGBgAAAABQABh6FO1AAAAABJRU5ErkJggg=='

export function sendRequest(url: string, method = 'GET', data = {}) {
    return new Promise((resolve, reject) => {
        const uri = new URL(url)
        const xhr = new XMLHttpRequest()
        xhr.open(method, uri)
        xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
        xhr.onload = function () {
            if (xhr.readyState === XMLHttpRequest.DONE) {
                if (xhr.status === 200) {
                    const result = JSON.parse(xhr.responseText)
                    if (result.status === 200) {
                        resolve(result)
                    } else {
                        reject(new Error(xhr.responseText))
                    }
                } else {
                    reject(new Error(JSON.stringify({ status: xhr.status, message: xhr.statusText })))
                }
            }
        }
        xhr.onerror = function () {
            reject(new Error(JSON.stringify({ status: 502, message: '网络错误' })))
        }
        xhr.send(JSON.stringify(data))
        // console.debug('[SendRequest]', uri, method, data)
    })
}

/**
 * Mapbox 组件配置参数类型定义
 */
export interface MapboxConfig {
    /**
     * 地图行政区域编码，最小支持到区县级别
     * @example 430000
     * @default ''
     * FIEXD: 目前仅支持湖南省，其他省份暂不支持
     */
    region?: string
    /**
     * 自动飞行到区域中心
     * @default true
     */
    autoFlyToRegionCenter?: boolean
    /**
     * Mapbox 构造函数参数
     * @link https://docs.mapbox.com/mapbox-gl-js/api/map/
     */
    options?: Partial<mapboxgl.MapboxOptions>
    /**
     * Mapbox 地图样式
     * @link https://docs.mapbox.com/mapbox-gl-js/style-spec/
     */
    style?: Partial<mapboxgl.Style>
    /**
     * 地图资源/数据源来源或归属描述信息
     */
    attribution?: {
        /**
         * 文本
         * @example 湖南省气象台
         * @default ''
         */
        text: string
        /**
         * 位置, 默认为 'bottom-right'
         * @example 'bottom-left', 'bottom-right'
         * @default 'bottom-right'
         */
        align?: 'bottom-left' | 'bottom-right'
    }
    /**
     * 扩展配置
     */
    extra?: {
        control?: MapControlOptions
    }

    // =============== 自定义事件 =================
    /**
     * 地图加载完成事件
     * @param data 事件数据, 由视图层在 Map 加载完成后传入的可序列化 JSON 数据
     */
    onLoaded?: (data: Recordable) => void

    /**
     * 监听请求数据源的请求回执
     * @param id 数据源 ID
     * @param url 请求地址
     * @param data 请求回来的数据（默认情况下将过滤掉一些大字段或数据集，例如：jsonObject、liveVos）
     */
    onSourceRequestHandle?: (data: { id: string; url: string; data: Recordable }) => void

    /**
     * 监听地图事件
     * 可以用来处理地图点击事件、自定义事件等
     * @param e 事件对象
     */
    onMapEvent?: (e: { type: 'click' | 'custom' | string; name: string; data: Recordable }) => void
}

/**
 * Mapbox 组件实例类型定义
 */
export interface MapboxInstance {
    /**
     * 设置地图行政区域编码，例如: 430000, 设置后地图的遮罩层将仅包含该区域的边界
     * FIXED: 暂时仅支持湖南省
     * @param region 地图显示行政区域编码
     * @param flyTo 是否飞行到区域中心
     */
    setRegion: (region: string, flyTo?: boolean) => void
    /**
     * 设置地图组件配置
     * @param config 地图配置
     */
    setConfig: (config: Partial<MapboxConfig>) => void
    /**
     * 地图组件是否准备好了
     */
    isReady: ComputedRef<boolean>

    /**
     * 监听地图事件
     * @param type 事件类型
     * @param layerId 图层 ID
     * @param listener 事件监听器
     * @link https://docs.mapbox.com/mapbox-gl-js/api/map/#map#on
     */
    on: (type: string, layerId: string, listener: (...args: any[]) => void) => void
    /**
     * 飞行到指定位置
     * @param options 飞行参数
     * @link https://docs.mapbox.com/mapbox-gl-js/api/map/#map#flyto
     */
    flyTo: (options: mapboxgl.FlyToOptions) => void
    /**
     * 添加数据源
     * @param id 数据源 ID
     * @param source 数据源
     * @link https://docs.mapbox.com/mapbox-gl-js/api/map/#map#addsource
     */
    addSource: (id: string, source: mapboxgl.AnySourceData) => void
    /**
     * 移除数据源
     * @param id 数据源 ID
     * @link https://docs.mapbox.com/mapbox-gl-js/api/map/#map#removesource
     */
    removeSource: (id: string) => void
    /**
     * 设置 GeoJSON 数据源，如果数据源不存在则创建
     * @param id 数据源 ID
     * @param url 数据源 URL，通常是一个后端的数据查询接口（色斑图查询接口、站点数据查询接口）
     * @param map 数据映射函数，用于处理后端返回的数据，将其转换为 GeoJSON 数据
     * @param filters 过滤对象，用于过滤响应 onSourceRequestHandle 事件时的数据，默认过滤掉 jsonObject、liveVos 字段
     * @link https://docs.mapbox.com/mapbox-gl-js/api/map/#map#addsource
     */
    setGeoJSONSourceForRequest: (
        id: string,
        url: string | GeoJSONSourceDataUrlParams,
        map?: (
            data: Recordable,
            util: HandlerUtilType,
        ) => GeoJSON.Feature<GeoJSON.Geometry> | GeoJSON.FeatureCollection<GeoJSON.Geometry> | any,
        filters?: string[],
    ) => void
    /**
     * 设置 GeoJSONSource 数据源 data
     * @param id 数据源 ID
     * @param data 数据
     * @param filter 过滤器, 可选, 过滤 JSON 数据, 使用 json path 语法
     * @link https://docs.mapbox.com/mapbox-gl-js/api/sources/#geojsonsource#setdata
     */
    setGeoJSONSourceData: (id: string, data: mapboxgl.GeoJSONSource['setData'] | Recordable, filter?: string) => void
    /**
     * 设置 VectorTileSource 数据源 tiles
     * @param id 数据源 ID
     * @param tiles 瓦片源
     * @link https://docs.mapbox.com/mapbox-gl-js/api/sources/#vectortilesource#settiles
     */
    setVectorTileSourceTiles: (id: string, tiles: string[]) => void
    /**
     * 更新 ImageSource 数据源 image url
     * @param id 数据源 ID
     * @param url 图片地址
     * @link https://docs.mapbox.com/mapbox-gl-js/api/sources/#imagesource#updateimage
     */
    updateRasterImage: (id: string, url: string) => void
    /**
     * 添加图层
     * @param layer 图层
     * @param beforeId 在指定图层之前插入
     * @param popup 是否添加点击弹窗提示
     * @link https://docs.mapbox.com/mapbox-gl-js/api/map/#map#addlayer
     */
    addLayer: (layer: mapboxgl.Layer, beforeId?: string, popup?: boolean) => void
    /**
     * 移除图层
     * @param id 图层 ID
     * @link https://docs.mapbox.com/mapbox-gl-js/api/map/#map#removelayer
     */
    removeLayer: (id: string) => void
    /**
     * 设置图层过滤器
     * @param layerId 图层 ID
     * @param filter 过滤器
     * @link https://docs.mapbox.com/mapbox-gl-js/api/map/#map#setfilter
     */
    setFilter: (layerId: string, filter: any[]) => void
    /**
     * 设置图层样式
     * @param layerId 图层 ID
     * @param name 样式名称
     * @param value 样式值
     * @link https://docs.mapbox.com/mapbox-gl-js/api/map/#map#setpaintproperty
     */
    setPaintProperty: (layerId: string, name: string, value: any) => void
    /**
     * 设置图层样式
     * @param layerId 图层 ID
     * @param name 样式名称
     * @param value 样式值
     * @link https://docs.mapbox.com/mapbox-gl-js/api/map/#map#setlayoutproperty
     */
    setLayoutProperty: (layerId: string, name: string, value: any) => void
    /**
     * 添加 Marker
     * @param id Marker ID
     * @param lngLat 经纬度
     * @param popup 弹窗内容
     * @param popupDefaultOpen 是否默认打开弹窗
     * @param imageUrl 自定义图标图片URL
     * @param iconSize 图标大小 [width, height]
     * @link https://docs.mapbox.com/mapbox-gl-js/api/markers/#marker
     */
    addMarker(id: string, lngLat: number[], popup?: string, popupDefaultOpen?: boolean, imageUrl?: string, iconSize?: [number, number]): void
    /**
     * 移除 Marker
     * @param id Marker ID
     */
    removeMarker(id: string): void

    /**
     * 移除所有Popup
     * @default
     */
    removePopup(): void
    /**
     * 加载图片资源
     * @default
     */
    loadImage(url: string, callback: (error?: Error, result?: HTMLImageElement | ImageBitmap) => void): void
    addImage(
        name: string,
        image:
            | HTMLImageElement
            | ArrayBufferView
            | { width: number; height: number; data: Uint8Array | Uint8ClampedArray }
            | ImageData
            | ImageBitmap,
        options?: { pixelRatio?: number; sdf?: boolean },
    ): this
}

export interface GeoJSONSourceDataUrlParams {
    url: string
    params: Recordable
}

/**
 * 站点数据类型定义
 */
export interface Station extends Recordable {
    /**
     * key 用于标识站点是什么类型的数据（例如：降水量、气温...）
     */
    key: string
    value: string | number
    suffix?: string
    stationCode?: string
    stationName?: string
    city: string
    cnty: string
    lon: string
    lat: string
    [key: string]: any
}

/**
 * 判断是否为非空对象
 * @param obj 对象
 * @returns 是否为非空对象
 */
function isPlainObject(obj: any): boolean {
    if (typeof obj !== 'object' || obj === null || obj === undefined) {
        return false
    }

    const proto = Object.getPrototypeOf(obj)
    if (proto === null) {
        return true
    }

    const constructor = Object.prototype.hasOwnProperty.call(proto, 'constructor') && proto.constructor
    return (
        typeof constructor === 'function' &&
        constructor instanceof constructor &&
        Function.prototype.toString.call(constructor) === Function.prototype.toString.call(Object)
    )
}

interface PointFeatureMap {
    /**
     * 时序按钮标记
     */
    timing?: boolean
    [key: string]: any
}

/**
 * 数据处理工具
 */
export const HandlerUtil = {
    isPlainObject,
    createDefaulPretPointFeature(
        station: Station | Recordable,
        map = {} as PointFeatureMap,
        isStation = true,
    ): GeoJSON.Feature<GeoJSON.Geometry> {
        return {
            type: 'Feature',
            geometry: {
                type: 'Point',
                coordinates: [Number(station.cenLon), Number(station.cenLat)],
            },
            properties: {
                ...station,
                key: station.key,
                suffix: station.suffix,
                value: station.extremeValue,
                popup: HandlerUtil.createPrePopupHtml(
                    station,
                    {
                        '{key}': `${station.extremeValue || '-'}{suffix}`,
                        ...map,
                    },
                    isStation,
                ),
            },
        }
    },
    createDefaultPointFeature(
        station: Station | Recordable,
        map = {} as PointFeatureMap,
        isStation = true,
        additional: Recordable = {},
    ): GeoJSON.Feature<GeoJSON.Geometry> {
        return {
            type: 'Feature',
            geometry: {
                type: 'Point',
                coordinates: [Number(station.lon), Number(station.lat)],
            },
            properties: {
                ...station,
                key: station.key,
                suffix: station.suffix,
                value: additional?.valueName ? station[additional.valueName] : station.value,
                popup: HandlerUtil.createDefaultPopupHtml(
                    station,
                    {
                        '{key}': `${(additional?.valueName ? station[additional.valueName] : station.value) || '-'
                            }{suffix}`,
                        ...map,
                    },
                    isStation,
                ),
            },
        }
    },
    createFeatureCollection(
        features: GeoJSON.Feature<GeoJSON.Geometry>[] = [],
    ): GeoJSON.FeatureCollection<GeoJSON.Geometry> {
        return {
            type: 'FeatureCollection',
            features: features || [],
        }
    },
    createPrePopupHtml(station: Station | Recordable, map: Recordable, isStation = true): string {
        const htmls = ['<div class="custom-popup">']

        if (isPlainObject(station) && isStation) {
            htmls.push(
                `<div class="popup-row"><span class="popup-title">地点：</span>${station.city || ''} ${station.cnty || ''
                } ${station.town || ''}</div>`,
            )
            htmls.push(`<div class="popup-row"><span class="popup-title">发生时间：</span>${station.occurTime}</div>`)
        }

        if (isPlainObject(map)) {
            // 增加内容换行布局
            const wrap = map?.wrap
            if (wrap) {
                delete map.wrap
            }

            for (const key in map) {
                const len = String(key === '{key}' ? station.key : key).length
                htmls.push(
                    `<div class="popup-row"><span class="popup-title" style="min-width: ${len + 1}em">${key}： </span>${wrap ? '</br>' : ''
                    } ${map[key] || '-'}</div>`,
                )
            }
        }

        htmls.push('</div>')
        return htmls.join('')
    },
    createDefaultPopupHtml(station: Station | Recordable, map: Recordable, isStation = true): string {
        const htmls = ['<div class="custom-popup">']

        if (isPlainObject(station) && isStation) {
            htmls.push(`<div class="popup-row"><span class="popup-title">站号：</span>${station.stationCode}</div>`)
            htmls.push(
                `<div class="popup-row"><span class="popup-title">站名：</span>${station.city || ''} ${station.cnty || ''
                } ${station.stationName || ''}</div>`,
            )
        }

        if (isPlainObject(map)) {
            // 时序按钮标记
            const timing = map.timing
            // 附件
            const attachment = map.attachment

            if (timing) {
                delete map.timing
            }

            if (attachment) {
                delete map.attachment
            }

            // 增加内容换行布局
            const wrap = map?.wrap
            if (wrap) {
                delete map.wrap
            }

            for (const key in map) {
                const len = String(key === '{key}' ? station.key : key).length
                if (key !== '{key}') {
                    htmls.push(
                        `<div class="popup-row"><span class="popup-title" style="min-width: ${len + 1}em">${key}： </span>${wrap ? '</br>' : ''
                        } ${map[key] || '-'}</div>`,
                    )
                }
            }

            if (timing) {
                htmls.push(`<div class="popup-row timing">{{timing}}</div>`)
            }

            if (attachment) {
                const imgs = (station?.imagePath || []).map((v) => ({ url: v, type: 'img' }))
                const videos = (station?.videoPath || []).map((v) => ({
                    url: v,
                    type: 'video',
                }))

                let str = ''
                imgs.concat(videos).forEach((v) => {
                    if (v.type === 'img') {
                        str += `<img onclick="onPopupContentEventHandle(src)" style="width:30px;margin-right:5px;" src="${v.url}" />`
                    }

                    if (v.type === 'video') {
                        // str += `<video  onclick="onPopupContentEventHandle(src)" style="width:30px;margin-right:5px;" src="${v.url}" />`
                        str += `<div class="popup-row timing">{{download}}</div>`
                    }
                })

                htmls.push(`<div class="popup-row"> <span class="popup-title">附件:</span>${str || '~'}</div>`)
            }
        }

        htmls.push('</div>')
        return htmls.join('')
    },
    createRegimenPopupHtml(station: any): string {
        // console.log('station', station)

        const htmls = ['<div class="custom-popup">']
        if (station?.no) {
            htmls.push(`<div class="popup-row"><span class="popup-title">编号：</span>${station.no}</div>`)
            htmls.push(`<div class="popup-row"><span class="popup-title">类型：</span>${station.typeName}</div>`)
            htmls.push(`<div class="popup-row"><span class="popup-title">监测值：</span>${station.value}</div>`)
        }
        // console.log('htmls', htmls)

        htmls.push('</div>')
        return htmls.join('')
    },

    PointGeoJSON(data: Recordable<any>[]) {
        // console.log('data', data)
        return data.map((item: any) => {
            // console.log('item', item)
            const coordinates = item.geometry?.geometry?.coordinates?.length
                ? toRaw(item.geometry?.geometry?.coordinates)
                : []
            // const coordinates = [112.98983, 28.11426]

            // console.log('coordinates', coordinates)

            const iconFn = item.type ? { icon: item.type } : {}
            // const popup = item?.popup ?? HandlerUtil.createRegimenPopupHtml(item)
            return {
                type: 'Feature',
                // geometry: item.geometry?.geometry,
                geometry: {
                    type: 'Point',
                    coordinates,
                },

                properties: {
                    ...item,
                    ...iconFn,
                    // popup,
                },
            }
        })
    },
}

export type HandlerUtilType = typeof HandlerUtil
