Source: mapwms.js

import axios from 'axios'
import xml2js from 'xml2js'
import Qs from 'qs'
import {
    BASELAYER,
    FOCUSLAYER,
    EXTRALAYER,
    codec,
    xmlns,
    helper
} from './helper/tools'
import eventbus from './eventbus'
import WFST from './helper/WFST'
import Init from './helper/Init'

const dpi = 90

/**
 *
 * 用于封装前端和geoserver的交互,不考虑前端GIS技术框架,只考虑和标准的OGC服务交互
 * 
 * @extends eventbus
 *
 * @example
 *
 * 示例一:
 * var wms = new mapwms('http://localhost:8085/geoserver','demo')
 * wms.initialize().then(() => { console.log('加载完毕') })
 * 
 * 示例二:
 * var wms = new mapwms('http://localhost:8085/geoserver','demo',{
 *  supportRightClick: true,
 *  hcolor: '#00ffff'
 * })
 * wms.initialize().then(() => { console.log('加载完毕') })
 */
class mapwms extends eventbus {
    // GeoServer 主机地址
    _service = '/geoserver'
    // GeoServer 调用的指定的服务,如果不指定该值,默认调用第一个服务
    _workspace = ''
    // GeoServer 调用的指定的图层组,如果不指定该值,默认调用第一个图层组
    _group = ''
    // 图层列表
    _layers = []
    // OGC配置
    _ogc = {
        url: '',
        service: 'WMS',
        version: '1.3.0',
        request: 'GetMap',
        crs: 'EPSG:3857',
        layers: '',
        format: 'image/png',
        transparent: true,
        tileSize: 512,
        zIndex: 1,
        buffer: 60, //缓冲距离,处理点符号被剪切的情况
        visible: true //初始可见性
    }
    // 功能选项
    _options = {
        //像素比,用于手动调整地图清晰度,取值为0时不起作用,取值大于1时增加清晰度(通常取值为2即已足够),取值小于1时降低清晰度
        tilePixelRatio: 0,
        //是否自动适配高清屏
        supportRetina: true,
        // 支持点击事件,值为true时,点击地图触发 queryclick 事件
        supportClick: true,
        // 支持右键点击事件,值为true时,点击地图触发 queryrightclick 事件
        supportRightClick: false,
        // 自动高亮选中的元素
        autoHighlight: true,
        // 自动取消高亮选中的元素(当未点击到地图元素时,是否取消之前的),始终高亮一个
        autoCancelHighlight: false,
        // 元素高亮颜色,仅在 autoHighlight = true 时有效
        hcolor: '#00ffff',
        // 是否进行点符号的重叠检测,取值为true时,对点符号进行重叠检测并抽稀;取值为false时,允许点符号重叠显示
        testconflict: true,
        //是否使用无缩放级别的显示
        nozoom: false,
    }
    // 高清屏
    _retina = false
    _devicePixelRatio = 1
    // ENV
    _envs = []
    // 显示的要素id,或要素id列表,数组为空时代表显示全部要素
    _features = []
    // CQL_FILTER 条件
    _CQL_FILTER = null
    //业务配置项
    CONFIG = {
        VERSION_WFS: '2.0.0',
        SRS_CLIENT: 'EPSG:4326',
        TOLER: 0.00021499999999718966,
        GEOField: 'geom',
        //标准比例尺
        SCALES: [1, 5, 10, 25, 50, 100, 250, 500, 1000, 2000, 4000, 8000, 16000, 32000, 64000, 125000, 250000, 500000, 1000000, 2000000, 4000000, 8000000, 16000000, 32000000, 64000000],
        //缩放级别
        ZOOMS: [0, 0, 0, 49370801, 28441893, 14867930, 7784872, 3988369, 1946218, 975269, 488567, 244022, 121912, 60957, 30479, 15239, 7619, 3809, 1904, 952, 476]
    }
    _wfst = null
    /**
     * 构造方法
     * @param {String} service GeoServer服务器地址,示例:http://localhost:8085/geoserver
     * @param {String} workspace GeoServer工作空间,示例:demo
     * @param {Object} options 选项配置
     * @param {Boolean} [options.supportClick] 支持点击事件,值为true时,点击地图触发 queryclick 事件
     * @param {Boolean} [options.supportRightClick] 支持右键点击事件,值为true时,点击地图触发 queryrightclick 事件
     * @param {Boolean} [options.autoHighlight] 自动高亮选中的元素
     * @param {Boolean} [options.autoCancelHighlight] 自动取消高亮选中的元素(当未点击到地图元素时,是否取消之前的),始终高亮一个
     * @param {Boolean} [options.supportRightClick] 支持右键点击事件,值为true时,点击地图触发 queryrightclick 事件
     * @param {Boolean} [options.hcolor] 元素高亮颜色,仅在 autoHighlight = true 时有效
     * @param {Object} ogc OGC选项配置
     * @param {String} [ogc.version] OGC版本,默认值 1.3.0
     * @param {Number} [ogc.tileSize] OGC 分块大小,默认值 512
     * @param {Number} [ogc.zIndex] OGC 显示层级,默认值 1
     * @param {Number} [ogc.buffer] OGC 缓冲距离,处理点符号被剪切的情况,默认值60
     *
     */
    constructor(service = '/geoserver', workspace = '', options = {}, ogc = {}) {
        super()

        if (!service) throw '必须指定GeoServer服务器地址,示例:http://localhost:8080/geoserver'
        this._service = service;
        this._workspace = workspace;
        this._wfst = new WFST(service, workspace)
        Object.assign(this._options, options)
        Object.assign(this._ogc, ogc)

        const devicePixelRatio = typeof window === 'undefined' ? 1 : window.devicePixelRatio
        this._retina = this._options.supportRetina && devicePixelRatio > 1
        if (this._retina) this._devicePixelRatio = this._options.tilePixelRatio == 0 ? devicePixelRatio : this._options.tilePixelRatio
        this.setConflict(this._options.testconflict)
        this.setNozoom(this._options.nozoom)
    }
    /**
     * 强制指定工作空间使用哪个图层组
     * @param {String} group 强制指定使用某图层组
     */
    forceLayerGroup(group) {
        this._group = group
    }
    /**
     * 执行初始化操作
     * OGC标准的WMS地图服务的GetMap接口的参数,包括VERSION、LAYERS、STYLES、FORMAT、TRANSPARENT等,CRS、BBOX、REQUEST、WIDTH、HEIGHT等参数请勿添加,例如:
     * {
     *      LAYERS: 'topp:states',
     *      VERSION:'1.3.0',
     *      FORMAT:'image/png'
     * }
     * @fires mapwms#wms:upload
     *
     */
    async initialize() {
        if (!this._workspace) {
            var req = await axios.get(`${this._service}/rest/workspaces`)
            var w = req.data.workspaces.workspace[0]
            this._workspace = w && w.name
        }
        if (this._workspace) {
            var req = await axios.get(`${this._service}/wfs?service=wms&version=1.3.0&request=GetCapabilities&namespace=${this._workspace}`)
            var parser = new xml2js.Parser({
                explicitArray: false,
                explicitRoot: false
            })
            var capabilities = await parser.parseStringPromise(req.data)
            var rootLayer = capabilities.Capability.Layer
            //过滤掉图层组(没有样式的图层)
            var layers = (rootLayer && rootLayer.Layer.filter(l => l.Style !== undefined).map(l => {
                return {
                    title: l.Title,
                    name: l.Name,
                    crs: l.CRS,
                    visible: true
                }
            })) || []
            var ogc = this._ogc
            try {
                var templayers = []
                var order = await helper.getLayerOrder(this._service, this._workspace, this._group)
                order.forEach(name => {
                    var layer = layers.find(p => p.name === name)
                    if (layer) return templayers.push(layer)
                })
                // 排序完成
                layers = templayers
            } catch (e) {
                console.warn(`排序失败:${e}`)
            }

            this._layers = layers

            ogc.url = `${this._service}/wms`
            // ogc.crs = layers[0] && layers[0].crs
            ogc.layers = layers.map(p => p.name).join(',')

            // 高清屏支持
            if (this._retina) {
                ogc.format_options = `dpi:${Math.floor(dpi * this._devicePixelRatio)}`
            }
            codec.encodeURI(ogc)
            let extras = {}
            // 全局环境变量
            if (this._envs.length > 0) extras.ENV = this._envs.join(';')
            this.onWmsUpload(BASELAYER, ogc, {
                ...extras,
                ts: new Date().valueOf()
            })
        } else {
            console.error('获取GeoServer服务器workspace失败。')
        }
    }
    /**
     * 刷新底图
     */
    refresh(extras) {
        var ogc = this._ogc
        extras = extras || {}
        // 全局环境变量
        if (this._envs.length > 0) extras.ENV = this._envs.join(';')

        // 生成CQL_FILTER
        // 由于 CQL_FILTER 和 FEATUREID 不允许同时存在,故当两条件同时成立时均转换为CQL_FILTER
        if (this._CQL_FILTER && this._features.length > 0) {
            let CQL_FILTER = `IN (${this._features.map(item => `'${item}'`).join(',')})`
            extras.CQL_FILTER = this._CQL_FILTER.split(";").map(p => `${p} AND ${CQL_FILTER}`).join(';')
        } else if (this._CQL_FILTER) {
            extras.CQL_FILTER = this._CQL_FILTER
        } else if (this._features.length > 0) {
            extras.FEATUREID = this._features.join(',')
        }
        codec.encodeURIComponent(extras)
        this.onWmsUpload(BASELAYER, ogc, {
            ...extras,
            ts: new Date().valueOf()
        })
    }
    /**
     * 执行空间查询,并以Promise的形式返回查询结果,当前仅支持基于 X、 Y、 WIDTH、 HEIGHT、 BBOX、 CRS、 LAYERS 方式进行查询
     * 
     * @fires mapwms#progress:loading
     * @fires mapwms#progress:loaded
     * 
     * @param {Number} X X,示意:721
     * @param {Number} Y Y,示意:207
     * @param {Number} WIDTH WIDTH,示意:1564
     * @param {Number} HEIGHT HEIGHT,示意: 784
     * @param {String} BBOX BBOX,示意:'32.43657757416502,105.83224442481992,32.44012705802473,105.8406343746185'
     * @param {String} [CRS=EPSG:4326] CRS,示意:'EPSG:4326'
     * @param {String} [LAYERS] 查询图层,默认值为null,代表取自动取值
     * @param {String} [QUERY_LAYERS] 查询图层,默认值为null,代表取LAYERS的值
     * @param {String} [VERSION=1.1.1] 版本,默认 '1.1.1'
     * @param {Number} [FEATURE_COUNT=1] FEATURE_COUNT,示意:1
     * @param {String} [INFO_FORMAT=application/json] INFO_FORMAT,示意:'application/json'
     * @param {Boolean} [TRANSPARENT=true] TRANSPARENT,示意:true
     * @returns {Promise} 查询结果
     */
    query(X, Y, WIDTH, HEIGHT, BBOX, LAYERS = null, QUERY_LAYERS = null, CRS = 'EPSG:4326', VERSION = '1.1.1', FEATURE_COUNT = 1, INFO_FORMAT = 'application/json', TRANSPARENT = true) {
        // reverse() 是为了让顶层元素先被查询
        LAYERS = LAYERS || this._layers.filter(p => p.visible).reverse().map(p => p.name).join(',')
        QUERY_LAYERS = QUERY_LAYERS || LAYERS
        this.onProgressLoading()
        var CQL_FILTER = this._features.length > 0 ? `IN (${this._features.map(item => `'${item}'`).join(',')})` : undefined
        if (CQL_FILTER && this._CQL_FILTER) {
            CQL_FILTER = this._CQL_FILTER.split(";").map(p => `${p} AND ${CQL_FILTER}`).join(';')
        } else if (this._CQL_FILTER) {
            CQL_FILTER = this._CQL_FILTER
        }
        return axios.post(`${this._service}/wms`, Qs.stringify({
            X: parseInt(X),
            Y: parseInt(Y),
            WIDTH,
            HEIGHT,
            BBOX,
            LAYERS,
            QUERY_LAYERS: LAYERS,
            CRS,
            VERSION,
            FEATURE_COUNT,
            INFO_FORMAT,
            TRANSPARENT,
            REQUEST: 'GetFeatureInfo',
            CQL_FILTER
        })).finally(() => {
            this.onProgressLoaded()
        })
    }
    /**
     * 通过要素id和要素业务属性查询要素
     * @param {String|String[]} [feature] 查询要素的id或id数组
     * @param {Object} [property] 查询业务属性
     * @param {Number} [count] 最大返回数量,默认值 undefined,代表无限制
     * @param {String} [layers] 查询图层,默认值 undefined,代表从当前对象中读取显示图层,数据值示意:d3:pe,d3:line
     */
    queryFeature(feature, property, count, layers, outputFormat = 'json') {
        var identifier = `ts${new Date().valueOf()}`
        var features = feature ? (Array.isArray(feature) ? feature : [feature]) : []
        // reverse() 是为了让顶层元素先被查询
        let LAYERS = layers || this._layers.filter(p => p.visible).map(p => p.name).reverse().join(',')
        let xmlBuilder = {
            'wfs:GetFeature': {
                $: {
                    service: 'WFS',
                    version: '2.0.0',
                    outputFormat,
                    count,
                    ...xmlns.value
                },
                'wfs:Query': LAYERS.split(',').map(layer => {
                    return {
                        $: {
                            typeNames: layer,
                            srsName: 'EPSG:4326',
                        },
                        'fes:Filter': identifier
                    }
                })
            }
        }
        let FILTER = new xml2js.Builder({
            headless: true
        }).buildObject({
            And: [
                ...Object.keys(property || {}).map(key => {
                    return {
                        'fes:PropertyIsEqualTo': {
                            'fes:ValueReference': key,
                            'fes:Literal': property[key]
                        }
                    }
                }),
                {
                    Or: features.map(f => {
                        return {
                            'fes:ResourceId': {
                                $: {
                                    rid: f
                                }
                            }
                        }
                    })
                },
            ]
        })
        var xml = new xml2js.Builder().buildObject(xmlBuilder).replace(new RegExp(identifier, 'g'), FILTER);
        return axios.post(`${this._service}/wfs`, xml, {
            headers: {
                'Content-Type': 'text/xml'
            }
        }).then(req => Promise.resolve(req.data))
    }
    /**
     * 数据层次解析
     * 
     * @param {(String|Object|Array)} [feature] 要素
     * @param {String} [color=#00ffff] 要素默认高亮颜色,十六进制格式,示意:'#00ffff'
     * 
     * @example
     * 
     * 示例一:this.setHilightFeature('pipe.1','#00ffff') // 设置一个高亮要素,pipe.1的高亮颜色为 '#00ffff'
     * 示例二:this.setHilightFeature({id:'pipe.1'},'#00ffff') // 设置一个高亮要素,pipe.1的高亮颜色为 '#00ffff'
     * 示例三:this.setHilightFeature({id:'pipe.1',color:'#00ffff'}) // 设置一个高亮要素,pipe.1的高亮颜色为 '#00ffff'
     * 示例四:this.setHilightFeature(['pipe.1','pipe.2'],'#00ffff') // 设置两个高亮要素,pipe.1、pipe.2的高亮颜色为 '#00ffff'
     * 示例五:this.setHilightFeature([{id:'pipe.1'},{id:'pipe.2'}],'#00ffff') // 设置两个高亮要素,pipe.1、pipe.2的高亮颜色为 '#00ffff'
     * 示例六:this.setHilightFeature([{id:'pipe.1',color:'#00ffff'},{id:'pipe.2',color:'#00ffff'}]) // 设置两个高亮要素,pipe.1、pipe.2的高亮颜色为 '#00ff00',注意此情况时一般使用 示例七,需要设置默认高亮色
     * 示例七:this.setHilightFeature([{id:'pipe.1',color:'#00ff00'},{id:'pipe.2'}],'#00ffff') // 设置两个高亮要素,pipe.1的高亮颜色为 '#00ff00',pipe.2的高亮颜色为默认值 #00ffff'
     * 示例八:this.setHilightFeature() // 清除高亮要素
     * 
     */
    hierarchicalAnalytical(feature, color = '#00ffff') {
        var ogc = Object.assign({}, this._ogc)
        ogc.zIndex = ogc.zIndex === undefined ? 1 : ogc.zIndex + 1
        var extras = {
            ts: new Date().valueOf(),
        }
        //已经存在的env变量
        let existEnv = this._envs.length > 0 ? this._envs.join(';') + ';' : '';
        // 不存在任何要素,清理高亮
        if (typeof feature === 'undefined') {
            ogc = undefined
        } else if (typeof feature === 'string') {
            //existEnv放前面,因为后面的env参数会覆盖前面的同名参数
            extras.ENV = existEnv + `hcolor:${color}`;
            extras.FEATUREID = feature
        } else if (Array.isArray(feature)) {
            let temp = {}
            feature.forEach(f => {
                if (typeof f === 'string') {
                    temp[color] = temp[color] ? temp[color] : []
                    temp[color].push(f)
                } else {
                    let hcolor = f.color || color
                    temp[hcolor] = temp[hcolor] ? temp[hcolor] : []
                    temp[hcolor].push(f.id)
                }
            })
            extras = Object.keys(temp).map(hcolor => {
                return {
                    ts: new Date().valueOf(),
                    ENV: existEnv + `hcolor:${hcolor}`,
                    FEATUREID: temp[hcolor].join(','),
                }
            })
            if (extras.length === 1) extras = extras[0]
            else extras.forEach(e => codec.encodeURIComponent(e))
        } else if (typeof feature === 'object') {
            extras.ENV = existEnv + `hcolor:${feature.color || color}`
            extras.FEATUREID = feature.id
        }
        codec.encodeURI(ogc)
        codec.encodeURIComponent(extras)
        return {
            ogc,
            extras
        }
    }
    /**
     * 设置要素高亮
     * 
     * @param {(String|Object|Array)} [feature] 高亮要素
     * @param {String} [color=#00ffff] 要素默认高亮颜色,十六进制格式,示意:'#00ffff'
     * 
     * @fires mapwms#wms:upload
     * 
     * @example
     * 
     * 示例一:this.setHilightFeature('pipe.1','#00ffff') // 设置一个高亮要素,pipe.1的高亮颜色为 '#00ffff'
     * 示例二:this.setHilightFeature({id:'pipe.1'},'#00ffff') // 设置一个高亮要素,pipe.1的高亮颜色为 '#00ffff'
     * 示例三:this.setHilightFeature({id:'pipe.1',color:'#00ffff'}) // 设置一个高亮要素,pipe.1的高亮颜色为 '#00ffff'
     * 示例四:this.setHilightFeature(['pipe.1','pipe.2'],'#00ffff') // 设置两个高亮要素,pipe.1、pipe.2的高亮颜色为 '#00ffff'
     * 示例五:this.setHilightFeature([{id:'pipe.1'},{id:'pipe.2'}],'#00ffff') // 设置两个高亮要素,pipe.1、pipe.2的高亮颜色为 '#00ffff'
     * 示例六:this.setHilightFeature([{id:'pipe.1',color:'#00ffff'},{id:'pipe.2',color:'#00ffff'}]) // 设置两个高亮要素,pipe.1、pipe.2的高亮颜色为 '#00ff00',注意此情况时一般使用 示例七,需要设置默认高亮色
     * 示例七:this.setHilightFeature([{id:'pipe.1',color:'#00ff00'},{id:'pipe.2'}],'#00ffff') // 设置两个高亮要素,pipe.1的高亮颜色为 '#00ff00',pipe.2的高亮颜色为默认值 #00ffff'
     * 示例八:this.setHilightFeature() // 清除高亮要素
     * 
     */
    setHilightFeature(feature, color = '#00ffff') {
        const {
            ogc,
            extras
        } = this.hierarchicalAnalytical(feature, color)
        this.onWmsUpload(FOCUSLAYER, ogc, extras)
    }
    /**
     * 设置分层要素
     * 
     * @param {(String|Object|Array)} [feature] 分层要素
     * @param {String} [color=#00ffff] 要素默认高亮颜色,十六进制格式,示意:'#00ffff'
     * @param {(String|Object|Array)} [layerName] 分层名,默认值为常量 EXTRALAYER
     * 
     * @fires mapwms#wms:upload
     * 
     * @example
     * 
     * 示例一:this.setHierarchyFeature('pipe.1','#00ffff') // 设置分层要素,pipe.1的高亮颜色为 '#00ffff'
     * 示例二:this.setHierarchyFeature({id:'pipe.1'},'#00ffff') //设置分层要素,pipe.1的高亮颜色为 '#00ffff'
     * 示例三:this.setHierarchyFeature({id:'pipe.1',color:'#00ffff'}) // 设置分层要素,pipe.1的高亮颜色为 '#00ffff'
     * 示例四:this.setHierarchyFeature(['pipe.1','pipe.2'],'#00ffff') // 设置分层要素,pipe.1、pipe.2的高亮颜色为 '#00ffff'
     * 示例五:this.setHierarchyFeature([{id:'pipe.1'},{id:'pipe.2'}],'#00ffff') // 设置分层要素,pipe.1、pipe.2的高亮颜色为 '#00ffff'
     * 示例六:this.setHierarchyFeature([{id:'pipe.1',color:'#00ffff'},{id:'pipe.2',color:'#00ffff'}]) // 设置分层要素,pipe.1、pipe.2的高亮颜色为 '#00ff00',注意此情况时一般使用 示例七,需要设置默认高亮色
     * 示例七:this.setHierarchyFeature([{id:'pipe.1',color:'#00ff00'},{id:'pipe.2'}],'#00ffff') // 设置分层要素,pipe.1的高亮颜色为 '#00ff00',pipe.2的高亮颜色为默认值 #00ffff'
     * 示例八:this.setHierarchyFeature() // 清除分层要素
     * 
     */
    setHierarchyFeature(feature, color = '#00ffff', layerName = EXTRALAYER) {
        const {
            ogc,
            extras
        } = this.hierarchicalAnalytical(feature, color)
        this.onWmsUpload(layerName, ogc, extras)
    }
    /**
     * 设置要素遮罩颜色
     * @param {String} color 要素遮罩颜色
     */
    mask(color) {
        var envs = this._envs.filter(p => !/hcolor/i.test(p))
        color && envs.push(`hcolor:${color}`)
        this._envs = envs
        this.refresh()
    }

    /**
     * 过滤要素
     * @param {String|Array<String>} feature 要素id或要素id列表,不输入值时代表不执行过滤操作
     */
    filterFeature(feature) {
        if (feature === undefined || feature === null) this._features = []
        else this._features = Array.isArray(feature) ? feature : [feature]
        this.refresh()
        return this
    }
    /**
     * 过滤要素,通过要素属性匹配
     * @param {Object} attribute 属性匹配
     * 
     * @example
     * 示例一:
     * this.filterAttribute({
     *      color:'#0040FF',
     *      subtype:'c2246ddf-36b8-4067-b498-ac6b002513f0'
     *  })
     * 示例二:
     * this.filterAttribute({
     *      color:['#0040FF','#FF8C00'],
     *      subtype:'c2246ddf-36b8-4067-b498-ac6b002513f0'
     *  })
     * 示例三:
     * this.filterAttribute({
     *      color:['#0040FF','#FF8C00'],
     *      subtype:['c2246ddf-36b8-4067-b498-ac6b002513f0']
     *  })
     */
    filterAttribute(attribute) {
        var count = this._layers.length
        if (!attribute || count === 0) {
            this._CQL_FILTER = null
            this.refresh()
        } else {
            var CQL_FILTER = []
            var c = function (attr) {
                if (Array.isArray(attr)) {
                    return attr.map(k => `'${k}'`).join(',')
                } else return `'${attr}'`
            }
            let preFilters = this._CQL_FILTER == null ? null : this._CQL_FILTER.split(';')
            for (var i = 0; i < count; i++) {
                let filter = Object.keys(attribute).map(key => `${key} IN (${c(attribute[key])})`).join(' AND ')
                if (preFilters == null) CQL_FILTER.push(filter)
                else CQL_FILTER[i] = `(${preFilters[i]}) AND (${filter})`
            }
            this._CQL_FILTER = CQL_FILTER.join(';')
            this.refresh()
        }
        return this
    }
    /**
     * 触发 wms:upload 事件
     * 
     * @fires mapwms#wms:upload
     */
    onWmsUpload(layerName, ogc, extras) {
        if (extras) {
            var pre = Array.isArray(extras) ? extras : [extras]
            // FEATUREID 大数据量时预处理
            pre.forEach(extra => {
                var suffx = encodeURIComponent(',')

                let FEATUREID = extra.FEATUREID && extra.FEATUREID.split(suffx)
                if (!FEATUREID || FEATUREID.length < 50) return;
                var count = this._layers.length
                var CQL_FILTER = []
                var OLDCQL_FILTER = extra.CQL_FILTER
                for (var i = 0; i < count; i++) {
                    var LAYERNAME = this._layers[i].name.split(':')[1]
                    var TEMPFEATUREID = FEATUREID.filter(f => f.startsWith(LAYERNAME)).map(f => `'${f}'`)
                    // 如果存在CQL_FILTER 合并 条件
                    if (OLDCQL_FILTER) {
                        CQL_FILTER.push(TEMPFEATUREID.length > 0 ? `IN (${TEMPFEATUREID.join(',')}) AND ${OLDCQL_FILTER}` : '1<>1')
                    } else {
                        CQL_FILTER.push(TEMPFEATUREID.length > 0 ? `IN (${TEMPFEATUREID.join(',')})` : '1<>1')
                    }
                }
                extra.CQL_FILTER = CQL_FILTER.join(';')
                delete extra.FEATUREID
            })
        }

        /**
         * 创建WMS格式图层,事件包含三个参数,第一个参数为图层名称,第二个参数为OGC参数对象或者数组,第三个参数extras为额外的参数
         * 
         * @event mapwms#wms:upload
         * @param {String} layerName 图层名称
         * @param {Object} ogc OGC对象
         * @param {String} ogc.url 请求地址
         * @param {String} ogc.service 服务,默认 wms
         * @param {String} ogc.version 版本,默认 1.3.0
         * @param {String} ogc.request
         * @param {String} ogc.crs 坐标系,默认 EPSG:3857
         * @param {String} ogc.layers
         * @param {String} ogc.format 输出格式,默认 image/png
         * @param {String} ogc.transparent 是否透明,默认true
         * @param {String} ogc.tileSize 瓦片大小,默认 512
         * @param {String} ogc.zIndex 层级
         * @param {Object} [extras] 额外的参数,可选参考值 ENV, CQL_FILTER
         * 
         */
        this.emit('wms:upload', layerName, ogc, extras)
    }
    /**
     * 触发 wms:upload 事件
     * 
     * @fires mapwms#progress:loading
     */
    onProgressLoading() {
        /**
         * 正在加载事件
         * 
         * @event mapwms#progress:loading
         * 
         */
        this.emit('progress:loading')
    }
    /**
     * 触发 wms:upload 事件
     * 
     * @fires mapwms#progress:loaded
     */
    onProgressLoaded() {
        /**
         * 加载完成事件
         * 
         * @event mapwms#progress:loaded
         * 
         */
        this.emit('progress:loaded')
    }

    //#region 编辑相关接口

    /**
     * 使用权限认证,传入用户名,密码,以使用geoserver的数据编辑接口
     * (暂时采用Basic认证方式,使用时配合https更安全)
     * @param {string} user 用户名 
     * @param {*} pwd 密码
     */
    useAuth(user, pwd) {
        if (this._wfst != null) {
            this._wfst.useAuth(user, pwd);
        }
    }

    /**
     * 添加点要素(或集合)
     * @param {object[]|object} datas 点要素对象(或集合)
     * @param {boolean} [datas.fixed=false] 是否使用矢量缩放,即要素是否按地图单位(米)绘制-会在size参数上加m作为米的单位后缀,否则按像素单位绘制
     * @param {number} datas.lng 经度或横坐标
     * @param {number} datas.lat 维度或纵坐标
     * @param {string} datas.type 设备类型id
     * @param {object} [datas.property] 属性对象
     * @param {object} [datas.style] 样式对象
     * @param {string} [datas.style.color] 16进制颜色值字符串
     * @param {number} [datas.style.size] 大小
     * @returns {Promise} 返回新建点的结果
     * @example
     * let data = {
     *  fixed:false,
     *  lng:105,
     *  lat:30,
     *  type:'',
     *  property:{
     *    status:0,
     *  },
     *  style:{
     *    color:'#ff0000',
     *    size:20,
     *  }
     * }
     * addPoint(data)或addPoint[(data]);
     */
    addPoint(datas) {
        return this._wfst.addPoint(datas);
    }

    /**
     * 添加线要素(或集合)
     * @param {object[]|object} datas 线要素对象(或集合)
     * @param {boolean} [datas.fixed=true] 是否使用矢量缩放,即要素是否按地图单位(米)绘制-会在size参数上加m作为米的单位后缀,否则按像素单位绘制
     * @param {object[]} datas.path 坐标点集合,长度大于1
     * @param {number} datas.path[][0] 经度或横坐标
     * @param {number} datas.path[][1] 维度或纵坐标
     * @param {string} datas.type 设备类型id
     * @param {object} [datas.property] 属性对象
     * @param {object} [datas.style] 样式对象
     * @param {string} [datas.style.color] 线颜色,16进制颜色值字符串
     * @param {number} [datas.style.size] 线宽
     * @returns {object} 返回格式化后点对象
     * @example
     * let data = {
     *  fixed:false,
     *  path:[[103,30],[103,31]]
     *  type:'',
     *  property:{
     *    status:0,
     *  },
     *  style:{
     *    color:'#ff0000',
     *    size:20,
     *  }
     * }
     * addPolyline(data)或addPolyline([data]);
     */
    addPolyline(datas) {
        return this._wfst.addPolyline(datas);
    }
    /**
     * 创建地图要素
     * @param {Object|Array} features:
     * [{
     *  name:'fitting'//图层名
     *  features:[{//该图层需要新增的要素集合
     *      properties:{name:'',x:'',y:''}//属性对象,具体属性由gis数据结构决定
     *      geometry:{}//几何图形的geojson格式对象
     *        }]
     * }]
     * @returns {Promise} 创建结果
     */
    createFeature(features) {
        return this._wfst.createFeature(features);
    }

    /**
     * 通过地图要素ID删除要素
     * @param {String|Array} featureId:需要删除的要素的ID集合,数组
     * @returns {Promise} 操作结果
     */
    removeFeature(featureId) {
        if (!featureId) return Promise.resolve();
        let featureIds = featureId instanceof Array ? featureId : [featureId];
        return this._wfst.removeFeature(featureIds);
    }
    /**
     * 通过筛选条件移除要素
     * @param {Object|Array} condition 筛选条件(或集合)
     * @param {string} condition.layer 图层类型
     * @param {object} [condition.geometry] 空间范围条件
     * @param {Array} [condition.where] 属性条件,用And连接多个条件
     * @param {string} condition.where[].key 属性名
     * @param {object} condition.where[].value 属性值
     * @returns {Promise} 操作结果
     */
    removeFeatureByCondition(filters) {
        return this._wfst.removeFeatureByCondition(filters);
    }

    /**
     * 更新要素
     * @param {object|Array} feature 要素(集合)
     * [
     *  {
     *    id:'',//要素id
     *    properties:{},//{name:'',x:'',y:''}//属性对象,可选参数,具体属性由gis数据结构决定
     *    geometry:{},几何图形的geosjon格式对象,可选参数
     * }
     * ]
     */
    updateFeature(feature) {
        return this._wfst.updateFeature(feature);
    }

    /**
     * 更新点要素(或集合)
     * @param {object|Array} data 点要素对象(或集合)
     * @param {string} data.id 要素id
     * @param {boolean} [data.fixed=false] 是否使用矢量缩放,即要素是否按地图单位(米)绘制-会在size参数上加m作为米的单位后缀,否则按像素单位绘制
     * @param {number} [data.lng] 经度或横坐标
     * @param {number} [data.lat] 维度或纵坐标
     * @param {string} [data.type] 设备类型id
     * @param {object} [data.property] 属性对象
     * @param {object} [data.style] 样式对象
     * @param {string} [data.style.color] 16进制颜色值字符串
     * @param {number} [data.style.size] 大小
     * @returns {Promise} 返回新建点的结果
     * @example
     * let data = {
     *  id:'fitting.8',
     *  fixed:false,
     *  lng:105,
     *  lat:30,
     *  type:'',
     *  property:{
     *    status:0,
     *  },
     *  style:{
     *    color:'#ff0000',
     *    size:20,
     *  }
     * }
     * updatePoints(data)或updatePoints([data]);
     */
    updatePoint(data) {
        return this._wfst.updatePoint(data);
    }

    /**
     * 更新线要素(或集合)
     * @param {object|Array} data 线要素对象(或集合)
     * @param {string} data.id 要素id
     * @param {boolean} [data.fixed=true] 是否使用矢量缩放,即要素是否按地图单位(米)绘制-会在size参数上加m作为米的单位后缀,否则按像素单位绘制
     * @param {object[]} [data.path] 坐标点集合,长度大于1
     * @param {number} [data.path[][0]] 经度或横坐标
     * @param {number} [data.path[][1]] 维度或纵坐标
     * @param {string} [data.type] 设备类型id
     * @param {object} [data.property] 属性对象
     * @param {object} [data.style] 样式对象
     * @param {string} [data.style.color] 线颜色,16进制颜色值字符串
     * @param {number} [data.style.size] 线宽
     * @returns {object} 返回格式化后点对象
     * @example
     * let data = {
     *  id:'pipe.8',
     *  fixed:false,
     *  path:[[103,30],[103,31]]
     *  type:'',
     *  property:{
     *    status:0,
     *  },
     *  style:{
     *    color:'#ff0000',
     *    size:20,
     *  }
     * }
     * updatePolylines(data)或updatePolylines([data]);
     */
    updatePolyline(data) {
        return this._wfst.updatePolyline(data);
    }

    /**
     * 按过滤条件更新要输属性
     * @param {string} layertype 图层
     * @param {object} valuses 属性对象 
     * @param {object} [filter] 过滤条件,缺失时表示修改该图层所有数据
     * @return {Promise}
     * @example
     * 暂时采用如下格式,并用And连接
     * let filter={
     *     where:[
     *         {
     *             key:'',
     *             value:''
     *        },{
     *             key:'',
     *             value:''
     *         }
     *     ]
     *  }
     */
    updateFeatureByCondition(layertype, valuses, filter) {
        return this._wfst.updateFeatureByCondition(layertype, valuses, filter);
    }
    //#endregion

    mapInfo = null; //暂存图层详细信息,需要时才查询一次,不多次查询
    /**
     * 查询图层详细信息
     * @param {boolean} 是否强制刷新图层信息,不使用缓存的信息
     */
    async getLayers(enforce) {
        try {
            if (this.mapInfo == null || enforce) {
                let info = await Init.initialize(this._service, this._workspace)
                this.mapInfo = info;
                return this.mapInfo;
            } else
                return this.mapInfo;
        } catch (error) {
            throw error;
        }
    }
    //#region 样式同步接口

    /**
     * 同步业务系统的设备样式到geoserver,按指定属性集合分类渲染
     * @param {object[]} icondata 符号信息集合
     * @param {number} category 点线类型标识,0-管线,否则为管点
     * @param {boolean} [icondata[].fixed=true] 设备或者管线是否已矢量缩放的形式进行呈现,默认值为true
     * @param {string} icondata[].type 设备类型id,不管是管道还是阀门还是其他设备,均需要填写该值
     * @param {object} [icondata[].property] 设备业务属性对象
     * @param {object} [icondata[].style] 设备样式属性
     * @param {number} [icondata[].style.content] 符号内容,即svg的xml字符串
     * @param {number} [icondata[].style.title] 符号名称
     * @param {number} [icondata[].style.zindex] 符号层级,默认与传入参数的顺序一致,有zindex值会绘制在无zindex值的符号上面,另外默认符号始终在最低层,线图层始终在点图层下面
     * @param {number} [icondata[].style.size] 图标大小或者管线宽度,注意 fixed 为真是代表矢量缩放,此时size的单位为m,否则size的单位是px
     * @param {string} [icondata[].style.color] 图标颜色或者管线颜色,16进制颜色值字符串
     * @param {number[]} [icondata[].style.zoom] 图标可见层级范围,参考高德地图,级别范围3-20
     * @param {boolean} [multicolor=false] 是否是多色图标,使用多色图标但该参数不为true时会导致符号不能高亮
     * @returns {Promise}
     * @example
     *
     * var icondata={
     *    category: 0,
     *    fixed: true,
     *    type: '20ab312e-d505-4c37-8b85-ac75007e2a31',
     *    property: {
     *        status:0
     *    },
     *    style:{
     *        title:'正常阀门',
     *        content:'<svg/>',
     *        zindex:1,
     *        size: 8,
     *        color: '#ffffff'
     *        zoom:[15,19]
     *    }
     * }
     *
     * util.syncDeviceStyle([icondata])
     */
    async syncDeviceStyle(icondata, multicolor) {
        if (!icondata) return;
        let self = this;
        try {
            let icondatas = icondata instanceof Array ? icondata : [icondata];
            let newIcons = icondatas.map((item) => self._formatIcon(item));
            //构建svg资源名称,并参数化svg(避免线符号?)
            let folder = helper._getTempName('svg-icon-');
            newIcons.forEach((item, index) => {
                item.contenturl = self._workspace + '/' + folder + '/' + index + ".svg";
            });
            //上传svg文件到,styles/{workspace}/{tempname}/{index}.svg
            let result = await self._uploadResourceBatchSync('styles', newIcons, multicolor);
            if (result) {
                let mapinfo = await self.getLayers();
                //构建样式
                let styles = [];
                mapinfo.layers.forEach(layer => {
                    let style = self._buildStyle(layer, newIcons);
                    if (style != null) styles.push({
                        layertype: layer.layertype,
                        stylename: helper._getTempName(layer.layertype + '-'),
                        style: style
                    });
                })
                let rep = await self._uploadStyleBatch(styles);
                if (rep.success) {
                    //异步删除旧的样式
                    self._deleteStyleBatch(rep.oldstyles);
                    //异步删除历史svg资源文件夹,注意不要删除刚新建的文件夹,不需等待删除完成
                    self._deleteOldStyleFolder(folder);
                } else throw '上传样式失败';
            } else throw '上传svg文件失败'
        } catch (error) {
            console.log(error);
            throw error;
        }
    }

    /**
     * 格式化图标信息
     * @param {object} data
     * @param {boolean} [data.fixed=true] 设备或者管线是否已矢量缩放的形式进行呈现,默认值为true
     * @param {string} data.type 设备类型id,不管是管道还是阀门还是其他设备,均需要填写该值
     * @param {object} [data.property] 设备业务属性对象
     * @param {object} [data.style] 设备样式属性
     * @param {number} [data[].style.content] 符号内容,即svg的xml字符串
     * @param {number} [data[].style.title] 符号名称
     * @param {number} [data.style.zindex] 符号层级
     * @param {number} [data.style.size] 图标大小或者管线宽度,注意 fixed 为真是代表矢量缩放,此时size的单位为m,否则size的单位是px
     * @param {string} [data.style.color] 图标颜色或者管线颜色,16进制颜色值字符串
     * @param {number[]} [data.style.zoom] 图标可见层级范围,参考高德地图
     * @returns {object} 返回格式化后的符号信息
     * return {
     * title:
     * properties:{},
     * content:
     * minscale:
     * maxscale:
     * zindex:
     * size:
     * color:
     * }
     */
    _formatIcon(data) {
        if (!data) throw '图标未定义';
        let newFixed = (data.fixed === null || data.fixed == undefined || typeof data.fixed !== 'boolean') ? true : data.fixed;
        let category = (data.category === null || data.category === undefined) ? null : data.category;
        if (data.type === undefined || data.type === null || (typeof data.type != 'string' && typeof data.type != 'number')) throw '缺少设备类型';
        let newStyle = (data.style === null || data.type === undefined) ? {} : Object.assign({}, data.style);
        if (newStyle.content === null || newStyle.content == undefined) throw '缺少符号';
        if (newStyle.title == null || newStyle === undefined) throw '缺少符号名称';
        //zindex默认取值为传入的图标序号
        let zindex = (typeof newStyle.zindex == 'number') ? newStyle.zindex : null;
        if (newFixed && /m/.test(newStyle.size) == false) newStyle.size += 'm'; //添加单位米
        if (newStyle.color !== null && newStyle.color !== undefined) {
            if (/#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]/.test(newStyle.color) == false) throw '颜色值异常';
            else {
                newStyle.color = newStyle.color;
            }
        }
        let minscale = 0;
        let maxscale = 0;
        if (newStyle.zoom instanceof Array && typeof newStyle.zoom[0] == 'number')
            maxscale = this._findScale(newStyle.zoom[0], false);
        if (newStyle.zoom instanceof Array && typeof newStyle.zoom[1] == 'number')
            minscale = this._findScale(newStyle.zoom[1], true);
        let newProperty = Object.assign({}, data.property, {
            subtype: data.type
        });
        let newdata = {
            properties: newProperty,
            title: newStyle.title,
            content: newStyle.content,
            minscale: minscale,
            maxscale: maxscale,
            zindex: zindex,
            size: newStyle.size,
            color: newStyle.color,
            category: category
        };
        return JSON.parse(JSON.stringify(newdata)); //去掉undifind属性
    }

    /**
     * 获取指定缩放级别的合适比例尺
     * @param {number} zoom 缩放级别
     * @param {Boolean} isMin 是否找最小比例尺
     */
    _findScale(zoom, isMin = false) {
        let index = parseInt(zoom);
        if (index > (this.CONFIG.ZOOMS.length - 1)) throw '缺少可参考的缩放级别';
        if (this.CONFIG.ZOOMS === undefined || this.CONFIG.ZOOMS === null || !Array.isArray(this.CONFIG.ZOOMS)) throw '缺少可参考缩放级别的客户端';
        let scale = isMin ? this.CONFIG.SCALES[0] : this.CONFIG.SCALES[this.CONFIG.SCALES.length - 1];
        for (let i = 0; i < this.CONFIG.SCALES.length; i++) {
            if (this.CONFIG.ZOOMS[index] != 0 && this.CONFIG.SCALES[i] > this.CONFIG.ZOOMS[index]) {
                scale = isMin ? parseInt(0.75 * this.CONFIG.SCALES[i]) : parseInt(1.5 * this.CONFIG.SCALES[i]); //由于高德地图的比例尺不规范,进行处理
                break;
            }
        }
        return scale;
    }

    /**
     *删除历史样式资源文件夹,可排除指定文件夹
     * @param {string} exceptFolder 排除文件夹名
     */
    _deleteOldStyleFolder(exceptFolder) {
        let self = this;
        let url = `${self._service}/rest/resource/styles/${self._workspace}?format=json`;
        axios.get(url).then((rep) => {
            if (rep.status == 401) {
                throw '缺少POST授权';
            }
            if (rep.status !== 200) {
                throw '查询资源清单失败';
            }
            if (rep.data.ResourceDirectory.children !== undefined) {
                let folders = rep.data.ResourceDirectory.children.child.map(item => item.name).filter(item => item != exceptFolder);
                let deleteTask = folders.map(folder => {
                    return axios({
                        url: `${self._service}/rest/resource/styles/${self._workspace}/${folder}`,
                        method: 'delete',
                    });
                });
                axios.all(deleteTask);
            }
        }).catch();
    }

    /**
     *参数化svg,用于通过geoserver wms的env参数动态改变svg颜色
     * @param {string} svg svg内容
     * @param {string} param 变量名
     * @param {boolean}multicolor
     */
    async _paramSVGcolor(svg, param, multicolor) {
        let newSVG = svg;
        let values = [];
        if (multicolor === true) {
            //svg颜色参数化:将svg中颜色值替换为:fill="param(hcolor)"
            let regFilter = /fill.*(?=>)"(\n|\r|\t)*?/g;
            regFilter = /#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]/g;
            if (regFilter.test(svg)) {
                values = svg.match(regFilter);
                values.forEach((value, index) => {
                    let newValue = `param(${param}) ${value}`;
                    newValue = `param(${param + index})`; //默认值由hcolor传入
                    newSVG = newSVG.replace(value, newValue); //replace all
                })
            }
        }
        //svg标签添加属性: fill="param(hcolor)"
        var parser = new xml2js.Parser();
        let svgObj = await parser.parseStringPromise(newSVG);
        svgObj.svg.$.stroke = svgObj.svg.$.fill = 'param(hcolor)';
        return {
            svg: new xml2js.Builder().buildObject(svgObj),
            colors: values
        };
    }

    /**
     *按顺序批量上传图标的svg文件
     * @param {string} folder 文件夹名
     * @param {string} datas 符号参数集合
     * @param {string} multicolor 是否是多色图标
     */
    async _uploadResourceBatchSync(folder, datas, multicolor) {
        let newDatas = [...datas];
        let data = null;
        while (data = newDatas.shift()) {
            //参数化svg
            let paramSVG = await this._paramSVGcolor(data.content, 'hcolor', multicolor);
            data.colors = paramSVG.colors;
            data.svg = paramSVG.svg;
            let rep = await axios({
                url: `${this._service}/rest/resource/${folder}/${data.contenturl}`,
                method: 'put',
                headers: {
                    'Content-Type': 'application/xml'
                },
                data: data.svg,
            });
            if (rep.status != 201) {
                throw '上传SVG失败'
            }
        }
        return true
    }

    /**
     *批量删除指定样式,即便是样式已关联到图层
     * @param {Array} stylenames 样式名称集合
     */
    _deleteStyleBatch(stylenames) {
        //使用style接口删除样式、删除样式和图层关联、最后用resource接口清理bak文件
        let self = this;
        let deleteTask = stylenames.map(stylename => {
            return axios({
                url: `${self._service}/rest/styles/${stylename}`,
                method: 'delete',
                params: {
                    recurse: true,
                    purge: true
                }
            });
        });
        axios.all(deleteTask).then(reps => {
            let result = true
            reps.forEach(rep => {
                if (rep.status != 200)
                    result = false;
            });
            if (result) {
                //用resource接口清理bak文件
                let deleteBakTask = [];
                stylenames.forEach(stylename => {
                    deleteBakTask.push(axios({
                        url: `${self._service}/rest/resource/styles/${stylename}.sld.bak`,
                        method: 'delete',
                    }));
                });
                axios.all(deleteBakTask);
            }
        })
    }

    /**
     *批量上传样式并设置为默认样式
     * @param {Array} styles 样式配置集合
     */
    async _uploadStyleBatch(styles) {
        let self = this;
        //批量上传样式
        let url = `${self._service}/rest/styles`;
        let uploadTasks = styles.map(style => {
            return axios({
                url: url,
                method: 'post',
                headers: {
                    'Content-Type': 'application/vnd.ogc.sld+xml;charset=utf-8',
                },
                data: style.style,
                params: {
                    name: style.stylename
                }
            });
        });
        let uploadreps = await axios.all(uploadTasks);
        uploadreps.forEach(rep => {
            if (rep.status == 401) throw '缺少POST授权';
            if (rep.status !== 201) {
                throw '上传样式失败';
            }
        });
        //批量查询图层现有样式
        let layerStyleTask = styles.map(style => {
            return axios({
                method: 'get',
                url: `${self._service}/rest/workspaces/${self._workspace}/layers/${style.layertype}`
            });
        });
        let layerreps = await axios.all(layerStyleTask);
        layerreps.forEach(layerrep => {
            if (layerrep.status != 200) throw '查询图层样式失败';
        });
        //修改图层样式并提交
        let defaultStyles = [];
        let setStyleTask = [];
        layerreps.forEach(layerrep => {
            let style = styles.filter(style => style.layertype == layerrep.data.layer.name)[0];
            defaultStyles.push(layerrep.data.layer.defaultStyle.name);
            let data = {
                "layer": {
                    "styles": {
                        "@class": "linked-hash-set",
                        "style": [{
                            "name": style.stylename,
                        }]
                    },
                    "defaultStyle": {
                        "name": style.stylename
                    }
                }
            };
            //保留原可选样式
            if (typeof layerrep.data.layer.styles !== 'undefined')
                data.layer.styles.style.push.apply(data.layer.styles.style, layerrep.data.layer.styles.style.map(item => item.name));
            setStyleTask.push(axios({
                method: 'put',
                url: `${self._service}/rest/workspaces/${self._workspace}/layers/${style.layertype}`,
                data: data,
                headers: {
                    'Content-Type': 'application/json',
                },
            }));
        });
        let setStyleReps = await axios.all(setStyleTask);
        setStyleReps.forEach(setStyleRep => {
            if (setStyleRep.status != 200) throw '设置图层样式失败';
        });
        //返回执行结果和需要删除的旧样式
        return {
            success: true,
            oldstyles: defaultStyles
        }
    }

    /**
     * 构建指定图层的样式文件内容
     * @param {Object} layer 
     * @param {Array} icons 
     */
    _buildStyle(layer, icons) {
        let styleXML = '';
        let rules = [];
        let fields = [];
        let otherNotFilters = [];
        let geotype = layer.attribute.filter(item => item.name == this.CONFIG.GEOField)[0].binding;
        //按zindex排序:有自定义顺序的排在后面,其他按传入顺序排在前面
        let icondata = null;
        if (/[L|l]ine/.test(geotype))
            icondata = icons.filter(item => item.category == 0)
        else if (/[P|p]oint/.test(geotype))
            icondata = icons.filter(item => item.category != 0);
        else throw '不支持的几何类型';
        //判断图层是否有angle字段,用于符号旋转
        let hasAngle = layer.attribute.filter((item) => item.name == 'angle').length > 0;
        let orderedIcondata = icondata.filter(item => item.zindex == null).concat(icondata.filter(item => item.zindex != null).sort((a, b) => a - b));
        orderedIcondata.forEach((data, index) => {
            let filters = [];
            Object.keys(data.properties).forEach(key => {
                filters.push(`<ogc:PropertyIsEqualTo><ogc:PropertyName>${key}</ogc:PropertyName><ogc:Literal>${data.properties[key]}</ogc:Literal></ogc:PropertyIsEqualTo>`);
                if (!fields.includes(key))
                    fields.push(key);
            });
            if (filters.length > 0) {
                let rule = this._buildStyleRule(orderedIcondata.length - index, data, filters, geotype, hasAngle);
                rules.push(rule);
                //添加缩放级别补充的符号,用于在展示爆管等临时结果时所有可见级别都可见地图

                otherNotFilters.push(`<ogc:Not><ogc:And>${filters.join('')}</ogc:And></ogc:Not>`);
            }
        });
        let otherNullFilters = fields.map(field => {
            return `<ogc:PropertyIsNull><ogc:PropertyName>${field}</ogc:PropertyName></ogc:PropertyIsNull>`;
        });
        let otherFilter = '';
        if (otherNotFilters.length > 0) {
            otherFilter = `<ogc:Filter><ogc:Or><ogc:And>${otherNotFilters.join('')}</ogc:And>${otherNullFilters.join('')}</ogc:Or></ogc:Filter>`
        }
        let otherSymbols = [];
        if (/[L|l]ine/.test(geotype))
            otherSymbols.push(`<Rule><Title>其它${layer.layername}</Title>${otherFilter + helper._getLineSymbolizer()}</Rule>`)
        else if (/[P|p]oint/.test(geotype)) {
            let defaultScale = `<MaxScaleDenominator>${this._findScale(14, false)}</MaxScaleDenominator>`;
            let otherSymbol = defaultScale + helper._getPointSymbolizer(0, hasAngle);
            otherSymbols.push(`<Rule><Title>其它${layer.layername}</Title>${defaultScale + otherFilter + otherSymbol}</Rule>`);
            let otherScale = `<MinScaleDenominator>${this._findScale(14, false)}</MinScaleDenominator>`;
            let otherScaleSymbol = otherScale + helper._getPointSymbolizer(0, hasAngle, null, true);
            otherSymbols.push(`<Rule><Title>其它${layer.layername + "_缩放补充"}</Title>${otherScale + otherFilter + otherScaleSymbol}</Rule>`)
        }
        //默认符号展示在点图层的最低层
        let orderedRules = [];
        orderedRules.push(`<FeatureTypeStyle>${otherSymbols.join('')}<VendorOption name="sortBy">zindex</VendorOption></FeatureTypeStyle>`);
        orderedRules = orderedRules.concat(rules);
        styleXML = `<?xml version="1.0" encoding="utf-8"?>
                <StyledLayerDescriptor version="1.0.0" xsi:schemaLocation="http://www.opengis.net/sld http://schemas.opengis.net/sld/1.0.0/StyledLayerDescriptor.xsd"
                  xmlns="http://www.opengis.net/sld"
                  xmlns:ogc="http://www.opengis.net/ogc"
                  xmlns:xlink="http://www.w3.org/1999/xlink"
                  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
                  <NamedLayer>
                    <UserStyle>
                    ${orderedRules.join('')}
                </UserStyle>
              </NamedLayer>
            </StyledLayerDescriptor>`;
        return styleXML;
    }

    /**
     * 构建样式的rule部分
     */
    _buildStyleRule(zindex, data, filters, geotype, hasAngle) {
        if (data && filters.length > 0) {
            let rules = [];
            let symboleXML = '';
            let filterXML = `<ogc:Filter><ogc:And>${filters.join('')}</ogc:And></ogc:Filter>`;
            let color = typeof data.color == 'undefined' ? '#000000' : data.color;
            let params = [`hcolor=\${env('hcolor',if_then_else(isLike(color,'#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]'),color,'${color}'))}`]; //svg根标签上的颜色值,
            params.push.apply(params, data.colors.map((item, index) => {
                return `hcolor${index}=\${env('hcolor',if_then_else(isLike(color,'#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]'),color,'${item}'))}`
            }));
            let newURL = `${data.contenturl}?${params.join('&amp;')}`; //&转义字符
            if (/[L|l]ine/.test(geotype))
                symboleXML = helper._getLineSymbolizer(data)
            else if (/[P|p]oint/.test(geotype))
                symboleXML = helper._getPointSymbolizer(zindex, hasAngle, Object.assign({
                    svghref: newURL,
                }, data));
            else throw '不支持的几何类型';
            let scale = '',
                otherZoom = 0;
            if (data.minscale != null && data.minscale != undefined && data.minscale > 0)
                scale += `<MinScaleDenominator>${data.minscale}</MinScaleDenominator>`;
            if (data.maxscale != null && data.maxscale != undefined && data.maxscale > 0) {
                scale += `<MaxScaleDenominator>${data.maxscale}</MaxScaleDenominator>`;
                otherZoom = data.maxscale;
            } else {
                //点的默认可见级别为:[14,]
                if (/[P|p]oint/.test(geotype)) {
                    scale += `<MaxScaleDenominator>${this._findScale(14, false)}</MaxScaleDenominator>`;
                    otherZoom = 14;
                }
            }
            rules.push(`<Rule><Title>${data.title}</Title>${filterXML + scale + symboleXML}</Rule>`)
            if (otherZoom > 0 && otherZoom < this.CONFIG.ZOOMS.length && /[P|p]oint/.test(geotype)) {
                let otherScale = `<MinScaleDenominator>${this._findScale(otherZoom, false)}</MinScaleDenominator>`;
                let otherSymbol = helper._getPointSymbolizer(zindex, hasAngle, Object.assign({
                    svghref: newURL,
                }, data), true);
                rules.push(`<Rule><Title>${data.title + '_缩放补充'}</Title>${filterXML + otherScale + otherSymbol}</Rule>`)
            }
            return `<FeatureTypeStyle>${rules.join('')}<VendorOption name="sortBy">zindex</VendorOption></FeatureTypeStyle>`;
        } else return null;
    }
    //#endregion

    /**
     * 设置是否进行点符号的重叠检测(使用前请保证地图使用了最新的样式)
     * @param {Boolean} [testconflict] 是否进行点符号的重叠检测,默认为true
     */
    setConflict(testconflict = true) {
        this._options.testconflict = testconflict;
        var envs = this._envs.filter(p => !/testconflict/i.test(p))
        envs.push(`testconflict:${testconflict}`)
        this._envs = envs
    }

    /**
     * 设置是否使用无缩放级别的显示,可在添加临时图层前使用
     * @param {Boolean} nozoom 是否使用无缩放级别的显示,默认为true
     */
    setNozoom(nozoom = true) {
        this._options.nozoom = nozoom;
        var envs = this._envs.filter(p => !/nozoom/i.test(p))
        if (nozoom)
            envs.push(`nozoom:1`) //nozoom参数设置为非空字符都可开启无缩放级别的显示
        this._envs = envs
    }

    /**
     * 空间过滤要素
     * @param {Array} pointList (WGS84)坐标集合,可构成多边形,可以是多个多边形构成的多边形数组
     * @param {string} [relationship] 几何关系,默认为‘INTERSECTS’-相交关系;可取值为INTERSECTS, DISJOINT, CONTAINS, WITHIN, TOUCHES,CROSSES, OVERLAPS and RELATE
     * 
     * @example
     * 示例五:过滤一个空间
     * this.filterGeometry([
     *    [105.816212, 32.433169],
     *    [105.816985, 32.428826],
     *    [105.820346, 32.424186],
     *    [105.829448, 32.43186],
     *    [105.826919, 32.43769],
     *    [105.817788, 32.437362],
     *    [105.816212, 32.433169],
     *  ])
     * 示例六:只过滤空间,并指定空间关系WITHIN-包含关系
     * this.filterGeometry([
     *    [105.816212, 32.433169],
     *    [105.816985, 32.428826],
     *    [105.820346, 32.424186],
     *    [105.829448, 32.43186],
     *    [105.826919, 32.43769],
     *    [105.817788, 32.437362],
     *    [105.816212, 32.433169],
     *  ],'WITHIN')
     * 示例七:过滤多个空间
     * this.filterGeometry([[
     *    [105.816212, 32.433169],
     *    [105.816985, 32.428826],
     *    [105.820346, 32.424186],
     *    [105.829448, 32.43186],
     *    [105.826919, 32.43769],
     *    [105.817788, 32.437362],
     *    [105.816212, 32.433169],
     *  ],[
          [105.856212, 32.433169],
          [105.856985, 32.428826],
          [105.860346, 32.424186],
          [105.869448, 32.43186],
          [105.866919, 32.43769],
          [105.857788, 32.437362],
          [105.856212, 32.433169],
        ]])
     */
    filterGeometry(pointList, relationship = 'INTERSECTS') {
        let count = this._layers.length
        const relations = ['INTERSECTS', 'DISJOINT', 'CONTAINS', 'WITHIN', 'TOUCHES', 'CROSSES', 'OVERLAPS', 'RELATE'];
        if (count > 0 && Array.isArray(pointList) && pointList.length > 0 && relations.includes(relationship)) {
            let geoFilters = [];
            if (Array.isArray(pointList[0][0])) {
                //多个多边形,使用setCRS方法指定坐标系,不采用SRID=4326;以避免使用分号造成的冲突
                pointList.filter(item => item.length > 4).forEach(points => {
                    geoFilters.push(`${relationship}(${this.CONFIG.GEOField},setCRS(POLYGON((${points.map(p => p[0] + ' ' + p[1]).join(',')})),'EPSG:4326'))`);
                })
            } else {
                //构造各图层的空间过滤条件
                geoFilters.push(`${relationship}(${this.CONFIG.GEOField},setCRS(POLYGON((${pointList.map(p => p[0] + ' ' + p[1]).join(',')})),'EPSG:4326'))`);
            }
            if (geoFilters.length > 0) {
                let filters = []
                let preFilters = this._CQL_FILTER == null ? null : this._CQL_FILTER.split(';')
                let geoFilter = geoFilters.join(' OR ');
                for (var i = 0; i < count; i++) {
                    if (preFilters == null) filters.push(geoFilter)
                    else filters[i] = `(${preFilters[i]}) AND (${geoFilter})`
                }
                this._CQL_FILTER = filters.join(';')
                this.refresh()
            }
        } else {
            this._CQL_FILTER = null
            this.refresh()
        }
        return this
    }

    /**
     * 创建工作空间
     * @param {object} [option] 配置信息,缺少时会参考当前实例的参数
     * 注意:不指定完整的数据库连接信息时,需要保证geoserver上已有postgis的存储,新建时会复用该存储的数据库连接信息
     * @param {string} [option.service] geoserver服务地址
     * @param {string} [option.workspace] 工作空间
     * @param {string} [option.database] 数据库实例名
     * @param {string} [option.host] 数据库host
     * @param {number} [option.port] 数据库端口
     * @param {string} [option.user] 数据库访问用户
     * @param {string} [option.passwd] 数据库访问密码
     * 
     * @example
     * var wms = new mapwms('http://localhost:8085/geoserver','demo')
     * 示例一:
     * //wms.initialize().then(() => { console.log('加载完毕') })//由于demo不存在,故不用调用initialize方法
     * //新建工作空间demo
     * wms.createWorkspace()
     * 示例二:
     * //新建工作空间demo2
     * wms.createWorkspace({
     *  workspace:'demo2'
     * })
     * 示例三:
     * //在新geoserver上创建工作空间demo3
     * wms.createWorkspace({
     *  service:'http://192.168.10.212:8080/geoserver',
     *  workspace:'demo3'
     * })
     * 示例四:
     * //新建工作空间dem04,指定完整的数据库连接信息
     * wms.createWorkspace({
     *        workspace: 'dem04',
     *        database: 'dem3_DB',
     *        host: '127.0.0.1',
     *        port: 2345,
     *        user: 'postgres',
     *        passwd:'admin'
     *    }
     */
    async createWorkspace(option) {
        //postgres:password@localhost:5432/db
        //在初始化工作空间时,若发现工作空间不存在,则新建工作空间;
        //创建存储的时候从已有的postgis存储创建,若不存在已有存储,则从option中获取数据库连接信息
        //关键:rest api获取的已有存储的加密数据库用户密码可以在新建存储的rest api中使用
        let {
            service,
            workspace,
            database,
            host,
            port,
            user,
            passwd
        } = option ? option : {};
        if (service == undefined) service = this._service;
        if (workspace == undefined) workspace = this._workspace;
        //工作空间已存在时,返回
        if (await Init.isExistWorkspace(service, workspace)) {
            return;
        }
        //处理输入参数
        //查询geoserver已有数据存储情况,用于判断各参数取值
        let dataStore = await Init.findDatastore(service, 'PostGIS');
        if (!database && !dataStore) throw new Error("缺少数据库实例")
        else if (!database && dataStore.database) database = dataStore['database'];
        if (!host && !dataStore) throw new Error("缺少数据库地址");
        else if (!host && dataStore.host) host = dataStore.host;
        if (!port && !dataStore) throw new Error("缺少参数据库端口");
        else if (!port && dataStore.port) port = dataStore.port;
        if (!user && !dataStore) throw new Error("缺少数据库用户名");
        else if (!user && dataStore.user) user = dataStore.user;
        if (!passwd && !dataStore) throw new Error("缺少数据库密码");
        if (!passwd && dataStore.passwd) passwd = dataStore.passwd;
        let dbtype = 'postgis';
        await Init.createWorkspace({
            service,
            workspace,
            dbtype,
            database,
            host,
            port,
            user,
            passwd
        });
    }

    /**
     * 删除工作空间
     * @param {object} [option] 配置信息,缺少时会参考当前实例的参数
     * @param {string} [option.service] geoserver服务地址
     * @param {string} [option.workspace] 工作空间
     */
    async removeWorkspace(option) {
        let {
            service,
            workspace,
        } = option ? option : {};
        if (service == undefined) service = this._service;
        if (workspace == undefined) workspace = this._workspace;
        return Init.removeWorkspace(service, workspace)
    }
}

export default mapwms