Skip to content

查询 ​

按点、按属性条件、按范围查询要素,以及基于查询结果的搜索、聚合、统计与导出。点击识别与导出静态图用的是地图服务的 MapServer 能力,其余示例用的是要素查询(FeatureServer)能力。

单次返回有上限,可分页

要素查询单次返回的要素数受服务端上限约束(示例平台为 1000 条),结果被截断时返回值的 properties.exceededTransferLimit 为 true。用 resultOffset、resultRecordCount 翻页,或用 client.query.allFeatures() 逐页取全;只要前几条时给 resultRecordCount 即可。需要总数时用 client.query.count() 单独统计。

点击识别要素 ​

点击地图,用 client.query.identify() 查出点击位置各图层的要素:传入 map 时 SDK 自动取当前视野与画布尺寸,tolerance 是点击容差(像素)。弹窗按图层列出名称与类别;识别结果带有几何,示例把第一个要素的形状转成 GeoJSON 高亮显示。加载后先识别地图中心的一点。

需要: 一个开启了 MapServer 能力、包含矢量图层的地图服务(示例用由土地利用、水体、道路组成的香港底图)。

js
/**
 * @title 点击识别要素
 * @description 点击栅格底图,用 MapServer 的 identify 查出点击位置各图层的要素:弹窗按图层列出名称与类别,并把第一个要素的形状高亮。加载后先识别九龙公园作演示。
 * @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,
    center: [114.1695, 22.3005],
    zoom: 15.5,
  })
  map.addControl(new maplibregl.NavigationControl())
  await new Promise((resolve) => map.once('load', resolve))
  client.maplibre.addRasterService(map, path)

  // 高亮层:identify 返回 Esri 几何(rings 为面、paths 为线),转成 GeoJSON 再显示
  map.addSource('hit', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } })
  map.addLayer({ id: 'hit-fill', type: 'fill', source: 'hit', filter: ['==', ['geometry-type'], 'Polygon'], paint: { 'fill-color': '#fab005', 'fill-opacity': 0.4 } })
  map.addLayer({ id: 'hit-line', type: 'line', source: 'hit', paint: { 'line-color': '#e8590c', 'line-width': 3 } })
  const toGeoJSON = (g) =>
    g && g.rings ? { type: 'Polygon', coordinates: g.rings } : g && g.paths ? { type: 'MultiLineString', coordinates: g.paths } : null

  const esc = (v) => String(v).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c])

  async function identifyAt(lngLat) {
    try {
      // 传 map 时 SDK 自动取当前视野与画布尺寸;tolerance 是点击容差(像素)
      const res = await client.query.identify(path, { point: [lngLat.lng, lngLat.lat], map, tolerance: 4 })
      const results = res.results || []
      const geometry = results[0] && toGeoJSON(results[0].geometry)
      map.getSource('hit').setData({ type: 'FeatureCollection', features: geometry ? [{ type: 'Feature', properties: {}, geometry }] : [] })
      const rows = results.slice(0, 8).map((r) => {
        const a = r.attributes || {}
        return `<tr><td style="color:#868e96;padding-right:8px">${esc(r.layerName)}</td><td>${esc(a.name || '(无名称)')}</td><td>${esc(a.fclass || '')}</td></tr>`
      })
      const more = results.length > 8 ? `<div style="color:#868e96">另有 ${results.length - 8} 个要素</div>` : ''
      new maplibregl.Popup({ maxWidth: '320px' })
        .setLngLat(lngLat)
        .setHTML(rows.length ? `<table style="font-size:12px">${rows.join('')}</table>${more}` : '这里没有要素')
        .addTo(map)
      return results.map((r) => `${r.layerName}:${(r.attributes || {}).name || (r.attributes || {}).fclass}`)
    } catch (err) {
      console.error('识别失败:', friendly(err))
      return []
    }
  }

  map.on('click', async (e) => console.log('识别结果:', (await identifyAt(e.lngLat)).join('、') || '无'))

  // 演示:识别九龙公园内的一点
  console.log('演示识别:', (await identifyAt(map.getCenter())).slice(0, 4).join('、'))
} catch (e) {
  console.error(friendly(e))
}
在沙箱中打开

属性过滤 ​

选一个预设条件或在输入框里写 where 条件,用 client.query.features() 查出道路并加粗显示,地图缩放到结果范围;client.query.count() 单独给出命中总数,超过单次上限时会提示只显示了前若干条。

需要: 一个开启了 FeatureServer 能力的服务;where 条件里只能使用该图层中实际存在的字段。

js
/**
 * @title 属性过滤
 * @description 选一个预设条件或自己写 where,用 features 查询道路并在地图方案上加粗显示,缩放到结果范围;count 单独给出命中总数(单次返回有上限)。
 * @category 查询
 * @service demo/roads
 * @map demo/map
 */
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径' : e && e.message)

try {
  // 拷到自己页面时:换成你开启了要素服务的路径;PRESETS 里的字段要换成你数据里实际存在的字段
  const path = services['demo/roads']
  const PRESETS = [
    ["name LIKE '%Nathan%'", '弥敦道'],
    ['maxspeed >= 100', '限速 ≥ 100 的道路'],
    ["fclass = 'trunk' AND tunnel = 'T'", '快速路隧道'],
    ["fclass = 'motorway'", '高速公路(超过单次上限)'],
  ]

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

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

  map.addSource('result', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } })
  map.addLayer({ id: 'result', type: 'line', source: 'result', layout: { 'line-cap': 'round' }, paint: { 'line-color': '#e8590c', 'line-width': 5 } })

  const bar = document.createElement('div')
  bar.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:13px/2 sans-serif'
  bar.innerHTML = `<select>${PRESETS.map(([w, t]) => `<option value="${w}">${t}</option>`).join('')}</select><br><input style="width:260px"> <button>查询</button><div data-label></div>`
  container.appendChild(bar)
  const [select, input, button, label] = ['select', 'input', 'button', '[data-label]'].map((s) => bar.querySelector(s))
  input.value = PRESETS[0][0]

  async function search() {
    button.disabled = true
    try {
      const where = input.value.trim() || '1=1'
      // 单次返回有上限,这里只显示第一页(更多可用 resultOffset 分页或 allFeatures 取全);总数用 count 单独取
      const [data, total] = await Promise.all([client.query.features(path, 0, { where, outFields: ['name', 'fclass'] }), client.query.count(path, 0, { where })])
      map.getSource('result').setData(data)
      const shown = data.features.length
      label.textContent = shown < total ? `命中 ${total} 条(显示前 ${shown} 条)` : `命中 ${total} 条`
      // 缩放到结果范围:递归遍历所有坐标求外包矩形
      const box = new maplibregl.LngLatBounds()
      const each = (c) => (typeof c[0] === 'number' ? box.extend(c) : c.forEach(each))
      for (const f of data.features) if (f.geometry) each(f.geometry.coordinates)
      if (!box.isEmpty()) map.fitBounds(box, { padding: 60, maxZoom: 16 })
      console.log('where =', where, '→', label.textContent)
    } catch (e) {
      label.textContent = '查询失败:' + friendly(e)
      console.error(label.textContent)
    } finally {
      button.disabled = false
    }
  }
  select.onchange = () => {
    input.value = select.value
    search()
  }
  button.onclick = search
  input.onkeydown = (e) => e.key === 'Enter' && search()
  await search()
} catch (e) {
  console.error(friendly(e))
}
在沙箱中打开

框选查询 ​

在地图上依次点击两个角点得到一个矩形,把 [minx, miny, maxx, maxy] 直接作为 geometry 做空间查询;矩形与框内兴趣点分别作为图层显示,面板列出数量最多的类别。加载后先框选视野中央的一块。

需要: 一个开启了 FeatureServer 能力的服务。

js
/**
 * @title 框选查询
 * @description 依次点击两个角点得到一个矩形,用这个范围查询兴趣点:框内要素画在地图上,面板列出数量最多的类别。加载后先框选视野中央一块作演示。
 * @category 查询
 * @service demo/pois
 * @map demo/map
 */
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径' : e && e.message)

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

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

  const empty = { type: 'FeatureCollection', features: [] }
  map.addSource('box', { type: 'geojson', data: empty })
  map.addSource('result', { type: 'geojson', data: empty })
  map.addLayer({ id: 'box-fill', type: 'fill', source: 'box', paint: { 'fill-color': '#1971c2', 'fill-opacity': 0.08 } })
  map.addLayer({ id: 'box-line', type: 'line', source: 'box', paint: { 'line-color': '#1971c2', 'line-width': 2, 'line-dasharray': [2, 2] } })
  map.addLayer({ id: 'result', type: 'circle', source: 'result', paint: { 'circle-radius': 4, 'circle-color': '#e8590c', 'circle-stroke-color': '#fff', 'circle-stroke-width': 1 } })

  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;min-width:180px'
  panel.textContent = '依次点击两个角点框选范围'
  container.appendChild(panel)

  async function query(bbox) {
    const [minx, miny, maxx, maxy] = bbox
    const ring = [[minx, miny], [maxx, miny], [maxx, maxy], [minx, maxy], [minx, miny]]
    map.getSource('box').setData({ type: 'Feature', properties: {}, geometry: { type: 'Polygon', coordinates: [ring] } })
    try {
      // geometry 直接给 [minx, miny, maxx, maxy],SDK 转成 Esri 范围;单次最多返回服务上限条数,更多可用 resultOffset 分页
      const data = await client.query.features(path, 0, { geometry: bbox, outFields: ['name', 'fclass'] })
      map.getSource('result').setData(data)
      const byClass = {}
      for (const f of data.features) byClass[f.properties.fclass] = (byClass[f.properties.fclass] || 0) + 1
      const top = Object.entries(byClass).sort((a, b) => b[1] - a[1]).slice(0, 5)
      // innerText 按换行分行显示,属性值不会被当成 HTML
      panel.innerText = [`框内兴趣点 ${data.features.length} 个`, ...top.map(([k, n]) => `${k}:${n}`)].join('\n')
      return `${data.features.length} 个;最多的类别 ${top.map(([k, n]) => `${k} ${n}`).join(',')}`
    } catch (err) {
      panel.textContent = '查询失败:' + friendly(err)
      return panel.textContent
    }
  }

  let first = null
  map.on('click', async (e) => {
    const p = [e.lngLat.lng, e.lngLat.lat]
    if (!first) {
      first = p
      panel.textContent = '已记下第一个角点,请点第二个角点'
      return
    }
    const bbox = [Math.min(first[0], p[0]), Math.min(first[1], p[1]), Math.max(first[0], p[0]), Math.max(first[1], p[1])]
    first = null
    console.log('框内兴趣点:', await query(bbox))
  })

  // 演示:框选视野中央三分之一的范围
  const b = map.getBounds()
  const dx = (b.getEast() - b.getWest()) / 3
  const dy = (b.getNorth() - b.getSouth()) / 3
  console.log('演示框选:', await query([b.getWest() + dx, b.getSouth() + dy, b.getEast() - dx, b.getNorth() - dy]))
} catch (e) {
  console.error(friendly(e))
}
在沙箱中打开

地名搜索 ​

输入关键字,用 where 的 LIKE 在兴趣点名称里搜索,结果按名称排序列在面板里;点击结果,地图飞到该地点并弹出名称。关键字里的单引号要写成两个,避免破坏 where 条件。加载后先搜「博物館」并飞到第一条结果。

需要: 一个开启了 FeatureServer 能力、带名称字段的点要素服务。

js
/**
 * @title 地名搜索
 * @description 输入关键字,用 where 的 LIKE 在兴趣点名称里搜索,结果列在面板里;点击结果飞到该地点并弹出名称。加载后先搜「博物館」并飞到第一条。
 * @category 查询
 * @service demo/pois
 * @map demo/map
 */
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径' : e && e.message)

try {
  // 拷到自己页面时:换成你开启了要素服务的路径;name 换成你数据里的名称字段
  const path = services['demo/pois']

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

  const panel = document.createElement('div')
  panel.style.cssText = 'position:absolute;top:8px;left:8px;bottom:28px;width:260px;z-index:2;background:#fff;padding:8px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.3);font:13px/1.6 sans-serif;display:flex;flex-direction:column'
  panel.innerHTML = '<div><input placeholder="名称关键字,如 博物館、戲院" style="width:170px"> <button>搜索</button></div><div data-n style="color:#868e96"></div><ol style="margin:0;padding-left:22px;overflow:auto;flex:1"></ol>'
  container.appendChild(panel)
  const [input, button, count, list] = ['input', 'button', '[data-n]', 'ol'].map((s) => panel.querySelector(s))

  const marker = new maplibregl.Marker({ color: '#e8590c' })
  const popup = new maplibregl.Popup({ offset: 30, closeButton: false })
  function go(f) {
    const lngLat = f.geometry.coordinates
    // padding 让出左侧面板的宽度,地点落在面板右边的可见区域中央
    map.flyTo({ center: lngLat, zoom: 17, padding: { left: 280 } })
    marker.setLngLat(lngLat).addTo(map)
    popup.setLngLat(lngLat).setText(`${f.properties.name}(${f.properties.fclass})`).addTo(map)
    return f.properties.name
  }

  async function search() {
    const keyword = input.value.trim()
    if (!keyword) return []
    // 单引号在 SQL 里要写成两个,避免关键字里的引号破坏 where
    const where = `name LIKE '%${keyword.replace(/'/g, "''")}%'`
    try {
      // 只取前 50 条;还有更多时 properties.exceededTransferLimit 为 true(可增大 resultOffset 翻页)
      const data = await client.query.features(path, 0, { where, outFields: ['name', 'fclass'], orderByFields: 'name', resultRecordCount: 50 })
      const more = data.properties?.exceededTransferLimit || data.features.length > 50
      count.textContent = `找到 ${Math.min(data.features.length, 50)} 个${more ? '(只列出前 50 个)' : ''}`
      list.replaceChildren(
        ...data.features.slice(0, 50).map((f) => {
          const li = document.createElement('li')
          li.textContent = f.properties.name
          li.style.cursor = 'pointer'
          li.onclick = () => go(f)
          return li
        }),
      )
      return data.features
    } catch (e) {
      count.textContent = '搜索失败:' + friendly(e)
      return []
    }
  }
  button.onclick = search
  input.onkeydown = (e) => e.key === 'Enter' && search()

  // 演示:搜「博物館」,飞到第一条结果
  input.value = '博物館'
  const found = await search()
  console.log('搜索「博物館」:', count.textContent, found.length ? `,飞到 ${go(found[0])}` : '')
} catch (e) {
  console.error(friendly(e))
}
在沙箱中打开

查询结果聚合 ​

按类别查询当前视野内的兴趣点,交给 MapLibre GeoJSON 数据源的 cluster 聚合显示:圆圈的大小、颜色与数字表示数量,点击圆圈放大展开。聚合数字需要字体,示例借用服务样式里的字体地址(glyphs)。移动地图后点「查询当前视野」刷新;视野内数量超过单次上限时提示放大后再查。

需要: 一个开启了 FeatureServer 能力的点要素服务。

js
/**
 * @title 查询结果聚合
 * @description 按类别查询当前视野内的兴趣点,交给 GeoJSON source 的 cluster 聚合显示:圆圈大小与数字表示数量,点击圆圈放大展开。移动地图后点「查询当前视野」刷新。
 * @category 查询
 * @service demo/pois
 * @map demo/map
 */
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径' : e && e.message)

try {
  // 拷到自己页面时:换成你开启了要素服务的点要素服务路径
  const path = services['demo/pois']

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

  // 聚合圆圈上的数字要用字体:地图方案样式没有 glyphs,借用服务样式里的字体地址
  map.setGlyphs((await client.maplibre.serviceStyle(path)).glyphs)

  // cluster: true 让 MapLibre 在浏览器里按缩放级别聚合点,属性 point_count 是聚合的点数
  map.addSource('result', { type: 'geojson', data: { type: 'FeatureCollection', features: [] }, cluster: true, clusterRadius: 50 })
  const size = ['step', ['get', 'point_count'], 14, 20, 18, 100, 24]
  map.addLayer({ id: 'clusters', type: 'circle', source: 'result', filter: ['has', 'point_count'], paint: { 'circle-color': ['step', ['get', 'point_count'], '#74c0fc', 20, '#fab005', 100, '#f03e3e'], 'circle-radius': size, 'circle-opacity': 0.85 } })
  map.addLayer({ id: 'count', type: 'symbol', source: 'result', filter: ['has', 'point_count'], layout: { 'text-field': ['get', 'point_count_abbreviated'], 'text-size': 12 } })
  map.addLayer({ id: 'single', type: 'circle', source: 'result', filter: ['!', ['has', 'point_count']], paint: { 'circle-radius': 5, 'circle-color': '#1c7ed6', 'circle-stroke-color': '#fff', 'circle-stroke-width': 1 } })

  const bar = document.createElement('div')
  bar.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:13px/2 sans-serif'
  bar.innerHTML = '<select><option value="restaurant">餐厅</option><option value="convenience">便利店</option><option value="bank">银行</option><option value="toilet">公厕</option></select> <button>查询当前视野</button><div data-n></div>'
  container.appendChild(bar)
  const [select, button, info] = ['select', 'button', '[data-n]'].map((s) => bar.querySelector(s))

  async function load() {
    const b = map.getBounds()
    const params = { where: `fclass = '${select.value}'`, geometry: [b.getWest(), b.getSouth(), b.getEast(), b.getNorth()] }
    try {
      // 单次返回有上限(更多可用 resultOffset 分页);这里同时 count,超出时提示放大地图
      const [data, total] = await Promise.all([client.query.features(path, 0, { ...params, outFields: ['name'] }), client.query.count(path, 0, params)])
      map.getSource('result').setData(data)
      info.textContent = `视野内${select.selectedOptions[0].textContent} ${total} 个` + (data.features.length < total ? `,只返回了 ${data.features.length} 个,请放大后再查` : '')
    } catch (e) {
      info.textContent = '查询失败:' + friendly(e)
    }
    return info.textContent
  }
  button.onclick = load
  select.onchange = load

  // 点击聚合圆圈:放大到它展开的级别
  map.on('click', 'clusters', async (e) => {
    const f = e.features[0]
    const zoom = await map.getSource('result').getClusterExpansionZoom(f.properties.cluster_id)
    map.easeTo({ center: f.geometry.coordinates, zoom })
  })
  map.on('mouseenter', 'clusters', () => (map.getCanvas().style.cursor = 'pointer'))
  map.on('mouseleave', 'clusters', () => (map.getCanvas().style.cursor = ''))

  console.log('首次查询:', await load())
} catch (e) {
  console.error(friendly(e))
}
在沙箱中打开

兴趣点分类统计 ​

每次地图停止移动,对每个类别各发一个 client.query.count()(只返回数量、并行发出),统计当前视野内的数量并画成条形图;条形颜色与地图上的圆点一致。地图移动期间返回的过期结果会被丢弃。

需要: 一个同时开启了矢量瓦片与要素服务的点要素服务。

js
/**
 * @title 兴趣点分类统计
 * @description 每次地图停止移动,用 count 并行统计当前视野内各类兴趣点的数量,画成条形图;条形颜色与地图上的圆点一致。
 * @category 查询
 * @service demo/pois
 * @map demo/map
 */
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径' : e && e.message)

try {
  // 拷到自己页面时:换成你的点要素服务(需开启矢量瓦片与要素服务);CATEGORIES 换成你数据里的类别
  const path = services['demo/pois']
  const CATEGORIES = [
    ['restaurant', '餐厅', '#e03131'],
    ['convenience', '便利店', '#2f9e44'],
    ['fast_food', '快餐', '#f08c00'],
    ['cafe', '咖啡馆', '#9c36b5'],
    ['bank', '银行', '#495057'],
    ['supermarket', '超市', '#1971c2'],
    ['pharmacy', '药房', '#0c8599'],
    ['hotel', '酒店', '#e64980'],
  ]

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

  // 地图上的圆点:只画统计的这几类,颜色与条形图一致
  map.addSource('poi-tiles', { type: 'vector', tiles: [client.services.vectorTileUrl(path)] })
  map.addLayer({
    id: 'poi-points',
    type: 'circle',
    source: 'poi-tiles',
    'source-layer': (await client.services.tileJson(path)).vector_layers[0].id,
    filter: ['in', ['get', 'fclass'], ['literal', CATEGORIES.map((c) => c[0])]],
    paint: { 'circle-radius': 3.5, 'circle-color': ['match', ['get', 'fclass'], ...CATEGORIES.flatMap(([v, , c]) => [v, c]), '#adb5bd'], 'circle-stroke-color': '#fff', 'circle-stroke-width': 0.5 },
  })

  const chart = document.createElement('div')
  chart.style.cssText = 'position:absolute;top:8px;left:8px;z-index:2;width:240px;background:#fff;padding:8px 10px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.3);font:12px/1.4 sans-serif'
  container.appendChild(chart)

  let latest = 0
  async function update() {
    const ticket = ++latest
    const b = map.getBounds()
    const geometry = [b.getWest(), b.getSouth(), b.getEast(), b.getNorth()]
    try {
      // 每类一个 count 请求(只返回数量),并行发出
      const counts = await Promise.all(CATEGORIES.map(([v]) => client.query.count(path, 0, { where: `fclass = '${v}'`, geometry })))
      if (ticket !== latest) return // 期间地图又动过,丢弃过期结果
      const max = Math.max(1, ...counts)
      chart.innerHTML = '<b>当前视野内兴趣点</b>' + CATEGORIES.map(([, name, color], i) =>
        `<div style="display:flex;align-items:center;gap:6px;margin-top:4px"><span style="width:44px">${name}</span><span style="height:12px;width:${(counts[i] / max) * 140}px;background:${color}"></span><span>${counts[i]}</span></div>`).join('')
      console.log('视野统计:', CATEGORIES.map(([, n], i) => `${n} ${counts[i]}`).join(','))
    } catch (e) {
      chart.textContent = '统计失败:' + friendly(e)
    }
  }
  map.on('moveend', update)
  await update()
} catch (e) {
  console.error(friendly(e))
}
在沙箱中打开

OGC API - Features 查询 ​

用 client.ogcApi 走 OGC API - Features:先读集合与可查询属性(queryables),再按 CQL2 条件(filter,SDK 自动加 filter-lang=cql2-text)与当前视野(bbox,经纬度)取一页要素,以 GeoJSON 显示在地图方案上。响应里 rel: 'next' 的链接表示还有下一页,示例用 offset 翻页;要一次读完可改用 client.ogcApi.allItems()。

需要: 一个开启了 OGC API 的矢量服务。

js
/**
 * @title OGC API - Features 查询
 * @description 用 ogcApi 读道路集合的可查询属性,再按 CQL2 条件与当前视野(bbox)取要素,显示在地图方案上;面板选条件、翻页,日志给出每页数量与是否还有下一页。
 * @category 查询
 * @service demo/roads
 * @map demo/map
 */
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在或未开启 OGC API,请检查服务路径' : e && e.message)

try {
  // 拷到自己页面时:换成你开启了 OGC API 的服务路径;条件里的字段换成你数据里的字段
  const path = services['demo/roads']
  const FILTERS = [["fclass = 'motorway'", '高速公路'], ["fclass IN ('trunk', 'primary') AND tunnel = 'T'", '快速路与主干道的隧道'], ['maxspeed >= 80', '限速 ≥ 80']]

  const map = new maplibregl.Map({ container, style: client.maplibre.mapStyleUrl(maps['demo/map']), transformRequest: client.transformRequest, center: [114.17, 22.32], zoom: 11 })
  map.addControl(new maplibregl.NavigationControl())
  await new Promise((resolve) => map.once('load', resolve))
  map.addSource('hits', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } })
  map.addLayer({ id: 'hits', type: 'line', source: 'hits', paint: { 'line-color': '#d6336c', 'line-width': 4 } })

  // 集合与可查询属性(CQL2 只能用这里列出的字段)
  const { collections } = await client.ogcApi.collections(path)
  const collection = collections[0].id
  const queryables = await client.ogcApi.queryables(path, collection)
  console.log('可查询属性:' + Object.keys(queryables.properties).join('、'))

  let offset = 0
  const run = async () => {
    const b = map.getBounds()
    const fc = await client.ogcApi.items(path, collection, { filter: select.value, bbox: [b.getWest(), b.getSouth(), b.getEast(), b.getNorth()], limit: 200, offset })
    map.getSource('hits').setData(fc)
    const next = fc.links.some((l) => l.rel === 'next')
    console.log(`第 ${offset / 200 + 1} 页:${fc.numberReturned} 条${next ? ',还有下一页' : ',已是最后一页'}`)
    more.disabled = !next
  }

  // 面板:条件下拉 + 查询 / 下一页
  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/2 sans-serif'
  const select = document.createElement('select')
  for (const [cql, text] of FILTERS) select.add(new Option(text, cql))
  const go = document.createElement('button')
  go.textContent = '在当前视野查询'
  go.onclick = () => ((offset = 0), run().catch((e) => console.error(friendly(e))))
  const more = document.createElement('button')
  more.textContent = '下一页'
  more.onclick = () => ((offset += 200), run().catch((e) => console.error(friendly(e))))
  panel.append(select, ' ', go, ' ', more)
  container.appendChild(panel)
  await run()
} catch (e) {
  console.error(friendly(e))
}
在沙箱中打开

导出静态图 ​

用 MapServer 的 export 接口按当前视野导出一张 PNG:范围用经纬度(bboxSR=4326),出图用 Web 墨卡托(imageSR=3857),与地图上看到的形状一致;f=json 时接口返回图片地址、尺寸与比例尺。SDK 没有 export 的专用方法,示例用 client.request() 调用,Key 照常附带;显示图片的 <img> 不经过 transformRequest,地址交给 client.withKey() 补 token。

需要: 一个开启了 MapServer 能力的地图服务。

js
/**
 * @title 导出静态图
 * @description 用 MapServer 的 export 接口按当前视野导出一张 PNG:接口返回图片地址、尺寸与比例尺,图片显示在右下角。适合生成报告配图、缩略图。
 * @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,
    center: [114.165, 22.285],
    zoom: 14,
  })
  map.addControl(new maplibregl.NavigationControl())
  await new Promise((resolve) => map.once('load', resolve))
  client.maplibre.addRasterService(map, path)

  const panel = document.createElement('div')
  panel.style.cssText = 'position:absolute;right:8px;bottom:28px;z-index:2;background:#fff;padding:8px;border-radius:6px;box-shadow:0 1px 4px rgba(0,0,0,.4);font:12px/1.6 sans-serif'
  panel.innerHTML = '<button>导出当前视野</button><div data-info></div><img style="display:block;width:320px;border:1px solid #ced4da;background:#f8f9fa">'
  container.appendChild(panel)
  const [button, info, img] = ['button', '[data-info]', 'img'].map((s) => panel.querySelector(s))

  async function exportView() {
    const b = map.getBounds()
    const canvas = map.getCanvas()
    // SDK 没有 export 的专用方法:用 client.request 调平台接口(地址相对 baseUrl,Key 照常附带)
    // bbox 用经纬度(bboxSR=4326),出图用 Web 墨卡托(imageSR=3857),与地图上看到的形状一致;f=json 返回图片地址
    const res = await client.request(`rest/geodata/services/${GeoBridge.encodeServicePath(path)}/MapServer/export`, {
      query: {
        bbox: [b.getWest(), b.getSouth(), b.getEast(), b.getNorth()].join(','),
        bboxSR: 4326,
        imageSR: 3857,
        size: `${canvas.clientWidth},${canvas.clientHeight}`,
        format: 'png',
        transparent: true,
        f: 'json',
      },
    })
    // <img> 不经过 transformRequest,受保护服务要用 withKey 把 token 补进地址
    img.src = client.withKey(res.href)
    info.textContent = `${res.width}×${res.height} 像素,比例尺约 1:${Math.round(res.scale)}`
    return info.textContent
  }

  button.onclick = async () => {
    button.disabled = true
    try {
      console.log('已导出:', await exportView())
    } catch (e) {
      console.error('导出失败:', friendly(e))
    } finally {
      button.disabled = false
    }
  }

  // 演示:加载后先导出一次
  console.log('已导出:', await exportView())
} catch (e) {
  console.error(friendly(e))
}
在沙箱中打开