You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

234 lines
7.4 KiB

/**
* 带图标的点
*/
import { action, computed, makeObservable, observable, reaction } from 'mobx';
import mapbox from 'mapbox-gl';
import * as turf from '@turf/turf';
import EventDispatcher from '../EventDispatcher';
import Icon from './Icon';
class IconPointRenderer extends EventDispatcher {
_topic = null;
_map = null;
_icons = {};
_dataSource = [];
get _sourceId() {
return `${this._topic}-icon-point-source`;
}
get _layerId() {
return `${this._topic}-icon-point-layer`;
}
get _pointFeatures() {
return this._dataSource.map(({ id, iconName, point, ...others }) => {
const { [iconName]: icon } = this._icons;
const { zoom: iconZoom = 1, offset: iconOffset = [0, 0] } = icon || {};
const feature = turf.point(point, { id, iconName, iconZoom, iconOffset, ...others });
feature.id = id;
return feature;
});
}
get _pointFeatureCollection() {
return turf.featureCollection(this._pointFeatures);
}
constructor(topic, icons = []) {
super(['mousemove', 'mouseleave', 'mouseenter']);
this._topic = topic;
if (!icons.length) {
throw new Error('入参icons至少包含一个图标元素');
}
icons.forEach(this.updateIcon);
makeObservable(this, {
_topic: observable,
_icons: observable,
_dataSource: observable,
_sourceId: computed,
_layerId: computed,
_pointFeatures: computed,
_pointFeatureCollection: computed,
loadDataSource: action,
updateIcon: action,
destroy: action,
});
reaction(() => this._dataSource, () => {
this._render();
});
reaction(() => this._icons, () => {
this._render();
});
}
setMap(mapInstance) {
if (this._map === mapInstance) return;
if (!(mapInstance instanceof mapbox.Map)) {
throw new Error('必须传入一个mapbox地图实例');
}
this._map = mapInstance;
}
// 载入数据
// list->item必须包含id、point、iconName(其中point如果是对象则必须包含lng、lat属性,如果是数组则必须是[lng, lat])
// list->item可以包含rotate,表示旋转角度
loadDataSource(list) {
if (!this._map) {
throw new Error('请先设置地图实例');
}
this._dataSource = (list || []).map(item => {
const { id, iconName, point } = item;
const newItem = { ...item };
if (!Array.isArray(point)) {
const { lng, lat } = point;
newItem.point = [lng, lat];
}
return (id >= 0 && iconName && Array.isArray(newItem.point)) ? newItem : null;
}).filter(Boolean);
if (!this._dataSource.length) {
console.warn('数据列表为空');
}
}
updateIcon = icon => {
if (!(icon instanceof Icon)) {
throw new Error(`入参icon必须是${Icon}的实例`);
}
this._icons = {
...this._icons,
[icon.name]: icon,
};
};
_loadIcons() {
if (!this._map) return;
Object.values(this._icons).forEach(icon => {
if (this._map.hasImage(icon.name)) {
this._map.updateImage(icon.name, icon.data);
} else {
this._map.addImage(icon.name, icon.data);
}
});
}
_render() {
if (!this._map) return;
this._loadIcons();
this._renderPoints();
}
_renderPoints() {
const source = this._map.getSource(this._sourceId);
if (!source) {
this._map.addSource(this._sourceId, {
type: 'geojson',
data: this._pointFeatureCollection,
});
this._map.addLayer({
id: this._layerId,
type: 'symbol',
source: this._sourceId,
paint: {
'icon-opacity': [
'case',
['boolean', ['feature-state', 'visible'], true],
1,
0,
],
// 'icon-translate': ['get', 'iconOffset'], // 缩放之后平移的像素值(mark:icon-translate不支持表达式)
},
layout: {
'icon-image': ['get', 'iconName'],
'icon-size': ['get', 'iconZoom'],
'icon-offset': ['get', 'iconOffset'], // 乘以缩放值后,才是最终偏移量
'icon-rotate': ['coalesce', ['get', 'rotate'], ['number', 0]],
'icon-allow-overlap': true,
},
});
this._map.on('click', this._layerId, this._onClick);
this._map.on('mouseenter', this._layerId, this._onMouseEnter);
this._map.on('mousemove', this._layerId, this._onMouseMove);
this._map.on('mouseleave', this._layerId, this._onMouseLeave);
} else {
source.setData(this._pointFeatureCollection);
}
}
_onClick = e => {
if (!e.features.length) return;
const { lngLat, point } = e;
const [{ properties: detail }] = e.features;
this._trigger('click', { lngLat, point, detail });
};
_onMouseMove = () => {
this._map.getCanvas().style.cursor = 'pointer';
};
_onMouseEnter = e => {
if (!e.features.length) return;
const { lngLat, point } = e;
const [{ properties: detail }] = e.features;
this._trigger('mouseenter', { lngLat, point, detail });
};
_onMouseLeave = e => {
this._map.getCanvas().style.cursor = '';
const { lngLat, point } = e;
this._trigger('mouseleave', { lngLat, point });
};
// 是否包含某个点
hasPoint(pointId) {
return this._dataSource.findIndex(({ id }) => pointId === id) >= 0;
}
// 隐藏某个点
hidePoint(pointId) {
if (!this._map || !this.hasPoint(pointId)) return;
if (this._map.getSource(this._sourceId)) this._map.setFeatureState({ source: this._sourceId, id: pointId }, { visible: false });
}
// 显示某个点
showPoint(pointId) {
if (!this._map || !this.hasPoint(pointId)) return;
if (this._map.getSource(this._sourceId)) this._map.setFeatureState({ source: this._sourceId, id: pointId }, { visible: true });
}
changeVisibility(stateValue) {
if (!this._map) return;
if (this._map.getLayer(this._layerId)) {
this._map.setLayoutProperty(this._layerId, 'visibility', stateValue ? 'visible' : 'none');
}
}
clear() {
if (!this._map) return;
if (this._map.getSource(this._sourceId)) {
this._map.off('click', this._layerId, this._onClick);
this._map.off('mousemove', this._layerId, this._onMouseMove);
this._map.off('mouseleave', this._layerId, this._onMouseLeave);
this._map.off('mouseenter', this._layerId, this._onMouseEnter);
this._map.removeLayer(this._layerId);
this._map.removeSource(this._sourceId);
}
}
destroy() {
this.icons = {};
this._dataSource = [];
this.clear();
this._map = null;
}
}
export default IconPointRenderer;