Appearance
查询
按点、按属性条件、按范围查询要素,以及基于查询结果的搜索、聚合、统计与导出。点击识别与导出静态图用的是地图服务的 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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[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))
}