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:
- mapbox://styles/mapbox/streets-v10
- mapbox://styles/mapbox/outdoors-v10
- mapbox://styles/mapbox/light-v9
- mapbox://styles/mapbox/dark-v9
- mapbox://styles/mapbox/satellite-v9
- mapbox://styles/mapbox/satellite-streets-v10
- mapbox://styles/mapbox/navigation-preview-day-v2
- mapbox://styles/mapbox/navigation-preview-night-v2
- mapbox://styles/mapbox/navigation-guidance-day-v2
- 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