Source: amapwms.js

import mapwms from './mapwms'
import {
    BASELAYER,
    FOCUSLAYER,
    EXTRALAYER,
    codec,
    transform
} from './helper/tools'
import anime from 'animejs'
import axios from 'axios'
import Qs from 'qs'
/**
 * 用于封装前端和geoserver的交互,不考虑前端GIS技术框架,只考虑和标准的OGC服务交互(基于高德地图)
 * @extends mapwms
 *
 * @example
 *
 * 示例一:
 * var wms = new amapwms('http://localhost:8085/geoserver','demo')
 * wms.initialize().then(() => { console.log('加载完毕') })
 * 示例二:
 * var wms = new amapwms('http://localhost:8085/geoserver','demo',{
 *  supportRightClick: true,
 *  hcolor: '#00ffff'
 * })
 * 示例三:
 * var wms = new amapwms('http://localhost:8085/geoserver','demo',{
 *  supportRightClick: true,
 *  hcolor: '#00ffff'
 * },{
 *  tileSize:512,
 *  zIndex:1,
 *  buffer:60
 * })
 * wms.initialize().then(() => { console.log('加载完毕') })
 */
class amapwms extends mapwms {
    // AMap实例
    _map = null
    // 缓存图层
    _cacheLayers = {}
    /**
     * 构造方法
     * @param {String} map 高德地图实例
     * @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.WGS84TOGCJ02] 图层纠偏,由WGS84坐标转换为火星坐标,默认值为false
     * @param {String} [options.hcolor] 元素高亮颜色,仅在 autoHighlight = true 时有效
     * @param {Boolean} [options.testconflict] 是否进行点符号的重叠检测,取值为true时,对点符号进行重叠检测并抽稀;取值为false时,允许点符号重叠显示
     * @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(map, service = '/geoserver', workspace = '', options = {}, ogc = {}) {
        super(service, workspace, options, ogc)
        this._map = map
        map && this.setMap(map)
    }
    /**
     * 设置高德地图对象
     * 
     * @fires amapwms#wms:initialized 初始化完成事件
     * 
     * @param {AMap} map 高德地图实例 
     */
    setMap(map) {
        // 重新设置地图对象前,需清理地图
        this.clear()
        if (!map) return
        this._map = map

        var {
            supportClick,
            supportRightClick
        } = this._options
        supportClick && map.on('click', this.mapClick.bind(this))
        supportRightClick && map.on('rightclick', this.mapRightClick.bind(this))
        this.on('wms:upload', this.createWMSLayer);
    }
    calculateOffset() {
        const map = this._map
        // map 对象存在证明已经加到地图中去了
        if (map) {
            const zoom = map.getZoom();
            const center = map.getCenter()
            const [lng, lat] = transform.gcj02towgs84(center.lng, center.lat)
            const p1 = map.lnglatToPixel(center, zoom)
            const p2 = map.lnglatToPixel(new AMap.LngLat(lng, lat), zoom)
            const l = Math.pow(2, 20 - zoom)
            const offsetX = Math.floor(p1.x - p2.x) * l
            const offsetY = Math.floor(p1.y - p2.y) * l
            return { offsetX, offsetY }
        }
    }
    // 创建WMS图层
    createWMSLayer(layerName, OGC, extras) {
        if (!layerName) throw '必须设置图层名称'
        var map = this._map
        var cacheLayers = this._cacheLayers
        var { WGS84TOGCJ02 } = this._options
        var ls = cacheLayers[layerName]
        if (!ls) cacheLayers[layerName] = ls = []

        while (ls.length > 0) ls.shift().setMap(null)

        if (OGC) {
            (Array.isArray(OGC) ? OGC : [OGC]).forEach(p => {
                (Array.isArray(extras) ? extras : [extras]).forEach(extra => {
                    let layer = null;

                    //适配高清屏时也用post,因为高德自带get请求不能使用两倍图片机制
                    if (this._retina || (extra !== undefined && ((extra.FEATUREID && extra.FEATUREID.length > 1024) || (extra.CQL_FILTER && extra.CQL_FILTER.length > 1024)))) {
                        //构建post wms layer
                        layer = this.createPostWMS(p, extra, !!WGS84TOGCJ02);
                    }
                    else if (!!WGS84TOGCJ02)
                        //构建post wms layer
                        layer = this.createPostWMS(p, extra, !!WGS84TOGCJ02);
                    else
                        layer = this.createGetWMS(p, extra)
                    layer.setMap(map);
                    ls.push(layer);
                });
            })
        }
        this.emit('wms:initialized', this);
    }
    /**
     * 构建基于get wms请求的图层
     * @param {*} ogc 
     * @param {*} extra 
     */
    createGetWMS(ogc, extra) {
        var { url, service, version, request, crs, layers, format, transparent, tileSize, zIndex, buffer, format_options } = ogc

        var layer = new AMap.TileLayer.WMS({
            url,
            blend: false,
            zIndex,
            tileSize,
            params: {
                transparent, format, service, version, request, layers, crs, buffer,
                format_options,
                ...extra
            }
        })
        return layer;
    }
    /**
     * 构建基于post wms的图层
     * @param {*} ogc 
     * @param {*} extra 
     * @param {number} offsetX 是否进行火星坐标纠偏,投影坐标系偏移量
     * @param {number} offsetY 是否进行火星坐标纠偏,投影坐标系偏移量
     */
    createPostWMS(ogc, extras, WGS84TOGCJ02 = false) {
        codec.decodeURIComponent(extras)

        var service = this._service
        var { layers: LAYERS, transparent: TRANSPARENT, tileSize, zIndex, buffer: BUFFER, format_options: FORMAT_OPTIONS } = ogc

        let postTileSize = this._retina ? Math.floor(tileSize * this._devicePixelRatio) : tileSize

        var c = function (r) {
            // 神秘公式,我也不知道为什么,只能找专业的人解释了,从高德源码参考的常量
            return { x: 0.14929107086948487 * r.x - 2.0037508342789244E7, y: 2.0037508342789244E7 - 0.14929107086948487 * r.y }
        }
        var self = this
        var createTile = async function (x, y, z, success, fail) {

            var l = Math.pow(2, 20 - z) * tileSize
            var a = { x: l * x, y: l * (y + 1) }
            var b = { x: l * (x + 1), y: l * y }

            if (WGS84TOGCJ02) {
                var offset = self.calculateOffset()

                a.x -= offset.offsetX
                b.x -= offset.offsetX
                a.y -= offset.offsetY
                b.y -= offset.offsetY
            }



            var { x: minx, y: miny } = c(a)
            var { x: maxx, y: maxy } = c(b)

            try {
                var req = await axios.post(`${service}/wms`, Qs.stringify({
                    TRANSPARENT,
                    LAYERS,
                    BUFFER,
                    FORMAT: 'image/png',
                    SERVICE: 'WMS',
                    VERSION: '1.3.0',
                    REQUEST: 'GetMap',
                    CRS: 'EPSG:3857',
                    WIDTH: postTileSize,
                    HEIGHT: postTileSize,
                    BBOX: [minx, miny, maxx, maxy].join(','),
                    FORMAT_OPTIONS,
                    ...extras
                }), {
                    responseType: 'blob'
                })
                var src = URL.createObjectURL(req.data)
                var img = document.createElement('img');
                img.src = src
                img.onload = function () {
                    URL.revokeObjectURL(src)
                }
                success(img)
            } catch (error) { fail(error) }
        }
        var layer = new AMap.TileLayer.Flexible({
            zIndex,
            tileSize,
            cacheSize: 2,
            createTile
        })
        return layer;
    }
    // 地图左键点击,
    mapClick(e) {
        var bounds = this._map.getBounds()
        var size = this._map.getSize()

        var X, Y, WIDTH, HEIGHT, BBOX;
        X = e.pixel.x
        Y = e.pixel.y
        WIDTH = size.width
        HEIGHT = size.height
        BBOX = `${bounds.southwest.lng},${bounds.southwest.lat},${bounds.northeast.lng},${bounds.northeast.lat}`

        this.query(X, Y, WIDTH, HEIGHT, BBOX).then(response => {
            var {
                autoHighlight,
                autoCancelHighlight,
                hcolor
            } = this._options
            if (!autoHighlight) return
            var feature = response.data.features[0]
            if (feature) this.setHilightFeature(feature.id, hcolor)
            else if (autoCancelHighlight) this.setHilightFeature()
            this.onQueryClick(e.lnglat, response.data)
        })
    }
    // 地图右键点击
    mapRightClick(e) {
        var bounds = this._map.getBounds()
        var size = this._map.getSize()

        var X, Y, WIDTH, HEIGHT, BBOX;
        X = e.pixel.x
        Y = e.pixel.y
        WIDTH = size.width
        HEIGHT = size.height
        BBOX = `${bounds.southwest.lng},${bounds.southwest.lat},${bounds.northeast.lng},${bounds.northeast.lat}`

        this.query(X, Y, WIDTH, HEIGHT, BBOX).then(response => {
            var {
                autoHighlight,
                autoCancelHighlight,
                hcolor
            } = this._options
            if (!autoHighlight) return
            var feature = response.data.features[0]
            if (feature) this.setHilightFeature(feature.id, hcolor)
            else if (autoCancelHighlight) this.setHilightFeature()
            this.onQueryRightClick(e.lnglat, response.data)
        })
    }
    /**
     * 触发点击事件
     * @fires amapwms#queryclick
     */
    onQueryClick(lnglat, data) {
        /**
         * 地图点击查询事件
         * @event amapwms#queryclick
         * @type {Object}
         * @property {Object} lnglat 经纬度
         * @property {Number} lnglat.lng 经度
         * @property {Number} lnglat.lat 纬度
         * @property {...Object} others 其他参数,注意这是others不是代表属性名,而是代表有其他的变量
         */
        this.emit('queryclick', {
            lnglat,
            ...data
        })
    }
    /**
     * 触发右键点击事件
     * @fires amapwms#queryrightclick
     */
    onQueryRightClick(lnglat, data) {
        /**
         * 地图右键点击查询事件
         * @event amapwms#queryrightclick
         * @type {Object}
         * @property {Object} lnglat 经纬度
         * @property {Number} lnglat.lng 经度
         * @property {Number} lnglat.lat 纬度
         * @property {...Object} others 其他参数,注意这是others不是代表属性名,而是代表有其他的变量
         */
        this.emit('queryrightclick', {
            lnglat,
            ...data
        })
    }
    /**
     * 设置地图缩放合适的视野级别
     * @param {String|Array<String>} feature 要素ID或要素ID数组
     * @example
     * 
     * this.fitView('pipe.131')
     * this.fitView(['pipe.132', 'pipe.133', 'pipe.131'])
     */
    fitView(feature) {
        if (feature === undefined || feature === null) return Promise.reject('缩放视野级别时,必选传入参考的要素')
        var map = this._map
        return this.queryFeature(feature).then((req) => {
            var c = {
                Point: function (coordinate) {
                    return new AMap.Marker({
                        position: new AMap.LngLat(coordinate[0], coordinate[1]),
                    })
                },
                LineString: function (coordinates) {
                    return new AMap.Polyline({
                        path: coordinates.map(
                            (coordinate) => new AMap.LngLat(coordinate[0], coordinate[1])
                        ),
                    })
                },
                MultiLineString: function (coordinates) {
                    return new AMap.Polyline({
                        path: coordinates[0].map(
                            (coordinate) => new AMap.LngLat(coordinate[0], coordinate[1])
                        ),
                    })
                },
            }
            // 计算显示范围
            var overlays = req.features
                .map(
                    (f) =>
                        c[f.geometry.type] && c[f.geometry.type](f.geometry.coordinates)
                )
                .filter((p) => p)
            if (overlays.length === 0) return Promise.reject('未查询到地图要素')
            map.setFitView(overlays, true)
            return Promise.resolve()
        })
    }
    /**
     * 使指定图层闪烁
     * @param {String} layerName 图层名称
     * @param {Object} options animejs 动画参数
     * @see {@link https://www.animejs.cn/documentation/#duration options animejs 动画参数}
     */
    twinkle(layerName, options) {
        var ls = this._cacheLayers[layerName]
        if (!ls || ls.length === 0) return
        this._animeObj = anime(
            {
                targets: { opacity: 1 },
                opacity: 0,
                direction: 'alternate',
                loop: true,
                easing: 'easeInOutSine',
                update: function (a) {
                    var opacity = a.animations[0].currentValue
                    ls.forEach(l => l.setOpacity(opacity))
                },
                ...options
            })
    }
    /**
     * 清理图层闪烁动画
     */
    clearTwinkle() {
        if (this._animeObj) {
            this._animeObj.pause()
            this._animeObj.seek(0)
        }
        this._animeObj = null
    }
    /**
     * 清理地图
     */
    clear() {
        this.clearTwinkle()
        this.off('wms:upload')
        var {
            supportClick,
            supportRightClick
        } = this._options
        // 清理事件
        supportClick && this._map && this._map.off('click')
        supportRightClick && this._map && this._map.off('rightclick')

        var cacheLayers = this._cacheLayers
        Object.keys(cacheLayers).forEach(layerName => {
            var ls = cacheLayers[layerName]
            if (!ls) cacheLayers[layerName] = ls = []
            while (ls.length > 0) ls.shift().setMap(null)
        });
    }

    /**
     * 上传高德地图元素覆盖物,当前仅支持 Map.Marker 和 AMap.Polyline
     *
     * 高德元素对象需设置 extData 数据
     *
     * @example
     *
     * var overlay = new AMap.Marker()
     *
     * overlay.setExtData({
     *    // *必填项,设备或者管线是否已矢量缩放的形式进行呈现,默认值为true
     *    fixed: true,
     *    // *必填项,设备类型id,不管是管道还是阀门还是其他设备,均需要填写该值
     *    type: '20ab312e-d505-4c37-8b85-ac75007e2a31',
     *    // 选填项,设备业务属性,支持自定义对象
     *    property: {
     *
     *    },
     *    // 选填项,设备样式属性,对于管线而言size是线宽度,对于点而言,size是图标大小
     *    style:{
     *        // 选填项,符号层级
     *        zindex:1,
     *        // 选填项,图标大小或者管线宽度,注意 fixed 为真是代表矢量缩放,此时size的单位为m,否则size的单位是px
     *        size: 8,
     *        // 选填项,图标颜色或者管线颜色,
     *        color: '#ffffff'
     *        zoom:[15,19]?暂不能实现
     *    }
     * })
     *
     * util.uploadOverlays(overlay) 或者 util.uploadOverlays([overlay])
     * @param {Object|Array} 高德元素对象或者高德元素对象数组
     * @returns {Promise}
     */
    uploadOverlays(overlay) {
        if (!overlay) return Promise.resolve()
        let overlays = Array.isArray(overlay) ? overlay : [overlay]

        // 插入管线
        let lines = overlays.filter(o => o instanceof AMap.Polyline).map(o => {
            let path = o.getPath().map(o => [o.lng, o.lat])
            let extData = o.getExtData()
            extData.property = Object.assign({}, extData.property, {
                status: 0 //默认设备状态,0-正常
            });
            return {
                path,
                property: extData.property,
                style: extData.style,
                type: extData.type,
                fixed: extData.fixed
            }
        });

        // 插入设备
        let points = overlays.filter(o => o instanceof AMap.Marker).map(o => {
            let pos = o.getPosition()
            let extData = o.getExtData()
            extData.property = Object.assign({}, extData.property, {
                status: 0 //默认设备状态,0-正常
            });
            return {
                lng: pos.lng,
                lat: pos.lat,
                property: extData.property,
                style: extData.style,
                type: extData.type,
                fixed: extData.fixed
            }
        });
        return Promise.all([this.addPolyline(lines), this.addPoint(points)]);
    }
    /**
     * 新建BASE图层(底图)
     */
    clearBaseLayer() {
        // todo 清理地图后,由于未设置查询条件导致要素还能查询出来
        this.onWmsUpload(BASELAYER)
    }
    /**
     * 新建自定义临时图层(自定义图层)
     * 
     * @param {String} layerName 图层名称
     * @param {(String|Object|Array)} [feature] 要素
     * @param {String} [color=#00ffff] 要素默认高亮颜色,十六进制格式,示意:'#00ffff'
     * @param {Boolean} [twinkle=false] 是否闪烁该图层,默认值为false
     * @param {Boolean} [duration=1000] 闪烁图层动画时长,默认值为1000
     * 
     * @example
     * 
     * 示例一:this.addCustomLayer('BASELAYER','pipe.1','#00ffff') // 设置一个高亮要素,pipe.1的高亮颜色为 '#00ffff'
     * 示例二:this.addCustomLayer('BASELAYER',{id:'pipe.1'},'#00ffff') // 设置一个高亮要素,pipe.1的高亮颜色为 '#00ffff'
     * 示例三:this.addCustomLayer('BASELAYER',{id:'pipe.1',color:'#00ffff'}) // 设置一个高亮要素,pipe.1的高亮颜色为 '#00ffff'
     * 示例四:this.addCustomLayer('BASELAYER',['pipe.1','pipe.2'],'#00ffff') // 设置两个高亮要素,pipe.1、pipe.2的高亮颜色为 '#00ffff'
     * 示例五:this.addCustomLayer('BASELAYER',[{id:'pipe.1'},{id:'pipe.2'}],'#00ffff') // 设置两个高亮要素,pipe.1、pipe.2的高亮颜色为 '#00ffff'
     * 示例六:this.addCustomLayer('BASELAYER',[{id:'pipe.1',color:'#00ffff'},{id:'pipe.2',color:'#00ffff'}]) // 设置两个高亮要素,pipe.1、pipe.2的高亮颜色为 '#00ff00',注意此情况时一般使用 示例七,需要设置默认高亮色
     * 示例七:this.addCustomLayer('BASELAYER',[{id:'pipe.1',color:'#00ff00'},{id:'pipe.2'}],'#00ffff') // 设置两个高亮要素,pipe.1的高亮颜色为 '#00ff00',pipe.2的高亮颜色为默认值 #00ffff'
     * 示例八:this.addCustomLayer('BASELAYER') // 清除自定义临时图层
     */
    addCustomLayer(layerName, feature, color, twinkle = false, duration = 1000) {
        this.setHierarchyFeature(feature, color || undefined, layerName)
        twinkle && this.twinkle(layerName, { duration })
    }
    /**
     * 新建自定义临时图层(自定义图层)
     * @param {String} layerName 图层名称
     */
    clearCustomLayer(layerName) {
        this.onWmsUpload(layerName)
    }

    /**
     * 新建自定义临时图层(拓展图层)
     * 
     * @param {(String|Object|Array)} [feature] 要素
     * @param {String} [color=#00ffff] 要素默认高亮颜色,十六进制格式,示意:'#00ffff'
     * @param {Boolean} [twinkle=false] 是否闪烁该图层,默认值为false
     * @param {Boolean} [duration=1000] 闪烁图层动画时长,默认值为1000
     * 
     * @example
     * 
     * 示例一:this.addExtraLayer('pipe.1','#00ffff') // 设置一个高亮要素,pipe.1的高亮颜色为 '#00ffff'
     * 示例二:this.addExtraLayer({id:'pipe.1'},'#00ffff') // 设置一个高亮要素,pipe.1的高亮颜色为 '#00ffff'
     * 示例三:this.addExtraLayer({id:'pipe.1',color:'#00ffff'}) // 设置一个高亮要素,pipe.1的高亮颜色为 '#00ffff'
     * 示例四:this.addExtraLayer(['pipe.1','pipe.2'],'#00ffff') // 设置两个高亮要素,pipe.1、pipe.2的高亮颜色为 '#00ffff'
     * 示例五:this.addExtraLayer([{id:'pipe.1'},{id:'pipe.2'}],'#00ffff') // 设置两个高亮要素,pipe.1、pipe.2的高亮颜色为 '#00ffff'
     * 示例六:this.addExtraLayer([{id:'pipe.1',color:'#00ffff'},{id:'pipe.2',color:'#00ffff'}]) // 设置两个高亮要素,pipe.1、pipe.2的高亮颜色为 '#00ff00',注意此情况时一般使用 示例七,需要设置默认高亮色
     * 示例七:this.addExtraLayer([{id:'pipe.1',color:'#00ff00'},{id:'pipe.2'}],'#00ffff') // 设置两个高亮要素,pipe.1的高亮颜色为 '#00ff00',pipe.2的高亮颜色为默认值 #00ffff'
     * 示例八:this.addExtraLayer() // 清除高亮要素
     */
    addExtraLayer(feature, color, twinkle = false, duration = 1000) {
        this.setHierarchyFeature(feature, color || undefined)
        twinkle && this.twinkle(EXTRALAYER, { duration })
    }
    /**
     * 清理自定义临时图层(拓展图层)
     */
    clearExtraLayer() {
        this.setHierarchyFeature()
    }
}

export default amapwms