Leaflet源码解析系列(六-4):Map 对象解读——设置 map state 的方法

266 阅读4分钟

这些方法主要用来是设置地图的zoomcenter,在设置的时候都会检查zoom是否符合minZoom~maxZoomcenter是否符合maxBounds

setView是重点方法,最终调用的是_tryAnimatedZoomtryAnimatedPan方法,通过设置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");
  },

  // ......
})