Today, I will take you to write a simple JS 2D map engine. Due to the limited space, this paper only realizes the function of loading tiles. The subsequent articles will gradually realize the functions of loading marker, vector data, vector tiles, event interaction and so on. At present, there are many excellent engines, such as OpenLayer, ArcGIS, Leaflet, etc. The purpose of this article is not to surpass, but to achieve its core functions and explain its basic principles with simplified codes. Because almost all two-dimensional map engines have the same idea of implementation, I always believe that the core of learning is to learn ideas and principles, so that we can calmly and unrestrained face the changing business.

1. Point/two-dimensional vector class: Point

class Point{ constructor(x, y) { this.x = x; this.y = y; } add(dx, dy) { return new Point( this.x + dx, this.y + dy ); } sub(dx, dy) { return new Point( this.x - dx, this.y - dy ); } multiply(mx, my) { return new Point( this.x * mx, this.y * my ); {if} divide (dx, dy) (0 = = dx | | 0 = = dy) {throw "divisor cannot be zero; } return new Point( this.x / dx, this.y / dy ); } remainder(rx, ry) { return new Point( this.x % rx, this.y % ry ); } round(num) { this.x = Math.round(this.x, num); this.y = Math.round(this.y, num); return this; } ceil() { this.x = Math.ceil(this.x); this.y = Math.ceil(this.y); return this; } floor() { this.x = Math.floor(this.x); this.y = Math.floor(this.y); return this; } toString() { return `x:${this.x},y:${this.y}`; }}Copy the code

Two, latitude and longitude category: LonLat

class LonLat{ constructor(lon, lat) { this.lon = lon; this.lat = lat; }}Copy the code

Iii. Surrounding box: BBox

class BBox{ constructor(options, top, right, buttom) { if(typeof options === "object") { this.left = options.left; this.top = options.top; this.right = options.right; this.buttom = options.buttom; }else { this.left = options; this.top = top; this.right = right; this.buttom = buttom; } } toBBox(leftTop, rightButtom) { return new BBox({ left: leftTop.x, top: leftTop.y, right: rightButtom.x, buttom: rightButtom.y }); }}Copy the code

Wgs84 to Web Mercator projection class: Project

//wgs84->epsg:3857
const R = 6378137;
const MAX_LATITUDE= 85.0511287798;
class Project{
    project (lonlat) {
        let d = Math.PI / 180,
            max = MAX_LATITUDE,
            lat = Math.max(Math.min(max, lonlat.lat), -max),
            sin = Math.sin(lat * d);

        return new Point(
            R * lonlat.lon * d,
            R * Math.log((1 + sin) / (1 - sin)) / 2);
    }

    unproject (point) {
        var d = 180 / Math.PI;
        return new LonLat(
            point.x * d / R,
            (2 * Math.atan(Math.exp(point.y / R)) - (Math.PI / 2)) * d);
    }

    bounds() {
        var d = R * Math.PI;
        return new BBox().toBBox(
            new Point(-d, d),
            new Point(d, -d)
        );
    }
}
Copy the code

Map class: Map

Constructor (options) {options = object. assign({id: "Map ", center: new LonLat(0, 0), zoom: 0}, options) this.id = options.id; this.container = document.querySelector(`#${this.id}`); this.container.className = "map-container"; this.center = options.center; this.zoom = options.zoom; This. Resolutions = options. Resolutions | | [156543.03392804103, 78271.516964020513, 39135.758482010257, 19567.879241005128, 9783.9396205025641, 4891.9698102512821, 2445.984905125641, 1222.9924525628205, 611.49622628141026, 305.74811314070513, 152.87405657035256, 76.437028285176282, 38.218514142588141, 19.109257071294071, 9.5546285356470353, 4.7773142678235176, 2.3886571339117588, 1.1943285669558794, 0.59716428347793971, 0.29858214173896985, 0.14929107086948493, 0.074645535434742463, 0.037322767717371232]; this.tileLayer = {}; this.mapPane = document.createElement("div"); this.mapPane.className = "map-pane"; this.container.appendChild(this.mapPane); this.mapTilePane = document.createElement("div"); this.mapTilePane.className = "map-tile-pane"; this.mapPane.appendChild(this.mapTilePane); this.des = document.createElement("div"); this.container.appendChild(this.des); this.des.className = "map-des"; This.des. InnerHTML = "GIS Daily "; this.size = new Point( this.container.clientWidth, this.container.clientHeight ); this.padding = 2; this.project = new Project(); } setCenter(centerLonLat, zoom) { this.center = centerLonLat; this.zoom = zoom; } addTileLayer(options) { let tile = this.tileLayer[options.id] = new Tile(options); tile.onAdd(this); }}Copy the code

例 句 : I’m looking for tiles

class Tile{ constructor(options) { options = Object.assign({tileSize: 256, dpi: 96}, options); this.tileSize = options.tileSize; this.dpi = options.dpi; This. origin = new Point(-20037508.342789244, 20037508.342789244); this.url = options.url; } onAdd(map) { this.map = map; this.tiles = []; this.render(); } render() { let map = this.map, zoom = map.zoom, center = map.center, resolutions = map.resolutions, resolution = resolutions[zoom], mapSize = map.size, padding = map.padding, project = map.project, tileSize = this.tileSize; //1 calculate the center tile coordinates let centerMeter = project.project(center); let meterPerTile = tileSize * resolution; let centerOffset = new Point(centerMeter.x - this.origin.x, this.origin.y - centerMeter.y); let centerTileCoords = centerOffset.divide(meterPerTile, meterPerTile).floor(); //2 Calculate the difference between the screen coordinates in the upper-left corner of the center tile and the center of the map. Let tileOffset = centerOffset. Remainder (meterPerTile, meterPerTile). resolution); Let tileCount = mapsize.divide (tileSize, tileSize).add(padding * 2, padding * 2).ceil(); let halfTileCount = tileCount.divide(2, 2); For (let I = 0, xLen = tilecount. x; i < xLen; i++) { for(let j = 0; j < tileCount.y; j++) { let coords = centerTileCoords.sub(halfTileCount.x, halfTileCount.y).add(i, j).floor(); let img = new Image(); img.src = this.url.replace("{z}", zoom).replace("{x}", coords.x).replace("{y}", coords.y); img.className = "map-tile"; map.mapTilePane.appendChild(img); let tilePx = coords.sub(centerTileCoords.x, centerTileCoords.y).multiply(tileSize, tileSize); let centerTilePx = mapSize.divide(2, 2).sub(tileOffset.x, tileOffset.y); let leftTop = tilePx.add(centerTilePx.x, centerTilePx.y); img.style.left = `${leftTop.x}px`; img.style.top = `${leftTop.y}px`; }}}}Copy the code

use

Let map = new map ({center: new LonLat(116.3, 39.85), zoom: 10}); map.addTileLayer({ id: "tile", url: https://c.tile.openstreetmap.org/{z}/{x}/{y}.png" });Copy the code

Well, it’s time for a miracle. Let’s see the results

http://180.76.171.45:8080/gisdaily/page/tile11.htmlLevel 10 effect

Level 11 effect