Leaflet源码解析系列(六-9):Map 对象解读——其他方法

379 阅读4分钟

屏幕和地图之间的坐标转换

在修改或者设置 map state 的时候,为了处理视野变化、动画效果通常会涉及地理坐标、投影坐标、屏幕坐标三个坐标系之间的转换。

export const Map = Evented.extend({
  // ......
  // 返回两个 zoom 层级之间的比例因子. 处理 zoom 动画的时候用到.
  getZoomScale(toZoom, fromZoom) {
    // TODO replace with universal implementation after refactoring projections
    const crs = this.options.crs;
    // 默认 fromZoom 就是当前地图的 zoom
    fromZoom = fromZoom === undefined ? this._zoom : fromZoom;
    // 结果是 2 的(from-to)次方
    return crs.scale(toZoom) / crs.scale(fromZoom);
  },

  // 返回地图从 fromZoom 缩放 scale 倍后将要设置的 zoom 层级。 getZoomScale 的逆向函数
  getScaleZoom(scale, fromZoom) {
    const crs = this.options.crs;
    fromZoom = fromZoom === undefined ? this._zoom : fromZoom;
    const zoom = crs.zoom(scale * crs.scale(fromZoom));
    return isNaN(zoom) ? Infinity : zoom;
  },

  // 将经纬度坐标按照 CRS 转换为投影坐标,然后根据 zoom 和 CRS 进行放射变换,转换为像素坐标,原点是 crs 的原点。
  project(latlng, zoom) {
    zoom = zoom === undefined ? this._zoom : zoom;
    return this.options.crs.latLngToPoint(toLatLng(latlng), zoom);
  },

   // project 的逆函数,根据 zoom 层级和 CRS 将像素坐标反算成经纬度
  unproject(point, zoom) {
    zoom = zoom === undefined ? this._zoom : zoom;
    return this.options.crs.pointToLatLng(toPoint(point), zoom);
  },

  // 传入一个屏幕坐标,返回对应的经纬度地理坐标 (结果和当前 zoom 层级有关).
  layerPointToLatLng(point) {
    // map 左上角的投影坐标(像素单位)加上屏幕坐标
    const projectedPoint = toPoint(point).add(this.getPixelOrigin());
    return this.unproject(projectedPoint);
  },

  // 传入经纬度,计算这个点相对于 map 左上角的像素距离,和 layerPointToLatLng 相反
  latLngToLayerPoint(latlng) {
    const projectedPoint = this.project(toLatLng(latlng))._round();
    return projectedPoint._subtract(this.getPixelOrigin());
  },

  // 使用 map crs 对一个经纬度进行 wrap
  // 默认的经纬度是围绕着国际日期变更线,因此值的范围是 -180 and +180 degrees.
  wrapLatLng(latlng) {
    return this.options.crs.wrapLatLng(toLatLng(latlng));
  },

  // 返回一个新的 bounds,新 bounds的 center 在 crs bounds 内
  wrapLatLngBounds(latlng) {
    return this.options.crs.wrapLatLngBounds(toLatLngBounds(latlng));
  },

  // 计算两个地理坐标的距离,默认单位是 米
  distance(latlng1, latlng2) {
    return this.options.crs.distance(toLatLng(latlng1), toLatLng(latlng2));
  },

  // 传入 map container 上的像素点,计算相对于 map pane 的像素点坐标
  containerPointToLayerPoint(point) {
    return toPoint(point).subtract(this._getMapPanePos());
  },

  // 传入 map pane 上的像素点,计算相对于 map container 的像素点坐标
  layerPointToContainerPoint(point) {
    return toPoint(point).add(this._getMapPanePos());
  },

  // 传入 map container 上的像素点,计算对应的经纬度(和当前 zoom center 有关)
  containerPointToLatLng(point) {
    const layerPoint = this.containerPointToLayerPoint(toPoint(point));
    return this.layerPointToLatLng(layerPoint);
  },

  // 传入经纬度,计算对应的在 map container 上的像素点坐标.
  latLngToContainerPoint(latlng) {
    return this.layerPointToContainerPoint(this.latLngToLayerPoint(toLatLng(latlng)));
  },

  // 获取鼠标事件相对于 map container 上的像素点坐标
  mouseEventToContainerPoint(e) {
    return DomEvent.getMousePosition(e, this._container);
  },

  // 获取鼠标事件相对于 map pane 上的像素点坐标
  mouseEventToLayerPoint(e) {
    return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(e));
  },

  // 获取鼠标事件相对应的经纬度
  mouseEventToLatLng(e) {
    // (MouseEvent)
    return this.layerPointToLatLng(this.mouseEventToLayerPoint(e));
  },
  
  /**  */
  _latLngToNewLayerPoint(latlng, zoom, center) {
    const topLeft = this._getNewPixelOrigin(center, zoom);
    return this.project(latlng, zoom)._subtract(topLeft);
  },

  _latLngBoundsToNewLayerBounds(latLngBounds, zoom, center) {
    const topLeft = this._getNewPixelOrigin(center, zoom);
    return toBounds([this.project(latLngBounds.getSouthWest(), zoom)._subtract(topLeft), this.project(latLngBounds.getNorthWest(), zoom)._subtract(topLeft), this.project(latLngBounds.getSouthEast(), zoom)._subtract(topLeft), this.project(latLngBounds.getNorthEast(), zoom)._subtract(topLeft)]);
  },

  // layer point of the current center
  _getCenterLayerPoint() {
    return this.containerPointToLayerPoint(this.getSize()._divideBy(2));
  },

  // offset of the specified place to the current center in pixels
  _getCenterOffset(latlng) {
    return this.latLngToLayerPoint(latlng).subtract(this._getCenterLayerPoint());
  },
  // ......
})

Geolocation 定位 api

export const Map = Evented.extend({
  // ......
  // Geolocation 相关方法
	// 尝试使用 Geolocation API 定位用户的位置, 定位成功后触发 locationfound 事件,传递 location data ,失败后触发 locationerror 事件
	// 可以设置 map 的视野自动定位到定位结果 (or to the world view if geolocation failed).
	// 如果网页不是 https 协议,在现代浏览器(Chrome 50 and newer)上会报错
	locate(options) {

		options = this._locateOptions = Util.extend({
			timeout: 10000,
			watch: false
			// setView: false
			// maxZoom: <Number>
			// maximumAge: 0
			// enableHighAccuracy: false
		}, options);

		// 如果浏览器不支持 geolocation
		if (!('geolocation' in navigator)) {
			this._handleGeolocationError({
				code: 0,
				message: 'Geolocation not supported.'
			});
			return this;
		}

		// 处理定位成功和失败的方法
		const onResponse = this._handleGeolocationResponse.bind(this),
		    onError = this._handleGeolocationError.bind(this);

		// 监听 geolocation 定位结果变化
		if (options.watch) {
			this._locationWatchId = navigator.geolocation.watchPosition(onResponse, onError, options);
		} else {
			// 没有持续监听,一次性的,直接获取定位结果
			navigator.geolocation.getCurrentPosition(onResponse, onError, options);
		}
		return this;
	},

	/** 清除 geolocation 监听并取消定位成功后设置 map 的视野 */
	stopLocate() {
		// 清除 geolocation 监听
		if (navigator.geolocation && navigator.geolocation.clearWatch) {
			navigator.geolocation.clearWatch(this._locationWatchId);
		}
		// 取消定位成功后设置 map 的视野
		if (this._locateOptions) {
			this._locateOptions.setView = false;
		}
		return this;
	},

	/** 处理 geolocation 定位出错 */
	_handleGeolocationError(error) {
		if (!this._container._leaflet_id) { return; }

		const c = error.code,
		    message = error.message ||
		            (c === 1 ? 'permission denied' :
		            (c === 2 ? 'position unavailable' : 'timeout'));

		if (this._locateOptions.setView && !this._loaded) {
			this.fitWorld();
		}

		// @section Location events
		// @event locationerror: ErrorEvent
		// Fired when geolocation (using the [`locate`](#map-locate) method) failed.
		this.fire('locationerror', {
			code: c,
			message: `Geolocation error: ${message}.`
		});
	},

	/** 处理定位成功 */
	_handleGeolocationResponse(pos) {
		if (!this._container._leaflet_id) { return; }
		/**
		 * pos 是 geolocation api 返回的数据,内容示例:
		 * GeolocationPosition:{
		 * 		coords:{
		 * 				accuracy:24, // 水平定位精度,米
		 * 				altitude:null, // 高程 
		 * 				altitudeAccuracy:null,// 高程定位精度,米
		 * 				heading:null, 
		 * 				latitude:29.531792,  // 维度
		 * 				longitude:106.559883, // 经度
		 * 				speed:null
		 * 		},
		 * 		timestamp:1672038332400
		 * }
		 */
		const lat = pos.coords.latitude,
		    lng = pos.coords.longitude,
		    latlng = new LatLng(lat, lng),
		    bounds = latlng.toBounds(pos.coords.accuracy * 2),  // 获取边长为2倍定位精度的 bounds
		    options = this._locateOptions;

		// 缩放到定位视野 
		if (options.setView) {
			const zoom = this.getBoundsZoom(bounds);
			this.setView(latlng, options.maxZoom ? Math.min(zoom, options.maxZoom) : zoom);
		}

		const data = {
			latlng,
			bounds,
			timestamp: pos.timestamp
		};

		// 把定位结果存放到 data 中 
		for (const i in pos.coords) {
			if (typeof pos.coords[i] === 'number') {
				data[i] = pos.coords[i];
			}
		}

		// 定位成功后,触发 locationfound 事件
		this.fire('locationfound', data);
	},

	/** 设置 map 的视野为指定 zoom 层级下尽可能覆盖全球的范围 */
	fitWorld(options) {
		return this.fitBounds([[-90, -180], [90, 180]], options);
	},
  // ......
})

其他方法

export const Map = Evented.extend({
  // ......
	// 当 map 初始化设置了 view 并且有至少一个图层,此时已经 loaded 了,就执行回调函数。
  whenReady(callback, context) {
    if (this._loaded) {
      callback.call(context || this, { target: this });
    } else {
      this.on("load", callback, context);
    }
    return this;
  },
  
  /** 检测是否禁用了点击事件 */
  _isClickDisabled(el) {
    while (el && el !== this._container) {
      if (el["_leaflet_disable_click"]) {
        return true;
      }
      el = el.parentNode;
    }
  },

  /** 检测拖拽移动或者拉框放大 */
  _draggableMoved(obj) {
    obj = obj.dragging && obj.dragging.enabled() ? obj : this;
    return (obj.dragging && obj.dragging.moved()) || (this.boxZoom && this.boxZoom.moved());
  },
  // ......
})