424 lines
16 KiB
JavaScript
424 lines
16 KiB
JavaScript
import * as Cesium from 'cesium'
|
||
import { SplitDirection } from 'cesium'
|
||
|
||
// 依赖:{ viewerOrThrow, store }
|
||
export function createLayerService(deps) {
|
||
const { viewerOrThrow, store } = deps
|
||
|
||
// 影像图层 zIndex 辅助函数
|
||
function nextZIndex() {
|
||
const zIndexValues = Object.values(store.layers)
|
||
.filter((record) => record && record.type === 'imagery')
|
||
.map((record) => (record.meta && typeof record.meta.zIndex === 'number' ? record.meta.zIndex : 0))
|
||
return (zIndexValues.length ? Math.max(...zIndexValues) : 0) + 1
|
||
}
|
||
|
||
// 按 zIndex 重新整理影像图层的叠放顺序
|
||
function adjustImageryOrder(viewer) {
|
||
try {
|
||
const imageryRecords = Object.values(store.layers)
|
||
.filter((record) => record && record.type === 'imagery' && record.obj)
|
||
.sort((a, b) => {
|
||
const aZ = a.meta && typeof a.meta.zIndex === 'number' ? a.meta.zIndex : 0
|
||
const bZ = b.meta && typeof b.meta.zIndex === 'number' ? b.meta.zIndex : 0
|
||
return aZ - bZ
|
||
})
|
||
// raiseToTop in ascending order so the highest ends top-most
|
||
imageryRecords.forEach((record) => {
|
||
try {
|
||
if (viewer.imageryLayers.contains(record.obj)) viewer.imageryLayers.raiseToTop(record.obj)
|
||
} catch (e) {}
|
||
})
|
||
syncImageryOrderMeta(viewer)
|
||
} catch (e) {}
|
||
}
|
||
|
||
/**
|
||
* @description 同步影像图层元数据里的排序索引,保持与 Cesium 实际顺序一致
|
||
*/
|
||
function syncImageryOrderMeta(viewer) {
|
||
try {
|
||
const imageryLayers = viewer.imageryLayers
|
||
const imageryRecords = Object.values(store.layers)
|
||
.filter((record) => record && record.type === 'imagery' && record.obj)
|
||
const lookup = new Map()
|
||
imageryRecords.forEach((record) => lookup.set(record.obj, record))
|
||
const count = imageryLayers.length
|
||
for (let i = 0; i < count; i += 1) {
|
||
const layer = imageryLayers.get(i)
|
||
const record = lookup.get(layer)
|
||
if (!record) continue
|
||
if (!record.meta) record.meta = {}
|
||
record.meta.zIndex = i
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
/**
|
||
* @description 同步矢量数据源图层顺序,记录在 meta.vectorOrder 中
|
||
*/
|
||
function syncVectorOrderMeta(viewer) {
|
||
try {
|
||
const dataSources = viewer.dataSources
|
||
const vectorRecords = Object.values(store.layers)
|
||
.filter((record) => record && (record.type === 'vector' || record.type === 'datasource') && record.obj)
|
||
const lookup = new Map()
|
||
vectorRecords.forEach((record) => lookup.set(record.obj, record))
|
||
const count = dataSources.length
|
||
for (let i = 0; i < count; i += 1) {
|
||
const ds = dataSources.get(i)
|
||
const record = lookup.get(ds)
|
||
if (!record) continue
|
||
if (!record.meta) record.meta = {}
|
||
record.meta.vectorOrder = i
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
return {
|
||
async addLayer(spec) {
|
||
const viewer = viewerOrThrow()
|
||
|
||
// 兼容新旧两种参数风格(新的 serviceConfig 与旧的直传 spec)
|
||
const layerSpec = spec
|
||
const layerType = layerSpec.type
|
||
const layerId = layerSpec.id || (layerType ? `${layerType}:${Date.now().toString(36)}` : `layer:${Date.now().toString(36)}`)
|
||
if (store.layers[layerId]) return layerId
|
||
|
||
const metadata = { ...(layerSpec.meta || {}) }
|
||
if (layerSpec.zIndex != null) metadata.zIndex = Number(layerSpec.zIndex)
|
||
if (metadata.zIndex == null && (layerType && layerType !== 'terrain' && layerType !== 'primitive' && layerType !== 'vector' && layerType !== 'datasource')) {
|
||
metadata.zIndex = nextZIndex()
|
||
}
|
||
|
||
const layerOptions = layerSpec.options || {}
|
||
const sourceUrl = layerSpec.url
|
||
|
||
let layerRecord = null
|
||
|
||
// 注册影像图层(ImageryLayer)的辅助方法
|
||
const registerImageryLayer = (provider, extraProps = {}) => {
|
||
const imageryLayer = viewer.imageryLayers.addImageryProvider(provider)
|
||
if (typeof layerOptions.opacity === 'number') imageryLayer.alpha = layerOptions.opacity
|
||
if (typeof layerOptions.visible === 'boolean') imageryLayer.show = layerOptions.visible
|
||
imageryLayer.splitDirection = SplitDirection.NONE
|
||
const record = {
|
||
id: layerId,
|
||
type: 'imagery',
|
||
obj: imageryLayer,
|
||
owned: true,
|
||
show: imageryLayer.show,
|
||
opacity: imageryLayer.alpha,
|
||
meta: metadata,
|
||
...extraProps,
|
||
}
|
||
store.layers[layerId] = record
|
||
adjustImageryOrder(viewer)
|
||
return record
|
||
}
|
||
|
||
// 注册矢量数据源的辅助方法
|
||
const registerVectorLayer = async (dataSource) => {
|
||
await viewer.dataSources.add(dataSource)
|
||
dataSource.show = layerOptions.visible !== false
|
||
const record = {
|
||
id: layerId,
|
||
type: 'vector',
|
||
obj: dataSource,
|
||
owned: true,
|
||
show: dataSource.show,
|
||
opacity: typeof layerOptions.opacity === 'number' ? layerOptions.opacity : 1,
|
||
meta: metadata,
|
||
}
|
||
store.layers[layerId] = record
|
||
syncVectorOrderMeta(viewer)
|
||
return record
|
||
}
|
||
|
||
// 旧版直映射类型分支
|
||
if (layerType === 'imagery' || layerType === 'baseImagery') {
|
||
const provider = layerSpec.source
|
||
layerRecord = registerImageryLayer(provider)
|
||
if (layerType === 'baseImagery') viewer.imageryLayers.lowerToBottom(layerRecord.obj)
|
||
return layerId
|
||
}
|
||
if (layerType === 'vector' || layerType === 'datasource') {
|
||
const dataSource = new Cesium.CustomDataSource(layerId)
|
||
layerRecord = await registerVectorLayer(dataSource)
|
||
return layerId
|
||
}
|
||
if (layerType === 'terrain') {
|
||
if ('terrain' in viewer) viewer.terrain = layerSpec.source
|
||
else viewer.scene.terrainProvider = layerSpec.source
|
||
layerRecord = {
|
||
id: layerId,
|
||
type: 'terrain',
|
||
obj: layerSpec.source,
|
||
owned: true,
|
||
show: true,
|
||
opacity: 1,
|
||
meta: metadata,
|
||
}
|
||
store.layers[layerId] = layerRecord
|
||
return layerId
|
||
}
|
||
if (layerType === 'primitive') {
|
||
const primitive = layerSpec.source
|
||
viewer.scene.primitives.add(primitive)
|
||
layerRecord = {
|
||
id: layerId,
|
||
type: 'primitive',
|
||
obj: primitive,
|
||
owned: true,
|
||
show: true,
|
||
opacity: 1,
|
||
meta: metadata,
|
||
}
|
||
store.layers[layerId] = layerRecord
|
||
return layerId
|
||
}
|
||
|
||
// React serviceConfig-style types
|
||
switch (layerType) {
|
||
case 'ArcGISTiledMapServiceLayer': {
|
||
const haveTemplateXYZ = typeof sourceUrl === 'string' && sourceUrl.includes('{z}/{y}/{x}')
|
||
if (!haveTemplateXYZ) {
|
||
const provider = await Cesium.ArcGisMapServerImageryProvider.fromUrl(sourceUrl, layerOptions)
|
||
registerImageryLayer(provider)
|
||
} else {
|
||
const provider = new Cesium.UrlTemplateImageryProvider({
|
||
url: sourceUrl,
|
||
tilingScheme: new Cesium.WebMercatorTilingScheme(),
|
||
maximumLevel: 18,
|
||
...layerOptions,
|
||
})
|
||
registerImageryLayer(provider)
|
||
}
|
||
break
|
||
}
|
||
case 'ArcGISDynamicMapServiceLayer':
|
||
case 'ArcGISImageMapServiceLayer': {
|
||
const provider = await Cesium.ArcGisMapServerImageryProvider.fromUrl(sourceUrl, {
|
||
enablePickFeatures: true,
|
||
...layerOptions,
|
||
})
|
||
registerImageryLayer(provider)
|
||
break
|
||
}
|
||
case 'GeoJSONServiceLayer': {
|
||
// Accept url or raw data in options.data
|
||
const data = layerOptions.data || sourceUrl
|
||
const dataSource = await Cesium.GeoJsonDataSource.load(data, layerOptions)
|
||
await registerVectorLayer(dataSource)
|
||
break
|
||
}
|
||
case 'WmsServiceLayer': {
|
||
const base = (sourceUrl || '').split('?')[0]
|
||
const queryParams = (sourceUrl || '').includes('?') ? new URLSearchParams((sourceUrl || '').split('?')[1]) : new URLSearchParams()
|
||
const provider = new Cesium.WebMapServiceImageryProvider({
|
||
url: base,
|
||
layers: queryParams.get('layers') || layerOptions.layers,
|
||
parameters: {
|
||
service: 'WMS',
|
||
version: queryParams.get('version') || layerOptions.version || '1.1.1',
|
||
request: 'GetMap',
|
||
format: queryParams.get('format') || layerOptions.format || 'image/png',
|
||
transparent: true,
|
||
cql_filter:queryParams.get('cql_filter')||'',
|
||
...layerOptions.extraParameters,
|
||
...layerOptions.parameters,
|
||
},
|
||
enablePickFeatures: true,
|
||
...layerOptions,
|
||
})
|
||
registerImageryLayer(provider)
|
||
break
|
||
}
|
||
case 'WmtsServiceLayer':
|
||
case 'TiandituVecLayer':
|
||
case 'TiandituImgLayer':
|
||
case 'TiandituCvaLayer': {
|
||
// Try to honor tk from url or options
|
||
const urlBase = (sourceUrl || '').split('?')[0]
|
||
const queryParams = (sourceUrl || '').includes('?') ? new URLSearchParams((sourceUrl || '').split('?')[1]) : new URLSearchParams()
|
||
const tk = queryParams.get('tk') || layerOptions.tk
|
||
const wmtsUrl = tk ? `${urlBase}?tk=${tk}` : urlBase
|
||
const provider = new Cesium.WebMapTileServiceImageryProvider({
|
||
url: wmtsUrl,
|
||
layer: queryParams.get('LAYER') || queryParams.get('layer') || layerOptions.layer || 'img',
|
||
style: 'default',
|
||
format: 'tiles',
|
||
tileMatrixSetID: layerOptions.tileMatrixSetID || 'w',
|
||
tilingScheme: new Cesium.WebMercatorTilingScheme(),
|
||
maximumLevel: layerOptions.maximumLevel || 18,
|
||
subdomains: layerOptions.subdomains || ['0', '1', '2', '3', '4', '5', '6', '7'],
|
||
...layerOptions,
|
||
})
|
||
registerImageryLayer(provider)
|
||
break
|
||
}
|
||
case 'WebTileLayer': {
|
||
const provider = new Cesium.UrlTemplateImageryProvider({
|
||
url: sourceUrl,
|
||
tilingScheme: new Cesium.WebMercatorTilingScheme(),
|
||
subdomains: layerOptions.subdomains || ['0', '1', '2', '3', '4', '5', '6', '7'],
|
||
...layerOptions,
|
||
})
|
||
registerImageryLayer(provider)
|
||
break
|
||
}
|
||
case 'TMSServiceLayer': { // TMS z/x/{reverseY}
|
||
const templateUrl = typeof sourceUrl === 'string' ? sourceUrl.replace('{y}', '{reverseY}') : sourceUrl
|
||
const providerOptions = {
|
||
url: templateUrl,
|
||
tilingScheme: new Cesium.WebMercatorTilingScheme(),
|
||
maximumLevel: layerOptions.maximumLevel || layerOptions.maxZoom || 22,
|
||
...layerOptions,
|
||
}
|
||
if (layerOptions.bounds) {
|
||
const bounds = layerOptions.bounds
|
||
providerOptions.rectangle = Cesium.Rectangle.fromDegrees(bounds.west, bounds.south, bounds.east, bounds.north)
|
||
}
|
||
const provider = new Cesium.UrlTemplateImageryProvider({ ...providerOptions, url: templateUrl })
|
||
registerImageryLayer(provider)
|
||
break
|
||
}
|
||
case 'TmsServiceLayer': { // XYZ z/x/y
|
||
const providerOptions = {
|
||
url: sourceUrl,
|
||
tilingScheme: new Cesium.WebMercatorTilingScheme(),
|
||
maximumLevel: layerOptions.maximumLevel || layerOptions.maxZoom || 22,
|
||
...layerOptions,
|
||
}
|
||
if (layerOptions.bounds) {
|
||
const bounds = layerOptions.bounds
|
||
providerOptions.rectangle = Cesium.Rectangle.fromDegrees(bounds.west, bounds.south, bounds.east, bounds.north)
|
||
}
|
||
const provider = new Cesium.UrlTemplateImageryProvider(providerOptions)
|
||
registerImageryLayer(provider)
|
||
break
|
||
}
|
||
case 'Cesium3DTileService': {
|
||
const tileset = await Cesium.Cesium3DTileset.fromUrl(sourceUrl, {
|
||
...layerOptions,
|
||
})
|
||
viewer.scene.primitives.add(tileset)
|
||
layerRecord = {
|
||
id: layerId,
|
||
type: 'primitive',
|
||
obj: tileset,
|
||
owned: true,
|
||
show: true,
|
||
opacity: 1,
|
||
meta: metadata,
|
||
}
|
||
store.layers[layerId] = layerRecord
|
||
break
|
||
}
|
||
default:
|
||
throw new Error('不支持的图层类型: ' + layerType)
|
||
}
|
||
|
||
return layerId
|
||
},
|
||
|
||
// 移除图层
|
||
removeLayer(id) {
|
||
const viewer = viewerOrThrow()
|
||
const record = store.layers[id]
|
||
if (!record) return false
|
||
try {
|
||
if (record.type === 'imagery') {
|
||
viewer.imageryLayers.remove(record.obj, true)
|
||
syncImageryOrderMeta(viewer)
|
||
} else if (record.type === 'vector' || record.type === 'datasource') {
|
||
viewer.dataSources.remove(record.obj, true)
|
||
syncVectorOrderMeta(viewer)
|
||
} else if (record.type === 'primitive') {
|
||
viewer.scene.primitives.remove(record.obj)
|
||
} else if (record.type === 'terrain') {
|
||
if ('terrain' in viewer) viewer.terrain = new Cesium.EllipsoidTerrain()
|
||
else viewer.scene.terrainProvider = new Cesium.EllipsoidTerrain()
|
||
}
|
||
} catch (e) {}
|
||
delete store.layers[id]
|
||
return true
|
||
},
|
||
|
||
// 显隐图层
|
||
showLayer(id, visible) {
|
||
const record = store.layers[id]
|
||
if (!record) return
|
||
if (record.type === 'imagery') record.obj.show = !!visible
|
||
else if (record.type === 'vector' || record.type === 'datasource') record.obj.show = !!visible
|
||
else if (record.type === 'primitive') record.obj.show = !!visible
|
||
record.show = !!visible
|
||
},
|
||
|
||
// 设置透明度
|
||
setOpacity(id, alpha) {
|
||
const record = store.layers[id]
|
||
if (!record) return
|
||
if (record.type === 'imagery') {
|
||
record.obj.alpha = alpha
|
||
record.opacity = alpha
|
||
} else {
|
||
record.opacity = alpha /* TODO: walk entities/materials */
|
||
}
|
||
},
|
||
|
||
// 调整图层顺序(上/下/置顶/置底)
|
||
moveLayer(id, direction) {
|
||
const viewer = viewerOrThrow()
|
||
const record = store.layers[id]
|
||
if (!record) return
|
||
if (record.type === 'imagery') {
|
||
const imageryLayers = viewer.imageryLayers
|
||
if (direction === 'up') imageryLayers.raise(record.obj)
|
||
else if (direction === 'down') imageryLayers.lower(record.obj)
|
||
else if (direction === 'top') imageryLayers.raiseToTop(record.obj)
|
||
else if (direction === 'bottom') imageryLayers.lowerToBottom(record.obj)
|
||
syncImageryOrderMeta(viewer)
|
||
} else if (record.type === 'vector' || record.type === 'datasource') {
|
||
const dataSources = viewer.dataSources
|
||
if (direction === 'up') dataSources.raise(record.obj)
|
||
else if (direction === 'down') dataSources.lower(record.obj)
|
||
else if (direction === 'top') dataSources.raiseToTop(record.obj)
|
||
else if (direction === 'bottom') dataSources.lowerToBottom(record.obj)
|
||
syncVectorOrderMeta(viewer)
|
||
}
|
||
},
|
||
|
||
// 设置卷帘(左右分屏)位置
|
||
setSplit(id, side) {
|
||
const record = store.layers[id]
|
||
if (!record || record.type !== 'imagery') return
|
||
const splitDirectionMap = {
|
||
left: SplitDirection.LEFT,
|
||
right: SplitDirection.RIGHT,
|
||
none: SplitDirection.NONE,
|
||
}
|
||
record.obj.splitDirection = splitDirectionMap[side] || SplitDirection.NONE
|
||
},
|
||
|
||
// 设置全局卷帘分割位置 [0,1]
|
||
setSplitPosition(position) {
|
||
const viewer = viewerOrThrow()
|
||
store.imagerySplitPosition = Math.min(1, Math.max(0, position))
|
||
try {
|
||
viewer.scene.imagerySplitPosition = store.imagerySplitPosition
|
||
} catch (e) {}
|
||
},
|
||
|
||
// 获取图层记录
|
||
getLayer(id) {
|
||
return store.layers[id]
|
||
},
|
||
|
||
// 列出所有图层记录
|
||
listLayers() {
|
||
return Object.values(store.layers)
|
||
},
|
||
}
|
||
}
|