这些方法主要用来是设置地图的zoom和center,在设置的时候都会检查zoom是否符合minZoom~maxZoom,center是否符合maxBounds。
setView是重点方法,最终调用的是_tryAnimatedZoom和tryAnimatedPan方法,通过设置transform进行动画执行和位置改变。
export const Map = Evented.extend({
// ......
// 设置地图 state 相关的方法
/** 根据指定的 center 和 zoom 层级设置 map 的视野,option 是动画相关的选项 */
setView(center, zoom, options) {
// 校准 zoom,符合minZoom~maxZoom
zoom = zoom === undefined ? this._zoom : this._limitZoom(zoom);
// 校准 center,符合 maxBounds
center = this._limitCenter(toLatLng(center), zoom, this.options.maxBounds);
options = options || {};
// 停止之前的动画
this._stop();
// map 已经加载好了,并且不是初始化第一次设置 view
if (this._loaded && !options.reset && options !== true) {
if (options.animate !== undefined) {
options.zoom = Util.extend({ animate: options.animate }, options.zoom);
options.pan = Util.extend({ animate: options.animate, duration: options.duration }, options.pan);
}
// 如果 zoom 层级没有变,就只进行 pan
const moved = this._zoom !== zoom ? this._tryAnimatedZoom && this._tryAnimatedZoom(center, zoom, options.zoom) : this._tryAnimatedPan(center, options.pan);
if (moved) {
// prevent resize handler call, the view will refresh after animation anyway
// 组织 resize 注册的监听函数,setView 会在动画结束后自动更新 map 视野
clearTimeout(this._sizeTimer);
return this;
}
}
// animation didn't start, just reset the map view
// 初始化地图的时候,动画还没开始,仅仅是设置地图的初始 view
this._resetView(center, zoom, options.pan && options.pan.noMoveStart);
return this;
},
/** 设置 zoom 级别 */
setZoom(zoom, options) {
// 如果 map 没有准备好,直接把当前 zoom 设置为
if (!this._loaded) {
this._zoom = zoom;
return this;
}
return this.setView(this.getCenter(), zoom, { zoom: options });
},
/** 放大 */
zoomIn(delta, options) {
// 如果不支持 css transforms,缩放倍数就是1,否则使用配置项里的缩放倍数
delta = delta || (Browser.any3d ? this.options.zoomDelta : 1);
return this.setZoom(this._zoom + delta, options);
},
/** 缩小 */
zoomOut(delta, options) {
// 如果不支持 css transforms,缩放倍数就是1,否则使用配置项里的缩放倍数
delta = delta || (Browser.any3d ? this.options.zoomDelta : 1);
return this.setZoom(this._zoom - delta, options);
},
// 以某个”经纬度坐标点“为中心缩放地图,缩放后这个点在页面上的位置不变 (e.g. used internally for scroll zoom and double-click zoom).
// 也支持以某个”像素坐标点“为中心缩放地图,缩放后这个点在页面上的位置不变 (relative to the top-left corner) .
setZoomAround(latlng, zoom, options) {
const scale = this.getZoomScale(zoom), // 缩放倍数(zoom/this.zoom),如果放大一倍,scale就是2的1次方
viewHalf = this.getSize().divideBy(2), // map 窗口大小
// 统一转换为屏幕坐标
containerPoint = latlng instanceof Point ? latlng : this.latLngToContainerPoint(latlng),
// 计算新中心的经纬度坐标
centerOffset = containerPoint.subtract(viewHalf).multiplyBy(1 - 1 / scale), // 相减,然后乘以倍数,计算出新中心距离center的插值
newCenter = this.containerPointToLatLng(viewHalf.add(centerOffset)); // 加上差值
// 调用 setView 设置 map 的视野
return this.setView(newCenter, zoom, { zoom: options });
},
/** 设置 map 视野为指定的经纬度,bounds 符合 minZoom~maxZoom 的要求 */
fitBounds(bounds, options) {
// 如果不是 Bounds,要默认转换一下
bounds = toLatLngBounds(bounds);
if (!bounds.isValid()) {
throw new Error("Bounds are not valid.");
}
// 获取 bounds 对应的 center 和 zoom,这两值要符合当前 map 的 bounds 和 zoom 限值
const target = this._getBoundsCenterZoom(bounds, options);
// 调用 setView 设置 map 的视野
return this.setView(target.center, target.zoom, options);
},
/** 设置 map 视野为指定的经纬度,有一个平滑的过度效果,与 fitBounds 类似 */
flyToBounds(bounds, options) {
// 与 fitBounds 类似的逻辑
const target = this._getBoundsCenterZoom(bounds, options);
return this.flyTo(target.center, target.zoom, options);
},
/** 根据 bounds 获取对应的 center 和 zoom */
_getBoundsCenterZoom(bounds, options) {
options = options || {};
bounds = bounds.getBounds ? bounds.getBounds() : toLatLngBounds(bounds);
// 左上角和右下角点位
const paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]),
paddingBR = toPoint(options.paddingBottomRight || options.padding || [0, 0]);
let zoom = this.getBoundsZoom(bounds, false, paddingTL.add(paddingBR));
zoom = typeof options.maxZoom === "number" ? Math.min(options.maxZoom, zoom) : zoom;
if (zoom === Infinity) {
return {
center: bounds.getCenter(),
zoom
};
}
const paddingOffset = paddingBR.subtract(paddingTL).divideBy(2),
swPoint = this.project(bounds.getSouthWest(), zoom),
nePoint = this.project(bounds.getNorthEast(), zoom),
center = this.unproject(swPoint.add(nePoint).divideBy(2).add(paddingOffset), zoom);
return {
center,
zoom
};
},
/**
* 当 bounds 适应到 map 的视野时候对应的 zoom 值,如果 inside 是 true,则是 map 视野适应到 bounds,获取到的 zoom 要大一些
* @param {*} bounds bounds 范围
* @param {*} inside map 的视野是否完全在 bounds 里
* @param {*} padding padding 内边距
* @returns zoom 层级
*/
getBoundsZoom(bounds, inside, padding) {
// (LatLngBounds[, Boolean, Point]) -> Number
bounds = toLatLngBounds(bounds);
padding = toPoint(padding || [0, 0]);
let zoom = this.getZoom() || 0;
const min = this.getMinZoom(),
max = this.getMaxZoom(),
nw = bounds.getNorthWest(),
se = bounds.getSouthEast(),
size = this.getSize().subtract(padding), // 裁剪内边距
boundsSize = toBounds(this.project(se, zoom), this.project(nw, zoom)).getSize(),
snap = Browser.any3d ? this.options.zoomSnap : 1,
scalex = size.x / boundsSize.x,
scaley = size.y / boundsSize.y,
scale = inside ? Math.max(scalex, scaley) : Math.min(scalex, scaley);
zoom = this.getScaleZoom(scale, zoom);
if (snap) {
zoom = Math.round(zoom / (snap / 100)) * (snap / 100); // don't jump if within 1% of a snap level
zoom = inside ? Math.ceil(zoom / snap) * snap : Math.floor(zoom / snap) * snap;
}
return Math.max(min, Math.min(max, zoom));
},
/** 设置 map 的视野,有一个平滑的 pan-zoom 动画 */
flyTo(targetCenter, targetZoom, options) {
options = options || {};
if (options.animate === false || !Browser.any3d) {
return this.setView(targetCenter, targetZoom, options);
}
this._stop();
const from = this.project(this.getCenter()),
to = this.project(targetCenter),
size = this.getSize(),
startZoom = this._zoom;
targetCenter = toLatLng(targetCenter);
targetZoom = targetZoom === undefined ? startZoom : targetZoom;
// 计算动画过渡
const w0 = Math.max(size.x, size.y),
w1 = w0 * this.getZoomScale(startZoom, targetZoom),
u1 = to.distanceTo(from) || 1,
rho = 1.42,
rho2 = rho * rho;
function r(i) {
const s1 = i ? -1 : 1,
s2 = i ? w1 : w0,
t1 = w1 * w1 - w0 * w0 + s1 * rho2 * rho2 * u1 * u1,
b1 = 2 * s2 * rho2 * u1,
b = t1 / b1,
sq = Math.sqrt(b * b + 1) - b;
// workaround for floating point precision bug when sq = 0, log = -Infinite,thus triggering an infinite loop in flyTo
const log = sq < 0.000000001 ? -18 : Math.log(sq);
return log;
}
function sinh(n) {
return (Math.exp(n) - Math.exp(-n)) / 2;
}
function cosh(n) {
return (Math.exp(n) + Math.exp(-n)) / 2;
}
function tanh(n) {
return sinh(n) / cosh(n);
}
const r0 = r(0);
function w(s) {
return w0 * (cosh(r0) / cosh(r0 + rho * s));
}
function u(s) {
return (w0 * (cosh(r0) * tanh(r0 + rho * s) - sinh(r0))) / rho2;
}
function easeOut(t) {
return 1 - Math.pow(1 - t, 1.5);
}
const start = Date.now(),
S = (r(1) - r0) / rho,
duration = options.duration ? 1000 * options.duration : 1000 * S * 0.8;
function frame() {
const t = (Date.now() - start) / duration,
s = easeOut(t) * S;
// 如果还在动画持续时间内
if (t <= 1) {
// 执行动画
this._flyToFrame = Util.requestAnimFrame(frame, this);
// 触发 map 注册的 move 和 zoom 事件(动画期间会一直触发)
this._move(this.unproject(from.add(to.subtract(from).multiplyBy(u(s) / u1)), startZoom), this.getScaleZoom(w0 / w(s), startZoom), { flyTo: true });
} else {
// 超过了动画持续时间,触发 map 注册的 move 和 zoom 、zoomend事件
this._move(targetCenter, targetZoom)._moveEnd(true);
}
}
// 触发 map 注册的 movestart 和 zoomstart 事件
this._moveStart(true, options.noMoveStart);
frame.call(this);
return this;
},
/** 将地图平移到指定的中心点 */
panTo(center, options) {
return this.setView(center, this._zoom, { pan: options });
},
/** 平移指定像素的距离 */
panBy(offset, options) {
offset = toPoint(offset).round();
options = options || {};
// 如果没有有效的平移距离,直接触发 moveend 事件
if (!offset.x && !offset.y) {
return this.fire("moveend");
}
// 如果我们平移得太远,Chrome中瓦片图层会出现问题,瓦片们消失或出现在错误的地方(轻微偏移) #2602
if (options.animate !== true && !this.getSize().contains(offset)) {
// 直接设置
this._resetView(this.unproject(this.project(this.getCenter()).add(offset)), this.getZoom());
return this;
}
if (!this._panAnim) {
this._panAnim = new PosAnimation();
this._panAnim.on(
{
step: this._onPanTransitionStep,
end: this._onPanTransitionEnd
},
this
);
}
// don't fire movestart if animating inertia
if (!options.noMoveStart) {
this.fire("movestart");
}
// 如果没有特别指明不进行动画,默认是有动画的
if (options.animate !== false) {
this._mapPane.classList.add("leaflet-pan-anim");
const newPos = this._getMapPanePos().subtract(offset).round();
this._panAnim.run(this._mapPane, newPos, options.duration || 0.25, options.easeLinearity);
} else {
this._rawPanBy(offset);
this.fire("move").fire("moveend");
}
return this;
},
/** 偏移 _mapPane 的 DOM 元素位置 */
_rawPanBy(offset) {
DomUtil.setPosition(this._mapPane, this._getMapPanePos().subtract(offset));
},
// 限制地图视野范围为给定的 bounds
setMaxBounds(bounds) {
bounds = toLatLngBounds(bounds);
// 如果已经注册福哦监听 moveend 函数,就取消注册
if (this.listens("moveend", this._panInsideMaxBounds)) {
this.off("moveend", this._panInsideMaxBounds);
}
if (!bounds.isValid()) {
this.options.maxBounds = null;
return this;
}
this.options.maxBounds = bounds;
// 如果地图已经load,立即检查是否超限
if (this._loaded) {
this._panInsideMaxBounds();
}
// 注册监听 moveend 函数
return this.on("moveend", this._panInsideMaxBounds);
},
// 设置 map 允许的最小 zoom 层级
setMinZoom(zoom) {
const oldZoom = this.options.minZoom;
this.options.minZoom = zoom;
if (this._loaded && oldZoom !== zoom) {
this.fire("zoomlevelschange"); // 触发事件
// 如果当前zoom小于最小zoom,立即设置为最小zoom
if (this.getZoom() < this.options.minZoom) {
return this.setZoom(zoom);
}
}
return this;
},
// 设置 map 允许的最大 zoom 层级
setMaxZoom(zoom) {
const oldZoom = this.options.maxZoom;
this.options.maxZoom = zoom;
if (this._loaded && oldZoom !== zoom) {
this.fire("zoomlevelschange");
// 如果当前zoom大于最大zoom,立即设置为最大zoom
if (this.getZoom() > this.options.maxZoom) {
return this.setZoom(zoom);
}
}
return this;
},
/** 平移 map 使其视图最接近 bounds,使用 option 设置是动画选项 */
panInsideBounds(bounds, options) {
this._enforcingBounds = true;
// 确保 center 在 bounds 内
const center = this.getCenter(),
newCenter = this._limitCenter(center, this._zoom, toLatLngBounds(bounds));
// bounds 对应的新 center 和当前map的center不一样,才进行平移,否则不做操作
if (!center.equals(newCenter)) {
this.panTo(newCenter, options);
}
this._enforcingBounds = false;
return this;
},
/** 平移 map 使其视图最接近 maxBounds */
_panInsideMaxBounds() {
if (!this._enforcingBounds) {
this.panInsideBounds(this.options.maxBounds);
}
},
// 把 map 平移最小量距离,使“latlng”可见,使用 padding options 设置更严格的边界。
// 如果 latlng 已经在当前map的显示范围内了,不会进行任何操作
panInside(latlng, options) {
options = options || {};
const paddingTL = toPoint(options.paddingTopLeft || options.padding || [0, 0]),
paddingBR = toPoint(options.paddingBottomRight || options.padding || [0, 0]),
pixelCenter = this.project(this.getCenter()),
pixelPoint = this.project(latlng),
pixelBounds = this.getPixelBounds(),
paddedBounds = toBounds([pixelBounds.min.add(paddingTL), pixelBounds.max.subtract(paddingBR)]),
paddedSize = paddedBounds.getSize();
if (!paddedBounds.contains(pixelPoint)) {
this._enforcingBounds = true;
const centerOffset = pixelPoint.subtract(paddedBounds.getCenter());
const offset = paddedBounds.extend(pixelPoint).getSize().subtract(paddedSize);
pixelCenter.x += centerOffset.x < 0 ? -offset.x : offset.x;
pixelCenter.y += centerOffset.y < 0 ? -offset.y : offset.y;
this.panTo(this.unproject(pixelCenter), options);
this._enforcingBounds = false;
}
return this;
},
/** 限制 center 经纬度点在 bounds 范围内 */
_limitCenter(center, zoom, bounds) {
if (!bounds) {
return center;
}
// center 投影后的像素坐标点
const centerPoint = this.project(center, zoom),
viewHalf = this.getSize().divideBy(2),
// center 在当前窗口对应的 bounds
viewBounds = new Bounds(centerPoint.subtract(viewHalf), centerPoint.add(viewHalf)),
// 计算center 对应的 bounds 和传入的 bounds 的中心点坐标差值 offset
offset = this._getBoundsOffset(viewBounds, bounds, zoom);
// 如果差值小于一个像素, 忽略. 这可以防止不稳定的投影导致的微小偏移量的无限循环。
if (Math.abs(offset.x) <= 1 && Math.abs(offset.y) <= 1) {
return center;
}
// 将 center 校准到 bounds 范围内
return this.unproject(centerPoint.add(offset), zoom);
},
/** 限制 offset 在 bounds 范围内 */
_limitOffset(offset, bounds) {
if (!bounds) {
return offset;
}
// 像素单位的 bounds
const viewBounds = this.getPixelBounds(),
newBounds = new Bounds(viewBounds.min.add(offset), viewBounds.max.add(offset));
// 新的 offset 加上了差值
return offset.add(this._getBoundsOffset(newBounds, bounds));
},
/** 限定 zoom 的最大值,如果超过max就取max,如果小于min就取min */
_limitZoom(zoom) {
const min = this.getMinZoom(),
max = this.getMaxZoom(),
snap = Browser.any3d ? this.options.zoomSnap : 1;
if (snap) {
// 取 snap 的整数倍
zoom = Math.round(zoom / snap) * snap;
}
return Math.max(min, Math.min(max, zoom));
},
/** 计算 pxBounds 在指定 zoom (没有指定则是map的当前zoom)时进入maxBounds所需的偏移量 */
_getBoundsOffset(pxBounds, maxBounds, zoom) {
// 计算 maxBounds 对应的像素 bounds
const projectedMaxBounds = toBounds(this.project(maxBounds.getNorthEast(), zoom), this.project(maxBounds.getSouthWest(), zoom)),
// 两个 bounds 左上角的点(像素坐标)的差
minOffset = projectedMaxBounds.min.subtract(pxBounds.min),
// 两个 bounds 右下角的点(像素坐标)的差
maxOffset = projectedMaxBounds.max.subtract(pxBounds.max),
// 通过计算 x 和 y 两个方向上的“距离/2”,计算两个 bounds 的偏移量
dx = this._rebound(minOffset.x, -maxOffset.x),
dy = this._rebound(minOffset.y, -maxOffset.y);
return new Point(dx, dy);
},
/** 计算两个数值之间的“距离/2” */
_rebound(left, right) {
// ???
return left + right > 0 ? Math.round(left - right) / 2 : Math.max(0, Math.ceil(left)) - Math.max(0, Math.floor(right));
},
// 修改state的时候用到的一些私有方法
// 动画过程中,重复触发 move 事件
_onPanTransitionStep() {
this.fire("move");
},
// 动画结束,触发 moveend 事件
_onPanTransitionEnd() {
this._mapPane.classList.remove("leaflet-pan-anim");
this.fire("moveend");
},
// ......
})