Skip to content
On this page

mapbox combined with cad drawings

mapbox is an interactive map rendered via webgl, while mxcad is also implemented using webgl rendering. Therefore, we can combine the custom layer provided by mapbox with the control instance rendering of mxcad to show the cad layer and map.

If you are not familiar with mapbox, please refer to the mapbox official documentation.

The following code is mapbox and mxcad combined with the simplest example, running effect please refer to the CAD example demo online.

For more details, please visit our official website: CAD and GIS Integration Instructions

demo source code

  1. Download the Cloud map development kit

Click here to see how to download cloud map development kit

  1. Find the demo source code

After downloading the cloud image development kit, unzip it, double-click Mx3dServer.exe, click Open GIS DEMO directory button to enter the demo source directory

  1. Run demo
sh
npm install
npm run dev

Precautions

  1. mapbox-gl version and patch The patches under the root directory of the demo source code must contain mapbox-gl patches, and mapbox-gl must be of 2.8.2; otherwise, the patches cannot be used normally

Copy the patches directory to the root directory of your project when implementing your own project

Then install the patch repair

sh
npm i patch-package -D
npx patch-package

Finally, add it to package.json so that the dependency package will be patched automatically when installed later.

json
"scripts": {
    "postinstall": "patch-package"
}
  1. Map class rewriting

It is necessary to rewrite the Map class of mapbox-gl and add some necessary methods when implementing it.

mxcad will use these methods, so just follow the Map class implemented in the latest demo source code.

Code example

  1. Install the dependency package
sh
# Install the corresponding dependency package first
npm install mapbox-gl mxcad gl-matrix
  1. Draw interface container
html
<div id="map" style="width: 100vw; height: 99vh; overflow: hidden;"></div>
  1. Rewrite the Map class as required
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

// Add map data
interface BaseMapData {
zoom?: number,
center?: [number, number],
pitch?: number,
sprite?: string,
sources: {
    [x: string]: AnySourceData
},
layers: AnyLayer[]
}


// Add parameters to the map
interface BaseMapOptions {
styleid: number | string,
isBaseMap?: boolean
}


// The extension overrides the Map class
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);
}

// Create canvas
protected createCavans(width: number, height: number) {
    const canvas = document.createElement('canvas')
    canvas.width = width
    canvas.height = height
    return canvas
}


// projectEx
// Used to calculate lnglat.alt (height)
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])
}
// Overrides participate in the calculation of the position change when lnglat.alt is present
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)
}

/**
 *  Add layer
 * @param layer
 * @param beforeId
*/
addLayer(layer: AnyLayer, beforeId?: string) {
    // By default, all layers that are added are identified as non-base
    if (!(layer as any).metadata) {
        (layer as any).metadata = {
            isBaseMap: false,
        };
    }
    super.addLayer(layer, beforeId);
    return this
}
/**
 * Switch map
 * @param {*} data
 * @param {*} options
 */
changeBaseMap(data: BaseMapData, options: BaseMapOptions) {
    let opt = Object.assign(options, {
        isBaseMap: true,
    });
    this._removeBaseStyle();
    this.addMapStyle(data, opt);
}
/**
 * Remove base map
 */
_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);
        }
    }
}
/**
* Load the standard mapbox style file
* @param {*} styleUrl
* @param {*} options
*/
addMapStyle(styleJson: BaseMapData, options: BaseMapOptions) {
    let { styleid, isBaseMap } = options;
    if (typeof styleJson != 'object') {
        throw new TypeError('addMapStyle requires object type argument ');
    }
    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);
    }
}

/**
 * Query the first non-base layer
 */
_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;
}
/**
 * Add a default layer group
 * Layer by point (mx.layer.symbol), line (mx.layer.line), surface (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: {}
            },
        },
    });
}
/** 
 * Create a dashed buffer array
 * */
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. Combine map layers with cad layers
ts
import mapboxgl from "mapbox-gl";
import { MxMap, mx_gcj02_To_gps84, mx_gps84_To_gcj02 } from "mxcad";
import { Map } from "./mapbox/mapbox";// Introduce the extended rewritten Map class

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

//  The location of the center in the drawing on the address, in units of latitude and longitude
let mapOrigin = [114.06825863001939, 22.54283198132819];

if (is_gcj02) {
    let hx = mx_gps84_To_gcj02(mapOrigin[0], mapOrigin[1]);
    mapOrigin = [hx.lng, hx.lat];
}

// Small = right, large = down
// CAD drawing in the center, CAD drawing unit
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 renders the HTML element of the map, or the string id of that element. This specified element cannot have child elements.
    container: "map",

    // Map minimum zoom level (0-24).
    minZoom: 0,

    // Map maximum zoom level (0-24).
    maxZoom: 24,

    // The geographic center point at the time of map initialization. If center is not set in the constructor's argument, Mapbox GL JS looks for it in the map style. If it is not defined in the style, then it defaults to [0, 0] Note: In order to be consistent with GeoJSON, Mapbox GL uses the longitude, latitude order (not latitude, longitude).
    center: mapOrigin as any,

    // Level of map initialization. zoom Mapbox GL JS looks in the map style if no parameter is set to the constructor. If it is not defined in the style, it will default to 0
    zoom: 16,

    // Mapbox configuration style for the map
    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 () {
    // Create 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, {
        // Provide the file to open Note... /assets/test.mxweb is the address of the file in the relative path,
        locateFile: (fileName: string) => {
        return new URL(`.. /.. /node_modules/mxcad/dist/wasm/${mode}/${fileName}`, import.meta.url).href
        },
        // vite can be used in this way to get the correct network address of the file
        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("Latitude and longitude coordinates:", JSON.stringify([lng, lat]));
    let pt = mapboxgl.MercatorCoordinate.fromLngLat(
        [lng, lat],
        0
    );
    let ptCAD = mx_map.mercatorCoord2CAD(pt.x, pt.y);
    console.log("CAD coordinate:", JSON.stringify(ptCAD))
});