Skip to content
On this page

mapbox与cad图纸结合

mapbox是通过webgl渲染的交互式地图,而 mxcad 也是利用webgl渲染实现的。所以我们可以通过 mapbox 提供的自定义图层与 mxcad 的控件实例渲染结合,将cad图层与地图展现出来。

如果不了解 mapbox 可以参考 mapbox官方文档

下面代码是 mapbox 和 mxcad 结合使用的最简示例,运行效果请参考 在线CAD示例demo

若想要了解更加详细的内容请前往我们的官网:CAD与GIS集成说明

demo源码

  1. 下载云图开发包

点击查看如何下载云图开发包

  1. 找到demo源码

下载好云图开发包后解压, 双击运行 Mx3dServer.exe, 点击打开GIS DEMO目录按钮进入demo源码目录

  1. 运行demo
sh
npm install
npm run dev

注意事项

  1. 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"
}
  1. Map类的重写

在实现时需要将mapbox-gl 的 Map类进行重写,添加一些必要的方法。

mxcad会使用到这些方法,所以只需要按照最新的demo源码中实现的Map类为准就好。

代码示例

  1. 安装对应依赖包
sh
# 先安装对应的依赖包
npm install mapbox-gl mxcad gl-matrix
  1. 绘制界面容器
html
<div id="map" style="width: 100vw; height: 99vh; overflow: hidden;"></div>
  1. 根据需求扩展重写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`;
        });
}
}
  1. 将地图图层与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))
});