唯杰地图 logo唯杰地图
VJMAP
EN

layer-events

vjmap/references/event/layer-events.md 6.1 KB182 行
原文

本文回答:图层级事件 —— map.on('click', layerId, handler) 只在命中特定图层要素时触发。

一、图层级 vs 全局级

map.on('click', handler);                // 全局:任意位置
map.on('click', 'my-layer', handler);    // 图层:只命中 my-layer 里的要素

图层级传第二参,要素信息在 e.features:

map.on('click', 'roads', e => {
  const feat = e.features[0];
  console.log(feat.properties.road_name);
});

二、支持的事件

事件 说明
click 命中要素时
dblclick 双击要素
mouseenter 进入要素
mouseleave 离开要素
mousemove 在要素上移动
mousedown / mouseup
contextmenu 右键
touchstart / touchend 移动端

三、常见用途

1 指针样式

map.on('mouseenter', 'buildings', () => map.getCanvas().style.cursor = 'pointer');
map.on('mouseleave', 'buildings', () => map.getCanvas().style.cursor = '');

2 点击显示 Popup

map.on('click', 'pois', e => {
  const f = e.features[0];
  new vjmap.Popup()
    .setLngLat(f.geometry.coordinates)
    .setHTML(`<b>${f.properties.name}</b>`)
    .addTo(map);
});

3 Hover 高亮(feature-state)

let hovered = null;
map.on('mousemove', 'roads', e => {
  if (hovered !== null) map.setFeatureState({ source: 'roads-src', id: hovered }, { hover: false });
  hovered = e.features[0].id;
  map.setFeatureState({ source: 'roads-src', id: hovered }, { hover: true });
});
map.on('mouseleave', 'roads', () => {
  // 用 !== null 而非 if (hovered):generateId 从 0 编号,0 为假值,第一个要素的 hover 会清不掉
  if (hovered !== null) map.setFeatureState({ source: 'roads-src', id: hovered }, { hover: false });
  hovered = null;
});

// 样式里
map.addLineLayer('roads', 'roads-src', {
  lineColor: ['case', ['boolean', ['feature-state', 'hover'], false], '#f00', '#888'],
  lineWidth: 3
});

四、e.features 数据

map.on('click', 'my-layer', e => {
  for (const f of e.features) {
    f.id                // 数字 / 字符串(需 promoteId 或 generateId)
    f.properties        // 对象
    f.geometry          // GeoJSON 几何
    f.layer             // 图层样式定义
    f.source            // 来源 id
    f.sourceLayer       // 矢量瓦片子图层
  }
});

五、多图层监听

['layer1', 'layer2', 'layer3'].forEach(id => {
  map.on('click', id, e => handle(e, id));
});

或用 queryRenderedFeatures 一次过滤:

map.on('click', e => {
  const feats = map.queryRenderedFeatures(e.point, {
    layers: ['layer1', 'layer2', 'layer3']
  });
  if (feats.length === 0) return;
  handle(feats[0]);
});

或直接用 map 上内置的 map-gl-utils 助手(layers 可传 id / 数组 / 正则,clickOneLayer 只收数组;都返回解绑函数):

const offPointer = map.hoverPointer(['layer1', 'layer2']);   // 悬停变手型
const offHover = map.hoverFeatureState('roads');             // 自动维护 feature-state hover(id 为 0 的要素不生效)
const offClick = map.clickOneLayer(['layer1', 'layer2', 'layer3'],
  ({ layer, feature }) => handle(feature, layer),            // 只派发给第一个命中的图层
  e => console.log('点到空白处', e.lngLat));                  // 全部未命中
// 另有 map.clickLayer / hoverLayer / hoverPopup / clickPopup
offClick();   // 解绑

六、事件触发顺序

同时绑定全局级和图层级时,触发顺序 = 注册顺序(图层级本质是包了一层 queryRenderedFeatures 过滤的普通监听器,源码 _createDelegatedListener 证实):

map.on('click', e => ...);               // 先注册先跑——即使图层级后注册也排后面
map.on('click', 'my-layer', e => ...);

⚠️ e.preventDefault() 不会阻止后续监听器执行(Evented.fire 不检查 defaultPrevented,只标记事件)——要阻止全局 handler,在全局 handler 里自查命中:

map.on('click', e => {
  if (map.queryRenderedFeatures(e.point, { layers: ['my-layer'] }).length) return;
  // 真正的"空白处"逻辑
});

七、结合 popup 关闭

let popup = null;
map.on('click', 'pois', e => {
  if (popup) popup.remove();
  popup = new vjmap.Popup().setLngLat(e.lngLat).setHTML(html(e.features[0])).addTo(map);
});
map.on('click', e => {
  // 全局 click:检查是否命中图层,没命中就关 popup
  const hit = map.queryRenderedFeatures(e.point, { layers: ['pois'] });
  if (hit.length === 0 && popup) { popup.remove(); popup = null; }
});

八、off 取消

const handler = e => { ... };
map.on('click', 'my-layer', handler);

// 取消
map.off('click', 'my-layer', handler);

必须传相同的 layerId 和 handler 引用。

九、常见坑

  1. 可先绑后加图层:委托监听每次事件时才 getLayer(layerId),图层不存在只是不触发;先绑后 addLayer、或 removeLayer 后重加都照常生效。
  2. 匿名 handler 无法 off:保留引用。
  3. e.features 空:可能 source 刚 setData 还没完;在 sourcedata 后。
  4. 重叠要素:handler 每次点击只调一次,e.features 含该图层在该点下的全部要素(自上而下),只要最上层取 [0];不同图层各自的图层级监听都会触发。
  5. 点击空白也 fire 全局 click:e.features undefined,不会 fire 图层级;用 queryRenderedFeatures 判命中。
  6. e.features 只在 handler 同步执行期间有效:_createDelegatedListener 在 handler 返回后立即 delete e.features——把事件对象存起来异步读、或 resolve(e) 后再访问,features 都是 undefined。要保留数据就在 handler 内同步取值/拷贝:const tag = e.features[0].properties.tag。
  7. 刚 addSource+addLayer 立刻 queryRenderedFeatures 会返回空:GeoJSON source 的瓦片由 worker 异步生成,需轮询或等 sourcedata 事件后再查(通常约 0.4–2s 才命中)。

相关