preface

As front-end development becomes more complex, maps (Gis) have become an essential part of most systems, from the most common Gis visualizations (points, lines, surfaces, various frames, interpolation) to 3D models, scene simulation, scene monitoring, and more. Mainstream smart park, smart city, digital twin and so on are basically inseparable from the development of webGis.

Through this article, we can learn the following:

  • Understand common webGis implementation methods
  • Create maps through Leaflet, cesium and mapBox
  • Draw Marker in Leaflet, Cesium and mapBox in different ways

The code in this article has been submitted to Github. Welcome star.

The code address

Preview the address

Gis scheme in front-end development

An overview of the

leaflet

Leaflet website

Leaflet is a modern and open source JavaScript library developed for building mobile device friendly interactive maps. It was developed by a team of professional contributors led by Vladimir Agafonkin, and although the code is only 38 KB, it has most of the functionality that a developer can use to develop an online map.

Leaflet can quickly build a simple map through a simple Api, and can quickly draw points, lines and planes by combining with other interfaces (Marker, Popup, Icon, Polyline, Polygon, etc.). There are also rich plug-ins in the community. Functions such as heat map, interpolation, aggregation, data visualization and so on can be implemented at low cost. It should be noted that Leaflet can only implement 2D maps.

cesium

Cesium website

Cesium is a javask-based mapping engine that uses WebGL. Cesium supports 3D,2D,2.5D map display, self draw graphics, highlight areas, and provide good touch support, and support most browsers and mobile.

The most important thing of CESium is that it can achieve three-dimensional effect. If there is a demand for loading model (similar to park model) and scene simulation in the project, the method of CESium can be used (in case of insufficient budget and other commercial solutions cannot be purchased).

mapBox

Mapbox website

Mapbox GL JS is a JavaScript library that uses WebGL to render interactive maps using Vector tiles and Mapbox styles as sources. It is part of the Mapbox GL ecosystem, which also includes Mapbox Mobile, a rendering engine written in C++ that is compatible with desktop and Mobile platforms.

Mapbox can also quickly achieve three-dimensional effects and load models. Compared with Cesium, the operation of Mapbox is simpler.

conclusion

The above is just a list of the author often contact a few technical solutions, there are also a lot of solutions on the market, such as OpenLayers, Baidu Map, Autonavi map, etc. SDKS provided by Baidu and Autonavi can also achieve simple GIS effects, but are not suitable for the development of complex effects. The author still recommends using professional GIS solutions for complex map effects. Let’s make a simple analogy for Leaflet, mapBox and cesium from the way of data management:

  • Leaflet manages data by layers. All data (points, lines, and planes) can be viewed as independent layers. Developers only need to mount or uninstall the corresponding layers.

  • Mapbox manages data in the form of resources. The most common way for Mapbox to manage data is to load standard geoJson data, and then specify the corresponding resource ID in subsequent map operations.

  • For general front-end development, Cesium recommends using an entity solution to manage the data in the map, everything being an entity.

In the process of gis code writing, it is necessary to pay attention to the optimization of the code, timely uninstall the monitoring of various events and data destruction, otherwise it is very easy to cause the map jam, especially for Cesium.

The map to create

In order to reduce the coupling relationship between Gis functions and front-end frameworks such as VUE and React, the author abstracts basic Gis functions out of basic classes.

leaflet

encapsulation


export default class LeafletService implements BaseMap {

    // Tile map address
    private layerUrl: string = 'http://{s}.tile.osm.org/{z}/{x}/{y}.png';
    
    constructor(props: LeafletInstanceOptions) {}

    /** * Initialize map instance *@param Type {MapTypeEnum} Map type *@param Props {LeafletInstanceOptions} Leaflet initialization parameter *@protected* /
    public async initMapInstance(type: MapTypeEnum, props: LeafletInstanceOptions) {
        const mapInstanceCache: any = await CommonStore.getInstance('LEAFLET');
        if (mapInstanceCache) {
            return mapInstanceCache;
        }
        const map: Map = new Map(props.id, {
            crs: CRS.EPSG3857,   // Specify the coordinate type
            center: [30.120].// Map center
            maxZoom: 18.// Max map zoom level
            minZoom: 5.// Minimum map zoom level
            maxBounds: latLngBounds(latLng(4.73), latLng(54.135)),
            zoom: 14.// Default scale level
            zoomControl: false.// Whether to display the zoom in/out control. props });// Initialize a WMS base map as a Leaflet map
        const titleLayer: TileLayer.WMS = new TileLayer.WMS(this.layerUrl,{
            format: 'image/png'.layers: 'National County @ National County'.transparent: true});// Add the base map to the map
        map.addLayer(titleLayer);
        // Cache the map instance
        CommonStore.setInstance(type, map);
        returnmap; }}Copy the code

New Map() is used to initialize the Map, where the first attribute is THE DOM container ID, and the second parameter type is described in MapOptions in index.d.ts in Leaflet.

use


const leafletProps: LeafletInstanceOptions = { id: 'leaflet-container'};
const instance = new LeafletService(leafletProps);
// Call the map to initialize the Leaflet
const map: any = await instance.initMapInstance('LEAFLET', { id: 'leaflet-container' });
// The map instance is mounted in the window
(window as any).leafletMap = map;
Copy the code

cesium

encapsulation


export default class CesiumService implements BaseMap{
    constructor(props: CesiumInstanceOptions) {}

    /** * Initialize map instance *@param Type {MapTypeEnum} Map type *@param Props {CesiumInstanceOptions} Initialize parameter *@protected* /
    public async initMapInstance(type: MapTypeEnum, props: CesiumInstanceOptions): Promise<any> {
        const mapInstanceCache: any = await CommonStore.getInstance('CESIUM');
        if (mapInstanceCache) {
            return mapInstanceCache;
        }
        // Instantiate the cesium map
        const map: Viewer = newViewer(props.id, { ... CesiumService.mergeOptions(props), }); CommonStore.setInstance(type, map);
        // Enable earth lightingmap.scene.globe.enableLighting = !! props.enableLighting;// Hide the bottom logo
        const logo: HTMLElement = document.querySelector('.cesium-viewer-bottom') as HTMLElement;
        if (logo) {
            logo.style.display = 'none';
        }
        // Enable 3d effects by default
        map.scene.morphTo3D(0.0);

        // Turn off fast anti-aliasing for clear text
        map.scene.postProcessStages.fxaa.enabled = false;
        map.scene.highDynamicRange = false;

        // Keep cameras out of the ground
        map.scene.screenSpaceCameraController.minimumZoomDistance = 2500;   // It used to be 100
        (map.scene.screenSpaceCameraController as any)._minimumZoomRate = 30000;   // Set the camera zoom rate
        map.clock.onTick.addEventListener(() = > {
            if (map.camera.pitch > 0) {
                map.scene.screenSpaceCameraController.enableTilt = false; }});return map;
    }

    /** * Merge parameters *@param props
     * @private* /
    private static mergeOptions(config: CesiumInstanceOptions): CesiumInstanceOptions {
        const defaultParams: CesiumInstanceOptions = {
            id: config.id,
            animation: config.animation || false.baseLayerPicker: config.baseLayerPicker || false.fullscreenButton: config.fullscreenButton || false.vrButton: config.vrButton || false.geocoder: config.geocoder || false.homeButton: config.homeButton || false.infoBox: config.infoBox || false.sceneModePicker: config.sceneModePicker || false.selectionIndicator: config.selectionIndicator || false.timeline: config.timeline || false.navigationHelpButton: config.navigationHelpButton || false.scene3DOnly: true.navigationInstructionsInitiallyVisible: false.showRenderLoopErrors: false.imageryProvider: (config.templateImageLayerUrl
                ? new UrlTemplateImageryProvider({
                    url: config.templateImageLayerUrl,
                })
                : null) as UrlTemplateImageryProvider,
        };
        returndefaultParams; }}Copy the code

Which initializes the Map using the new Map () operation, one of the first property to dom container id, the second parameter types. The index of the cesium in which s in the Viewer. ConstructorOptions description;

use


const cesiumProps: CesiumInstanceOptions = { id: 'cesium-container' };
const mapInstance = new CesiumService(cesiumProps);
// Initialize the cesium map
const map: Viewer = await this.cesiumMapInstance.initMapInstance('CESIUM', { id: 'cesium-container' });
// The map instance is mounted in the window
(window as any).cesiumMap = map;
Copy the code

mapbox

encapsulation


export default class MapBoxService extends MapService {
    constructor(props: MapBoxInstanceOptions) {
        super(a); }/** * Initialize map instance {MapTypeEnum} Map type *@param Type {MapBoxInstanceOptions} Map initialization parameter *@param props
     * @protected* /
    public async initMapInstance(type: MapTypeEnum, props: MapBoxInstanceOptions) {
        const mapInstanceCache: any = await CommonStore.getInstance('MAPBOX');
        if (mapInstanceCache) {
            return mapInstanceCache;
        }
        const map: Map = new Map({
            container: props.id,
            style: 'mapbox://styles/mapbox/satellite-v9'.// Mapbox presets several styles
            center: [120.30].pitch: 60.bearing: 80.maxZoom: 18.minZoom: 5.zoom: 9.// You need to go to the official mapbox website to register the application to get the token
            accessToken: 'pk.eyJ1IjoiY2FueXVlZ29uZ3ppIiwiYSI6ImNrcW9sOW5jajAxMDQyd3AzenlxNW80aHYifQ.0Nz5nOOxi4-qqzf2od3ZRA'. props }); CommonStore.setInstance(type, map);
        returnmap; }}Copy the code

New Map() is used to initialize the Map, where the first property is the DOM container ID, and the second parameter type is described in mapbox index.d.ts MapboxOptions.

The default style of the Mapbox is as follows:

  1. mapbox://styles/mapbox/streets-v10
  2. mapbox://styles/mapbox/outdoors-v10
  3. mapbox://styles/mapbox/light-v9
  4. mapbox://styles/mapbox/dark-v9
  5. mapbox://styles/mapbox/satellite-v9
  6. mapbox://styles/mapbox/satellite-streets-v10
  7. mapbox://styles/mapbox/navigation-preview-day-v2
  8. mapbox://styles/mapbox/navigation-preview-night-v2
  9. mapbox://styles/mapbox/navigation-guidance-day-v2
  10. mapbox://styles/mapbox/navigation-guidance-night-v2

use


const mapboxProps: MapBoxInstanceOptions = { id: 'mapbox-container' };
const instance = new MapBoxService(mapboxProps);
this.setMapInstance({ mapType: 'MAPBOX', instance });
const map: any = await instance.initMapInstance('MAPBOX', { id: 'mapbox-container' });
// The map instance is mounted in the window
(window as any).mapboxMap = map;

Copy the code

Point to draw

leaflet

There are many ways to draw Marker in Leaflet, and three are mainly listed here: CircleMarker (ordinary circle), IconMarker (icon) and DivIconMarker (Dom); In the case of big data, rendering point positions by DivIconMarker will cause page delay, so the solution with the lowest cost is to turn the point position layer into canvas layer and add it to the map.

CircleMarker

Rendering point position by CircleMarker is the most basic way in Leaflet. Marker can be quickly created by new CircleMarker(). The first parameter is the array of position information (the first parameter is dimension, the second parameter is longitude). The second parameter is CircleMarkerOptions (see index.d.ts in Leaflet for this parameter).

/** * Render normal dots */
async function renderNormalCircleMarkerLeaflet() {
      Each piece of data contains latitude and longitude information
      const dataJson: any[] = await import('.. /.. /mock/stationList1.json');
      const markerList: any[] = [];
      for (let i = 0; i < dataJson.length; i++) {
          // Convert the latitude and longitude
          const latitude = parseFloat(dataJson[i].latitude);
          const longitude = parseFloat(dataJson[i].longitude);
          // Leaflet is special. Marker location information dimension is in front and longitude is in behind
          const marker: any = new CircleMarker([latitude, longitude], {
              radius: 8}); markerList.push(marker); }// Add all markers to a layer group. When removing points, you only need to remove the whole layer
      const layerGroup: LayerGroup = new LayerGroup(markerList, {});
      // Add the layer group to the map
      (window as any).leafletMap.addLayer(layerGroup);
}

Copy the code

IconMarker

IconMarker is a common way to render points in Leaflet. Markers of different ICONS are rendered according to the type of point position. Markers can be quickly created by new Marker(). The second parameter is MarkerOptions (see Index. D. ts of Leaflet for parameters). Marker of this type mainly needs to build an Icon of Icon type.

/** * Render IconMarker */
async function renderNormalIconMarkerLeaflet() {
      Each piece of data contains latitude and longitude information
      const dataJson: any[] = await import('.. /.. /mock/stationList1.json');
      const markerList: any[] = [];
      for (let i = 0; i < dataJson.length; i++) {
          const latitude = parseFloat(dataJson[i].latitude);
          const longitude = parseFloat(dataJson[i].longitude);
          // Create an icon
          const icon: Icon = new Icon({
              // Specify the image of icon
              iconUrl: require('.. /.. /assets/map/site.png')});// Leaflet is special. Marker location information dimension is in front and longitude is in behind
          const marker: any = new Marker([latitude, longitude], {
              icon: icon,
          });
          markerList.push(marker);
      }
      // Add all markers to a layer group. When removing points, you only need to remove the whole layer
      const layerGroup: LayerGroup = new LayerGroup(markerList, {});
      // Add the layer group to the map
      (window as any).leafletMap.addLayer(layerGroup);
}

Copy the code

DivIconMarker

DivIconMarker is a way of using Dom to render points in Leaflet, which is generally mainly used to draw point positions with overly complex effects. Marker can be quickly created by new Marker(). The first parameter is the array of position information (the first is dimension, the second is longitude). The second parameter is MarkerOptions (see Index. D. ts of Leaflet for parameters). Marker of this type mainly needs to build an icon of DivIcon type.

/** * Render DivMarker */
async function renderDivIconMarkerLeaflet() {
      Each piece of data contains latitude and longitude information
      const dataJson: any[] = await import('.. /.. /mock/stationList1.json');
      const markerList: any[] = [];
      for (let i = 0; i < dataJson.length; i++) {
          const latitude = parseFloat(dataJson[i].latitude);
          const longitude = parseFloat(dataJson[i].longitude);
          // Create an icon of type DOM
          const icon: DivIcon = new DivIcon({
              html: `
                        <div class="leaflet-icon-item">
                          <span>${i}</span>
                        </div>
                      `.className: 'leaflet-div-icon'.// Specify a class for marker
          });
          // Leaflet is special. Marker location information dimension is in front and longitude is in behind
          const marker: any = new Marker([latitude, longitude], {
              icon: icon,
          });
          markerList.push(marker);
      }
      // Add all markers to a layer group. When removing points, you only need to remove the whole layer
      const layerGroup: LayerGroup = new LayerGroup(markerList, {});
      // Add the layer group to the map
      (window as any).leafletMap.addLayer(layerGroup);
}

Copy the code

cesium

There are mainly two ways to draw Marker in Cesium. The first way is to draw in Entity mode, and the second way is to draw point positions by loading geoJson data.

Entity

Entity mode is the most basic class in the cesium. You can draw any layer based on the Entity. The Entity constructor has many parameters.

/** * Render the Entity type */
async function renderEntityMarkerCesium() {
    // Simulate point data
    const dataJson: any[] = await import('.. /.. /mock/stationList1.json');
    const markerList: Entity[] = [];
    for (let i = 0; i < dataJson.length; i++) {
        const latitude = parseFloat(dataJson[i].latitude);
        const longitude = parseFloat(dataJson[i].longitude);
        // Create an entity
        const marker: Entity = new Entity({
            name: dataJson[i].name,   // The name of the point
            description: JSON.stringify(dataJson[i]),  // Bind each point to some other property
            position: Cartesian3.fromDegrees(longitude, latitude),  // Convert the longitude and latitude coordinates WGS84 to Cartesian3
            billboard: {
                image: require('.. /.. /assets/map/site-5.png'), // Point to the picture
                scale: 1.pixelOffset: new Cartesian2(0, -10),  // The position offset}}); normalIcon.push(marker); (window as any).cesiumMap.entities.add(marker);
    }
    return markerList;
}
Copy the code

geoJson

The geoJson data format is the most common data interaction format in geospatial systems. Cesium can load data to the map using the geojsondatasorce.load () method, and then reload the point entity information.

For those not familiar with geoJson, see geoJson Data Interaction

/** * build a standard GEO data */
async function builGeoJsonCesium() {
    // Simulate point data
    const dataJson: any[] = await import('.. /.. /mock/stationList1.json');
    // Declare a standard GEO data
    let GeoJsonHeader: any = {
        type: 'FeatureCollection'.crs: {
            type: 'name'.properties: { name: 'urn: ogc: def: CRS: ogc: 1.3: CRS84'}},features: features,
    };
    for (let i = 0; i < dataJson.length; i++) {
        constpoint = { ... dataJson[i] };// Convert the longitude and latitude
        const latitude = parseFloat(dataJson[i].latitude);
        const longitude = parseFloat(dataJson[i].longitude);
        let featureItem = {
            type: 'Feature'.properties: { ...point },
            geometry: { type: 'Point'.coordinates: [longitude, latitude, 0]}}; GeoJsonHeader.features.push(featureItem); }return GeoJsonHeader;
}
  

/** * Render the point of type geoJson */
async function renderEntityMarkerCesium() {
    // Simulate point data
    const dataJson: any[] = await import('.. /.. /mock/stationList1.json');
    // Build a geoJson data
    const geoJson = await builGeoJsonCesium();
    // Load the geoJosn data into the map, and then unload the geoJsonResource if you want to remove the point
    const geoJsonResource = await GeoJsonDataSource.load(geoJson);
    geoJsonMarker = await (window as any).cesiumMap.dataSources.add(geoJsonResource);
    const entities = geoJsonResource.entities.values;
    for (let i = 0; i < entities.length; i++) {
        const entity = entities[i];
        entity.billboard = undefined;
        entity.point = new PointGraphics({
            color: Color.RED,
            pixelSize: 10}); }return markerList;
}
Copy the code

mapbox

There are two main ways to draw Marker in mapbox. The first is to draw by Marker, and the second is to draw point positions by loading geoJson data. In this paper, only the second scheme is used to render point positions, and mapbox rendering points requires many tool functions. So it’s encapsulated here as a utility class.

// Utility functions
class MapBoxUtil {
    /** * Add data resource (update data resource) *@param SourceName <string> Resource name *@param JsonData <GeoJson> Geographic data *@param map
     * @param Options <Object> (Optional) */
    public async addSourceToMap(sourceName: string, jsonData: any, map: Map, options: Record<string.any> = {}) {
          If not, add resources to the map, otherwise update the data with setData
          if(! map.getSource(sourceName)) { map.addSource(sourceName, {type: 'geojson'.data: jsonData, ... options }); }else {
              const source: AnySourceImpl = map.getSource(sourceName);
              (source as any).setData(jsonData); }}/** * Add images to map *@param ImagesObj {Object} Image Object *@param Map {Object} mapBox */
    public async loadImages(imagesObj: Record<string.any>, map: any) {
        return new Promise(async (resolve) => {
            try {
                let imageLoadPromise: any[] = [];
                for (let key in imagesMap) {
                    let imgSource: string = key;
                    if(! (window as any)._imgSourcePath) {
                        (window as any)._imgSourcePath = {};
                    }
                    if(! (window as any)._imgSourcePath.hasOwnProperty(imgSource)) {
                        (window as any)._imgSourcePath[imgSource] = imagesMap[key];
                    }
                    if(! map.hasImage(imgSource)) {// Picture data
                        let imageData: any;
                        try {
                            // Here is the base64 file
                            imageData = imagesMap[imgSource];
                        } catch (e) {
                            throw new Error(e);
                        }
                        let img = new Image();
                        img.src = imageData;
                        imageLoadPromise.push(
                            new Promise(resolve= > {
                                img.onload = e= > {
                                    // Avoid repeated loading
                                    if(! map.hasImage(imgSource)) { map.addImage(imgSource, img); } resolve(e); }; })); }}if(imageLoadPromise.length ! = =0) {
                    await Promise.all(imageLoadPromise);
                    resolve(imagesMap);
                } else{ resolve(imagesMap); }}catch (e) {
                console.log(e); resolve(imagesMap); }}); }/** * Render normal Marker layer to map *@param layerOption
     * @param map
     * @param andShow
     * @param beforeLayerId* /
    public async renderMarkerLayer(layerOption: Record<string.any>, map: Map, andShow = true, beforeLayerId? :string) {
        return new Promise(resolve= > {
            // Check whether the source referenced by the layer exists
            let layerId: string = layerOption.id;
            let tempSource: string = layerOption.source;
            if(! tempSource || (Object.prototype.toString.call(tempSource) === '[object String]' && !map.getSource(tempSource))) {
                throw new Error(` (_renderMapLayer:) layer${layerId}Directed resources${tempSource}There is no `);
            }
            if(! (window as any)._mapLayerIdArr) {
                (window as any)._mapLayerIdArr = [];
            }
            // window._maplayeridarr records the id of the layer loaded
            if(! (window as any)._mapLayerIdArr.includes(layerId) && layerId.indexOf('Cluster') = = = -1) {(window as any)._mapLayerIdArr.push(layerId);
            }
            // Load the layer
            if(! map.getLayer(layerId)) { map.addLayer(layerOptionas mapboxgl.AnyLayer, beforeLayerId);
                return resolve(layerId);
            } else {
                // This layer already exists in the map
                if (andShow) this.showOrHideMapLayerById(layerId, 'show', map);
                // The layer name is no longer returned. (Without binding the event again)resolve(layerId); }}); }}Copy the code
const mapBoxUtil = new MapBoxUtil()
/** * Build the standard GeoJson data *@param dataList
 * @param code* /
function buildGeoJSONDataMapBox(dataList: any[], code: string) {
    let GeoJsonHeader: any = {
        type: 'FeatureCollection'.crs: {
            type: 'name'.properties: { name: 'urn: ogc: def: CRS: ogc: 1.3: CRS84'}},features: features,
    };
    for (let i = 0; i < dataList.length; i++) {
        constpoint = { ... dataList[i] };let lon = parseFloat(point.longitude);
        let lat = parseFloat(point.latitude);
        // TODO judgment error, later improvement
        let coordinates = lon > lat ? [lon, lat, 0] : [lat, lon, 0]; // The longitude and latitude records are reversed
        // Add the symbolImgName field to match the icon resource.
        if (code) {
            point['typeCode'] = point.hasOwnProperty('typeCode')? point.typeCode : code; point['symbolImgName'] = 'site5';   // Specify the id of the image
        }
        let featureItem = {
            type: 'Feature'.properties: { ...point },
            geometry: { type: 'Point'.coordinates: coordinates },
        };
        GeoJsonHeader.features.push(featureItem);
    }
    return GeoJsonHeader;
}

/** * Render the Entity type */
async function renderResourceMarkerMapBox() {
    const dataJson: any[] = await import('.. /.. /mock/stationList1.json');
    await mapBoxUtil.loadImages({
        site5: require('.. /.. /assets/map/site-5.png'),},window as any).mapboxMap);
    const sourceId: string = 'test-source';
    let jsonData = buildGeoJSONDataMapBox(dataJson, '1');
    await mapBoxUtil.addSourceToMap(sourceId, jsonData, (window as any).mapboxMap);
    return await mapBoxUtil.renderMarkerLayer(
        {
            id: 'test-layer'./ / layer id
            type: 'symbol'.// Specify marker type
            source: sourceId,     // Resources needed to render the point position
            filter: ['= ='.'typeCode'.'1'].// Specify fields
            layout: {
                'icon-image': '{symbolImgName}'.// The source of the image
                'icon-size': 0.8.'icon-ignore-placement': true.// Ignore collision detection
                visibility: 'visible',}},window as any).mapboxMap,
    );
}
Copy the code

The last

This article gives a brief introduction to three common webGis schemes, from map initialization to point rendering. The next article mainly introduces how to achieve custom point frame under the three technical schemes. All GIS effects in this article can be previewed through the online address.

The author is not a professional GIS development, if there are professional problems in the article wrong, welcome you to correct.

The code address

Preview the address