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