屏幕和地图之间的坐标转换
在修改或者设置 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());
},
// ......
})