Skip to content

图层 ​

在空样式或地图方案上自己组织图层:栅格切片、WMTS、TMS、OGC API 瓦片、WMS、矢量瓦片,以及它们之间的切换与叠加。

切片地址的行列顺序

平台 MapServer 的切片地址是 …/tile/{z}/{y}/{x},行(y)在前、列(x)在后,与许多 XYZ 服务的 {z}/{x}/{y} 相反。使用 SDK 的 client.services.tileUrl() 与 client.maplibre.addRasterService() 时已经处理好;自己拼地址时请注意顺序。

栅格切片 ​

用 client.maplibre.addRasterService() 把 MapServer 的切片作为栅格图层加到地图上。地图的样式加载完成(load 事件)之后才能添加数据源与图层。左上角的面板实时显示地图中心落在哪张切片上:级别、行号、列号、这张切片的地址与图片,可以直观看到"行在前"的地址顺序。

需要: 一个提供 MapServer 切片的地图服务。

js
/**
 * @title 栅格切片
 * @description 把香港底图 MapServer 的切片作为栅格图层加到地图上;面板实时显示地图中心所在切片的 z/行/列、地址与图片。注意切片地址是 {z}/{y}/{x},行在前。
 * @category 图层
 * @service demo/basemap
 */
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径' : e && e.message)

try {
  // 拷到自己页面时:换成你的地图服务路径(服务须开启 MapServer 切片)
  const path = services['demo/basemap']

  const map = new maplibregl.Map({
    container,
    style: { version: 8, sources: {}, layers: [] },
    transformRequest: client.transformRequest,
  })
  map.addControl(new maplibregl.NavigationControl())

  // 样式加载完成后才能加 source 与图层
  await new Promise((resolve) => map.once('load', resolve))

  // addRasterService 用 tileUrl() 的模板建 raster source,已处理好行在前的顺序
  client.maplibre.addRasterService(map, path)

  // 缩放到服务的数据范围:extent() 读服务自身的公开元数据(经纬度),匿名也可用;取不到时保持默认视野
  const bounds = await client.services.extent(path)
  if (bounds) map.fitBounds(bounds, { padding: 20, animate: false })

  // 面板:地图中心落在哪张切片上(Web 墨卡托切片编号公式),以及这张切片的地址与图片
  const panel = document.createElement('div')
  panel.style.cssText = 'position:absolute;top:8px;left:8px;z-index:2;background:#fff;padding:8px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.3);font:12px/1.6 sans-serif;max-width:300px;word-break:break-all'
  const text = document.createElement('div')
  const img = document.createElement('img')
  img.style.cssText = 'width:128px;height:128px;border:1px solid #ccc;display:block;margin-top:4px'
  panel.append(text, img)
  container.appendChild(panel)

  const template = client.services.tileUrl(path)
  function showCenterTile() {
    const z = Math.round(map.getZoom())
    const { lng, lat } = map.getCenter()
    const n = 2 ** z
    const x = Math.floor(((lng + 180) / 360) * n)
    const rad = (lat * Math.PI) / 180
    const y = Math.floor(((1 - Math.log(Math.tan(rad) + 1 / Math.cos(rad)) / Math.PI) / 2) * n)
    const url = template.replace('{z}', z).replace('{y}', y).replace('{x}', x)
    text.textContent = `中心切片 z=${z} 行 y=${y} 列 x=${x}:${url}`
    // <img> 不经过 transformRequest,受保护服务要用 withKey 把 token 补进地址
    img.src = client.withKey(url)
    return url
  }
  map.on('moveend', showCenterTile)
  console.log('切片地址模板(行在前):', template)
  console.log('当前中心切片:', showCenterTile())
} catch (e) {
  console.error(friendly(e))
}
在沙箱中打开

WMTS 作为栅格源 ​

用 client.maplibre.addWmtsService() 读取 WMTS 能力文档,选出 Web 墨卡托矩阵集(默认 WebMercatorQuad,其次 GoogleMapsCompatible),把 ResourceURL 模板里的 TileMatrix、TileRow、TileCol 换成 MapLibre 的 {z}、{y}、{x} 后作为栅格数据源。瓦片地址不带 Key,由 transformRequest 附加。左上角列出能力文档里的矩阵集:坐标系与级数;经纬度矩阵集(WorldCRS84Quad、CGCS2000Quad)MapLibre 无法使用,见下面的 OpenLayers 与 Cesium 示例。

需要: 一个开启了 WMTS 的切片地图服务。

js
/**
 * @title WMTS 作为栅格源
 * @description 用 maplibre.addWmtsService 读取香港底图的 WMTS 能力文档,按 Web 墨卡托矩阵集把瓦片模板交给 MapLibre;面板列出能力文档里的矩阵集(坐标系与级数),日志给出所用的瓦片模板。
 * @category 图层
 * @service demo/basemap
 */
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在或未开启 WMTS,请检查服务路径' : e && e.message)

try {
  // 拷到自己页面时:换成你开启了 WMTS 的地图服务路径
  const path = services['demo/basemap']

  const map = new maplibregl.Map({
    container,
    style: { version: 8, sources: {}, layers: [] },
    // 瓦片地址里不放 Key,由 transformRequest 按 Key 的传递方式附加
    transformRequest: client.transformRequest,
  })
  map.addControl(new maplibregl.NavigationControl())
  await new Promise((resolve) => map.once('load', resolve))

  // MapLibre 只认 Web 墨卡托:默认选 WebMercatorQuad,其次 GoogleMapsCompatible;经纬度矩阵集请改用 OpenLayers 或三维示例里的 WMTS 影像
  const handle = await client.maplibre.addWmtsService(map, path)
  console.log('WMTS 瓦片模板:', map.getStyle().sources[handle.sourceId].tiles[0])

  const bounds = await client.services.extent(path)
  if (bounds) map.fitBounds(bounds, { padding: 20, animate: false })

  // 面板:能力文档解析后的矩阵集(wmtsCapabilities 返回 { layers, tileMatrixSets })
  const caps = await client.services.wmtsCapabilities(path)
  const layer = caps.layers[0]
  const panel = document.createElement('div')
  panel.style.cssText = 'position:absolute;top:8px;left: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.7 sans-serif'
  const title = document.createElement('b')
  title.textContent = `图层 ${layer.identifier}(${layer.formats.join('、')})`
  panel.appendChild(title)
  for (const id of layer.tileMatrixSets) {
    const tms = caps.tileMatrixSets.find((t) => t.identifier === id)
    const row = document.createElement('div')
    row.textContent = `${id}:${tms ? tms.supportedCrs : '?'},${tms ? tms.tileMatrices.length : 0} 级`
    panel.appendChild(row)
  }
  container.appendChild(panel)
} catch (e) {
  console.error(friendly(e))
}
在沙箱中打开

TMS 瓦片 ​

TMS 1.0.0 的 TileMapService 列出服务提供的瓦片集(矩阵集 × 图片格式);client.services.tmsUrl(path, 矩阵集, 格式) 给出瓦片模板 …/tms/1.0.0/{服务名}@{矩阵集}@{格式}/{z}/{x}/{y}.{格式}。TMS 的行号自下而上,MapLibre 的数据源要写 scheme: 'tms'。左上角在 PNG 与 JPEG 之间切换。

需要: 一个开启了 TMS 的切片地图服务。

js
/**
 * @title TMS 瓦片
 * @description 读取 TMS 1.0.0 的 TileMapService 列出可用瓦片集(矩阵集 × 格式),用 services.tmsUrl 生成瓦片模板交给 MapLibre;TMS 的行号自下而上,source 要写 scheme: 'tms'。面板切换 PNG 与 JPEG。
 * @category 图层
 * @service demo/basemap
 */
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在或未开启 TMS,请检查服务路径' : e && e.message)

try {
  // 拷到自己页面时:换成你开启了 TMS 的地图服务路径
  const path = services['demo/basemap']

  // TileMapService 是 XML:列出每个 TileMap 的地址末段与坐标系(global-mercator、global-geodetic 是 GDAL 等客户端用的别名)
  const xml = await client.request(client.services.tmsUrl(path), { as: 'text' })
  const doc = new DOMParser().parseFromString(xml, 'application/xml')
  const tileMaps = Array.from(doc.getElementsByTagName('TileMap')).map((n) => `${n.getAttribute('href').split('/').pop()}:${n.getAttribute('srs')}`)
  console.log('TileMapService 中的瓦片集:\n' + tileMaps.join('\n'))

  const map = new maplibregl.Map({
    container,
    style: { version: 8, sources: {}, layers: [] },
    transformRequest: client.transformRequest,
  })
  map.addControl(new maplibregl.NavigationControl())
  await new Promise((resolve) => map.once('load', resolve))

  // 瓦片模板 …/tms/1.0.0/{服务名}@WebMercatorQuad@png/{z}/{x}/{y}.png
  const show = (format) => {
    if (map.getLayer('tms')) map.removeLayer('tms')
    if (map.getSource('tms')) map.removeSource('tms')
    const tiles = client.services.tmsUrl(path, 'WebMercatorQuad', format)
    map.addSource('tms', { type: 'raster', tiles: [tiles], tileSize: 256, scheme: 'tms' })
    map.addLayer({ id: 'tms', type: 'raster', source: 'tms' })
    console.log('TMS 瓦片模板:', tiles)
  }
  show('png')

  const bounds = await client.services.extent(path)
  if (bounds) map.fitBounds(bounds, { padding: 20, animate: false })

  // 面板:切换格式(JPEG 无透明,海面等空白处为白色)
  const panel = document.createElement('div')
  panel.style.cssText = 'position:absolute;top:8px;left: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 format of ['png', 'jpg']) {
    const label = document.createElement('label')
    label.style.marginRight = '10px'
    const radio = document.createElement('input')
    radio.type = 'radio'
    radio.name = 'fmt'
    radio.checked = format === 'png'
    radio.onchange = () => show(format)
    label.append(radio, ' ' + format.toUpperCase())
    panel.appendChild(label)
  }
  container.appendChild(panel)
} catch (e) {
  console.error(friendly(e))
}
在沙箱中打开

OGC API - Tiles ​

client.maplibre.addOgcApiTiles() 读取 OGC API 的瓦片集元数据(WebMercatorQuad),取出瓦片模板与级别范围交给 MapLibre:底图服务的地图瓦片(kind: 'map',PNG)作底,道路集合的矢量瓦片(kind: 'vector',MVT)叠在上面。矢量瓦片的图层名(source-layer)就是集合 id;不传 layers 时 SDK 按几何类型生成默认样式,示例自己写了按道路等级配色的线图层,点击道路弹出名称与等级。

需要: 一个开启了 OGC API 的切片地图服务,和一个开启了 OGC API 的矢量服务。

js
/**
 * @title OGC API - Tiles
 * @description 用 maplibre.addOgcApiTiles 加载 OGC API 瓦片:香港底图的地图瓦片(PNG)作底,道路集合的矢量瓦片(MVT)叠在上面并按道路等级配色。瓦片模板与级别范围都取自瓦片集元数据。
 * @category 图层
 * @service demo/basemap
 * @service demo/roads
 */
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在或未开启 OGC API,请检查服务路径' : e && e.message)

try {
  // 拷到自己页面时:换成你开启了 OGC API 的服务路径;集合 id 见 ogcApi.collections()
  const basemap = services['demo/basemap']
  const roads = services['demo/roads']

  const map = new maplibregl.Map({
    container,
    style: { version: 8, sources: {}, layers: [] },
    transformRequest: client.transformRequest,
    center: [114.172, 22.305],
    zoom: 13,
  })
  map.addControl(new maplibregl.NavigationControl())
  await new Promise((resolve) => map.once('load', resolve))

  // 地图瓦片:整个服务(不给 collection)的 WebMercatorQuad 瓦片集
  await client.maplibre.addOgcApiTiles(map, basemap, { kind: 'map' })

  // 矢量瓦片:道路集合;source-layer 即集合 id,自己写图层时按字段配色
  const { collections } = await client.ogcApi.collections(roads)
  const id = collections[0].id
  const color = ['match', ['get', 'fclass'], 'motorway', '#e8590c', 'trunk', '#f08c00', 'primary', '#1971c2', '#868e96']
  const handle = await client.maplibre.addOgcApiTiles(map, roads, {
    kind: 'vector',
    collection: id,
    layers: [{ id: 'roads', type: 'line', 'source-layer': id, paint: { 'line-color': color, 'line-width': ['interpolate', ['linear'], ['zoom'], 10, 0.6, 16, 3] } }],
  })
  console.log(`矢量瓦片集合 ${id}:图层 ${handle.layerIds.join('、')},级别 ${map.getSource(handle.sourceId).minzoom}–${map.getSource(handle.sourceId).maxzoom}`)

  // 点击道路查看属性(矢量瓦片带全部字段)
  map.on('click', handle.layerIds[0], (e) => {
    const p = e.features[0].properties
    new maplibregl.Popup().setLngLat(e.lngLat).setText(`${p.name || '(无名)'} · ${p.fclass}`).addTo(map)
  })

  const legend = document.createElement('div')
  legend.style.cssText = 'position:absolute;top:8px;left: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.8 sans-serif'
  legend.innerHTML = [['#e8590c', '高速'], ['#f08c00', '快速路'], ['#1971c2', '主干道'], ['#868e96', '其他']]
    .map(([c, t]) => `<div><span style="display:inline-block;width:18px;height:3px;background:${c};vertical-align:middle"></span> ${t}</div>`)
    .join('')
  container.appendChild(legend)
} catch (e) {
  console.error(friendly(e))
}
在沙箱中打开

切换底图 ​

页面上的两个按钮在"栅格切片"(服务端出图)与"矢量瓦片"(三个矢量瓦片服务叠加,浏览器端渲染)之间切换底图。addRasterService、addVectorTileService 返回的对象带 remove(),用它移除当前底图后再加另一种,地图的中心与缩放保持不变。示例先显示栅格底图,几秒后自动切到矢量底图,之后可用按钮来回切换。

需要: 一个提供 MapServer 切片的地图服务,以及一个或多个矢量瓦片服务。

js
/**
 * @title 切换底图
 * @description 两个按钮在「栅格切片」(服务端出图的香港底图)与「矢量瓦片」(土地利用、水体、道路三个矢量瓦片服务,浏览器端渲染)之间切换;handle.remove() 移除当前底图,中心与缩放不变。
 * @category 图层
 * @service demo/basemap
 * @service demo/landuse
 * @service demo/water
 * @service demo/roads
 */
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径' : e && e.message)

try {
  // 拷到自己页面时:换成你的地图服务(栅格)与矢量瓦片服务路径
  const raster = services['demo/basemap']
  const vectors = [services['demo/landuse'], services['demo/water'], services['demo/roads']]

  const map = new maplibregl.Map({
    container,
    style: { version: 8, sources: {}, layers: [{ id: 'bg', type: 'background', paint: { 'background-color': '#f1f3f5' } }] },
    transformRequest: client.transformRequest,
    center: [114.172, 22.298],
    zoom: 15.5,
  })
  map.addControl(new maplibregl.NavigationControl())
  // 切片、图标等资源加载失败时 MapLibre 发 error 事件:转成提示,不影响其余内容显示
  map.on('error', (e) => console.warn('地图资源加载失败:', e.error && e.error.message))
  await new Promise((resolve) => map.once('load', resolve))

  // 当前底图的图层句柄(矢量底图由三个服务组成,所以是数组)
  let handles = []
  let busy = false
  const buttons = {}

  async function use(kind) {
    if (busy) return
    busy = true
    try {
      // remove() 只移除这次加入的图层与 source,不会动地图的中心与缩放
      handles.forEach((h) => h.remove())
      handles = []
      if (kind === 'raster') handles.push(client.maplibre.addRasterService(map, raster))
      // 按顺序叠加:先加的在下面(土地利用 → 水体 → 道路)
      else for (const path of vectors) handles.push(await client.maplibre.addVectorTileService(map, path))
      for (const [k, b] of Object.entries(buttons)) b.disabled = k === kind
      const layers = handles.flatMap((h) => h.layerIds)
      console.log('当前底图:', kind === 'raster' ? '栅格切片' : '矢量瓦片', ',图层:', layers.join('、'))
    } catch (e) {
      console.error('切换失败:', friendly(e))
    } finally {
      busy = false
    }
  }

  const bar = document.createElement('div')
  bar.style.cssText = 'position:absolute;top:8px;left:8px;z-index:2;display:flex;gap:6px'
  for (const [kind, label] of [['raster', '栅格切片(MapServer)'], ['vector', '矢量瓦片(VectorTileServer)']]) {
    const b = document.createElement('button')
    b.textContent = label
    b.style.cssText = 'padding:4px 10px;cursor:pointer'
    b.onclick = () => use(kind)
    buttons[kind] = b
    bar.appendChild(b)
  }
  container.appendChild(bar)

  // 先显示栅格底图,再自动切到矢量,便于对比两种底图(之后可用按钮来回切换)
  await use('raster')
  setTimeout(() => use('vector'), 3000)
} catch (e) {
  console.error(friendly(e))
}
在沙箱中打开

底图上叠加矢量瓦片 ​

用空样式建图,地图加载完成后先加栅格切片底图,再用 addVectorTileService() 把兴趣点矢量瓦片服务叠加在上面。叠加层的 source 与图层 id 都带 gb:服务路径: 前缀,不会与已有图层冲突;返回的 layerIds 可以用来开关显示。左上角的复选框开关叠加层,并统计视野内的兴趣点数。

需要: 一个提供 MapServer 切片的地图服务和一个矢量瓦片服务。

js
/**
 * @title 底图上叠加矢量瓦片
 * @description 地图 load 之后先加香港底图的栅格切片,再把兴趣点矢量瓦片服务叠加上去;复选框开关叠加层,面板显示视野内的兴趣点数。
 * @category 图层
 * @service demo/basemap
 * @service demo/pois
 */
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径' : e && e.message)

try {
  // 拷到自己页面时:换成你的地图服务(底图)与矢量瓦片服务路径
  const basemap = services['demo/basemap']
  const pois = services['demo/pois']

  const map = new maplibregl.Map({
    container,
    style: { version: 8, sources: {}, layers: [] },
    transformRequest: client.transformRequest,
    // 尖沙咀一带,兴趣点密集
    center: [114.172, 22.298],
    zoom: 16,
  })
  map.addControl(new maplibregl.NavigationControl())
  // 切片、图标等资源加载失败时 MapLibre 发 error 事件:转成提示,不影响其余内容显示
  map.on('error', (e) => console.warn('地图资源加载失败:', e.error && e.error.message))

  // 样式加载完成后才能加 source 与图层
  await new Promise((resolve) => map.once('load', resolve))

  client.maplibre.addRasterService(map, basemap)

  // 矢量瓦片服务的 source 与图层 id 都以 `gb:服务路径:` 为前缀,不会和已有图层冲突
  // 空样式没有 glyphs 与 sprite,addVectorTileService 会设为服务样式的,注记与图标才能显示
  const overlay = await client.maplibre.addVectorTileService(map, pois)
  console.log('叠加的图层:', overlay.layerIds.join('、'))

  const panel = document.createElement('div')
  panel.style.cssText = 'position:absolute;top:8px;left: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.8 sans-serif'
  const label = document.createElement('label')
  const box = document.createElement('input')
  box.type = 'checkbox'
  box.checked = true
  label.append(box, ' 显示兴趣点(矢量瓦片)')
  const stat = document.createElement('div')
  panel.append(label, stat)
  container.appendChild(panel)

  // 开关叠加层:对句柄里的每个图层改 visibility
  box.onchange = () => {
    for (const id of overlay.layerIds) map.setLayoutProperty(id, 'visibility', box.checked ? 'visible' : 'none')
  }

  // 每次地图静止后统计视野内渲染出的兴趣点(同一要素可能同时有圆点与注记,按 osm_id 去重)
  let logged = false
  map.on('idle', () => {
    const shown = box.checked ? map.queryRenderedFeatures({ layers: overlay.layerIds }) : []
    const count = new Set(shown.map((f) => f.properties.osm_id)).size
    stat.textContent = `视野内兴趣点:${count} 个`
    if (!logged && count > 0) {
      logged = true
      console.log(stat.textContent)
    }
  })
} catch (e) {
  console.error(friendly(e))
}
在沙箱中打开

WMS 叠加 ​

在地图方案底图上叠加 WMS 图层。WMS 图层名由 client.services.layers() 取得图层列表、再用 GeoBridge.wmsLayerNames() 算出(与平台的命名规则一致);GetMap 地址里写 {bbox-epsg-3857},MapLibre 会按每张切片的范围替换,WMS 就能按切片请求。添加图层时传 beforeId,把 WMS 插在道路之下。左上角的滑块调透明度,图例图片来自 client.services.legendUrl()。

需要: 一个开启了 WMS 的地图服务,以及作为底图的地图方案(也可以换成其他底图)。

js
/**
 * @title WMS 叠加
 * @description 在地图方案底图上叠加土地利用服务的 WMS 图层(插在道路下面),滑块调透明度,面板显示 WMS 图例。
 * @category 图层
 * @service demo/landuse
 * @map demo/map
 */
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在或未开启 WMS,请检查服务路径' : e && e.message)

try {
  // 拷到自己页面时:换成你开启了 WMS 的地图服务路径与你的底图(地图方案 id)
  const path = services['demo/landuse']

  const map = new maplibregl.Map({
    container,
    style: client.maplibre.mapStyleUrl(maps['demo/map']),
    transformRequest: client.transformRequest,
    center: [114.172, 22.302],
    zoom: 15,
  })
  map.addControl(new maplibregl.NavigationControl())
  await new Promise((resolve) => map.once('load', resolve))

  // WMS 图层名:按 MapServer 图层列表算出(与平台的命名规则一致),取第一个图层
  const { layers } = await client.services.layers(path)
  const layerName = GeoBridge.wmsLayerNames(layers)[0]

  // MapLibre 会把 {bbox-epsg-3857} 替换成每张切片的范围,于是 WMS GetMap 也能按切片请求
  const getMap = `${client.services.wmsUrl(path)}?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&LAYERS=${encodeURIComponent(layerName)}` +
    '&STYLES=&CRS=EPSG:3857&BBOX={bbox-epsg-3857}&WIDTH=256&HEIGHT=256&FORMAT=image/png&TRANSPARENT=true'
  map.addSource('wms', { type: 'raster', tiles: [getMap], tileSize: 256 })

  // beforeId:插在底图第一个线图层(道路)之下,道路仍压在 WMS 图片上面
  const firstLine = map.getStyle().layers.find((l) => l.type === 'line')
  map.addLayer({ id: 'wms', type: 'raster', source: 'wms', paint: { 'raster-opacity': 0.7 } }, firstLine && firstLine.id)

  // 面板:透明度滑块 + 图例
  const panel = document.createElement('div')
  panel.style.cssText = 'position:absolute;top:8px;left:8px;z-index:2;background:#fff;padding:8px 10px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.3);font:13px/1.8 sans-serif'
  const title = document.createElement('div')
  title.textContent = `WMS 图层 ${layerName}`
  const slider = document.createElement('input')
  slider.type = 'range'
  slider.min = '0'
  slider.max = '100'
  slider.value = '70'
  const value = document.createElement('span')
  value.textContent = ' 70%'
  slider.oninput = () => {
    map.setPaintProperty('wms', 'raster-opacity', slider.value / 100)
    value.textContent = ` ${slider.value}%`
  }
  // 图例是一张图片:<img> 不经过 transformRequest,受保护服务用 withKey 补 token
  const legend = document.createElement('img')
  legend.src = client.withKey(client.services.legendUrl(path, layerName))
  legend.style.display = 'block'
  panel.append(title, '透明度 ', slider, value, legend)
  container.appendChild(panel)

  console.log('WMS 图层:', layerName, ',插在图层', firstLine ? firstLine.id : '(最上层)', '之下')
} catch (e) {
  console.error(friendly(e))
}
在沙箱中打开