feat(map地图): 初始化地图配置,引入相关js文件作为参考

This commit is contained in:
NICE CODE BY DEV 2024-05-21 10:42:01 +08:00
parent 1f6c7ccb18
commit e3e3c05ae0
16 changed files with 1141 additions and 34 deletions

View File

@ -1,40 +1,43 @@
import 'mapbox-gl/dist/mapbox-gl.css';
import Map from 'react-map-gl';
import './index.less';
import React from 'react';
import React, { forwardRef, useImperativeHandle, useRef } from 'react';
import { MapProps } from './interface';
import { merge } from './utils';
import { MAP_CENTER, defaultMapConfig } from './constants';
const MapBox: React.FC<MapProps> = (props) => {
export interface MapRefProps {
}
const MapBox = forwardRef<MapRefProps, MapProps>((props, ref) => {
const {
style = {},
children,
mapRef,
onLoad,
mapCenter = MAP_CENTER,
mapConfig = {},
height = 600,
width = '100%',
...others
} = props || {};
const mapRef = useRef(null)
useImperativeHandle(ref, () => ({}))
return (
//@ts-ignore
<Map
ref={(e) => {
if (mapRef) {
mapRef.current = e!;
}
}}
ref={mapRef}
initialViewState={{ ...mapCenter, zoom: 10 }}
onLoad={(e) => {
onLoad && onLoad(e);
}}
style={{ width: '100%', height: 600, ...style }}
{...merge(defaultMapConfig, mapConfig)}
initialViewState={{ ...mapCenter, zoom: 10 }}
{...others}
style={{ width: width, height: height, ...style }}
{...merge(defaultMapConfig, others)}
>
{children}
</Map>
);
};
});
export default MapBox;

View File

@ -1,9 +1,11 @@
export const mapboxAccessToken =
'pk.eyJ1IjoiZGluZ2xpMTIzIiwiYSI6ImNra204ODhjczBobTgyeHJ6MmJpZHMxNWgifQ.NbKrXh_hb2gvjr5CEMDnyQ';
export const MAP_CENTER = {
export const MAP_CENTER = {
longitude: 120.2667694313269,
latitude: 30.180942826533766,
}; //地图中心
const MapUrl = 'http://10.0.0.120:30003/map';
export const defaultMapConfig = {
mapboxAccessToken,
@ -39,4 +41,4 @@ export const defaultMapConfig = {
},
],
},
};
};

View File

@ -13,12 +13,14 @@ const demo = () => {
zoom: map?.getMaxZoom(),
});
};
return (
<div>
<MapBox onLoad={handleMapLoad}
mapRef={mapRef}
style={{ width: '100%', height: 300 }}/>
</div>
<MapBox
onLoad={handleMapLoad}
ref={mapRef}
width='100%'
height='100vh'
/>
);
};

View File

@ -8,6 +8,7 @@ title: 快速上手
<embed src="../README.md" ></embed>
<code src="./demo/basic.tsx">基本用法</code>
## API
| 参数 | 说明 | 类型 | 默认值 | 版本 |
@ -18,3 +19,9 @@ title: 快速上手
| children | 内部元素 | JSX.Element或JSX.Element[]或Array<JSX.Element或undefined> | {} | - |
| mapConfig | 地图配置 | MapConfigProps | defaultMapConfig | - |
| onLoad | 地图加载事件 | function | ()=>{} | - |
## 参考文档
[官方文档](https://docs.mapbox.com/mapbox-gl-js/example)
[react-map-gl](https://visgl.github.io/react-map-gl/examples)
[mapbox-gl-draw](https://github.com/mapbox/mapbox-gl-draw)

View File

@ -1,19 +1,18 @@
import { CSSProperties } from "react";
import { MapRef, MapStyle } from "react-map-gl";
import { MapboxMap, MapRef, MapStyle } from "react-map-gl";
export interface MapProps {
onLoad?: (e: mapboxgl.MapboxEvent<undefined>) => void;
mapRef?: React.MutableRefObject<MapRef | undefined>;
style?: CSSProperties;
children?: JSX.Element | JSX.Element[] | Array<JSX.Element | undefined>;
mapConfig?: MapConfigProps
mapCenter: {longitude: number, latitude: number}
}
export interface MapConfigProps {
export interface MapProps extends Partial<MapboxMap> {
mapboxAccessToken?: string; //token
minZoom?: number; //最小层级
maxZoom?: number; //最大层级
dragRotate?: boolean; //是否支持拖拽旋转
mapStyle?: MapStyle; //地图样式
}
height?: number | string;
width?: string | number;
onLoad?: (e: mapboxgl.MapboxEvent<undefined>) => void;
mapRef?: MapRef;
style?: CSSProperties;
children?: JSX.Element | JSX.Element[] | Array<JSX.Element | undefined>;
mapCenter?: {
longitude: number, latitude: number
}
}

View File

@ -0,0 +1,100 @@
export const classes = {
CONTROL_BASE: 'mapboxgl-ctrl',
CONTROL_PREFIX: 'mapboxgl-ctrl-',
CONTROL_BUTTON: 'mapbox-gl-draw_ctrl-draw-btn',
CONTROL_BUTTON_LINE: 'mapbox-gl-draw_line',
CONTROL_BUTTON_POLYGON: 'mapbox-gl-draw_polygon',
CONTROL_BUTTON_POINT: 'mapbox-gl-draw_point',
CONTROL_BUTTON_TRASH: 'mapbox-gl-draw_trash',
CONTROL_BUTTON_COMBINE_FEATURES: 'mapbox-gl-draw_combine',
CONTROL_BUTTON_UNCOMBINE_FEATURES: 'mapbox-gl-draw_uncombine',
CONTROL_GROUP: 'mapboxgl-ctrl-group',
ATTRIBUTION: 'mapboxgl-ctrl-attrib',
ACTIVE_BUTTON: 'active',
BOX_SELECT: 'mapbox-gl-draw_boxselect',
};
export const sources = {
HOT: 'mapbox-gl-draw-hot',
COLD: 'mapbox-gl-draw-cold',
};
export const cursors = {
ADD: 'add',
MOVE: 'move',
DRAG: 'drag',
POINTER: 'pointer',
NONE: 'none',
};
export const types = {
POLYGON: 'polygon',
LINE: 'line_string',
POINT: 'point',
};
export const geojsonTypes = {
FEATURE: 'Feature',
POLYGON: 'Polygon',
LINE_STRING: 'LineString',
POINT: 'Point',
FEATURE_COLLECTION: 'FeatureCollection',
MULTI_PREFIX: 'Multi',
MULTI_POINT: 'MultiPoint',
MULTI_LINE_STRING: 'MultiLineString',
MULTI_POLYGON: 'MultiPolygon',
};
export const modes = {
DRAW_LINE_STRING: 'draw_line_string',
DRAW_POLYGON: 'draw_polygon',
DRAW_POINT: 'draw_point',
SIMPLE_SELECT: 'simple_select',
DIRECT_SELECT: 'direct_select',
STATIC: 'static',
};
export const events = {
CREATE: 'draw.create',
DELETE: 'draw.delete',
UPDATE: 'draw.update',
SELECTION_CHANGE: 'draw.selectionchange',
MODE_CHANGE: 'draw.modechange',
ACTIONABLE: 'draw.actionable',
RENDER: 'draw.render',
COMBINE_FEATURES: 'draw.combine',
UNCOMBINE_FEATURES: 'draw.uncombine',
};
export const updateActions = {
MOVE: 'move',
CHANGE_COORDINATES: 'change_coordinates',
};
export const meta = {
FEATURE: 'feature',
MIDPOINT: 'midpoint',
VERTEX: 'vertex',
};
export const activeStates = {
ACTIVE: 'true',
INACTIVE: 'false',
};
export const interactions = [
'scrollZoom',
'boxZoom',
'dragRotate',
'dragPan',
'keyboard',
'doubleClickZoom',
'touchZoomRotate',
];
export const LAT_MIN = -90;
export const LAT_RENDERED_MIN = -85;
export const LAT_MAX = 90;
export const LAT_RENDERED_MAX = 85;
export const LNG_MIN = -270;
export const LNG_MAX = 270;

View File

@ -0,0 +1,24 @@
const doubleClickZoom = {
enable(ctx) {
setTimeout(() => {
if (
!ctx.map ||
!ctx.map.doubleClickZoom ||
!ctx._ctx ||
!ctx._ctx.store ||
!ctx._ctx.store.getInitialConfigValue
)
return;
if (!ctx._ctx.store.getInitialConfigValue('doubleClickZoom')) return;
ctx.map.doubleClickZoom.enable();
}, 0);
},
disable(ctx) {
setTimeout(() => {
if (!ctx.map || !ctx.map.doubleClickZoom) return;
ctx.map.doubleClickZoom.disable();
}, 0);
},
};
export default doubleClickZoom;

View File

@ -0,0 +1,69 @@
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import doubleClickZoom from './doubleClickZoom';
import * as turf from '@turf/turf';
const { circle, distance, helpers: turfHelpers } = turf;
const drawCircleMode = { ...MapboxDraw.modes.draw_polygon };
drawCircleMode.onSetup = function () {
const polygon = this.newFeature({
type: 'Feature',
properties: {
isCircle: true,
center: [],
},
geometry: {
type: 'Polygon',
coordinates: [],
},
});
this.addFeature(polygon);
this.clearSelectedFeatures();
doubleClickZoom.disable(this);
// dragPan.disable(this);
this.updateUIClasses({ mouse: 'add' });
this.activateUIButton('Polygon');
this.setActionableState({
trash: true,
});
return {
polygon,
currentVertexPosition: 0,
};
};
drawCircleMode.onClick = drawCircleMode.onTap = function (state, e) {
const currentCenter = state.polygon.properties.center;
if (currentCenter.length === 0) {
// dragPan.disable(this)
state.polygon.properties.center = [e.lngLat.lng, e.lngLat.lat];
} else {
// dragPan.enable(this);
return this.changeMode('simple_select', { featureIds: [state.polygon.id] });
}
};
drawCircleMode.onDrag = drawCircleMode.onMouseMove = function (state, e) {
const center = state.polygon.properties.center;
if (center.length > 0) {
const distanceInKm = distance(
turfHelpers.point(center),
turfHelpers.point([e.lngLat.lng, e.lngLat.lat]),
{
units: 'kilometers',
}
);
const circleFeature = circle(center, distanceInKm);
state.polygon.incomingCoords(circleFeature.geometry.coordinates);
state.polygon.properties.radiusInKm = distanceInKm;
state.polygon.properties.lastClickCoord = [e.lngLat.lng, e.lngLat.lat];
}
};
//它决定当前 Drew 数据存储中的哪些特性将在地图上呈现。
//所有传递给“显示”的特性都将被渲染,因此可以为每个内部特性传递多个显示特性。
//有关如何制作显示特性的建议,请参阅‘ styling-pullin API.md
drawCircleMode.toDisplayFeatures = function (state, geojson, display) {
const isActivePolygon = geojson.properties.id === state.polygon.id;
geojson.properties.active = isActivePolygon ? 'true' : 'false';
display(geojson);
};
export default drawCircleMode;

View File

@ -0,0 +1,131 @@
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import createSupplementaryPoints from '@mapbox/mapbox-gl-draw';
import moveFeatures from '@mapbox/mapbox-gl-draw';
import constrainFeatureMovement from '@mapbox/mapbox-gl-draw';
import createVertex from '@mapbox/mapbox-gl-draw';
import * as turf from '@turf/turf';
const { constants } = MapboxDraw;
const { circle, distance, helpers: turfHelpers } = turf;
function createSupplementaryPointsForCircle(geojson) {
const { properties, geometry } = geojson;
if (!properties.user_isCircle) return null;
const supplementaryPoints = [];
const vertices = geometry.coordinates[0].slice(0, -1);
for (let index = 0; index < vertices.length; index += Math.round(vertices.length / 4)) {
supplementaryPoints.push(createVertex(properties.id, vertices[index], `0.${index}`, false));
}
return supplementaryPoints;
}
const drawDirectMode = { ...MapboxDraw.modes.direct_select };
drawDirectMode.dragFeature = function (state, e, delta) {
moveFeatures(this.getSelected(), delta);
this.getSelected()
.filter((feature) => feature.properties.isCircle)
.map((circle) => circle.properties.center)
.forEach((center) => {
center[0] += delta.lng;
center[1] += delta.lat;
});
state.dragMoveLocation = e.lngLat;
};
drawDirectMode.dragVertex = function (state, e, delta) {
//圆处理
if (state.feature.properties.isCircle) {
const center = state.feature.properties.center;
const movedVertex = [e.lngLat.lng, e.lngLat.lat];
const radius = distance(turfHelpers.point(center), turfHelpers.point(movedVertex), {
units: 'kilometers',
});
const circleFeature = circle(center, radius);
state.feature.incomingCoords(circleFeature.geometry.coordinates);
state.feature.properties.radiusInKm = radius;
return;
}
//矩形处理
if (state.feature.properties.isRect) {
state.selectedCoordPaths.forEach((coordPath) => {
const selectCoord = state.feature.getCoordinate(coordPath);
//更新边缘2点
const [featureIndex, coordIndex] = coordPath.split('.');
const coordinates = state.feature.getCoordinates()[featureIndex];
//对立点判断
const coordPosMap = {
1: '3',
2: '0',
3: '1',
0: '2',
};
const mapCoord = state.feature.getCoordinate(`${featureIndex}.${coordPosMap[coordIndex]}`);
//如果对立点和坐标xy 一致 则返回
if (mapCoord[0] === e.lngLat.lng || mapCoord[1] === e.lngLat.lat) {
return;
}
for (let i = 0; i < 4; i++) {
const coord = coordinates[i];
if (coordIndex == i) {
continue;
}
if (coord[0] === selectCoord[0]) {
state.feature.updateCoordinate(`${featureIndex}.${i}`, e.lngLat.lng, coord[1]);
continue;
}
if (coord[1] === selectCoord[1]) {
state.feature.updateCoordinate(`${featureIndex}.${i}`, coord[0], e.lngLat.lat);
continue;
}
}
//更新拖拽的点
state.feature.updateCoordinate(coordPath, e.lngLat.lng, e.lngLat.lat);
});
return;
}
//其他走回默认
const selectedCoords = state.selectedCoordPaths.map((coordPath) =>
state.feature.getCoordinate(coordPath)
);
const selectedCoordPoints = selectedCoords.map((coords) => ({
type: constants.geojsonTypes.FEATURE,
properties: {},
geometry: {
type: constants.geojsonTypes.POINT,
coordinates: coords,
},
}));
const constrainedDelta = constrainFeatureMovement(selectedCoordPoints, delta);
for (let i = 0; i < selectedCoords.length; i++) {
const coord = selectedCoords[i];
state.feature.updateCoordinate(
state.selectedCoordPaths[i],
coord[0] + constrainedDelta.lng,
coord[1] + constrainedDelta.lat
);
}
};
drawDirectMode.toDisplayFeatures = function (state, geojson, push) {
if (state.featureId === geojson.properties.id) {
geojson.properties.active = constants.activeStates.ACTIVE;
push(geojson);
const supplementaryPoints = geojson.properties.user_isCircle
? createSupplementaryPointsForCircle(geojson)
: createSupplementaryPoints(geojson, {
map: this.map,
midpoints: true,
selectedPaths: state.selectedCoordPaths,
});
supplementaryPoints.forEach(push);
} else {
geojson.properties.active = constants.activeStates.INACTIVE;
push(geojson);
}
this.fireActionable(state);
};
export default drawDirectMode;

View File

@ -0,0 +1,165 @@
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import doubleClickZoom from './doubleClickZoom';
const { constants, lib } = MapboxDraw;
const drawLineSelectMode = {
//当模式启动时,这个函数将被调用。
//draw.changeMode(drawLineSelectMode,params}); 切换模式时params = opts
//返回的值应该是一个对象,并将传递给所有其他生命周期函数
onSetup: function (opts) {
const featureId = opts.featureId;
let line;
let currentVertexPosition = 0;
let direction = 'forward';
if (featureId) {
line = this.getFeature(featureId);
if (!line) {
throw new Error('Could not find a feature with the provided featureId');
}
let from = opts.from;
if (from && from.type === 'Feature' && from.geometry && from.geometry.type === 'Point') {
from = from.geometry;
}
if (from && from.type === 'Point' && from.coordinates && from.coordinates.length === 2) {
from = from.coordinates;
}
if (!from || !Array.isArray(from)) {
throw new Error(
'Please use the `from` property to indicate which point to continue the line from'
);
}
const lastCoord = line.coordinates.length - 1;
if (
line.coordinates[lastCoord][0] === from[0] &&
line.coordinates[lastCoord][1] === from[1]
) {
currentVertexPosition = lastCoord + 1;
line.addCoordinate(currentVertexPosition, ...line.coordinates[lastCoord]);
} else if (line.coordinates[0][0] === from[0] && line.coordinates[0][1] === from[1]) {
direction = 'backwards';
currentVertexPosition = 0;
line.addCoordinate(currentVertexPosition, ...line.coordinates[0]);
} else {
throw new Error(
'`from` should match the point at either the start or the end of the provided LineString'
);
}
} else {
line = this.newFeature({
type: constants.geojsonTypes.FEATURE,
properties: {},
geometry: {
type: constants.geojsonTypes.LINE_STRING,
coordinates: [],
},
});
currentVertexPosition = 0;
this.addFeature(line);
}
this.clearSelectedFeatures();
doubleClickZoom.disable(this);
this.updateUIClasses({ mouse: 'add' }); //"+"
this.setActionableState({
//添加地图事件'draw.actionable'
trash: true,
combineFeatures: false,
uncombineFeatures: false,
});
return {
line,
currentVertexPosition,
direction,
};
},
clickAnywhere: function (state, e) {
if (
(state.currentVertexPosition > 0 &&
lib.isEventAtCoordinates(e, state.line.coordinates[state.currentVertexPosition - 1])) ||
(state.direction === 'backwards' &&
lib.isEventAtCoordinates(e, state.line.coordinates[state.currentVertexPosition + 1]))
) {
return this.changeMode(constants.modes.SIMPLE_SELECT, {
featureIds: [state.line.id],
});
}
this.updateUIClasses({ mouse: constants.cursors.ADD });
state.line.updateCoordinate(state.currentVertexPosition, e.lngLat.lng, e.lngLat.lat);
if (state.direction === 'forward') {
state.currentVertexPosition++;
state.line.updateCoordinate(state.currentVertexPosition, e.lngLat.lng, e.lngLat.lat);
} else {
state.line.addCoordinate(0, e.lngLat.lng, e.lngLat.lat);
}
},
clickOnVertex: function (state) {
//点击在已经有点上
state.line.properties.name = 'line_select';
return this.changeMode(constants.modes.SIMPLE_SELECT, { featureIds: [state.line.id] });
},
onMouseMove: function (state, e) {
state.line.updateCoordinate(state.currentVertexPosition, e.lngLat.lng, e.lngLat.lat);
if (lib.CommonSelectors.isVertex(e)) {
//再次将鼠标放在之前已经有的点上,改变鼠标样式
this.updateUIClasses({ mouse: constants.cursors.POINTER });
}
},
onClick: function (state, e) {
//再次将鼠标放在之前已经有的点上
if (lib.CommonSelectors.isVertex(e)) return this.clickOnVertex(state, e);
this.clickAnywhere(state, e);
},
onKeyUp: function (state, e) {
if (lib.CommonSelectors.isEnterKey(e)) {
this.changeMode(constants.modes.SIMPLE_SELECT, { featureIds: [state.line.id] });
} else if (lib.CommonSelectors.isEscapeKey(e)) {
this.deleteFeature([state.line.id], { silent: true });
this.changeMode(constants.modes.SIMPLE_SELECT);
}
},
onStop: function (state) {
doubleClickZoom.enable(this);
if (this.getFeature(state.line.id) === undefined) return;
state.line.removeCoordinate(`${state.currentVertexPosition}`); //双击停止时,最后两个点位是一样的
if (state.line.isValid()) {
//'draw.create'
this.map.fire(constants.events.CREATE, {
features: [state.line.toGeoJSON()],
});
} else {
this.deleteFeature([state.line.id], { silent: true });
this.changeMode(constants.modes.SIMPLE_SELECT, {}, { silent: true });
}
},
onTrash: function (state) {
this.deleteFeature([state.line.id], { silent: true });
this.changeMode(constants.modes.SIMPLE_SELECT);
},
//它决定当前 Drew 数据存储中的哪些特性将在地图上呈现。
//所有传递给“显示”的特性都将被渲染,因此可以为每个内部特性传递多个显示特性。
//有关如何制作显示特性的建议,请参阅‘ styling-pullin API.md
toDisplayFeatures: function (state, geojson, display) {
const isActiveLine = geojson.properties.id === state.line.id;
geojson.properties.active = isActiveLine
? constants.activeStates.ACTIVE
: constants.activeStates.INACTIVE;
if (!isActiveLine) {
display(geojson);
return;
}
if (geojson.geometry.coordinates.length < 2) return;
geojson.properties.meta = 'line_distance';
geojson.properties.name = 'line_distance';
display(
lib.createVertex(
state.line.id,
geojson.geometry.coordinates[
state.direction === 'forward' ? geojson.geometry.coordinates.length - 2 : 1
],
`${state.direction === 'forward' ? geojson.geometry.coordinates.length - 2 : 1}`,
false
)
);
display(geojson);
},
};
export default drawLineSelectMode;

View File

@ -0,0 +1,125 @@
const doubleClickZoom = {
enable: (ctx) => {
setTimeout(() => {
// First check we've got a map and some context.
if (
!ctx.map ||
!ctx.map.doubleClickZoom ||
!ctx._ctx ||
!ctx._ctx.store ||
!ctx._ctx.store.getInitialConfigValue
)
return;
// Now check initial state wasn't false (we leave it disabled if so)
if (!ctx._ctx.store.getInitialConfigValue('doubleClickZoom')) return;
ctx.map.doubleClickZoom.enable();
}, 0);
},
disable(ctx) {
setTimeout(() => {
if (!ctx.map || !ctx.map.doubleClickZoom) return;
// Always disable here, as it's necessary in some cases.
ctx.map.doubleClickZoom.disable();
}, 0);
},
};
const DrawRectangle = {
// When the mode starts this function will be called.
onSetup: function () {
const rectangle = this.newFeature({
type: 'Feature',
properties: {
isRect: true,
},
geometry: {
type: 'Polygon',
coordinates: [[]],
},
});
this.addFeature(rectangle);
this.clearSelectedFeatures();
doubleClickZoom.disable(this);
this.updateUIClasses({ mouse: 'add' });
this.setActionableState({
trash: true,
});
return {
rectangle,
};
},
// support mobile taps
onTap: function (state, e) {
// emulate 'move mouse' to update feature coords
if (state.startPoint) this.onMouseMove(state, e);
// emulate onClick
this.onClick(state, e);
},
// Whenever a user clicks on the map, Draw will call `onClick`
onClick: function (state, e) {
// if state.startPoint exist, means its second click
//change to simple_select mode
if (
state.startPoint &&
state.startPoint[0] !== e.lngLat.lng &&
state.startPoint[1] !== e.lngLat.lat
) {
this.updateUIClasses({ mouse: 'pointer' });
state.endPoint = [e.lngLat.lng, e.lngLat.lat];
this.changeMode('simple_select', { featuresId: state.rectangle.id });
}
// on first click, save clicked point coords as starting for rectangle
const startPoint = [e.lngLat.lng, e.lngLat.lat];
state.startPoint = startPoint;
},
onMouseMove: function (state, e) {
// if startPoint, update the feature coordinates, using the bounding box concept
// we are simply using the startingPoint coordinates and the current Mouse Position
// coordinates to calculate the bounding box on the fly, which will be our rectangle
if (state.startPoint) {
state.rectangle.updateCoordinate('0.0', state.startPoint[0], state.startPoint[1]); //minX, minY - the starting point
state.rectangle.updateCoordinate('0.1', e.lngLat.lng, state.startPoint[1]); // maxX, minY
state.rectangle.updateCoordinate('0.2', e.lngLat.lng, e.lngLat.lat); // maxX, maxY
state.rectangle.updateCoordinate('0.3', state.startPoint[0], e.lngLat.lat); // minX,maxY
state.rectangle.updateCoordinate('0.4', state.startPoint[0], state.startPoint[1]); //minX,minY - ending point (equals to starting point)
}
},
// Whenever a user clicks on a key while focused on the map, it will be sent here
onKeyUp: function (state, e) {
if (e.keyCode === 27) return this.changeMode('simple_select');
},
onStop: function (state) {
doubleClickZoom.enable(this);
this.updateUIClasses({ mouse: 'none' });
this.activateUIButton();
// check to see if we've deleted this feature
if (this.getFeature(state.rectangle.id) === undefined) return;
//remove last added coordinate
state.rectangle.removeCoordinate('0.4');
if (state.rectangle.isValid()) {
this.map.fire('draw.create', {
features: [state.rectangle.toGeoJSON()],
});
} else {
this.deleteFeature([state.rectangle.id], { silent: true });
this.changeMode('simple_select', {}, { silent: true });
}
},
toDisplayFeatures: function (state, geojson, display) {
const isActivePolygon = geojson.properties.id === state.rectangle.id;
geojson.properties.active = isActivePolygon ? 'true' : 'false';
if (!isActivePolygon) return display(geojson);
// Only render the rectangular polygon if it has the starting point
if (!state.startPoint) return;
return display(geojson);
},
onTrash: function (state) {
this.deleteFeature([state.rectangle.id], { silent: true });
this.changeMode('simple_select');
},
};
export default DrawRectangle;

View File

@ -0,0 +1,86 @@
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import createSupplementaryPoints from '@mapbox/mapbox-gl-draw';
import moveFeatures from '@mapbox/mapbox-gl-draw';
import createVertex from '@mapbox/mapbox-gl-draw';
import { lineToPoly } from './utils';
const { constants } = MapboxDraw;
function createSupplementaryPointsForCircle(geojson) {
const { properties, geometry } = geojson;
if (!properties.user_isCircle) return null;
const supplementaryPoints = [];
const vertices = geometry.coordinates[0].slice(0, -1);
for (let index = 0; index < vertices.length; index += Math.round(vertices.length / 4)) {
supplementaryPoints.push(createVertex(properties.id, vertices[index], `0.${index}`, false));
}
return supplementaryPoints;
}
const drawSimpleSelectMode = { ...MapboxDraw.modes.simple_select };
drawSimpleSelectMode.dragMove = function (state, e) {
// Dragging when drag move is enabled
state.dragMoving = true;
e.originalEvent.stopPropagation();
const delta = {
lng: e.lngLat.lng - state.dragMoveLocation.lng,
lat: e.lngLat.lat - state.dragMoveLocation.lat,
};
moveFeatures(this.getSelected(), delta);
this.getSelected()
.filter((feature) => feature.properties.isCircle)
.map((circle) => circle.properties.center)
.forEach((center) => {
center[0] += delta.lng;
center[1] += delta.lat;
});
state.dragMoveLocation = e.lngLat;
};
drawSimpleSelectMode.toDisplayFeatures = function (state, geojson, display) {
geojson.properties.active = this.isSelected(geojson.properties.id)
? constants.activeStates.ACTIVE
: constants.activeStates.INACTIVE;
if (geojson.properties.user_name === 'line_select') {
const union = lineToPoly(geojson);
display(union);
}
display(geojson);
this.fireActionable();
//如果是线 每次都创建点
if (
geojson?.properties.active !== constants.activeStates.ACTIVE &&
geojson.geometry.type === constants.geojsonTypes.LINE_STRING &&
geojson.properties.user_name !== 'line_select'
) {
const points = createSupplementaryPoints(geojson);
points.forEach(display);
}
if (
geojson.properties.active !== constants.activeStates.ACTIVE ||
geojson.geometry.type === constants.geojsonTypes.POINT
) {
return;
}
let supplementaryPoints;
if (geojson.properties.user_isCircle) {
supplementaryPoints = createSupplementaryPointsForCircle(geojson);
} else {
supplementaryPoints = createSupplementaryPoints(geojson);
}
supplementaryPoints.forEach(display);
// if(geojson.properties.)
};
//阻止框选图形拖拽
drawSimpleSelectMode.onTap = drawSimpleSelectMode.onClick = function () {};
export default drawSimpleSelectMode;

View File

@ -0,0 +1,14 @@
import doubleClickZoom from './doubleClickZoom';
var StaticMode = {};
StaticMode.onSetup = function () {
this.setActionableState(); // default actionable state is false for all actions
doubleClickZoom.disable(this); //静态model 不运行双击
return {};
};
StaticMode.toDisplayFeatures = function (state, geojson, display) {
display(geojson);
};
export default StaticMode;

View File

@ -0,0 +1,144 @@
//自定义画框样式
const mapboxDrawStyle = [
// ACTIVE (being drawn)
// line stroke
{
id: 'gl-draw-line',
type: 'line',
filter: [
'all',
['==', '$type', 'LineString'],
['!=', 'mode', 'static'],
['==', 'active', 'true'],
],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': 'rgba(246,67,72,1)',
'line-dasharray': [0.2, 2],
'line-width': 2,
},
},
{
id: 'gl-draw-line-not-active',
type: 'line',
filter: ['all', ['==', '$type', 'LineString'], ['!=', 'active', 'true']],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': 'rgba(246,67,72,1)',
'line-width': 2,
},
},
// polygon fill
{
id: 'gl-draw-polygon-fill',
type: 'fill',
filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static']],
paint: {
'fill-color': 'rgba(246,67,72,0.2)',
'fill-outline-color': 'rgba(246,67,72,0.2)',
},
},
// polygon outline stroke
// This doesn't style the first edge of the polygon, which uses the line stroke styling instead
{
id: 'gl-draw-polygon-stroke-active',
type: 'line',
filter: [
'all',
['==', '$type', 'Polygon'],
['!=', 'mode', 'static'],
['==', 'active', 'false'],
],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': 'rgba(246,67,72,1)',
'line-width': 2,
},
},
{
id: 'gl-draw-polygon-stroke-active-select',
type: 'line',
filter: ['all', ['==', '$type', 'Polygon'], ['!=', 'mode', 'static'], ['==', 'active', 'true']],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': 'rgba(246,67,72,1)',
'line-dasharray': [0.2, 2],
'line-width': 2,
},
},
// vertex point halos
{
id: 'gl-draw-polygon-and-line-vertex-halo-active',
type: 'circle',
filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']],
paint: {
'circle-radius': 5,
'circle-color': '#FFF',
},
},
// vertex points
{
id: 'gl-draw-polygon-and-line-vertex-active',
type: 'circle',
filter: ['all', ['==', 'meta', 'vertex'], ['==', '$type', 'Point'], ['!=', 'mode', 'static']],
paint: {
'circle-radius': 3,
'circle-color': 'rgba(246,67,72,1)',
},
},
// INACTIVE (static, already drawn)
// line stroke
{
id: 'gl-draw-line-static',
type: 'line',
filter: ['all', ['==', '$type', 'LineString'], ['==', 'mode', 'static']],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#E13F3F',
'line-width': 3,
},
},
// polygon fill
{
id: 'gl-draw-polygon-fill-static',
type: 'fill',
filter: ['all', ['==', '$type', 'Polygon'], ['==', 'mode', 'static']],
paint: {
'fill-color': 'rgba(225, 63, 63, 0.2)',
'fill-outline-color': 'rgba(225, 63, 63, 0.2)',
},
},
// polygon outline
{
id: 'gl-draw-polygon-stroke-static',
type: 'line',
filter: ['all', ['==', '$type', 'Polygon'], ['==', 'mode', 'static']],
layout: {
'line-cap': 'round',
'line-join': 'round',
},
paint: {
'line-color': '#E13F3F',
'line-width': 3,
},
},
];
export default mapboxDrawStyle;

View File

@ -0,0 +1,74 @@
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import type { DrawModes } from '@mapbox/mapbox-gl-draw';
import type { MapboxMap } from 'react-map-gl';
import drawLineSelectMode from './drawLineSelectMode.draw.js';
import drawCircleMode from './drawCircleMode.draw.js';
import drawRectMode from './drawRectMode.draw.js';
import drawDirectMode from './drawDirectMode.draw.js';
import drawSimpleSelectMode from './drawSimpleSelectMode.draw.js';
import drawStaticMode from './drawStaticMode.draw.js';
import mapboxDrawStyle from './drawStyle';
interface DrawControl {
modes?: DrawModes;
}
function noop(): void {
/* do nothing */
}
//mapbox-gl-draw api
//https://github.com/mapbox/mapbox-gl-draw/blob/main/docs/MODES.md#life-cycle-functions
function drawControl(
map: MapboxMap,
params: DrawControl = {},
defaultMode = 'static',
onDrawCreate = () => {}
) {
const { modes: paramModes = {} } = params;
if (!map) {
return;
}
/**
* MapboxDraw
* draw_line_string 线
* draw_polygon
* draw_point
* simple_select
* direct_select
*
*
* draw_line_select; 1
* direct_select;
* simple_select;
* draw_circle;1
* draw_rect;1
*
* MapboxDraw的
* draw_polygon
* draw_line_string 线
*/
const draw = new MapboxDraw({
defaultMode: defaultMode,
displayControlsDefault: false, // 取消默认的按钮
styles: mapboxDrawStyle,
modes: {
...MapboxDraw.modes,
draw_line_select: drawLineSelectMode,
draw_line_string: drawLineSelectMode,
draw_circle: drawCircleMode,
draw_rect: drawRectMode,
direct_select: drawDirectMode,
simple_select: drawSimpleSelectMode,
static: drawStaticMode,
...paramModes,
},
});
map.addControl(draw);
map.on('draw.create', onDrawCreate || noop);
return draw;
}
export default drawControl;

View File

@ -0,0 +1,162 @@
import * as turf from '@turf/turf';
import union from '@turf/union';
// 获取2个点的距离
export const getDistance = (point1, point2) => {
const { x: x1, y: y1 } = point1;
const { x: x2, y: y2 } = point2;
return Math.abs(Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)));
};
//获取两点(格式例如:[120,30])间物理像素距离
export function getPixelDistance(map, pointA, pointB) {
const pointAPixel = map.transform.locationPoint(pointA); // A点的像素坐标位置
const pointBPixel = map.transform.locationPoint(pointB); // B点的像素坐标位置
// A、B两点的像素坐标距离
return getDistance(pointAPixel, pointBPixel);
}
//判断是否是0值 给一个偏移量
export const isZero = (floatValue) => {
return floatValue > -0.00001 && floatValue < 0.00001;
};
//判断2个向量是否平行
export const isParallel = (vectorA, vectorB) => {
const { x: x1, y: y1 } = vectorA;
const { x: x2, y: y2 } = vectorB;
return isZero(x1 * y2 - y1 * x2);
};
//判断一个点是否在线段上 判断向量叉乘是否为0 且x坐标在线段2端点间
export const isPointOnLine = (vectorA, path) => {
const [vectorB, vectorC] = path;
//与端点重合
if (
(vectorA.x === vectorB.x && vectorA.y === vectorB.y) ||
(vectorA.x === vectorC.x && vectorA.y === vectorC.y)
) {
return true;
}
//在同一竖直方向,线段竖直,点在该线段所在的直线上
if (isZero(vectorB.x - vectorC.x) && isZero(vectorB.x - vectorA.x)) {
//已判定点在直线上,若点在两端点中间,即点在线段上
if (
(vectorA.y < vectorC.y && vectorA.y > vectorB.y) ||
(vectorA.y < vectorB.y && vectorA.y > vectorC.y)
) {
return true;
}
return false;
}
//在同一水平方向
if (isZero(vectorB.y - vectorC.y) && isZero(vectorB.y - vectorA.y)) {
if (
(vectorA.x < vectorC.x && vectorA.x > vectorB.x) ||
(vectorA.x < vectorB.x && vectorA.x > vectorC.x)
) {
return true;
}
return false;
}
// 线段倾斜,此时线段所在直线存在斜率
// 点在直线上AB与AC斜率相等且有共同点A此时AC与AB重合即点A在直线BC上
if (
isZero(
(vectorB.y - vectorA.y) / (vectorB.x - vectorA.x) -
(vectorA.y - vectorC.y) / (vectorA.x - vectorC.x)
)
) {
if (
(vectorB.y - vectorA.y) * (vectorA.y - vectorC.y) > 0 &&
(vectorB.x - vectorA.x) * (vectorA.x - vectorC.x) > 0
) {
return true;
}
return false;
}
};
//获取线段重合部分,如果重合返回合并后的线段 如果不重合 返回传入的线段
//see https://blog.csdn.net/qq_39108767/article/details/81673921
//see https://www.cnblogs.com/tuyang1129/p/9390376.html
export const getLineCoincide = (path1, path2) => {
let paths = [];
const [vectorA, vectorB] = path1;
const [vectorC, vectorD] = path2;
const isOnLineA = isPointOnLine(vectorA, [vectorC, vectorD]);
const isOnLineB = isPointOnLine(vectorB, [vectorC, vectorD]);
const isOnLineC = isPointOnLine(vectorC, [vectorA, vectorB]);
const isOnLineD = isPointOnLine(vectorD, [vectorA, vectorB]);
const isCollinear = isParallel(
{ x: vectorA.x - vectorB.x, y: vectorA.y - vectorB.y },
{ x: vectorC.x - vectorD.x, y: vectorC.y - vectorD.y }
);
//下面6中情况代表合并
if (isOnLineA && isOnLineC && isCollinear) {
paths = [[vectorB, vectorD]];
}
if (isOnLineA && isOnLineD && isCollinear) {
paths = [[vectorB, vectorC]];
}
if (isOnLineB && isOnLineC && isCollinear) {
paths = [[vectorA, vectorD]];
}
if (isOnLineB && isOnLineD && isCollinear) {
paths = [[vectorA, vectorC]];
}
if (isOnLineA && isOnLineB && isCollinear) {
paths = [[vectorC, vectorD]];
}
if (isOnLineC && isOnLineD && isCollinear) {
paths = [[vectorA, vectorB]];
}
//未匹配到上述情况,则没有重合 原样返回
if (paths.length === 0) {
paths = [path1, path2];
}
return paths;
};
// 计算与纬线的角度,正方向向上
const calcAng = (point, p) => (Math.atan2(point[1] - p[1], point[0] - p[0]) * 180) / Math.PI + 180;
// 多段折线转polygon,直线左右默认范围50米
export const lineToPoly = (geojson, r = 50) => {
const linesPolygon = [];
const circlePolygon = [];
for (let i = 0; i < geojson.geometry.coordinates.length - 1; i++) {
const [pointA, pointB] = [geojson.geometry.coordinates[i], geojson.geometry.coordinates[i + 1]];
const line = turf.lineString([pointA, pointB]);
const ang = calcAng(pointA, pointB);
// 与经线的夹角为偏移方向,右正左负, 右偏移-左偏移 = 180
const translatedPolyA = turf.transformTranslate(line, r / 1000, -ang);
const translatedPolyB = turf.transformTranslate(line, r / 1000, -ang + 180);
const _line = turf.lineString([
// 逆时针闭合
...translatedPolyA.geometry.coordinates.reverse(),
...translatedPolyB.geometry.coordinates,
]);
const linePolygon = turf.lineToPolygon(_line);
linesPolygon.push(linePolygon);
circlePolygon.push(turf.circle(pointA, r / 1000));
if (i == geojson.geometry.coordinates.length - 2) {
circlePolygon.push(turf.circle(pointB, r / 1000));
}
}
let _union;
try {
//todo: 新版本union和老版本行为不一致 先用老版本 后续观察原因
// _union = turf.union(...circlePolygon, ...linesPolygon);
_union = union(...circlePolygon, ...linesPolygon);
} catch (e) {
console.error(e);
}
return _union;
};