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.
 
 
 
 

554 lines
20 KiB

/**
* 任意多边形在地图上显示
*/
import { action, computed, makeObservable, observable, reaction } from 'mobx';
import mapbox from 'mapbox-gl';
import * as turf from '@turf/turf';
import EventDispatcher from './EventDispatcher';
class ShapeRenderer extends EventDispatcher {
_topic = null;
// 地图实例
_map = null;
_dataSource = [];
// 是否完成初始渲染
isRendered = false;
// 鼠标经过的形状id
_hoveredShapeId = null;
// 显示配置项
_defaultOptions = {
// 显示填充
showFill: true,
// 显示描边
showStroke: true,
// 显示标签
showLabel: true,
// 显示标签(仅鼠标经过时)
showLabelOnOver: false,
};
_options = {};
// 视觉选项
_defaultStyle = {
fillColor: 'rgba(255, 255, 0, 0.1)',
fillHoverColor: 'rgba(255, 255, 0, 0.3)',
strokeColor: 'rgba(255, 255, 0, 0.5)',
strokeHoverColor: 'rgba(255, 255, 0, 1)',
labelColor: 'rgba(0, 0, 0, 1)',
labelStrokeColor: 'rgba(255, 255, 255, 1)',
labelMinZoom: 3,
};
_style = {};
get _sourceId() {
return {
FILL: `${this._topic}-shape-fill-source`,
STROKE: `${this._topic}-shape-stroke-source`,
LABEL: `${this._topic}-shape-label-source`,
};
}
get _layerId() {
return {
FILL: `${this._topic}-shape-fill-layer`,
STROKE: `${this._topic}-shape-stroke-layer`,
LABEL: `${this._topic}-shape-label-layer`,
};
}
// 形状填充色表达式
get _shapeFillColorExpression() {
return [
'case',
[
'any',
['boolean', ['feature-state', 'hover'], false],
['boolean', ['feature-state', 'highlight'], false],
],
this._options.showFill ? this._style.fillHoverColor : 'rgba(0, 0, 0, 0)',
this._options.showFill ? this._style.fillColor : 'rgba(0, 0, 0, 0)',
];
}
// 形状描边色表达式
get _shapeStrokeColorExpression() {
return [
'case',
[
'any',
['boolean', ['feature-state', 'hover'], false],
['boolean', ['feature-state', 'highlight'], false],
],
this._options.showStroke ? this._style.strokeHoverColor : 'rgba(0, 0, 0, 0)',
this._options.showStroke ? this._style.strokeColor : 'rgba(0, 0, 0, 0)',
];
}
// 形状标签透明度表达式
get _shapeLabelOpacityExpression() {
if (this._options.showLabelOnOver) {
return [
'case',
[
'any',
['boolean', ['feature-state', 'hover'], false],
['boolean', ['feature-state', 'highlight'], false],
],
1,
0,
];
}
return [
'interpolate', ['linear'],
['zoom'],
this._style.labelMinZoom - 0.01, 0,
this._style.labelMinZoom, 1,
];
}
// 形状填充
get _shapeFillFeatures() {
return this._dataSource.map(({ id, points, ...others }) => {
const newPoints = Array.isArray(points[0]) ? points : points.map(({ lng, lat }) => [lng, lat]);
const lineFeature = turf.lineString(newPoints, { id, ...others });
const polygonFeature = turf.lineToPolygon(lineFeature);
polygonFeature.id = id;
return polygonFeature;
});
}
// 形状填充集合
get _shapeFillFeatureCollection() {
return turf.featureCollection(this._shapeFillFeatures);
}
// 形状描边
get _shapeStrokeFeatures() {
return this._shapeFillFeatures.map(polygonFeature => {
const lineFeature = turf.polygonToLine(polygonFeature);
const { id } = lineFeature.properties;
lineFeature.id = id;
return lineFeature;
});
}
// 形状描边集合
get _shapeStrokeFeatureCollection() {
return turf.featureCollection(this._shapeStrokeFeatures);
}
// 形状中心点
get _shapeCenterFeatures() {
return this._shapeFillFeatures.map(polygonFeature => {
const feature = turf.centerOfMass(polygonFeature);
feature.id = polygonFeature.id;
feature.properties = polygonFeature.properties;
return feature;
});
}
// 形状中心点集合
get _shapeCenterFeatureCollection() {
return turf.featureCollection(this._shapeCenterFeatures);
}
// 形状集合包围盒
get _shapeBoundingBox() {
return turf.bbox(this._shapeFillFeatureCollection);
}
constructor(topic, mapInstance = null) {
super(['rendered', 'mouse_move', 'mouse_leave']);
this._topic = topic;
if (mapInstance) this.setMap(mapInstance);
this.updateOptions();
this.updateStyle();
makeObservable(this, {
_topic: observable,
_dataSource: observable,
_options: observable,
_style: observable,
_sourceId: computed,
_layerId: computed,
_shapeFillColorExpression: computed,
_shapeStrokeColorExpression: computed,
_shapeLabelOpacityExpression: computed,
_shapeFillFeatures: computed,
_shapeFillFeatureCollection: computed,
_shapeStrokeFeatures: computed,
_shapeStrokeFeatureCollection: computed,
_shapeCenterFeatures: computed,
_shapeCenterFeatureCollection: computed,
_shapeBoundingBox: computed,
loadDataSource: action,
updateOptions: action,
updateStyle: action,
destroy: action,
});
reaction(() => this._dataSource, () => {
this._render();
});
}
// 更新配置项
updateOptions(options = {}) {
if (Object.keys(options).length) {
this._options = {
...this._options,
...options,
};
this._refreshVisibility();
} else {
this._options = {
...this._defaultOptions,
};
}
}
// 更新视觉样式
updateStyle(style = {}) {
if (Object.keys(style).length) {
this._style = {
...this._style,
...style,
};
this._repaintStyle();
} else {
this._style = {
...this._defaultStyle,
};
}
}
setMap(mapInstance) {
if (this._map === mapInstance) return;
if (!(mapInstance instanceof mapbox.Map)) {
throw new Error('必须传入一个mapbox地图实例');
}
this._map = mapInstance;
}
// 载入数据(list->item必须包含id、name、points数组,points->item如果是对象则必须包含lng、lat属性,如果是数组则必须是[lng, lat])
loadDataSource(list) {
if (!this._map) {
throw new Error('请先设置地图实例');
}
this._dataSource = (list || []).map(item => {
const { id, points } = item;
return (id >= 0 && Array.isArray(points) && points.length >= 3) ? item : null;
}).filter(Boolean);
}
// 是否包含某个多边形
hasShape(shapeId) {
return this._dataSource.findIndex(({ id }) => shapeId === id) >= 0;
}
// 获取某个形状数据
getShape(shapeId) {
return this._dataSource.find(({ id }) => shapeId === id);
}
// 获取所有形状的中心点
getAllCenters() {
return this._shapeCenterFeatures.map(feature => turf.getCoord(feature));
}
_render() {
if (!this._map) return;
if (this._options.showFill) this._renderShapeFill();
if (this._options.showStroke) this._renderShapeStroke();
if (this._options.showLabel) this._renderShapeLabel();
this.isRendered = true;
this._trigger('rendered');
}
// 渲染形状填充
_renderShapeFill() {
const source = this._map.getSource(this._sourceId.FILL);
if (!source) {
this._map.addSource(this._sourceId.FILL, {
type: 'geojson',
data: this._shapeFillFeatureCollection,
});
this._map.addLayer({
id: this._layerId.FILL,
type: 'fill',
source: this._sourceId.FILL,
paint: {
'fill-color': this._shapeFillColorExpression,
'fill-opacity': [
'case',
['boolean', ['feature-state', 'visible'], true],
1,
0,
],
},
});
this._map.on('click', this._layerId.FILL, this._onClick);
this._map.on('mousemove', this._layerId.FILL, this._onMouseMove);
this._map.on('mouseleave', this._layerId.FILL, this._onMouseLeave);
} else {
source.setData(this._shapeFillFeatureCollection);
}
}
// 渲染形状描边
_renderShapeStroke() {
const source = this._map.getSource(this._sourceId.STROKE);
if (!source) {
this._map.addSource(this._sourceId.STROKE, {
type: 'geojson',
data: this._shapeStrokeFeatureCollection,
});
this._map.addLayer({
id: this._layerId.STROKE,
type: 'line',
source: this._sourceId.STROKE,
paint: {
'line-color': this._shapeStrokeColorExpression,
'line-width': 2,
'line-opacity': [
'case',
['boolean', ['feature-state', 'visible'], true],
1,
0,
],
},
layout: {
'line-cap': 'round',
},
});
} else {
source.setData(this._shapeStrokeFeatureCollection);
}
}
_renderShapeLabel() {
const source = this._map.getSource(this._sourceId.LABEL);
if (!source) {
this._map.addSource(this._sourceId.LABEL, {
type: 'geojson',
data: this._shapeCenterFeatureCollection,
});
this._map.addLayer({
id: this._layerId.LABEL,
type: 'symbol',
source: this._sourceId.LABEL,
layout: {
'text-field': '{name}',
'text-size': 12,
'text-allow-overlap': true,
},
paint: {
'text-color': this._style.labelColor,
'text-halo-color': this._style.labelStrokeColor,
'text-halo-width': 1,
'text-opacity': this._shapeLabelOpacityExpression,
},
});
} else {
source.setData(this._shapeCenterFeatureCollection);
}
}
_onClick = e => {
if (!e.features.length) return;
const { lngLat, point } = e;
const [{ properties: detail }] = e.features;
this._trigger('click', { lngLat, point, detail });
};
_onMouseMove = e => {
this._map.getCanvas().style.cursor = 'pointer';
if (e.features.length > 0) {
if (this._hoveredShapeId !== null) {
const id = this._hoveredShapeId;
if (this._map.getSource(this._sourceId.FILL)) this._map.setFeatureState({ source: this._sourceId.FILL, id }, { hover: false });
if (this._map.getSource(this._sourceId.STROKE)) this._map.setFeatureState({ source: this._sourceId.STROKE, id }, { hover: false });
if (this._map.getSource(this._sourceId.LABEL)) this._map.setFeatureState({ source: this._sourceId.LABEL, id }, { hover: false });
}
const [{ id }] = e.features;
if (this._map.getSource(this._sourceId.FILL)) this._map.setFeatureState({ source: this._sourceId.FILL, id }, { hover: true });
if (this._map.getSource(this._sourceId.STROKE)) this._map.setFeatureState({ source: this._sourceId.STROKE, id }, { hover: true });
if (this._map.getSource(this._sourceId.LABEL)) this._map.setFeatureState({ source: this._sourceId.LABEL, id }, { hover: true });
this._hoveredShapeId = id;
}
const { lngLat, point } = e;
const [{ properties: detail }] = e.features;
this._trigger('mouse_move', { lngLat, point, detail });
};
_onMouseLeave = e => {
this._map.getCanvas().style.cursor = '';
if (this._hoveredShapeId !== null) {
if (this._map.getSource(this._sourceId.FILL)) this._map.setFeatureState({ source: this._sourceId.FILL, id: this._hoveredShapeId }, { hover: false });
if (this._map.getSource(this._sourceId.STROKE)) this._map.setFeatureState({ source: this._sourceId.STROKE, id: this._hoveredShapeId }, { hover: false });
if (this._map.getSource(this._sourceId.LABEL)) this._map.setFeatureState({ source: this._sourceId.LABEL, id: this._hoveredShapeId }, { hover: false });
}
const { lngLat, point } = e;
let detail = {};
if (this._hoveredShapeId !== null) {
const { id, points, ...others } = this._dataSource.find(item => item.id === this._hoveredShapeId) || {};
detail = { id, ...others };
}
this._trigger('mouse_leave', { lngLat, point, detail });
this._hoveredShapeId = null;
};
// 隐藏某个形状
hideShape(shapeId) {
if (!this._map || !this.hasShape(shapeId)) return;
if (this._map.getSource(this._sourceId.FILL)) this._map.setFeatureState({ source: this._sourceId.FILL, id: shapeId }, { visible: false });
if (this._map.getSource(this._sourceId.STROKE)) this._map.setFeatureState({ source: this._sourceId.STROKE, id: shapeId }, { visible: false });
}
// 显示某个形状
showShape(shapeId) {
if (!this._map || !this.hasShape(shapeId)) return;
if (this._map.getSource(this._sourceId.FILL)) this._map.setFeatureState({ source: this._sourceId.FILL, id: shapeId }, { visible: true });
if (this._map.getSource(this._sourceId.STROKE)) this._map.setFeatureState({ source: this._sourceId.STROKE, id: shapeId }, { visible: true });
}
// 高亮某个形状(或取消高亮)
highlightShape(shapeId, stateValue = true) {
if (!this._map || !this.hasShape(shapeId)) return;
if (this._map.getSource(this._sourceId.FILL)) this._map.setFeatureState({ source: this._sourceId.FILL, id: shapeId }, { highlight: stateValue });
if (this._map.getSource(this._sourceId.STROKE)) this._map.setFeatureState({ source: this._sourceId.STROKE, id: shapeId }, { highlight: stateValue });
}
// 高亮一组形状(或取消高亮)
highlightShapes(keyName, keyValue, stateValue = true) {
if (!this._map) return;
turf.featureEach(this._shapeFillFeatureCollection, currentFeature => {
const { id } = currentFeature;
if (currentFeature.properties[keyName] === keyValue) this.highlightShape(id, stateValue);
});
}
// 刷新可见性
_refreshVisibility() {
if (!this._map) return;
if (this._map.getLayer(this._layerId.FILL)) {
this._renderShapeFill();
this._map.setLayoutProperty(this._layerId.FILL, 'visibility', this._options.showFill ? 'visible' : 'none');
}
if (this._map.getLayer(this._layerId.STROKE)) {
this._renderShapeStroke();
this._map.setLayoutProperty(this._layerId.STROKE, 'visibility', this._options.showStroke ? 'visible' : 'none');
}
if (this._map.getLayer(this._layerId.LABEL)) {
this._renderShapeLabel();
this._map.setLayoutProperty(this._layerId.LABEL, 'visibility', this._options.showLabel ? 'visible' : 'none');
}
}
// 重绘样式
_repaintStyle() {
if (!this._map) return;
if (this._map.getLayer(this._layerId.FILL)) {
this._map.setPaintProperty(this._layerId.FILL, 'fill-color', this._shapeFillColorExpression);
}
if (this._map.getLayer(this._layerId.STROKE)) {
this._map.setPaintProperty(this._layerId.STROKE, 'line-color', this._shapeStrokeColorExpression);
}
if (this._map.getLayer(this._layerId.LABEL)) {
this._map.setPaintProperty(this._layerId.LABEL, 'text-color', this._style.labelColor);
this._map.setPaintProperty(this._layerId.LABEL, 'text-halo-color', this._style.labelStrokeColor);
this._map.setPaintProperty(this._layerId.LABEL, 'text-opacity', this._shapeLabelOpacityExpression);
}
}
// 缩放到某个形状的包围盒
fitShape(shapeId, { top = 0, bottom = 0, left = 0, right = 0 } = {}, cut = 120) {
if (!this._map || !this.hasShape(shapeId)) return;
const { points } = this._dataSource.find(({ id }) => id === shapeId) || {};
if (!points) return;
const newPoints = Array.isArray(points[0]) ? points : points.map(({ lng, lat }) => [lng, lat]);
const pointFeature = turf.multiPoint(newPoints);
this._map.setPadding({ top: 0, bottom: 0, left: 0, right: 0 });
this._map.fitBounds(turf.bbox(pointFeature), {
duration: 2000,
padding: {
top: top + cut,
bottom: bottom + cut,
left: left + cut,
right: right + cut,
},
});
}
// 缩放到所有形状的包围盒
fitView({ top = 0, bottom = 0, left = 0, right = 0 } = {}, cut = 120) {
if (!this._dataSource.length) return;
this._map.setPadding({ top: 0, bottom: 0, left: 0, right: 0 });
this._map.fitBounds(this._shapeBoundingBox, {
duration: 2000,
padding: {
top: top + cut,
bottom: bottom + cut,
left: left + cut,
right: right + cut,
},
});
}
// 对其到所有形状的中心
alignCenter(centerOffset = 0) {
if (!this._dataSource.length) return;
const feature = turf.bboxPolygon(this._shapeBoundingBox);
const center = turf.center(feature);
this._map.panTo(turf.getCoord(center), {
offset: [centerOffset, 0],
});
}
clear() {
if (!this._map) return;
this.isRendered = false;
if (this._map.getSource(this._sourceId.FILL)) {
this._map.off('click', this._layerId.FILL, this._onClick);
this._map.off('mousemove', this._layerId.FILL, this._onMouseMove);
this._map.off('mouseleave', this._layerId.FILL, this._onMouseLeave);
this._map.removeLayer(this._layerId.FILL);
this._map.removeSource(this._sourceId.FILL);
}
if (this._map.getSource(this._sourceId.STROKE)) {
this._map.removeLayer(this._layerId.STROKE);
this._map.removeSource(this._sourceId.STROKE);
}
if (this._map.getSource(this._sourceId.LABEL)) {
this._map.removeLayer(this._layerId.LABEL);
this._map.removeSource(this._sourceId.LABEL);
}
}
destroy() {
this.clear();
this._map = null;
this._dataSource = [];
this.updateStyle();
this.updateOptions();
}
}
export default ShapeRenderer;