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