Appearance
OpenLayers 对照
平台的服务是标准的切片、矢量瓦片与 OGC 接口,不局限于 MapLibre。这几个示例用 OpenLayers 加载同一平台的服务。OpenLayers 不经过 SDK 的 transformRequest,也不能加请求头,所以地址统一交给 client.withKey() 补上 token 参数;访问公开服务时没有 Key,地址保持不变。
OpenLayers:XYZ 切片与 WMS
土地利用服务的 MapServer 切片作为 XYZ 底图:地址用 client.services.tileUrl() 取得({z}/{y}/{x},行在前;OpenLayers 按占位符名替换,顺序不影响)。道路服务的 WMS 作为叠加层:图层名由 client.services.layers() 与 GeoBridge.wmsLayerNames() 算出。右上角的面板分别开关两个图层,并调 WMS 的透明度。OpenLayers 的视图默认是 Web 墨卡托,经纬度中心点要先用 ol.proj.fromLonLat() 转换。
需要: 一个提供 MapServer 切片的地图服务和一个开启了 WMS 的地图服务。
js
在沙箱中打开/**
* @title OpenLayers:XYZ 切片与 WMS
* @description 用 OpenLayers 加载同一平台的服务:土地利用的 MapServer 切片(XYZ)作底图,道路的 WMS 作叠加层;地址交给 withKey 补 token,WMS 图层名按 MapServer 图层列表算出;面板开关图层并调 WMS 透明度。
* @category 对照
* @service demo/landuse
* @service demo/roads
*/
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径' : e && e.message)
try {
// 拷到自己页面时:换成你的切片地图服务与开启了 WMS 的服务路径
const landuse = services['demo/landuse']
const roads = services['demo/roads']
// OpenLayers 不能加请求头,所以用 withKey 把 token 补进地址(匿名时原样返回)
// 注意:只有属于 baseUrl 的地址才会补 token;平台写进文档的地址以 webBaseUrl 为前缀,两者不一致时 Key 不会随这些请求发送
const layerName = GeoBridge.wmsLayerNames((await client.services.layers(roads)).layers)[0]
const xyz = new ol.layer.Tile({
// tileUrl() 的模板是 {z}/{y}/{x}(行在前),OpenLayers 按占位符名替换,顺序不影响
source: new ol.source.XYZ({ url: client.withKey(client.services.tileUrl(landuse)), maxZoom: 20 }),
})
const wms = new ol.layer.Image({
opacity: 0.8,
source: new ol.source.ImageWMS({ url: client.withKey(client.services.wmsUrl(roads)), params: { LAYERS: layerName }, ratio: 1 }),
})
const map = new ol.Map({
target: container,
layers: [xyz, wms],
// OpenLayers 的视图默认是 Web 墨卡托,经纬度要先用 fromLonLat 转换;道路图层在 11 级以上才出图
view: new ol.View({ center: ol.proj.fromLonLat([114.172, 22.31]), zoom: 14 }),
})
// 面板:两个图层的开关 + WMS 透明度
const panel = document.createElement('div')
panel.style.cssText = 'position:absolute;top:8px;right:8px;z-index:2;background:#fff;padding:6px 10px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.3);font:13px/1.9 sans-serif'
for (const [layer, text] of [[xyz, 'XYZ 切片:土地利用'], [wms, `WMS:${layerName}(道路)`]]) {
const label = document.createElement('label')
label.style.display = 'block'
const box = document.createElement('input')
box.type = 'checkbox'
box.checked = true
box.onchange = () => layer.setVisible(box.checked)
label.append(box, ' ' + text)
panel.appendChild(label)
}
const slider = document.createElement('input')
slider.type = 'range'
slider.value = '80'
slider.oninput = () => wms.setOpacity(slider.value / 100)
panel.append('WMS 透明度 ', slider)
container.appendChild(panel)
// 出图失败时 OpenLayers 只发事件,不抛错:在这里转成日志
wms.getSource().on('imageloaderror', () => console.error('WMS 出图失败,请检查服务是否开启 WMS'))
wms.getSource().once('imageloadend', () => console.log('WMS 图层', layerName, '已出图,视野缩放级别', map.getView().getZoom().toFixed(1)))
xyz.getSource().once('tileloadend', () => console.log('XYZ 切片已加载:', client.services.tileUrl(landuse)))
} catch (e) {
console.error(friendly(e))
}OpenLayers:矢量瓦片
用 ol.layer.VectorTile 与 ol.format.MVT 加载矢量瓦片,地址来自 client.services.vectorTileUrl()。样式函数按道路等级返回不同颜色与线宽(同一种样式只建一次,避免每帧新建对象);鼠标移到道路上,右上角显示道路名称与等级。
需要: 一个矢量瓦片服务(示例用香港道路,按 fclass 字段配色)。
js
在沙箱中打开/**
* @title OpenLayers:矢量瓦片
* @description 用 OpenLayers 的 VectorTile 图层与 MVT 格式加载道路矢量瓦片,样式函数按道路等级配色;鼠标移到道路上,右上角显示道路名称与等级。
* @category 对照
* @service demo/roads
*/
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径' : e && e.message)
try {
// 拷到自己页面时:换成你的矢量瓦片服务路径
const path = services['demo/roads']
const COLORS = { motorway: '#e8590c', trunk: '#f08c00', primary: '#f59f00', secondary: '#74b816', tertiary: '#1c7ed6' }
// 每种等级的样式只建一次,样式函数里复用,避免每帧新建对象
const styles = {}
function roadStyle(feature) {
const fclass = feature.get('fclass')
const color = COLORS[fclass] || '#adb5bd'
styles[color] ??= new ol.style.Style({ stroke: new ol.style.Stroke({ color, width: COLORS[fclass] ? 2.5 : 0.8 }) })
return styles[color]
}
const layer = new ol.layer.VectorTile({
// MVT 格式解析 pbf;地址模板是 {z}/{y}/{x}.pbf,OpenLayers 按占位符名替换。OpenLayers 不能加请求头,用 withKey 补 token
source: new ol.source.VectorTile({ format: new ol.format.MVT(), url: client.withKey(client.services.vectorTileUrl(path)) }),
style: roadStyle,
})
const map = new ol.Map({
target: container,
layers: [layer],
// OpenLayers 的视图默认是 Web 墨卡托,经纬度要先用 fromLonLat 转换
view: new ol.View({ center: ol.proj.fromLonLat([114.172, 22.298]), zoom: 16 }),
})
const info = document.createElement('div')
info.style.cssText = 'position:absolute;top:8px;right:8px;z-index:2;background:#fff;padding:6px 10px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.3);font:13px/1.6 sans-serif'
info.textContent = '把鼠标移到道路上'
container.appendChild(info)
// 悬停:取像素处的第一个要素(矢量瓦片要素只读,属性用 get 取)
function describe(pixel) {
const f = map.forEachFeatureAtPixel(pixel, (x) => x, { hitTolerance: 4 })
info.textContent = f ? `${f.get('name') || '(无名称)'} 等级:${f.get('fclass')}` : '把鼠标移到道路上'
return info.textContent
}
map.on('pointermove', (e) => describe(e.pixel))
// 演示:瓦片渲染完成后,描述画面中心处的道路
map.once('rendercomplete', () => {
const features = layer.getFeaturesInExtent(map.getView().calculateExtent(map.getSize()))
console.log('视野内道路要素:', features.length, ';中心处:', describe(map.getPixelFromCoordinate(map.getView().getCenter())))
})
} catch (e) {
console.error(friendly(e))
}OpenLayers:WMTS 与 OGC API Features
client.openlayers.wmtsSource() 按 WMTS 能力文档建 ol.source.WMTS,能力文档中的矩阵集都能用:右上角在经纬度的 WorldCRS84Quad(视图 EPSG:4326)与 Web 墨卡托的 GoogleMapsCompatible(视图 EPSG:3857)之间切换,视图投影必须与矩阵集一致。高速公路由 client.openlayers.ogcApiVectorSource() 按 CQL2 条件从 OGC API - Features 分页读完后叠加,切换投影时自动按新投影重读。SDK 不依赖 ol 包:wmtsSource() 可以直接传全量构建的 ol 命名空间(按需引入时传 { WMTSCapabilities, WMTS, optionsFromCapabilities }),ogcApiVectorSource() 传 { VectorSource, GeoJSON }。
需要: 一个开启了 WMTS 的切片地图服务和一个开启了 OGC API 的矢量服务。
js
在沙箱中打开/**
* @title OpenLayers:WMTS 与 OGC API Features
* @description 用 openlayers.wmtsSource 按能力文档建 WMTS 源,可在经纬度矩阵集(WorldCRS84Quad,EPSG:4326 视图)与 Web 墨卡托(GoogleMapsCompatible)之间切换;道路高速公路由 openlayers.ogcApiVectorSource 按 CQL2 条件分页读取叠加。
* @category 对照
* @service demo/basemap
* @service demo/roads
*/
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在或未开启 WMTS / OGC API,请检查服务路径' : e && e.message)
try {
// 拷到自己页面时:换成你开启了 WMTS 的地图服务与开启了 OGC API 的服务路径
const basemap = services['demo/basemap']
const roads = services['demo/roads']
// wmtsSource 可以直接传全量构建的 ol 命名空间;ogcApiVectorSource 要传 { VectorSource, GeoJSON } 两个构造函数
const { collections } = await client.ogcApi.collections(roads)
const olVector = { VectorSource: ol.source.Vector, GeoJSON: ol.format.GeoJSON }
const vector = new ol.layer.Vector({
source: client.openlayers.ogcApiVectorSource(olVector, roads, collections[0].id, { params: { filter: "fclass = 'motorway'" } }),
style: new ol.style.Style({ stroke: new ol.style.Stroke({ color: '#e8590c', width: 3 }) }),
})
const tiles = new ol.layer.Tile()
const map = new ol.Map({ target: container, layers: [tiles, vector] })
// 切换矩阵集:视图投影必须与矩阵集坐标系一致,换投影时重建视图并保持中心
const use = async (matrixSet, projection) => {
const center = map.getView().getCenter() ? ol.proj.toLonLat(map.getView().getCenter(), map.getView().getProjection()) : [114.17, 22.32]
tiles.setSource(await client.openlayers.wmtsSource(ol, basemap, { matrixSet }))
map.setView(new ol.View({ projection, center: ol.proj.fromLonLat(center, projection), zoom: 12 }))
vector.getSource().refresh()
console.log(`WMTS 矩阵集 ${matrixSet},视图 ${projection}`)
}
await use('WorldCRS84Quad', 'EPSG:4326')
vector.getSource().on('featuresloadend', (e) => console.log(`OGC API Features:高速公路 ${e.features.length} 段`))
const panel = document.createElement('div')
panel.style.cssText = 'position:absolute;top:8px;right:8px;z-index:2;background:#fff;padding:6px 10px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.3);font:13px/1.9 sans-serif'
for (const [set, proj, text] of [['WorldCRS84Quad', 'EPSG:4326', '经纬度 WorldCRS84Quad'], ['GoogleMapsCompatible', 'EPSG:3857', 'Web 墨卡托 GoogleMapsCompatible']]) {
const label = document.createElement('label')
label.style.display = 'block'
const radio = document.createElement('input')
radio.type = 'radio'
radio.name = 'tms'
radio.checked = set === 'WorldCRS84Quad'
radio.onchange = () => use(set, proj).catch((e) => console.error(friendly(e)))
label.append(radio, ' ' + text)
panel.appendChild(label)
}
container.appendChild(panel)
} catch (e) {
console.error(friendly(e))
}