mapbox与cad图纸结合
mapbox是通过webgl渲染的交互式地图,而 mxcad 也是利用webgl渲染实现的。所以我们可以通过 mapbox 提供的自定义图层与 mxcad 的控件实例渲染结合,将cad图层与地图展现出来。
如果不了解 mapbox 可以参考 mapbox官方文档
下面代码是 mapbox 和 mxcad 结合使用的最简示例,运行效果请参考 在线CAD示例demo。
若想要了解更加详细的内容请前往我们的官网:CAD与GIS集成说明
demo源码
- 下载云图开发包
- 找到demo源码
下载好云图开发包后解压, 双击运行 Mx3dServer.exe
, 点击打开GIS DEMO目录按钮进入demo源码目录
- 运行demo
sh
npm install
npm run dev
注意事项
- mapbox-gl 版本与补丁 demo源码根目录下的 patches 包含mapbox-gl的补丁, 且 mapbox-gl 必须是2.8.2的版本 否则无法保证可以正常使用
在自己项目实现时需要将patches目录拷贝到自己项目的根目录下
然后安装进行补丁修复
sh
npm i patch-package -D
npx patch-package
最后在package.json中添加 这样以后安装依赖包时会自动打补丁
json
"scripts": {
"postinstall": "patch-package"
}
- Map类的重写
在实现时需要将mapbox-gl 的 Map类进行重写,添加一些必要的方法。
mxcad会使用到这些方法,所以只需要按照最新的demo源码中实现的Map类为准就好。
代码示例
- 安装对应依赖包
sh
# 先安装对应的依赖包
npm install mapbox-gl mxcad gl-matrix
- 绘制界面容器
html
<div id="map" style="width: 100vw; height: 99vh; overflow: hidden;"></div>
- 根据需求扩展重写Map类
ts
import { type AnyLayer, Map as _Map, type AnySourceData, LngLat, Point } from "mapbox-gl"
import mapboxgl from "mapbox-gl";
import "mapbox-gl/dist/mapbox-gl.css";
import { vec4 } from 'gl-matrix';
import { MxMapAddRasterTileLayer } from "mxcad";
const _project = _Map.prototype.project
// 添加地图的数据
interface BaseMapData {
zoom?: number,
center?: [number, number],
pitch?: number,
sprite?: string,
sources: {
[x: string]: AnySourceData
},
layers: AnyLayer[]
}
// 添加地图的参数
interface BaseMapOptions {
styleid: number | string,
isBaseMap?: boolean
}
// 扩展重写Map类
export class Map extends _Map {
public dom_mousePos(event: any) {
return this.dom_mousePos_imp(this.getCanvasContainer(), event)
}
public lnglat_to_mercator(lng: any, lat: any): any {
let pt = mapboxgl.MercatorCoordinate.fromLngLat(
[lng, lat],
0
);
return pt;
}
public mercator_to_lnglat(x:number, y:number, z:number): any {
let mecatorcoord = new mapboxgl.MercatorCoordinate(x, y, z);
return mecatorcoord.toLngLat();
}
public addRasterTileLayer(layerList: any[], key?: string) {
return MxMapAddRasterTileLayer(this, layerList, key);
}
public mercatorCoordinate_from_LngLat(lngLat: number[], modelAltitude: number): any {
return mapboxgl.MercatorCoordinate.fromLngLat(
lngLat as any,
modelAltitude
);
}
protected getScaledPoint(el: HTMLElement, rect: ClientRect, e: MouseEvent | WheelEvent | Touch) {
const scaling = el.offsetWidth === rect.width ? 1 : el.offsetWidth / rect.width;
return new Point(
(e.clientX - rect.left) * scaling,
(e.clientY - rect.top) * scaling
);
}
protected dom_mousePos_imp(el: HTMLElement, e: MouseEvent | WheelEvent) {
const rect = el.getBoundingClientRect();
return this.getScaledPoint(el, rect, e);
}
// 创建canvas
protected createCavans(width: number, height: number) {
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
return canvas
}
// projectEx
// 用于计算lnglat.alt(高度)
projectEx(t: mapboxgl.LngLat, e: number) {
const _map = this as any
const i = _map.transform.locationCoordinate(LngLat.convert(t)),
n = [i.x * _map.transform.worldSize, i.y * _map.transform.worldSize, null != e ? e : i.toAltitude(), 1] as any;
return vec4.transformMat4(n, n, _map.transform.pixelMatrix), new Point(n[0] / n[3], n[1] / n[3])
}
// 重写 当lnglat.alt存在时参与到位置变换的计算中
project(lnglat: any): Point {
const pr = _project.bind(this)
return arguments.length >= 1 && "object" == typeof arguments[0] && arguments[0].alt ?
this.projectEx(arguments[0], arguments[0].alt) :
pr(lnglat)
}
/**
* 添加图层
* @param layer
* @param beforeId
*/
addLayer(layer: AnyLayer, beforeId?: string) {
// 默认所有通过添加的图层都标识为不是底图
if (!(layer as any).metadata) {
(layer as any).metadata = {
isBaseMap: false,
};
}
super.addLayer(layer, beforeId);
return this
}
/**
* 切换地图
* @param {*} data
* @param {*} options
*/
changeBaseMap(data: BaseMapData, options: BaseMapOptions) {
let opt = Object.assign(options, {
isBaseMap: true,
});
this._removeBaseStyle();
this.addMapStyle(data, opt);
}
/**
* 移除底图
*/
_removeBaseStyle() {
let { layers } = this.getStyle();
for (let layer of layers) {
if (!(layer as any).metadata || ((layer as any).metadata && (layer as any).metadata.isBaseMap == true)) {
this.removeLayer(layer.id);
}
}
}
/**
* 加载标准mapbox样式文件
* @param {*} styleUrl
* @param {*} options
*/
addMapStyle(styleJson: BaseMapData, options: BaseMapOptions) {
let { styleid, isBaseMap } = options;
if (typeof styleJson != 'object') {
throw new TypeError('addMapStyle需要传入对象类型参数');
}
let { zoom, center, pitch } = styleJson;
Object.keys(styleJson.sources).forEach((key) => {
if (!this.getSource(key)) {
this.addSource(key, styleJson.sources[key]);
}
});
if (styleJson.sprite) {
this._addImages(styleJson.sprite);
}
const layerMetaData = {
isBaseMap: isBaseMap || false,
aid: `${styleid}`,
};
for (const layer of styleJson.layers) {
let layerid = layer.id;
(layer as any).metadata = layerMetaData;
if (!this.getLayer(layerid)) {
let firstSpeLayer = this._findFirstSpeLayer();
if (isBaseMap && firstSpeLayer) {
this.addLayer(layer, firstSpeLayer.id);
} else {
this.addLayer(layer);
}
}
}
if (zoom) {
this.setZoom(zoom);
}
if (pitch) {
this.setPitch(pitch);
}
if (center) {
this.setCenter(center);
}
}
/**
* 查询第一个非底图图层
*/
_findFirstSpeLayer() {
let { layers } = this.getStyle();
for (let layer of layers) {
if ((layer as any).metadata && (layer as any).metadata.isBaseMap == false) {
return layer;
}
}
return null;
}
/**
* 添加默认图层组
* 按照点(mx.layer.symbol)、线(mx.layer.line)、面(mx.layer.fill) 分层
*/
addGroupLayer() {
this.addLayer({
id: 'mx.layer.fill',
type: 'fill',
source: {
type: 'geojson',
data: {
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [[]]
},
properties: {}
},
},
});
this.addLayer({
id: 'mx.layer.line',
type: 'line',
source: {
type: 'geojson',
data: {
type: "Feature",
geometry: {
type: "LineString",
coordinates: []
},
properties: {}
},
},
});
this.addLayer({
id: 'mx.layer.symbol',
type: 'symbol',
source: {
type: 'geojson',
data: {
type: "Feature",
geometry: {
type: "Point",
coordinates: []
},
properties: {}
},
},
});
}
/**
* 创建虚线缓冲数组
* */
createDashArraySeq(dash: number[], speed = 1) {
let dashArraySeq = [dash];
for (let i = speed, len = dash[0] + dash[1]; i < len; i += speed) {
const arr: any = [];
if (i <= len - dash[0]) {
arr.push(0, i, dash[0], dash[1] - i);
} else {
const leftFillCnt = i - (len - dash[0]);
arr.push(leftFillCnt, dash[1], dash[0] - leftFillCnt, 0);
}
dashArraySeq.push(arr);
}
return dashArraySeq;
}
_addImages(spritePath: string) {
let self = this;
fetch(`${spritePath}.json`)
.then((result) => result.json())
.then((spriteJson) => {
const img = new Image();
img.onload = function () {
Object.keys(spriteJson).forEach((key) => {
const spriteItem = spriteJson[key];
const { x, y, width, height } = spriteItem;
const canvas = self.createCavans(width, height);
const context = canvas.getContext('2d') as CanvasRenderingContext2D
context.drawImage(img, x, y, width, height, 0, 0, width, height);
const base64Url = canvas.toDataURL('image/png');
self.loadImage(base64Url, (error, simg: HTMLImageElement | ImageBitmap | undefined) => {
if (error) {
console.error(error)
return
}
if (self.hasImage(key)) {
self.removeImage(key);
}
simg && self.addImage(key, simg);
});
});
};
img.crossOrigin = 'anonymous';
img.src = `${spritePath}.png`;
});
}
}
- 将地图图层与cad图层相结合
ts
import mapboxgl from "mapbox-gl";
import { MxMap, mx_gcj02_To_gps84, mx_gps84_To_gcj02 } from "mxcad";
import { Map } from "./mapbox/mapbox";// 引入扩展重写后的Map类
let load_local_title = false;
load_local_title = true;
let is_gcj02 = false;
let cadFile = new URL("../../public/demo/road.dwg.mxweb", import.meta.url).href;
let mapurl = "./map/{z}/{x}/{y}/tile.png";
// 图纸中的中心在地址上的位置,单位经纬度
let mapOrigin = [114.06825863001939, 22.54283198132819];
if (is_gcj02) {
let hx = mx_gps84_To_gcj02(mapOrigin[0], mapOrigin[1]);
mapOrigin = [hx.lng, hx.lat];
}
// 小=右,大=下
// CAD图纸中的中心中,CAD图纸单位
let cadOrigin = [116275.977014, 19273.279085];
let meterInCADUnits = 1.0;
const accessToken = "";
mapboxgl.accessToken = accessToken;
const noStyle = {
version: 8,
sources: {
},
glyphs: "mapbox://fonts/mapbox/{fontstack}/{range}.pbf",
layers: [
],
} as mapboxgl.Style;
let map = new Map({
// Mapbox GL JS 进行地图渲染的 HTML 元素,或该元素的字符串 id 。该指定元素不能有子元素。
container: "map",
// 地图最小缩放级别(0-24)。
minZoom: 0,
// 地图最大缩放级别(0-24)。
maxZoom: 24,
// 地图初始化时的地理中心点。如果构造函数的参数中没有设置 center ,Mapbox GL JS 会在地图样式中进行查找。如果样式中也没定义的话,那么它将默认为 [0, 0] 注意: 为了与 GeoJSON 保持一致,Mapbox GL 采用经度,纬度的顺序 (而不是纬度,经度)。
center: mapOrigin as any,
// 地图初始化时的层级。如果构造函数的参数中没有设置 zoom Mapbox GL JS 会在地图样式中进行查找。如果样式中也没定义的话,那么它将默认为 0
zoom: 16,
// 地图的 Mapbox 配置样式
style: noStyle
});
let mx_map = new MxMap;
mx_map.setCoordinatePointAlignment(mapOrigin, cadOrigin, meterInCADUnits);
mx_map.mxcad.on("openFileComplete", () => {
console.log("onOpenFileComplete");
});
const mode = "SharedArrayBuffer" in window ? "2d" : "2d-st"
map.on('style.load', async function () {
// 创建 MxMap CAD.
map.addGroupLayer();
if (load_local_title) {
map.addLayer({
id: "TitleImageLayer",
type: "raster",
source: {
"type": "raster",
"tiles": [mapurl],
"tileSize": 256
}
});
}
else {
map.addRasterTileLayer([["gdslwzj", "GaoDe.Normal_NoTag.Map"]]);
}
mx_map.create(map, {
// 提供要打开的文件 注意../assets/test.mxweb 是相对路径下的文件地址,
locateFile: (fileName: string) => {
return new URL(`../../node_modules/mxcad/dist/wasm/${mode}/${fileName}`, import.meta.url).href
},
// 在vite中可用通过这样的方式得到该文件正确的的网络地址
fileUrl: cadFile,
middlePan: true,
viewBackgroundColor: load_local_title ? { red: 0, green: 0, blue: 0 } : { red: 255, green: 255, blue: 255 }
});
})
map.on('remove', () => {
})
map.on("click", async function (e) {
let { lng, lat } = e.lngLat;
if (is_gcj02) {
let gps84 = mx_gcj02_To_gps84(lng, lat);
lng = gps84.lng;
lat = gps84.lat;
}
console.log("经纬度坐标:", JSON.stringify([lng, lat]));
let pt = mapboxgl.MercatorCoordinate.fromLngLat(
[lng, lat],
0
);
let ptCAD = mx_map.mercatorCoord2CAD(pt.x, pt.y);
console.log("CAD坐标:", JSON.stringify(ptCAD))
});