# Web地图呈现原理

·  阅读 413

## 地图投影

（以赤道本初子午线为原点）

``````L.Projection.Mercator = {
R: 6378137,
R_MINOR: 6356752.314245179,

bounds: L.bounds([-20037508.34279, -15496570.73972], [20037508.34279, 18764656.23138]),

project: function (latlng) {
var d = Math.PI / 180,
r = this.R,
y = latlng.lat * d,
tmp = this.R_MINOR / r,
e = Math.sqrt(1 - tmp * tmp),
con = e * Math.sin(y);

var ts = Math.tan(Math.PI / 4 - y / 2) / Math.pow((1 - con) / (1 + con), e / 2);
y = -r * Math.log(Math.max(ts, 1E-10));

return new L.Point(latlng.lng * d * r, y);
},

unproject: function (point) {
var d = 180 / Math.PI,
r = this.R,
tmp = this.R_MINOR / r,
e = Math.sqrt(1 - tmp * tmp),
ts = Math.exp(-point.y / r),
phi = Math.PI / 2 - 2 * Math.atan(ts);

for (var i = 0, dphi = 0.1, con; i < 15 && Math.abs(dphi) > 1e-7; i++) {
con = e * Math.sin(phi);
con = Math.pow((1 - con) / (1 + con), e / 2);
dphi = Math.PI / 2 - 2 * Math.atan(ts * con) - phi;
phi += dphi;
}

return new L.LatLng(phi * d, point.x * d / r);
}
};

``````/*
* @namespace Projection
* @projection L.Projection.SphericalMercator
*
* Spherical Mercator projection — the most common projection for online maps,
* used by almost all free and commercial tile providers. Assumes that Earth is
* a sphere. Used by the `EPSG:3857` CRS.
*/

L.Projection.SphericalMercator = {

R: 6378137,
MAX_LATITUDE: 85.0511287798,

project: function (latlng) {
var d = Math.PI / 180,
max = this.MAX_LATITUDE,
lat = Math.max(Math.min(max, latlng.lat), -max),
sin = Math.sin(lat * d);

return new L.Point(
this.R * latlng.lng * d,
this.R * Math.log((1 + sin) / (1 - sin)) / 2);
},

unproject: function (point) {
var d = 180 / Math.PI;

return new L.LatLng(
(2 * Math.atan(Math.exp(point.y / this.R)) - (Math.PI / 2)) * d,
point.x * d / this.R);
},

bounds: (function () {
var d = 6378137 * Math.PI;
return L.bounds([-d, -d], [d, d]);
})()
};

## 地图的组织方式

``````//示例来自：http://www.cnblogs.com/naaoveGIS/
//这里的像素是设备像素

1英寸=2.54厘米；
1英寸=96像素；
最终换算的单位是米；
如果当前地图比例尺为1:125000000，则代表图上1米等于实地125000000米；
米和像素间的换算公式：
1英寸=0.0254米=96像素
1像素=0.0254/96 米
则根据1：125000000比例尺，图上1像素代表实地距离是 125000000*0.0254/96 = 33072.9166666667米。

``````r=6378137
resolution=2*PI*r/(2^zoom*256)

r为Web墨卡托投影时选取的地球半径，2PIr代表地球周长，地球周长除以该级别下所有瓦片像素得到分辨率。

## 从经纬度到地图瓦片

1、经纬度转Web墨卡托；

2、Web墨卡托转世界平面点；

3、世界平面点转瓦片像素坐标；

4、瓦片像素坐标转瓦片行列号

``````// @method latLngToPoint(latlng: LatLng, zoom: Number): Point
// Projects geographical coordinates into pixel coordinates for a given zoom.
latLngToPoint: function (latlng, zoom) {
var projectedPoint = this.projection.project(latlng),
scale = this.scale(zoom);

return this.transformation._transform(projectedPoint, scale);
},
// @method scale(zoom: Number): Number
// Returns the scale used when transforming projected coordinates into
// pixel coordinates for a particular zoom. For example, it returns
// `256 * 2^zoom` for Mercator-based CRS.
scale: function (zoom) {
return 256 * Math.pow(2, zoom);
},

transform对应的代码为：

``````/*
* @class Transformation
* @aka L.Transformation
*
* Represents an affine transformation: a set of coefficients `a`, `b`, `c`, `d`
* for transforming a point of a form `(x, y)` into `(a*x + b, c*y + d)` and doing
* the reverse. Used by Leaflet in its projections code.
*
* @example
*
* ```js
* var transformation = new L.Transformation(2, 5, -1, 10),
*     p = L.point(1, 2),
*     p2 = transformation.transform(p), //  L.point(7, 8)
*     p3 = transformation.untransform(p2); //  L.point(1, 2)
* ```
*/

// factory new L.Transformation(a: Number, b: Number, c: Number, d: Number)
// Creates a `Transformation` object with the given coefficients.
L.Transformation = function (a, b, c, d) {
this._a = a;
this._b = b;
this._c = c;
this._d = d;
};

L.Transformation.prototype = {
// @method transform(point: Point, scale?: Number): Point
// Returns a transformed point, optionally multiplied by the given scale.
// Only accepts actual `L.Point` instances, not arrays.
transform: function (point, scale) { // (Point, Number) -> Point
return this._transform(point.clone(), scale);
},

// destructive transform (faster)
_transform: function (point, scale) {
scale = scale || 1;
point.x = scale * (this._a * point.x + this._b);
point.y = scale * (this._c * point.y + this._d);
return point;
},

// @method untransform(point: Point, scale?: Number): Point
// Returns the reverse transformation of the given point, optionally divided
// by the given scale. Only accepts actual `L.Point` instances, not arrays.
untransform: function (point, scale) {
scale = scale || 1;
return new L.Point(
(point.x / scale - this._b) / this._a,
(point.y / scale - this._d) / this._c);
}
};

``````/*
* @namespace CRS
* @crs L.CRS.EPSG3857
*
* The most common CRS for online maps, used by almost all free and commercial
* tile providers. Uses Spherical Mercator projection. Set in by default in
* Map's `crs` option.
*/

L.CRS.EPSG3857 = L.extend({}, L.CRS.Earth, {
code: 'EPSG:3857',
projection: L.Projection.SphericalMercator,

transformation: (function () {
var scale = 0.5 / (Math.PI * L.Projection.SphericalMercator.R);
return new L.Transformation(scale, 0.5, -scale, 0.5);
}())
});

L.CRS.EPSG900913 = L.extend({}, L.CRS.EPSG3857, {
code: 'EPSG:900913'
});

``````256 * 2^zoom * coord.x
256 * 2^zoom * coord.y

``````//tileSize = 256
xIndex = pixelCoord.x / tileSize;
yIndex = pixelCoord.y / tileSize;

``````Math.pow(2, mapZoom) - yIndex - 1

## 加载一屏地图

``````_getTiledPixelBounds: function (center) {
var map = this._map,
mapZoom = map._animatingZoom ? Math.max(map._animateToZoom, map.getZoom()) : map.getZoom(),
scale = map.getZoomScale(mapZoom, this._tileZoom),
pixelCenter = map.project(center, this._tileZoom).floor(),
halfSize = map.getSize().divideBy(scale * 2);

},

``````_pxBoundsToTileRange: function (bounds) {
var tileSize = this.getTileSize();
return new L.Bounds(
bounds.min.unscaleBy(tileSize).floor(),
bounds.max.unscaleBy(tileSize).ceil().subtract([1, 1]));
},

``````_setView: function (center, zoom, noPrune, noUpdate) {
var tileZoom = Math.round(zoom);
if ((this.options.maxZoom !== undefined && tileZoom > this.options.maxZoom) ||
(this.options.minZoom !== undefined && tileZoom < this.options.minZoom)) {
tileZoom = undefined;
}

var tileZoomChanged = this.options.updateWhenZooming && (tileZoom !== this._tileZoom);

if (!noUpdate || tileZoomChanged) {

this._tileZoom = tileZoom;

}

// 1、创建该级别容器瓦片
// 2、 设置zIndex
// 3、设置本级别图层基准点origin
// 4、设置值本级别容器的偏移量
this._updateLevels();
// 1、得到世界的像素bounds
// 2、得通过像素范围除以tileSize得到能够覆盖世界的瓦片范围
// 3、得到坐标系经度和纬度范围内的瓦片范围
this._resetGrid();

if (tileZoom !== undefined) {
// 加载可视范围内的瓦片
// 1、计算可视区域的像素范围
// 2、 将像素范围转变成瓦片格网范围
// 3、计算一个buffer的格网范围
// 4、将不再当前范围内已加载的瓦片打上标签
// 5、如果zoom发生变化重新进行setView
// 6、将格网范围内的tile放入一个数组中
// 7、对数组进行排序，靠近中心点的先加载
// 8、创建瓦片
//     (1) 计算瓦片在地图上的偏移量 coords * tileSize - origin
//     (2) 加载瓦片数据（图片或者矢量数据）
//  (3) 设置图片位置 setPosition
this._update(center);
}

if (!noPrune) {
this._pruneTiles(); // 移除不在范围内的tile; retainParent部分尚没看懂，可能是按照瓦片金字塔保留
}

// Flag to prevent _updateOpacity from pruning tiles during
// a zoom anim or a pinch gesture
this._noPrune = !!noPrune;
}
//将地图的新中心点移到地图中央
this._setZoomTransforms(center, zoom);
},

## 3D地图的加载

3D地图其实比2D多了一个环节，那就是墨卡托与3D世界坐标，3D世界与屏幕像素之间的转换。如果我们不想自找麻烦，通常3D坐标都是以米为单位，保持跟墨卡托的坐标单位一致，但是一般不直接以墨卡托的原点做3D世界的原点，因为墨卡托坐标比较大，后续计算精度是个问题。所以一般以用户设置的center转换成的墨卡托坐标为原点来建立3D的世界坐标系。

``````_getPixelMeterRatio(target) {
target = target ? target : this.controls.target;
let distance = this.camera.position.distanceTo(target);
let top = this.camera instanceof PerspectiveCamera ?
(Math.tan(this.camera.fov / 2 * DEG2RAD) * distance) :
this.camera.top / this.camera.zoom;
let meterPerPixel = 2 * top / this.container.clientHeight;

return meterPerPixel;
}

``````/**
* Return all coordinates that could cover this transform for a covering
* zoom level.
* @param {Object} options
* @param {number} options.tileSize
* @param {number} options.minzoom
* @param {number} options.maxzoom
* @param {boolean} options.roundZoom
* @param {boolean} options.reparseOverscaled
* @param {boolean} options.renderWorldCopies
* @returns {Array<Tile>} tiles
*/
coveringTiles(
options: {
tileSize: number,
minzoom?: number,
maxzoom?: number,
roundZoom?: boolean,
reparseOverscaled?: boolean,
renderWorldCopies?: boolean
}
) {
let z = this.coveringZoomLevel(options);
const actualZ = z;

if (options.minzoom !== undefined && z < options.minzoom) return [];
if (options.maxzoom !== undefined && z > options.maxzoom) z = options.maxzoom;

const centerCoord = this.pointCoordinate(this.centerPoint, z);
const centerPoint = new Point(centerCoord.column - 0.5, centerCoord.row - 0.5);
// 利用屏幕四个点求ndc坐标，ndc坐标转3D坐标，根据线性关系求出交点
const cornerCoords = [
this.pointCoordinate(new Point(0, 0), z),
this.pointCoordinate(new Point(this.width, 0), z),
this.pointCoordinate(new Point(this.width, this.height), z),
this.pointCoordinate(new Point(0, this.height), z)
];
return tileCover(z, cornerCoords, options.reparseOverscaled ? actualZ : z, this._renderWorldCopies)
.sort((a, b) => centerPoint.dist(a.canonical) - centerPoint.dist(b.canonical));
}

``````pointCoordinate(p: Point, zoom?: number) {
if (zoom === undefined) zoom = this.tileZoom;

const targetZ = 0;
// since we don't know the correct projected z value for the point,
// unproject two points to get a line and then find the point on that
// line with z=0

const coord0 = [p.x, p.y, 0, 1];
const coord1 = [p.x, p.y, 1, 1];

vec4.transformMat4(coord0, coord0, this.pixelMatrixInverse);
vec4.transformMat4(coord1, coord1, this.pixelMatrixInverse);

const w0 = coord0[3];
const w1 = coord1[3];
const x0 = coord0[0] / w0;
const x1 = coord1[0] / w1;
const y0 = coord0[1] / w0;
const y1 = coord1[1] / w1;
const z0 = coord0[2] / w0;
const z1 = coord1[2] / w1;

const t = z0 === z1 ? 0 : (targetZ - z0) / (z1 - z0);

return new Coordinate(
interp(x0, x1, t) / this.tileSize,
interp(y0, y1, t) / this.tileSize,
this.zoom)._zoomTo(zoom);
}