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
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;
|
|
|