Appearance
样式与交互
矢量瓦片在浏览器里渲染,样式与交互完全由前端决定:按属性配色、热力图、悬停高亮、筛选、弹窗,都不需要服务端重新出图。本页示例自己用矢量瓦片写图层,常用的几步是:
- 瓦片地址用
client.services.vectorTileUrl(),作为vector数据源的tiles。 - 图层的
source-layer取自client.services.tileJson()返回的vector_layers[0].id(平台上与服务名相同)。 - 创建地图时传
transformRequest: client.transformRequest,瓦片请求才会带上 Key。
按道路等级配色
不用服务自带的样式,自己写道路图层:match 表达式按 fclass 字段给每个等级配色、设线宽,interpolate 让线宽随缩放变化。左上角的图例可以点击,把某一等级从 filter 中去掉或加回。
需要: 一个线要素的矢量瓦片服务,且有表示等级的字段(示例用香港道路的 fclass);示例另用一个水体服务作陪衬。
js
在沙箱中打开/**
* @title 按道路等级配色
* @description 不用服务自带样式,自己用矢量瓦片写图层:按 fclass 字段给道路配色、按等级与缩放级别设线宽;图例可点击开关某一等级。
* @category 样式与交互
* @service demo/roads
* @service demo/water
*/
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径' : e && e.message)
try {
// 拷到自己页面时:换成你的道路与水体矢量瓦片服务;CLASSES 换成你数据里等级字段的取值
const roads = services['demo/roads']
const water = services['demo/water']
const CLASSES = [
['motorway', '高速公路', '#e8590c', 4],
['trunk', '快速路', '#f08c00', 3.5],
['primary', '主干道', '#f59f00', 3],
['secondary', '次干道', '#74b816', 2.2],
['tertiary', '支路', '#1c7ed6', 1.6],
['residential', '街巷', '#868e96', 1],
]
const map = new maplibregl.Map({
container,
style: { version: 8, sources: {}, layers: [{ id: 'bg', type: 'background', paint: { 'background-color': '#f8f9fa' } }] },
transformRequest: client.transformRequest,
// 沙田一带:既有吐露港公路等快速路,也有主干道与街巷
center: [114.19, 22.383],
zoom: 15,
})
map.addControl(new maplibregl.NavigationControl())
await new Promise((resolve) => map.once('load', resolve))
// 矢量瓦片的 source-layer 名取自 tiles.json 的 vector_layers(平台上与服务名相同)
const layerOf = async (path) => (await client.services.tileJson(path)).vector_layers[0].id
map.addSource('water', { type: 'vector', tiles: [client.services.vectorTileUrl(water)] })
map.addLayer({ id: 'water', type: 'fill', source: 'water', 'source-layer': await layerOf(water), paint: { 'fill-color': '#a5d8ff' } })
// 数据驱动样式:match 按 fclass 取颜色与线宽,interpolate 让线宽随缩放变化
const match = (i, fallback) => ['match', ['get', 'fclass'], ...CLASSES.flatMap((c) => [c[0], c[i]]), fallback]
map.addSource('roads', { type: 'vector', tiles: [client.services.vectorTileUrl(roads)] })
map.addLayer({
id: 'roads',
type: 'line',
source: 'roads',
'source-layer': await layerOf(roads),
filter: ['in', ['get', 'fclass'], ['literal', CLASSES.map((c) => c[0])]],
layout: { 'line-cap': 'round', 'line-join': 'round' },
paint: { 'line-color': match(2, '#adb5bd'), 'line-width': ['interpolate', ['linear'], ['zoom'], 10, ['*', 0.4, match(3, 1)], 16, ['*', 2.5, match(3, 1)]] },
})
// 图例:点击某一等级把它从 filter 里去掉或加回
const shown = new Set(CLASSES.map((c) => c[0]))
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.9 sans-serif;cursor:pointer;user-select:none'
for (const [fclass, name, color] of CLASSES) {
const row = document.createElement('div')
row.innerHTML = `<span style="display:inline-block;width:22px;height:4px;vertical-align:middle;margin-right:6px;background:${color}"></span>${name}`
row.onclick = () => {
if (shown.has(fclass)) shown.delete(fclass)
else shown.add(fclass)
row.style.opacity = shown.has(fclass) ? 1 : 0.35
map.setFilter('roads', ['in', ['get', 'fclass'], ['literal', [...shown]]])
}
legend.appendChild(row)
}
container.appendChild(legend)
// 地图首次静止后,按等级统计视野内渲染出的道路段数
map.once('idle', () => {
const counts = {}
for (const f of map.queryRenderedFeatures({ layers: ['roads'] })) counts[f.properties.fclass] = (counts[f.properties.fclass] || 0) + 1
console.log('视野内各等级道路段数:', CLASSES.map((c) => `${c[1]} ${counts[c[0]] || 0}`).join(','))
})
} catch (e) {
console.error(friendly(e))
}兴趣点热力图
用兴趣点矢量瓦片画 heatmap 图层,放大到 15 级以上逐渐淡出、过渡为圆点。左上角的下拉框只看某一类兴趣点(两个图层用同一个 filter),滑块调热力半径。
需要: 一个点要素的矢量瓦片服务;底图用地图方案。
js
在沙箱中打开/**
* @title 兴趣点热力图
* @description 用兴趣点矢量瓦片画热力图,放大到 15 级以上逐渐过渡为圆点;下拉框只看某一类兴趣点,滑块调热力半径。
* @category 样式与交互
* @service demo/pois
* @map demo/map
*/
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径' : e && e.message)
try {
// 拷到自己页面时:换成你的点要素矢量瓦片服务与底图(地图方案 id)
const path = services['demo/pois']
const map = new maplibregl.Map({
container,
style: client.maplibre.mapStyleUrl(maps['demo/map']),
transformRequest: client.transformRequest,
center: [114.175, 22.305],
zoom: 13,
})
map.addControl(new maplibregl.NavigationControl())
await new Promise((resolve) => map.once('load', resolve))
// source-layer 名取自 tiles.json 的 vector_layers
const sourceLayer = (await client.services.tileJson(path)).vector_layers[0].id
map.addSource('poi-tiles', { type: 'vector', tiles: [client.services.vectorTileUrl(path)] })
// 热力图:15 级以后淡出;圆点:14 级以后淡入
map.addLayer({
id: 'heat',
type: 'heatmap',
source: 'poi-tiles',
'source-layer': sourceLayer,
paint: {
'heatmap-radius': 12,
'heatmap-intensity': ['interpolate', ['linear'], ['zoom'], 10, 0.1, 13, 0.25, 15, 0.6],
'heatmap-color': ['interpolate', ['linear'], ['heatmap-density'], 0, 'rgba(33,102,172,0)', 0.2, '#67a9cf', 0.4, '#d1e5f0', 0.6, '#fddbc7', 0.8, '#ef8a62', 1, '#b2182b'],
'heatmap-opacity': ['interpolate', ['linear'], ['zoom'], 14, 0.9, 16, 0],
},
})
map.addLayer({
id: 'points',
type: 'circle',
source: 'poi-tiles',
'source-layer': sourceLayer,
minzoom: 14,
paint: { 'circle-radius': 4, 'circle-color': '#b2182b', 'circle-stroke-color': '#fff', 'circle-stroke-width': 1, 'circle-opacity': ['interpolate', ['linear'], ['zoom'], 14, 0, 15, 1] },
})
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/2 sans-serif'
panel.innerHTML = `<div>类别 <select>${[['', '全部'], ['restaurant', '餐厅'], ['cafe', '咖啡馆'], ['convenience', '便利店'], ['bank', '银行'], ['toilet', '公厕']]
.map(([v, t]) => `<option value="${v}">${t}</option>`).join('')}</select></div><div>半径 <input type="range" min="4" max="40" value="12"></div><div data-zoom></div>`
container.appendChild(panel)
// 过滤:两个图层用同一个 filter;半径:只改热力图的 paint 属性
panel.querySelector('select').onchange = (e) => {
const filter = e.target.value ? ['==', ['get', 'fclass'], e.target.value] : null
map.setFilter('heat', filter)
map.setFilter('points', filter)
console.log('只看:', e.target.selectedOptions[0].textContent)
}
panel.querySelector('input').oninput = (e) => map.setPaintProperty('heat', 'heatmap-radius', Number(e.target.value))
const zoomInfo = panel.querySelector('[data-zoom]')
const showZoom = () => (zoomInfo.textContent = `缩放 ${map.getZoom().toFixed(1)}:${map.getZoom() < 15 ? '热力图' : '圆点'}(放大到 15 级查看单个兴趣点)`)
map.on('zoomend', showZoom)
showZoom()
// 热力图图层不能用 queryRenderedFeatures 查询,改数已加载瓦片里的要素(同一要素可能跨瓦片重复,按 osm_id 去重)
map.once('idle', () => {
const loaded = map.querySourceFeatures('poi-tiles', { sourceLayer })
console.log('热力图已渲染,已加载兴趣点:', new Set(loaded.map((f) => f.properties.osm_id)).size, '个')
})
} catch (e) {
console.error(friendly(e))
}建筑悬停高亮
鼠标移到建筑上时,用 feature-state 把这栋建筑涂成橙色,面板显示名称与类型。feature-state 需要要素 id,数据源的 promoteId 把唯一字段提升为 id。示例加载后先高亮视野内一栋有名称的建筑。
需要: 一个面要素的矢量瓦片服务,且有唯一字段(示例用香港建筑的 osm_id)。
js
在沙箱中打开/**
* @title 建筑悬停高亮
* @description 鼠标移到建筑上时用 feature-state 把它涂成橙色,面板显示建筑名称与类型;加载后先高亮地图中心的建筑作演示。
* @category 样式与交互
* @service demo/buildings
* @map demo/map
*/
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径' : e && e.message)
try {
// 拷到自己页面时:换成你的面要素矢量瓦片服务;promoteId 换成你数据里唯一的字段
const path = services['demo/buildings']
const map = new maplibregl.Map({
container,
style: client.maplibre.mapStyleUrl(maps['demo/map']),
transformRequest: client.transformRequest,
// 中环一带
center: [114.1585, 22.2825],
zoom: 16.5,
})
map.addControl(new maplibregl.NavigationControl())
await new Promise((resolve) => map.once('load', resolve))
// feature-state 需要要素 id:promoteId 把属性 osm_id 提升为 id
const sourceLayer = (await client.services.tileJson(path)).vector_layers[0].id
map.addSource('building-tiles', { type: 'vector', tiles: [client.services.vectorTileUrl(path)], promoteId: 'osm_id' })
map.addLayer({
id: 'building-fill',
type: 'fill',
source: 'building-tiles',
'source-layer': sourceLayer,
paint: {
// 按 feature-state 的 hover 取颜色:悬停的建筑为橙色,其余半透明蓝灰
'fill-color': ['case', ['boolean', ['feature-state', 'hover'], false], '#fd7e14', '#748ffc'],
'fill-opacity': ['case', ['boolean', ['feature-state', 'hover'], false], 0.9, 0.35],
'fill-outline-color': '#364fc7',
},
})
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:200px'
panel.textContent = '把鼠标移到建筑上'
container.appendChild(panel)
// 取消上一个的高亮,再高亮这一个(f 为空表示鼠标不在建筑上)
let hovered = null
function highlight(f) {
if (hovered !== null) map.setFeatureState({ source: 'building-tiles', sourceLayer, id: hovered }, { hover: false })
hovered = f ? f.id : null
map.getCanvas().style.cursor = f ? 'pointer' : ''
if (!f) return (panel.textContent = '把鼠标移到建筑上')
map.setFeatureState({ source: 'building-tiles', sourceLayer, id: hovered }, { hover: true })
return (panel.textContent = `${f.properties.name || '(无名称)'} 类型:${f.properties.type || '未标注'}`)
}
map.on('mousemove', (e) => highlight(map.queryRenderedFeatures(e.point, { layers: ['building-fill'] })[0]))
map.getCanvas().addEventListener('mouseleave', () => highlight(null))
// 演示:瓦片加载完成后,先高亮视野内第一栋有名称的建筑
map.once('idle', () => {
const named = map.queryRenderedFeatures({ layers: ['building-fill'] }).find((f) => f.properties.name)
console.log('演示高亮:', named ? highlight(named) : '视野内没有带名称的建筑')
})
} catch (e) {
console.error(friendly(e))
}兴趣点类别筛选
一排按钮按类别筛选兴趣点:setFilter 只改显示,不重新请求瓦片。同时用 client.query.count() 向要素服务统计全港该类兴趣点的总数,显示在左下角。
需要: 一个同时开启了矢量瓦片与要素服务的点要素服务。
js
在沙箱中打开/**
* @title 兴趣点类别筛选
* @description 一排按钮按 fclass 筛选兴趣点矢量瓦片(setFilter 即时生效,不重新请求瓦片),同时用要素查询的 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'],
['cafe', '咖啡馆', '#9c36b5'],
['fast_food', '快餐', '#f08c00'],
['convenience', '便利店', '#2f9e44'],
['supermarket', '超市', '#1971c2'],
['bank', '银行', '#495057'],
]
const map = new maplibregl.Map({
container,
style: client.maplibre.mapStyleUrl(maps['demo/map']),
transformRequest: client.transformRequest,
// 旺角一带
center: [114.17, 22.318],
zoom: 15,
})
map.addControl(new maplibregl.NavigationControl())
await new Promise((resolve) => map.once('load', resolve))
const sourceLayer = (await client.services.tileJson(path)).vector_layers[0].id
map.addSource('poi-tiles', { type: 'vector', tiles: [client.services.vectorTileUrl(path)] })
map.addLayer({
id: 'poi-points',
type: 'circle',
source: 'poi-tiles',
'source-layer': sourceLayer,
// 按类别取颜色;其余类别画成灰色
paint: { 'circle-radius': 5, 'circle-color': ['match', ['get', 'fclass'], ...CATEGORIES.flatMap(([v, , c]) => [v, c]), '#adb5bd'], '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;display:flex;flex-wrap:wrap;gap:6px;max-width:70%'
const info = document.createElement('div')
info.style.cssText = 'position:absolute;bottom:28px;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 sans-serif'
container.append(bar, info)
async function select(value, label, button) {
for (const b of bar.children) b.style.fontWeight = b === button ? 'bold' : 'normal'
// 矢量瓦片已在浏览器里,setFilter 只改显示
map.setFilter('poi-points', value ? ['==', ['get', 'fclass'], value] : ['in', ['get', 'fclass'], ['literal', CATEGORIES.map((c) => c[0])]])
try {
// 总数来自要素服务:count 只返回数量,不传要素
const total = await client.query.count(path, 0, { where: value ? `fclass = '${value}'` : `fclass IN (${CATEGORIES.map((c) => `'${c[0]}'`).join(',')})` })
info.textContent = `${label}:全港共 ${total} 个`
console.log(info.textContent)
} catch (e) {
info.textContent = ''
console.error('统计失败:', friendly(e))
}
}
for (const [value, label, color] of [['', '全部', '#212529'], ...CATEGORIES]) {
const b = document.createElement('button')
b.innerHTML = `<span style="color:${color}">●</span> ${label}`
b.style.cssText = 'padding:4px 10px;cursor:pointer;background:#fff;border:1px solid #ced4da;border-radius:14px'
b.onclick = () => select(value, label, b)
bar.appendChild(b)
}
// 演示:默认只看餐厅
await select('restaurant', '餐厅', bar.children[1])
} catch (e) {
console.error(friendly(e))
}点击要素弹窗
点击兴趣点或道路,直接读矢量瓦片里的要素属性,用弹窗显示,不再请求服务端。道路画成透明的粗线,便于点中;属性值写进弹窗前要做 HTML 转义。示例加载后先为视野内一个兴趣点打开弹窗。
需要: 一个或多个矢量瓦片服务。
js
在沙箱中打开/**
* @title 点击要素弹窗
* @description 点击兴趣点或道路,直接读矢量瓦片里的要素属性弹窗显示,不再请求服务端;加载后先为视野内一个兴趣点打开弹窗作演示。
* @category 样式与交互
* @service demo/pois
* @service demo/roads
* @map demo/map
*/
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径' : e && e.message)
try {
// 拷到自己页面时:换成你的矢量瓦片服务与底图(地图方案 id)
const pois = services['demo/pois']
const roads = services['demo/roads']
const map = new maplibregl.Map({
container,
style: client.maplibre.mapStyleUrl(maps['demo/map']),
transformRequest: client.transformRequest,
// 铜锣湾一带
center: [114.1835, 22.2805],
zoom: 16,
})
map.addControl(new maplibregl.NavigationControl())
await new Promise((resolve) => map.once('load', resolve))
// 两个可点击的图层:道路画成透明的粗线方便点中,兴趣点画成圆点
const layerOf = async (path) => (await client.services.tileJson(path)).vector_layers[0].id
map.addSource('road-tiles', { type: 'vector', tiles: [client.services.vectorTileUrl(roads)] })
map.addLayer({ id: 'roads-hit', type: 'line', source: 'road-tiles', 'source-layer': await layerOf(roads), paint: { 'line-width': 10, 'line-opacity': 0 } })
map.addSource('poi-tiles', { type: 'vector', tiles: [client.services.vectorTileUrl(pois)] })
map.addLayer({
id: 'poi-points',
type: 'circle',
source: 'poi-tiles',
'source-layer': await layerOf(pois),
paint: { 'circle-radius': 6, 'circle-color': '#1c7ed6', 'circle-stroke-color': '#fff', 'circle-stroke-width': 2 },
})
// 属性值来自数据,写进 HTML 前要转义
const esc = (v) => String(v).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c])
const popup = new maplibregl.Popup({ maxWidth: '280px' })
function show(f, lngLat) {
const kind = f.layer.id === 'poi-points' ? '兴趣点' : '道路'
const rows = Object.entries(f.properties).filter(([, v]) => v !== '' && v !== null).map(([k, v]) => `<tr><td style="color:#868e96;padding-right:8px">${esc(k)}</td><td>${esc(v)}</td></tr>`)
popup.setLngLat(lngLat).setHTML(`<b>${kind}</b><table style="font-size:12px">${rows.join('')}</table>`).addTo(map)
return `${kind} ${f.properties.name || f.properties.fclass}`
}
// 点击时取最上面的要素:兴趣点优先于道路
map.on('click', (e) => {
const f = map.queryRenderedFeatures(e.point, { layers: ['poi-points', 'roads-hit'] })[0]
if (f) console.log('点击了', show(f, e.lngLat))
})
for (const id of ['poi-points', 'roads-hit']) {
map.on('mouseenter', id, () => (map.getCanvas().style.cursor = 'pointer'))
map.on('mouseleave', id, () => (map.getCanvas().style.cursor = ''))
}
// 演示:地图静止后,给视野内第一个有名称的兴趣点打开弹窗
map.once('idle', () => {
const f = map.queryRenderedFeatures({ layers: ['poi-points'] }).find((x) => x.properties.name)
if (f) console.log('演示弹窗:', show(f, f.geometry.coordinates))
})
} catch (e) {
console.error(friendly(e))
}土地利用专题图
按 fclass 给土地利用面分类设色,图例里每一类附上用 client.query.count() 统计的地块数(各类并行请求,只返回数量)。点击图例项只看这一类,再点恢复全部。
需要: 一个同时开启了矢量瓦片与要素服务的面要素服务,且有分类字段。
js
在沙箱中打开/**
* @title 土地利用专题图
* @description 按 fclass 给土地利用面分类设色,图例里每一类附上要素服务统计的地块数;点击图例项只看这一类,再点恢复全部。
* @category 样式与交互
* @service demo/landuse
* @service demo/water
*/
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径' : e && e.message)
try {
// 拷到自己页面时:换成你的面要素服务(需开启矢量瓦片与要素服务);CLASSES 换成你数据里的分类取值
const path = services['demo/landuse']
const water = services['demo/water']
const CLASSES = [
['residential', '住宅', '#ffa8a8'],
['commercial', '商业', '#da77f2'],
['industrial', '工业', '#adb5bd'],
['park', '公园', '#8ce99a'],
['forest', '林地', '#2b8a3e'],
['grass', '草地', '#c0eb75'],
['farmland', '农田', '#ffe066'],
['cemetery', '墓地', '#868e96'],
]
const map = new maplibregl.Map({
container,
style: { version: 8, sources: {}, layers: [{ id: 'bg', type: 'background', paint: { 'background-color': '#f1f3f5' } }] },
transformRequest: client.transformRequest,
// 沙田一带:住宅、工业、公园与林地交错。地块密集,单张矢量瓦片的要素数有上限,缩得太小会缺地块
center: [114.195, 22.385],
zoom: 13.5,
})
map.addControl(new maplibregl.NavigationControl())
await new Promise((resolve) => map.once('load', resolve))
const layerOf = async (p) => (await client.services.tileJson(p)).vector_layers[0].id
map.addSource('landuse', { type: 'vector', tiles: [client.services.vectorTileUrl(path)] })
map.addLayer({
id: 'landuse',
type: 'fill',
source: 'landuse',
'source-layer': await layerOf(path),
// 不在 CLASSES 里的类别画成浅灰
paint: { 'fill-color': ['match', ['get', 'fclass'], ...CLASSES.flatMap(([v, , c]) => [v, c]), '#dee2e6'] },
})
map.addSource('water', { type: 'vector', tiles: [client.services.vectorTileUrl(water)] })
map.addLayer({ id: 'water', type: 'fill', source: 'water', 'source-layer': await layerOf(water), paint: { 'fill-color': '#74c0fc' } })
// 图例:每一类的地块数用 count 并行统计(只传数量,不传要素)
const counts = await Promise.all(CLASSES.map(([v]) => client.query.count(path, 0, { where: `fclass = '${v}'` })))
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.9 sans-serif;cursor:pointer;user-select:none'
let only = null
CLASSES.forEach(([value, name, color], i) => {
const row = document.createElement('div')
row.innerHTML = `<span style="display:inline-block;width:14px;height:14px;vertical-align:-2px;margin-right:6px;background:${color}"></span>${name} <span style="color:#868e96">${counts[i]}</span>`
row.onclick = () => {
only = only === value ? null : value
map.setFilter('landuse', only ? ['==', ['get', 'fclass'], only] : null)
for (const r of legend.children) r.style.opacity = !only || r === row ? 1 : 0.35
console.log(only ? `只看${name}` : '显示全部类别')
}
legend.appendChild(row)
})
container.appendChild(legend)
console.log('各类地块数:', CLASSES.map(([, n], i) => `${n} ${counts[i]}`).join(','))
} catch (e) {
console.error(friendly(e))
}栅格与矢量对比
左右两张地图联动:左边是 MapServer 栅格切片(服务端出图),右边是地图方案(浏览器渲染)。一边移动时把中心、缩放、旋转与倾斜同步到另一边,并用一个标志位防止两边互相触发。
需要: 一个提供 MapServer 切片的地图服务和一个地图方案。
js
在沙箱中打开/**
* @title 栅格与矢量对比
* @description 左右两张地图联动:左边是服务端出图的栅格切片,右边是浏览器渲染的矢量地图方案;拖动或缩放任意一边,另一边跟着走。
* @category 样式与交互
* @service demo/basemap
* @map demo/map
*/
// 出错时给出可理解的提示(服务不存在多半是 demo 服务映射或服务路径有误)
const friendly = (e) => (e && e.status === 404 ? '服务不存在,请检查服务路径' : e && e.message)
try {
// 拷到自己页面时:换成你的地图服务路径与地图方案 id;页面里放两个并排的容器
const raster = services['demo/basemap']
const mapId = maps['demo/map']
// 在 container 里并排放两个容器,各带一个标签
container.style.position = 'relative'
function pane(left, text) {
const el = document.createElement('div')
el.style.cssText = `position:absolute;top:0;bottom:0;left:${left};width:50%;${left === '0' ? 'border-right:2px solid #fff;box-sizing:border-box' : ''}`
const tag = document.createElement('div')
tag.textContent = text
tag.style.cssText = 'position:absolute;top:8px;left:8px;z-index:2;background:rgba(0,0,0,.65);color:#fff;padding:3px 8px;border-radius:4px;font:12px sans-serif'
el.appendChild(tag)
container.appendChild(el)
return el
}
const view = { center: [114.172, 22.298], zoom: 15 }
const left = new maplibregl.Map({
container: pane('0', '栅格切片(MapServer,服务端出图)'),
style: { version: 8, sources: {}, layers: [] },
transformRequest: client.transformRequest,
...view,
})
const right = new maplibregl.Map({
container: pane('50%', '矢量地图方案(浏览器渲染)'),
style: client.maplibre.mapStyleUrl(mapId),
transformRequest: client.transformRequest,
...view,
})
right.addControl(new maplibregl.NavigationControl())
await new Promise((resolve) => left.once('load', resolve))
client.maplibre.addRasterService(left, raster)
// 联动:一边移动时把中心、缩放、旋转、倾斜同步到另一边;syncing 防止互相触发形成循环
let syncing = false
function follow(from, to) {
from.on('move', () => {
if (syncing) return
syncing = true
to.jumpTo({ center: from.getCenter(), zoom: from.getZoom(), bearing: from.getBearing(), pitch: from.getPitch() })
syncing = false
})
}
follow(left, right)
follow(right, left)
// 演示:右图平移到旺角并放大一级,左图跟随
right.once('idle', () => {
right.jumpTo({ center: [114.17, 22.318], zoom: 16 })
console.log('联动后左图中心:', left.getCenter().lng.toFixed(4), left.getCenter().lat.toFixed(4), '缩放', left.getZoom().toFixed(1))
})
} catch (e) {
console.error(friendly(e))
}