import { Zondy, Collection, WMTSStyle } from '../../../base'
import { LayerType, LoadStatus } from '../../../base/enum'
import { defaultValue, Log, toJSON } from '../../../util'
import { WMTSServer } from '../../../service'
import WMTSSubLayer from './WMTSSubLayer'
import TileMatrixSet from '../../../base/TileMatrixSet'
import { Extent, Point, SpatialReference } from '../../../base/geometry'
import {
getEPSGCodeFromOGCSupportedCRSString,
getEPSGFromTileMatrixSetId,
getTileResolution,
initCorporationType
} from '../support/Utils'
import OGCLayer from './OGCLayer'
import TileInfo from '../support/TileInfo'
import LOD from '../support/LOD'
import Projection from '../../../base/Projection'
/**
* WMTS图层,<br>
* 目前二维上支持4326(包括4490,4214以及4610),3857以及EPSG支持的自定义坐标系,三维上仅支持4326(包括4490,4214以及4610)以及3857坐标系,WMTS服务会自动读取元信息上的坐标系,不需要用户指定
* <br><br>[ES5引入方式]:<br/>
* Zondy.Layer.WMTSLayer() <br/>
* [ES6引入方式]:<br/>
* import { WMTSLayer } from "@mapgis/webclient-common" <br/>
* <br/>
* 针对图层的操作请在图层加载完毕事件中进行<br/>
* Layer.on('layerview-created', function (result) {<br/>
* console.log("加载完毕:", result.layer)<br/>
* });<br/>
* 如果不想在该事件中放入业务代码,则请确认图层资源已加载完毕后再进行操作<br/>
* if(layer.loadStatus === 'loaded') {<br/>
* // 你的业务逻辑<br/>
* }
* @class WMTSLayer
* @moduleEX LayerModule
* @extends OGCLayer
* @fires Layer#图层加载完毕事件
* @fires Layer#图层销毁完毕事件
* @fires Layer#图层更新完毕事件
* @fires Layer#图层显隐更新完毕事件
* @fires Layer#图层透明度更新完毕事件
* @fires Layer#图层顺序更新完毕事件
* @fires Layer#图层刷新完毕事件
* @param {Object} options 构造参数
* @param {String} [options.url] 服务基地址,支持如下服务:<br/>
* 1、支持MapGIS的WMTS服务,格式为:http://{ip}:{port}/igs/rest/services/{ServiceName}/WMTSServer,请注意,当在IGS中发布自定义坐标系的WMTS服务时,可能会有部分地图无法正确的进行预览,
* 并且该服务中的范围参数是错误的,请自行指定跳转范围,或从该服务对应的瓦片服务中获取,参考示例:<a href='#MapIGS'>[加载MapGIS的WMTS服务]</a><br/>
* 2、支持ArcGIS的WMTS服务,格式为:http://{ip}:{port}/arcgis/rest/services/{ServiceName}/MapServer/WMTS,参考示例:<a href='#ArcGIS'>[加载ArcGIS的WMTS服务]</a><br/>
* 3、支持天地图的WMTS服务:格式为:http://t{0~6的随机数}.tianditu.gov.cn/{图层类型}_{坐标系}/wmts,<br/>
* 其中天地图层类型如下:<br/>
* vec:矢量底图、img:卫星影像底图、cva: 矢量注记图层(中文)、eva: 矢量注记图层(英文)、cia: 注记图层(中文)、eia: 注记图层(英文)、ter: 地形晕渲底图、cta: 注记(中文)、ibo: 全球国界<br/>
* 坐标系如下:<br/>
* w:墨卡托坐标系(3857)、c:经纬度坐标系(4326)<br/>
* 参考示例:<a href='#tdt'>[加载天地图的WMTS服务]</a><br/>
* @param {Number} [options.opacity = 1] 图层透明度,0到1之间的值,0为完全透明,1为不透明,参考示例:<a href='#opacity'>[修改图层透明度]
* @param {Boolean} [options.visible = true] 图层显示或隐藏,true则显示,false则隐藏,参考示例:<a href='#visible'>[设置图层显隐]
* @param {Number} [options.minScale = 0] 最小缩放级数,仅会请求级数大于等于minScale的图片
* @param {Number} [options.maxScale = 19] 最大缩放级数,仅会请求级数小于等于maxScale的图片
* @param {String} [options.tokenKey = 'token'] token名
* @param {String} [options.tokenValue] token值,只有当tokenValue存在时,才会绑定token
* @param {Polygon|Extent|Circle|null} [options.clippingArea = null] 图层空间裁剪范围,仅支持多边形裁剪、矩形裁剪、圆形裁剪
*
* @summary <h5>支持如下方法:</h5>
* <a href='#load'>[1、加载图层资源]</a><br/>
* <a href='#findSublayerById'>[2、根据子图层id查询图层]</a><br/>
* <a href='#fromServerUrl'>[3、通过url创建图层对象]</a><br/>
* <a href='#fromJSON'>[4、通过传入的json构造并返回一个新的几何对象]</a><br/>
* [5、导出为json对象]{@link OGCLayer#toJSON}<br/>
* [6、克隆几何对象]{@link OGCLayer#clone}
*
* @example <caption><h7 id='MapIGS'>加载WMTS图层</h7></caption>
* // ES5引入方式
* const { Map, MapView, SceneView } = Zondy
* const { WMTSLayer } = Zondy.Layer
* // ES6引入方式
* import { Map, MapView, SceneView, WMTSLayer } from "@mapgis/webclient-common"
* // 初始化图层管理容器
* const map = new Map();
* // 初始化二维地图视图对象
* const mapView = new MapView({
* // 视图id
* viewId: "viewer-id",
* // 图层管理容器
* map: map
* });
* // 初始化三维地图视图对象
* const mapView = new SceneView({
* // 视图id
* viewId: "viewer-id",
* // 图层管理容器
* map: map
* });
* // 初始化WMTS图层
* const wmtsLayer = new WMTSLayer({
* // 服务基地址
* url: 'http://{ip}:{port}/igs/rest/services/{serviceName}/WMTSServer'
* });
* // 将图层加入容器中
* map.add(wmtsLayer);
* // 图层加载完毕
* wmsLayer.on('layerview-created', function (result) {
* console.log("加载完毕:", result.layer)
* // 视点跳转
* sceneView.flyTo({
* extent: result.layer.extent
* })
* });
*
* @example <caption><h7 id='ArcGIS'>添加ArcGIS的WMTS图层示例</h7></caption>
* // ES5引入方式
* const { Map, MapView, SceneView } = Zondy
* const { WMTSLayer } = Zondy.Layer
* // ES6引入方式
* import { Map, MapView, SceneView, WMTSLayer } from "@mapgis/webclient-common"
* // 初始化图层管理容器
* const map = new Map();
* // 初始化二维地图视图对象
* const mapView = new MapView({
* // 视图id
* viewId: "viewer-id",
* // 图层管理容器
* map: map
* });
* // 初始化三维地图视图对象
* const mapView = new SceneView({
* // 视图id
* viewId: "viewer-id",
* // 图层管理容器
* map: map
* });
* // 初始化WMTS图层
* const wmtsLayer = new WMTSLayer({
* url: 'http://{ip}:{port}/arcgis/rest/services/{serviceName}/MapServer/WMTS'
* });
* // 将图层加入容器中
* map.add(wmtsLayer);
* // 图层加载完毕
* wmsLayer.on('layerview-created', function (result) {
* console.log("加载完毕:", result.layer)
* // 视点跳转
* sceneView.flyTo({
* extent: result.layer.extent
* })
* });
*
* @example <caption><h7 id='tdt'>加载天地图的WMTS服务</h7></caption>
* // ES5引入方式
* const { Map, MapView, SceneView } = Zondy
* const { WMTSLayer } = Zondy.Layer
* // ES6引入方式
* import { Map, MapView, SceneView, WMTSLayer } from "@mapgis/webclient-common"
* // 初始化图层管理容器
* const map = new Map();
* // 初始化二维地图视图对象
* const mapView = new MapView({
* // 视图id
* viewId: "viewer-id",
* // 图层管理容器
* map: map
* });
* // 初始化三维地图视图对象
* const mapView = new SceneView({
* // 视图id
* viewId: "viewer-id",
* // 图层管理容器
* map: map
* });
* 初始化WMTS图层
* const wmtsLayer = new WMTSLayer({
* // 加载经纬度的影像地图
* url: 'http://t6.tianditu.gov.cn/img_c/wmts',
* // 天地图必须加token,且token名为tk
* tokenKey: 'tk',
* // token请在天地图官网申请
* tokenValue: '天地图token'
* });
* // 将图层加入容器中
* map.add(wmtsLayer);
* // 图层加载完毕
* wmsLayer.on('layerview-created', function (result) {
* console.log("加载完毕:", result.layer)
* // 视点跳转
* sceneView.flyTo({
* extent: result.layer.extent
* })
* });
*
* @example <caption><h7 id='opacity'>修改图层透明度</h7></caption>
* // ES5引入方式
* const { WMTSLayer } = Zondy.Layer
* // ES6引入方式
* import { WMTSLayer } from "@mapgis/webclient-common"
* // 初始化时设置
* const wmtsLayer = new WMTSLayer({
* // 服务基地址
* url: 'http://{ip}:{port}/igs/rest/services/{serviceName}/WMTSServer',
* // 设置透明度
* opacity: 1.0
* });
* // 将图层加入容器中
* map.add(wmtsLayer);
*
* // 加载完成后设置
* wmsLayer.on('layerview-created', function (result) {
* console.log("加载完毕:", result.layer)
* // 设置透明度
* wmtsLayer.opacity = 0.5
* })
*
* @example <caption><h7 id='visible'>设置图层显隐</h7></caption>
* // ES5引入方式
* const { WMTSLayer } = Zondy.Layer
* // ES6引入方式
* import { WMTSLayer } from "@mapgis/webclient-common"
* // 初始化时设置
* const wmtsLayer = new WMTSLayer({
* // 服务基地址
* url: 'http://{ip}:{port}/igs/rest/services/{serviceName}/WMTSServer',
* // 设置图层显隐
* visible: true
* });
* // 将图层加入容器中
* map.add(wmtsLayer);
*
* // 加载完成后设置
* wmsLayer.on('layerview-created', function (result) {
* console.log("加载完毕:", result.layer)
* // 设置图层显隐
* wmtsLayer.visible = !wmtsLayer.visible
* })
*
* @example <caption><h7>删除图层</h7></caption>
* map.remove(wmtsLayer)
*/
class WMTSLayer extends OGCLayer {
constructor(options) {
super(options)
options = defaultValue(options, {})
/**
* 图层类型
* @readonly
* @member {type} WMTSLayer.prototype.type
*/
this.type = LayerType.wmts
/**
* 当前处于活动状态的子图层
* @member {WMTSSubLayer} WMTSLayer.prototype.activeLayer
*/
this.activeLayer = defaultValue(options.activeLayer, new WMTSSubLayer())
/**
* WMTSSublayer 子图层对象的集合
* @readonly
* @member {Array<WMTSSubLayer>} WMTSLayer.prototype.sublayers
*/
this.sublayers = defaultValue(options.sublayers, new Collection())
if (!(this.sublayers instanceof Collection)) {
this.sublayers = new Collection(this.sublayers)
}
/**
* 服务基地址
* @readonly
* @member {String} WMTSLayer.prototype.url
*/
this.url = defaultValue(options.url, undefined)
/**
* 空间参考
* @readonly
* @member {SpatialReference} WMTSLayer.prototype.spatialReference
*/
this.spatialReference = SpatialReference.fromJSON(options.spatialReference)
/**
* 图块矩阵集的集合
* @readonly
* @member {Array<TileMatrixSet>} WMTSLayer.prototype.tileMatrixSets
*/
this.tileMatrixSets = defaultValue(options.tileMatrixSets, [])
/**
* WMTS 公司代码
* @readonly
* @member {String} WMTSLayer.prototype.corporationType
*/
this.corporationType = initCorporationType(this.url)
/**
* 最小缩放级别
* @member {Number} WMTSLayer.prototype.minScale
*/
this.minScale = defaultValue(options.minScale, 0)
/**
* 最大缩放级别
* @member {Number} WMTSLayer.prototype.maxScale
*/
this.maxScale = defaultValue(options.maxScale, 19)
/**
* 图片宽度
* @readonly
* @member {Number} WMTSLayer.prototype.imageWidth
*/
this.imageWidth = 256
/**
* 图片高度
* @readonly
* @member {Number} WMTSLayer.prototype.imageHeight
*/
this.imageHeight = 256
// 空间裁剪范围
this._clippingArea = defaultValue(options.clippingArea, null)
// 地图服务对象
this._mapServer = new WMTSServer({
url: this.url
})
}
/**
* 通过传入的json构造并返回一个新的几何对象<a id='fromJSON'></a>
* @param {Object} [json] JSON对象
* @example <caption><h7>通过传入的json构造并返回一个新的几何对象</h7></caption>
* // ES5引入方式
* const { WMTSLayer } = Zondy.Layer
* // ES6引入方式
* import { WMTSLayer } from "@mapgis/webclient-common"
* const json = {
* // 服务基地址
* url: 'http://{ip}:{port}/igs/rest/services/{serviceName}/WMTSServer'
* }
* const wmtsLayer = new WMTSLayer.fromJSON(json)
*/
static fromJSON(json) {
json = defaultValue(json, {})
const _layer = new WMTSLayer(json)
_layer.imageWidth = json.imageWidth
_layer.imageHeight = json.imageHeight
return _layer
}
/**
* @description 转换为json对象
* @return {Object} json对象
*/
toJSON() {
const json = super.toJSON()
json.type = this.type
json.activeLayer = toJSON(this.activeLayer, WMTSSubLayer)
json.url = this.url
json.spatialReference = toJSON(this.spatialReference, SpatialReference)
json.corporationType = this.corporationType
json.minScale = this.minScale
json.maxScale = this.maxScale
json.imageWidth = this.imageWidth
json.imageHeight = this.imageHeight
const _sublayers = []
this.sublayers.forEach(function (sublayer) {
_sublayers.push(toJSON(sublayer, WMTSSubLayer))
})
json.sublayers = _sublayers
const _tileMatrixSets = []
for (let i = 0; i < this.tileMatrixSets.length; i++) {
_tileMatrixSets.push(toJSON(this.tileMatrixSets[i], TileMatrixSet))
}
json.tileMatrixSets = _tileMatrixSets
return json
}
/**
* 加载图层资源<a id='load'></a>
* @example <caption><h7>不加载图层,仅获取图层信息</h7></caption>
* // ES5引入方式
* const { WMTSLayer } = Zondy.Layer
* // ES6引入方式
* import { WMTSLayer } from "@mapgis/webclient-common"
* // 初始化图层
* const wmtsLayer = new WMTSLayer({
* // 服务基地址
* url: 'http://{ip}:{port}/igs/rest/services/{serviceName}/WMTSServer'
* });
* wmtsLayer.load().then((result) => {
* // 获取完图层信息
* console.log(wmtsLayer)
* })
*/
load() {
return super.load()
}
/**
* 子类加载服务端资源方法
* @private
* */
_load() {
const self = this
return this._mapServer.getCapabitities().then((result) => {
Log.info('MapServer信息查询成功:', result)
const data = result.data
// IGS2.0服务
if (
self.url.indexOf('/igs/rest/services/') > -1 ||
self.url.indexOf('/arcgis/rest/services/') > -1 ||
self.url.indexOf('.tianditu.gov.cn/') > -1
) {
// 获取tileMatrixSets
data.tileMatrixSet.forEach((tileMatrixSet) => {
const _epsgCode = getEPSGCodeFromOGCSupportedCRSString(
tileMatrixSet.supportedCRS
)
const _spatialReference = new SpatialReference({
wkid: _epsgCode
})
const _topLeftCorner =
tileMatrixSet.tileMatrix[0].topLeftCorner.split(' ')
const _lods = []
tileMatrixSet.tileMatrix.forEach(function (tileMatrix, index) {
const _lod = new LOD()
// identifier不一定与level的数字对应,可能是一个字符串形式
_lod.level = tileMatrix.identifier
_lod.scale = Number(tileMatrix.scaleDenominator)
if (tileMatrix.identifier) {
// 存在两种情况,其一是参照系:级数,其二是只为级数
const _pos = tileMatrix.identifier.lastIndexOf(':')
if (_pos >= 0) {
_lod.levelValue = parseInt(
tileMatrix.identifier.substring(_pos + 1)
)
} else {
_lod.levelValue = parseInt(tileMatrix.identifier)
}
}
_lod.resolution = getTileResolution(
tileMatrixSet.identifier,
self.corporationType,
_lod.scale,
_spatialReference
)
_lods.push(_lod)
})
// 经纬度或者高斯坐标左上角的点坐标是反的,YX
let _coordinates
// 经纬度坐标系
if (_spatialReference.isGeographic) {
_coordinates = [
Number(_topLeftCorner[1]),
Number(_topLeftCorner[0])
]
}
// 高斯坐标系
else if (
(_spatialReference.wkid >= 2327 &&
_spatialReference.wkid <= 2442) ||
(_spatialReference.wkid >= 4491 &&
_spatialReference.wkid <= 4554) ||
(_spatialReference.wkid >= 21413 &&
_spatialReference.wkid <= 21423) ||
(_spatialReference.wkid >= 21473 && _spatialReference.wkid <= 21483)
) {
_coordinates = [
Number(_topLeftCorner[1]),
Number(_topLeftCorner[0])
]
}
// 天地图的3857坐标系是反的,YX
else if (self.url.indexOf('.tianditu.gov.cn/') > -1) {
// 中文注记墨卡托服务的原点是经纬度,因此要转成墨卡托坐标,由于原点纬度超过了85.05度,之上的投影都是错误的,因此写死,不进行投影
// 参考资料https://zh.wikipedia.org/wiki/Web%E5%A2%A8%E5%8D%A1%E6%89%98%E6%8A%95%E5%BD%B1
if (self.url.indexOf('/_w/') > -1) {
_coordinates = [-20037508.3427892, 20037508.3427892]
} else {
_coordinates = [
Number(_topLeftCorner[1]),
Number(_topLeftCorner[0])
]
}
}
// 其他坐标系是正的,XY
else {
_coordinates = [
Number(_topLeftCorner[0]),
Number(_topLeftCorner[1])
]
}
const _tileInfo = new TileInfo({
origin: new Point({
coordinates: _coordinates,
spatialReference: _spatialReference
}),
size: [
Number(tileMatrixSet.tileMatrix[0].tileWidth),
Number(tileMatrixSet.tileMatrix[0].tileHeight)
],
lods: _lods,
spatialReference: _spatialReference
})
self.tileMatrixSets.push(
new TileMatrixSet({
extent: '',
id: tileMatrixSet.identifier,
tileInfo: _tileInfo,
layer: self,
tileMatrix: tileMatrixSet.tileMatrix,
identifier: tileMatrixSet.identifier,
supportedCRS: tileMatrixSet.supportedCRS
})
)
})
// 设置activeLayer以及坐标系
if (Array.isArray(data.layer)) {
data.layer.forEach((layer, index) => {
if (index === 0) {
self.activeLayer = self._createWMTSSubLayer(layer, data)
self.spatialReference = new SpatialReference(
getEPSGFromTileMatrixSetId(layer.tileMatrixSetId)
)
}
self.sublayers.add(self._createWMTSSubLayer(layer, data))
})
}
// 地图文档中只有一个图层的情况
else {
if (typeof data.layer.boundingBox.lowerCorner === 'string') {
data.layer.boundingBox.lowerCorner =
data.layer.boundingBox.lowerCorner.split(' ')
}
if (typeof data.layer.boundingBox.upperCorner === 'string') {
data.layer.boundingBox.upperCorner =
data.layer.boundingBox.upperCorner.split(' ')
}
// 遍历所有的tileMatrixSets,找到identifier包含028mm_GB的子元素
// 暂时这样做,没找到更好的处理方法
for (let i = 0; i < self.tileMatrixSets.length; i++) {
if (
self.tileMatrixSets[i].identifier.indexOf('028mm_GB') > -1 ||
self.tileMatrixSets[i].identifier === 'default' ||
self.tileMatrixSets[i].identifier === 'default028mm' ||
self.tileMatrixSets[i].identifier === 'w' ||
self.tileMatrixSets[i].identifier === 'c'
) {
// 获取坐标系wkid
const supportedCRS =
self.tileMatrixSets[i].supportedCRS.split(':')
const wkid = Number(supportedCRS[supportedCRS.length - 1])
// 初始化坐标系
const spatialReference = new SpatialReference({
wkid
})
// 获取瓦片大小
self.imageWidth = defaultValue(
Number(self.tileMatrixSets[i].tileMatrix[0].tileWidth),
256
)
self.imageHeight = defaultValue(
Number(self.tileMatrixSets[i].tileMatrix[0].tileHeight),
256
)
self._spatialReference = spatialReference
// 设置地图范围
self.extent = self._getExtentFromBoundingBox(
data.layer.boundingBox,
self.url
)
}
}
// 指定激活图层
self.activeLayer = self._createWMTSSubLayer(
data.layer,
self.tileMatrixSets
)
// 新增子图层
self.sublayers.add(
self._createWMTSSubLayer(data.layer, self.tileMatrixSets)
)
}
return new Promise((resolve) => {
self.loadStatus = LoadStatus.loaded
self.loaded = true
resolve(self)
})
}
})
}
/**
* @description 根据id查询子图层<a id='findSublayerById'></a>
* @param {Object} id 子图层的id
* @return {WMTSSubLayer} 找到的子图层对象
*/
findSublayerById(id) {
return this.sublayers.items[id]
}
/**
* 从WMTS的boundingBox参数中构造外包盒,注意天地图的范围坐标是YX,其他的都是XY
* @private
* @param {Object} boundingBox WMTS的boundingBox属性
* @param {String} url 服务基地址,用来判定服务厂商
* @return {Extent} 图层范围
* */
_getExtentFromBoundingBox(boundingBox, url) {
let _extent
// 天地图的范围坐标是反的
if (url.indexOf('.tianditu.gov.cn/') > -1) {
_extent = new Extent({
xmin: Number(boundingBox.lowerCorner[0]),
ymin: Number(boundingBox.lowerCorner[1]),
xmax: Number(boundingBox.upperCorner[0]),
ymax: Number(boundingBox.upperCorner[1]),
spatialReference: SpatialReference.fromJSON(this.spatialReference)
})
}
// 其他的是正的
else {
_extent = new Extent({
xmin: Number(boundingBox.lowerCorner[1]),
ymin: Number(boundingBox.lowerCorner[0]),
xmax: Number(boundingBox.upperCorner[1]),
ymax: Number(boundingBox.upperCorner[0]),
spatialReference: SpatialReference.fromJSON(this.spatialReference)
})
}
return _extent
}
_createWMTSSubLayer(layerJson, tileMatrixSets) {
// 获取图层范围
const _extent = this._getExtentFromBoundingBox(
layerJson.boundingBox,
this.url
)
return new WMTSSubLayer({
identifier: layerJson.identifier,
title: layerJson.title,
description: 'WMTS子图层',
imageFormat: layerJson.format,
imageFormats: [layerJson.format],
styleId: Array.isArray(layerJson.Style)
? layerJson.Style[0].identifier
: layerJson.Style.identifier,
styles: [
new WMTSStyle({
id: layerJson.Style.identifier,
title: layerJson.Style.title
})
],
tileMatrixSets,
tileMatrixSetId: tileMatrixSets[0].identifier,
extent: _extent,
resourceURLTemplates: []
})
}
}
/**
* 通过url创建图层对象<a id='fromServerUrl'></a>
* @param {String} url 服务基地址
* @return {WMTSLayer} 新的图层对象
* @example <caption><h7>通过url创建图层对象</h7></caption>
* // ES5引入方式
* const { WMTSLayer } = Zondy.Layer
* // ES6引入方式
* import { WMTSLayer } from "@mapgis/webclient-common"
* // 初始化图层
* const url = 'http://{ip}:{port}/igs/rest/services/{serviceName}/WMTSServer';
* const wmtsLayer = new WMTSLayer.fromServerUrl(url);
* */
WMTSLayer.fromServerUrl = function (url) {
return new WMTSLayer({
url
})
}
Object.defineProperties(WMTSLayer.prototype, {
/**
* 空间裁剪范围
* @member WMTSLayer.prototype.clippingArea
* @type {Polygon|Extent|Circle|null}
*/
clippingArea: {
get() {
return this._clippingArea
},
set(value) {
this._clippingArea = value
this._fireUpdateEvent('空间裁剪区域', [
{
name: 'clippingArea',
params: [this._clippingArea],
dataChanged: true,
operationType: 'property'
}
])
}
}
})
Zondy.Layer.WMTSLayer = WMTSLayer
export default WMTSLayer