Mapboxgl draw 自定义标绘之二:图标、文本、圆形的创建与编辑,重写原模式相关方法,保持当前模式

发布时间 2023-04-12 10:46:28作者: 宇宙野牛

mapbox-gl-draw官网给出的扩展模式终于无法满足需求,需要diy了。
因为是diy,所以不存在前文说的commonjs打包报错的问题,呵呵。
前文:Mapboxgl draw 自定义标绘:圆、矩形、自由多边形、上传读取geojson (有的概念可能会接续前文不做解释)

参考链接

自定义MODE例子:https://github.com/mapbox/mapbox-gl-draw/blob/main/docs/MODES.md#writing-custom-modes
原有MODE源码:https://github.com/mapbox/mapbox-gl-draw/tree/main/src/modes
一些可能躲不开需要改造的工具方法:https://github.com/mapbox/mapbox-gl-draw/tree/main/src/lib

图标、文本模式

draw默认的点图层是circle layer,现需要改造成可以显示图标和文本标签的symbol layer。

  1. 修改styles
// 点
{
	id: "highlight-active-points", // 激活状态的点
	type: "symbol",
	filter: ["all", ["==", "$type", "Point"], ["==", "meta", "feature"], ["==", "active", "true"]],
	layout: {
		"icon-image": ["get", "user_icon"],
		"icon-size": ["get", "user_iconSize"],
		"text-field": ["get", "user_text"],
		"text-size": 16,
	},
	paint: {
		"text-color": "#D20C0C",
		"text-halo-width": 2,
		"text-halo-color": "#fff",
	},
},
{
	id: "points-are-blue", // 非激活状态的点。两个图层样式都需要修改
	type: "symbol",
	filter: ["all", ["==", "$type", "Point"], ["==", "meta", "feature"], ["==", "active", "false"]],
	layout: {
		"icon-image": ["get", "user_icon"],
		"icon-size": ["get", "user_iconSize"],
		"text-field": ["get", "user_text"],
		"text-size": 16,
	},
	paint: {
		"text-color": "#D20C0C",
		"text-halo-width": 2,
		"text-halo-color": "#fff",
	},
},
  1. 创建图标模式
let IconMode = {};

// 在changeMode时,用icon和iconSize定义图标和图标大小
IconMode.onSetup = function (opts) {
	let state = {};
	state.icon = opts.icon;
	state.iconSize = opts.iconSize;
	return state;
};

IconMode.onClick = function (state, e) {
	let point = this.newFeature({
		type: "Feature",
		properties: {
			icon: state.icon,
			iconSize: state.iconSize,
		},
		geometry: {
			type: "Point",
			coordinates: [e.lngLat.lng, e.lngLat.lat],
		},
	});
	this.addFeature(point);
	return this.changeMode("simple_select"); // 为了跟其他已有模式保持一致,其他模式添加完一个图形后会切换到simple_select
};

IconMode.toDisplayFeatures = function (state, geojson, display) {
	display(geojson);
};
  1. 创建文本模式(目前还有点bug,有时无法成功看到添加的文本)
let TextMode = {};

TextMode.onSetup = function () {
	let state = {};
	return state;
};

TextMode.onClick = function (state, e) {
	let _this = this;
	let coordinates = [e.lngLat.lng, e.lngLat.lat];

        // 点击先创建一个输入框,获取要添加的文本内容
	if (document.getElementById("draw-text-input")) {
		document.body.removeChild(document.getElementById("draw-text-input"));
	}
	let textDom = document.createElement("input");
	textDom.setAttribute("type", "text"); // 输入框的类型
	textDom.setAttribute("name", "textDom"); // 输入框的名字
	textDom.setAttribute("id", "draw-text-input"); // 输入框的id
	textDom.style.left = e.point.x + "px"; // 定位,跟随鼠标位置出现
	textDom.style.top = e.point.y + 30 + "px";
	textDom.style.position = "absolute";
	textDom.style.zIndex = "2"; // 可能需要设置层级以看到这个输入框
	textDom.onkeydown = function (ie) {
                // 键盘回车事件
		if (ie.keyCode === 13) {
			let text = ie.target.value;
			let point = _this.newFeature({
				type: "Feature",
				properties: {
					text: text,
				},
				geometry: {
					type: "Point",
					coordinates: coordinates,
				},
			});
			_this.addFeature(point);
			document.body.removeChild(ie.target);
			return _this.changeMode("simple_select");
		}
	};
	document.body.appendChild(textDom);
	textDom.focus(); // 自动聚焦
};

TextMode.toDisplayFeatures = function (state, geojson, display) {
	display(geojson);
};

圆形的创建与编辑,原模式部分重写

  1. 创建圆模式
    这个模式的创建方式是点击地图一点确定圆心,鼠标移动过程中圆随着变化半径,再次点击确定半径,生成圆。
let _this = this;
let RadiusCircleMode = {};

RadiusCircleMode.onSetup = function (opts) {
	let state = { clickState: false };
	return state;
};

RadiusCircleMode.onClick = function (state, e) {
	state.clickState = !state.clickState; // true: 当前正要点击划半径;false: 画完半径,要创建圆
	if (state.clickState) {
		state.coordinates = [e.lngLat.lng, e.lngLat.lat];
	} else {
		this.deleteFeature("draw_circle_temp"); // 删除鼠标滑动过程中创建及更新的临时圆形
		let circle = this.newFeature(_this.createCircleFeature(state.coordinates, [e.lngLat.lng, e.lngLat.lat]));
		this.addFeature(circle);
		// 以下为触发map的draw.create事件,返回创建的图形的Feature,为了跟其他已有模式统一动作
		let coordinates = JSON.parse(JSON.stringify(circle.coordinates));
		coordinates[0].push(coordinates[0][0]); // 生成闭合图形,根据业务需要使用;不需要的话就没必要作此处理
		const feature = JSON.parse(
			JSON.stringify({
				id: circle.id,
				type: "Feature",
				geometry: {
					type: "Polygon",
					properties: {},
					coordinates: coordinates,
				},
			})
		);
		this.map.fire("draw.create", { features: [feature] });
		return this.changeMode("simple_select");
	}
};

RadiusCircleMode.onMouseMove = function (state, e) {
	if (state.clickState) {
                // 创建半径随鼠标移动而改变的临时圆形
		let tempFeature = this.getFeature("draw_circle_temp");
		if (!tempFeature) {
			this.addFeature(
				this.newFeature(Object.assign(_this.createCircleFeature(state.coordinates, [e.lngLat.lng, e.lngLat.lat]), { id: "draw_circle_temp" }))
			);
		} else {
			let newFeature = _this.createCircleFeature(state.coordinates, [e.lngLat.lng, e.lngLat.lat]);
                        // draw的Feature类自带的setProperty(更新单个属性),setCoordinates(更新全部坐标)
			tempFeature.setProperty("radiusInKm", newFeature.properties.radiusInKm);
			tempFeature.setCoordinates(newFeature.geometry.coordinates); 
		}
	}
};

RadiusCircleMode.toDisplayFeatures = function (state, geojson, display) {
	display(geojson);
};
  1. 根据鼠标的起点和终点坐标创建圆(创建、编辑时都用到)
    需要用到turf.js。
import * as turf from "@turf/turf";

createCircleFeature(startPos, endPos) {
	const radius = this.calDistance(startPos, endPos);
	let options = {
		properties: { isCircle: true, center: startPos, radiusInKm: radius },
	};
	return turf.circle(startPos, radius, options);
}
calDistance(start, end) {
	let from = turf.point(start);
	let to = turf.point(end);
	let options = { units: "kilometers" };
	return turf.distance(from, to, options);
}
  1. 重写simple_select
    重写只列出了画圆时需要重写的部分方法,举一反三需要做其他个性化修改可以在上面链接指向的源码里自行查找。
    要注意重写时不要影响其他模式的正常原有功能(如果故意影响当我没说)。
    我改的时候是先保证拿过来以后其他功能正常,再针对需要的模式小动部分逻辑。
let SimpleSelectModeOverride = MapboxDraw.modes.simple_select;

// 拖拽圆时修改属性中的圆心坐标
SimpleSelectModeOverride.dragMove = function (state, e) {
	state.dragMoving = true;
	e.originalEvent.stopPropagation();

	const delta = {
		lng: e.lngLat.lng - state.dragMoveLocation.lng,
		lat: e.lngLat.lat - state.dragMoveLocation.lat,
	};

        ////// custom start
	const features = this.getSelected();
	features.forEach((feature) => {
		if (feature.properties && feature.properties.isCircle) {
			let oldCenter = feature.properties.center;
			feature.setProperty("center", [oldCenter[0] + delta.lng, oldCenter[1] + delta.lat]);
		}
	});
	moveFeatures(features, delta);
        ////// custom end

        // moveFeatures(this.getSelected(), delta); 原代码是这一句

	state.dragMoveLocation = e.lngLat;
};
  1. 重写direct_select
    这里是编辑模式控制拖拽圆的可编辑顶点时改变圆的半径(否则圆会被当成普通多边形拽变形)
let DirectModeOverride = MapboxDraw.modes.direct_select;
// 拖拽顶点改变圆的半径
DirectModeOverride.dragVertex = function (state, e, delta) {
	const selectedCoords = state.selectedCoordPaths.map((coord_path) => state.feature.getCoordinate(coord_path));
        ////// custom start
	if (state.feature.properties && state.feature.properties.isCircle) {
		let newFeature = _this.createCircleFeature(state.feature.properties.center, [e.lngLat.lng, e.lngLat.lat]);
		state.feature.setProperty("radiusInKm", newFeature.properties.radiusInKm);
		state.feature.setCoordinates(newFeature.geometry.coordinates);
	} else {
                // 以下是原本对普通多边形拖拽顶点的处理
		const selectedCoordPoints = selectedCoords.map((coords) => ({
			type: "Feature",
			properties: {},
			geometry: {
				type: "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);
		}
	}
        ////// custom end
};
// 画完圆,选中,拖拽,释放,之后再在圆中点击,会变成direct_mode下的拖拽
DirectModeOverride.dragFeature = function (state, e, delta) {
        ////// custom start
	const features = this.getSelected();
	features.forEach((feature) => {
		if (feature.properties && feature.properties.isCircle) {
			console.log("direct_select delta", delta);
			let oldCenter = feature.properties.center;
			feature.setProperty("center", [oldCenter[0] + delta.lng, oldCenter[1] + delta.lat]);
		}
	});
	moveFeatures(features, delta);
        ////// custom end

        // moveFeatures(this.getSelected(), delta); 
	state.dragMoveLocation = e.lngLat;
};

ps. 重写时用到的两个工具方法moveFeatures和constrainFeatureMovement在参考链接里可以找到,改一下就可以用。

保持当前标绘模式

draw的默认逻辑是画完一个图形后,自动进入simple_select,这时你可以选中刚画的图形编辑,或者去做别的地图操作。但有时遇到需要一直保持当前标绘模式,以下思路供参考:

let _this = this;
// 监听draw.modechange事件,有变化时强制变回当前记录的activeMode
this._map.on("draw.modechange", (e) => {
	if (_this._activeMode && e.mode === "simple_select") {
                // 顺便还可以自定义标绘类的onFeaturesChange这样的事件,比如这里是模式改变时触发Features变化的事件,可以用于判断当前有没有draw画下的图形,and so on
		typeof _this._option.onFeaturesChange === "function" && _this._option.onFeaturesChange(_this.haveFeatures());
		switch (_this._activeMode) {
			case "point":
				_this.drwawPoint();
				break;
			case "line":
				_this.drwawLine();
				break;
			case "rectangle":
				_this.drawRectangle();
				break;
			case "icon":
				_this.drawIcon(_this._iconType);
				break;
			case "text":
				_this.drawText();
				break;
		}
	}
});