The official MapboxGL documentation is full of APIS, cases, and style standards. Most of the functionality in basic development is covered in the Demo. Some of the things I encountered in the process of use that I think can be summarized are quite miscellaneous. I hope it can be helpful to you. Follow-up, continuous improvement.

Note: The following key code functions are written in a class that inherits mapBoxgl. Map, i.e. : This refers to the mapBoxgl. Map instance.

Switch the reproduction

I briefly explained the implementation principle in my previous article here.

When initializing the map in mapbox-GL, we pass in the style.json or style.json address, of course, as the base image, and then draw all the layers as thematic images. When we switch the base map, we can not affect the upper thematic map, and it is better not to refresh the thematic map.

My previous version of the implementation added an empty layer to the top layer by default after the map load was complete. Below the empty layer is the base image and above is the thematic image. This is not good, I personally think that each layer should have a field to identify whether it is a base image or a thematic image, drawing will not help us to identify. The addLayer method of the map I overwrote defaults the isBaseMap of the metadata object in the layer to false unless specifically specified. This way, except for creating a map and passing in a map with no Metada information, all layers will contain an isBaseMap identifier whenever we call addLayer. Therefore, to determine whether it is a base map, we can do the following:

  • In the Layer object, there is no metadata field, is the base map;
  • The layer object contains metadata, but metadata. IsBaseMap is true.
    addLayer(layer, beforeId) {
            if(! layer.metadata) { layer.metadata = {isBaseMap: false}; }super.addLayer(layer, beforeId);
        }
Copy the code
        /** * Toggle maps *@param {*} data
         * @param {*} options* /
    changeBaseMap(data, options) {
            let opt = Object.assign(options, {
                isBaseMap: true});this._removeBaseStyle();
            this.addMapStyle(data, opt);
        }
       /** ** remove the base map */
    _removeBaseStyle() {
            let { layers } = this.getStyle();
            for (let layer of layers) {
                if(! layer.metadata || (layer.metadata && layer.metadata.isBaseMap ==true)) {
                    this.removeLayer(layer.id); }}}/** * Load the standard mapbox style file *@param {*} styleUrl
         * @param {*} options* /
    addMapStyle(styleJson, options) {
            let { styleid, isBaseMap } = options;
            if (typeofstyleJson ! ='object') {
                throw new TypeError('addMapStyle requires an 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.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); }}/** * Parse Sprite image, wear canvas to draw icon *@param {*} spritePath* /
    _addImages(spritePath) {
            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 = createCavans(width, height);
                            const context = canvas.getContext('2d');
                            context.drawImage(img, x, y, width, height, 0.0, width, height);
                            const base64Url = canvas.toDataURL('image/png');
                            self.loadImage(base64Url, (error, simg) = > {
                                if (self.hasImage(key)) {
                                    self.removeImage(key);
                                }
                                // console.log(1);
                                self.addImage(key, simg);
                            });
                        });
                    };
                    img.crossOrigin = 'anonymous';
                    img.src = `${spritePath}.png`;
                });
        }
        /** * Query the first non-base layer */
    _findFirstSpeLayer() {
            let { layers } = this.getStyle();
            for (let layer of layers) {
                if (layer.metadata && layer.metadata.isBaseMap == false) {
                    returnlayer; }}return null;
        }
Copy the code

Map service loading

I explained this briefly in an earlier article. Look at this.

Here we consider WGS84, CGCS2000, web Mercator (EPSG:4326, EPSG:4490, EPSG:3857).

  • wgs84
  • cgcs2000
  • Web Mercator

Native MapBoxGL supports 3857; For wGS84 and CGCS2000 spherical coordinates, we use the mapBoxGL extension library to load @CGCS2000 /mapbox-gl; We loaded the map service, regardless of the difference between WGS84 and CGCS2000. In fact, their reference ellipsoid is very similar, and there is only a slight difference in ellipsoid constant in flattening. Although there is a slight difference, such a slight difference can be ignored under the current measurement accuracy level.

If it is 4326 or 4490, it can be used@cgcs2000/mapbox-glFor service loading, just change the following corresponding 3857 to 4490 or 4326.

ArcGIS slicing service

       /** * Load the ArcGIS slice service *@param {*} url
         * @param {*} options* /
    addArcGISTileLayer(url, options) {
            let { layerid } = options;
            this.addSource(layerid, {
                type: 'raster'.tiles: [`${url}/tile/{z}/{y}/{x}`].tileSize: 256});this.addLayer({
                id: layerid,
                type: 'raster'.source: layerid,
                layout: {},
                paint: {},}); }Copy the code

ArcGIS dynamic service

              /** * Load arcGIS dynamic service *@param {*} url
         * @param {*} options* /
    addArcGISDynamicLayer(url, options) {
        let { layerid,layers } = options;
        this.addSource(layerid, {
            type: 'raster'.tiles: [`${url}/export? dpi=96&transparent=true&format=png8&bbox=&SRS=EPSG:3857&STYLES=${layers}&WIDTH=256&HEIGHT=256&f=imageBBOX={bbox-epsg-3857}`].tileSize: 256});this.addLayer({
            id: layerid,
            type: 'raster'.source: layerid,
            layout: {},
            paint: {},}); }Copy the code

WMTS

       /** * Load the WMS service *@param {*} url
         * @param {*} options* /
    addWMSLayer(url, options) {
            let { layerid, layers } = options;
            this.addSource(layerid, {
                type: 'raster'.tiles: [
                    `${url}? SERVICE = WMS&VERSION = 1.1.1 & REQUEST = GetMap&FORMAT = image/png&TRANSPARENT = true&tiled = true&LAYERS =${layers}&exceptions=application/vnd.ogc.se_inimage&tiles&WIDTH=256&HEIGHT=256&SRS=EPSG:3857&STYLES=&BBOX={bbox-epsg-3857}`,].tileSize: 256});this.addLayer({
                id: layerid,
                type: 'raster'.source: layerid,
                paint: {},}); }Copy the code

WMTS

      /** * Load WMTS *@param {*} url
         * @param {layerid,layer} options* /
    addWMTSLayer(url, options) {
            let { layerid, layer } = options;
            this.addSource(layerid, {
                type: 'raster'.tiles: [
                    `
                 ${url}? SERVICE=WMTS&REQUEST=GetTile&layer=${layer}& Version = 1.0.0 & TILEMATRIX = EPSG: 900913: {} z & TILEMATRIXSET = EPSG: 900913 & format = image % 2 FPNG & TileCol = {x} & TileRow = {} y `,].tileSize: 256});this.addLayer({
                id: layerid,
                type: 'raster'.source: layerid,
                paint: {},}); }Copy the code

The model is loaded

The official website has the case of loading GLTF model. The official use of three.js combined with mapbox-GL custom Layer, however, this is too primitive. I recommend Threebox, and I’ve written about mapBoxGL + three.js development practices. But the writing is too simple. Here’s another library forked with threebox to try.

MapboxGL camera syncs with Cesium camera

In the previous project, we wanted to load tilting camera data in 3D-tiles format in mapBoxgl. The technical directions were deck.gl and loaders.gl. Finally came out, but not in terms of data optimization performance. Finally, the technical route of mapBoxGL + Cesium fusion is selected. If mapBox + Cesium is combined, naturally it is to solve the problem of camera synchronization in switching scenes. See ** here for implementation details.

Mapbox – gl camera

  • Center Location center
  • Pitch map Angle
  • Bearing map rotation Angle
  • Zoom Map level

Cesium camera

Camera position and Angle (Euler Angle)

  • Position Camera position
  • Pitch Angle of camera head up and head down
  • Heading camera left and right, shake head Angle
  • Roll the camera rotates an Angle along the axis of looking direction

The key formula

  • Calculate mapbox map zoom level according to cesium camera height
function getElevationByZoom(map, zoom) {
	// Long axis 6378137
 return (2 * Math.PI * 6378137.0) / Math.pow(2, zoom) / 2 / Math.tan(map.transform._fov / 2);
}
Copy the code
  • Calculate the camera height in Cesium according to zoom level in Mapbox
function getZoomByElevation(map, elevation) {
	return Math.log2((2 * Math.PI * 6378137.0)/(2 * elevation * Math.tan(map.transform._fov / 2)));
}
Copy the code

Control layer order

Initially, when developing with MapBox, I found it inconvenient to control the layer order, compared to OpenLayers. Even though the second argument to mapBoxgl’s addLayer method allows us to pass in beforeId, which is which layer the current layer is inserted in front of, if you don’t, it is added to the top. This is fine, but make sure you keep the beforeId layer in the map. In some scenarios, we want all of my addLayer layers to be grouped by dots, lines, faces, and volumes. Whenever I add a layer, I want the dots to always be on top, and then the lines, and then the faces and volumes. When I add a face, I put the current face on top of the group of faces, so that I don’t have to load a big face and bam, it covers all the layers, which is bad.

The implementation method is also relatively simple. After the map load is completed, we secretly load multiple empty layers in the map. Here is the grouping boundary we say. BeforeId points to these boundaries, which must exist because you added them yourself after the map load is complete.

    /** * Add the default layer group */
    addGroupLayer() {
            this.addLayer({
                id: 'cityfun.null.fill'.type: 'fill'.source: {
                    type: 'geojson'.data: null,}});this.addLayer({
                id: 'cityfun.null.line'.type: 'line'.source: {
                    type: 'geojson'.data: null,}});this.addLayer({
                id: 'cityfun.null.symbol'.type: 'symbol'.source: {
                    type: 'geojson'.data: null,}}); }/// Use it this way -- it is easy to control the layer order
this.addLayer(layer,'cityfun.null.line') 
Copy the code

Cancel the map subscription event

The mapBoxgl event.off method can unsubscribe a map event. Note that unsubscribe a map event requires passing in a callback reference. We can either register the event off or destroy the component off.

bindMapEvent() {
  const self = this;
  if (this.callback) {
    this.map.off('click'.this.callback);
  }
  this.callback = function (e) {
    // this
    // self
  };
  this.map.on('click'.this.callback);
}
Copy the code

Resolve layer event conflicts

If we need to bind a click event to two layers on a map that are superimposed and repasted on each other, clicking on the top layer will trigger the callback function for the bottom layer. This is of course natural, as we do need to subscribe to click events for both layers. In some scenarios, we want to only trigger the click event on the top layer.

E.preventdefault () will prevent mapbox from writing the default behavior, it will not prevent some conflicts in our own code. Fortunately, after calling e.preventDefault(), e.de Faultprevented is changed to true. So we can do this:

Note:

  • Using this method does not prevent the callback function from being called. In fact, it is still callede.defaultPreventedTrue prevents subsequent code from executing.
  • Layer-01 on top of layer-02,*Line of code should be there支那Execute in front.
let callback_01 = function (e) {
        if (e.defaultPrevented) {
          return;
        }
        e.preventDefault();
       // do something
      };
let callback_02 = function (e) {
        if (e.defaultPrevented) {
          return;
        }
        e.preventDefault();
       // do something
      };
 mapboxmap.on('click'.'layer-01',callback_01);  / / *
 mapboxmap.on('click'.'layer-02',callback_02); / / * *
Copy the code

Business module layer string problem

Our front-end framework uses Angular. We develop many systems, most of which share a common base map. That is, the base diagram of service module switching is not refreshed with service module switching. Asynchronous programming, you do not know when the network request initiated by each business module will get the data, before the data above, I left the current module, data is drawn to the layer, that is wrong. So, you need to manage asynchronous tasks for each module.

Ng httpClient network requests are returned as a subscriptable stream. When multiple interface requests are made, the business module component is destroyed before the result is responded, and we manually unsubscribe to avoid leaving the current module and continuing to subscribe to the data returned by the drawing interface.

 for (const item of this.buslines) {
    const xhr = this.http.get(`./assets/mock.data/busEcode/20161212_${item}.txt`,
      { responseType: 'text' as 'json' }).subscribe(encodeGPS= > {
          // map draw gps line
        });
      this.xhrs.push(xhr);
    }
 
// You should unsubscribe when the build is destroyed
this.xhrs.forEach(item= > {
      item.unsubscribe();
});
Copy the code

Cannot hover highlight layer elements

Note that if you open its GeoJSON address, each feature has an ID field, which is required. This is not the ID of the properties feature. If you are a GeoJSON data source, you can set generateId to true in the data source, click here. Normally you just set this. But… There is a bug, I don’t know if you have encountered it, but it’s not easy to describe it, but some elements do not unhighlight when hover leaves. Finally, I manually set an ID for each feature. If you have encountered similar problems, try adding feature ID attributes manually.

generatedFeatureId(geojson) {
    if (geojson && geojson.features) {
      geojson.features.forEach((element, index) = > {
        element['id'] = index + 1;
      });
      return geojson;
    }
    return geojson;
}
Copy the code

Map location

Turf is a good analytical library for handling space. In the case of the mabpx fitBounds function, we fit into an element, or possibly a layer. Instead of calculating the outboard rectangle yourself, use the Turf space handling function directly.

import { bbox } from '@turf/turf';
var bound = bbox({
          type: 'FeatureCollection'.features: [],});this.mapboxglmap.fitBounds(bound, { padding: 50 });

Copy the code