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('&')}`; //&转义字符
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