Leaflet.js/Proj4.js自定义地图投影


Leaflet自定义投影方法

最近在做海图的web版本,网络上提供了很多地图加载瓦片的方法。
1、主流是引入Proj4作为插件使用,如果想做循环显示,会发现这个插件对于L.Map中worldcopyjump此类的方法支持并不好。
2、不使用Proj4的方法也可以加载自定义的投影方法,当然必须是3857、3395此类基本转换的变种。太复杂的变换还是使用Proj4比较靠谱,也许效果不好,最少还是可以显示的。

Proj4


Leaflet官网上就能下载到,国内几家做Web端GIS的底层技术大部分基于OpenLayer与Leaflet,由于Leaflet基础组件的轻便,广受青睐。

 merc.x = 20037508.34279;
 merc.y = 15496570.73972;
 // 加载投影
 var r = new Array(19);
 for (var i = 0; i < r.length; i++) {
 	r[i] = merc.x * 2 / (Math.pow(2, i) * 256);
 }
 var crs = new L.Proj.CRS(
 	"EPSG:3395",
 	"+proj=merc +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs",
 	{
 		resolutions: r,
 		origin: [-merc.x, merc.y],
 		bounds: L.bounds([-merc.x, -merc.y], [merc.x, merc.y])
 	});

标准的3395->https://epsg.io/3395

我定义的海图格式不是标准的3395,但是由于瓦片是公司自定义的,所以切图的经纬度边界
[Lng,Lat]->[-180,-80],[180,80]->[-20037508.34279,-15496570.73972],[20037508.34279,15496570.73972]

定义海图各层resolutions分辨率、原点、边界,加载到L.Map上就OK了。

Proj4解释一下

网上大部分文档到此就结束了,看一下L.Proj.CRS这个类

L.Proj.CRS = L.Class.extend({
		includes: L.CRS,

		options: {
			transformation: new L.Transformation(1, 0, -1, 0)
		},

L.Proj.CRS继承L.CRS,定义transformation这个option。说一下L.CRS这个类在Leaflet源码中是如何使用的。
贴一下Leaflet L.CRS.EPSG3395 源码

var EPSG3395 = extend({}, Earth, {
	code: 'EPSG:3395',
	projection: Mercator,

	transformation: (function () {
		var scale = 0.5 / (Math.PI * Mercator.R);
		return toTransformation(scale, 0.5, -scale, 0.5);
	}())
});

var Earth = extend({}, CRS, {
	wrapLng: [-180, 180],

	// Mean Earth Radius, as recommended for use by
	// the International Union of Geodesy and Geophysics,
	// see http://rosettacode.org/wiki/Haversine_formula
	R: 6371000,

	// distance between two geographical points using spherical law of cosines approximation
	distance: function (latlng1, latlng2) {
		var rad = Math.PI / 180,
		    lat1 = latlng1.lat * rad,
		    lat2 = latlng2.lat * rad,
		    a = Math.sin(lat1) * Math.sin(lat2) +
		        Math.cos(lat1) * Math.cos(lat2) * Math.cos((latlng2.lng - latlng1.lng) * rad);

		return this.R * Math.acos(Math.min(a, 1));
	}
});


好了,到Earth这层已经可以了,发现一个问题没有,3395定义的球体是正球基础上定义椭球。地球定义没有问题之后,在leaflet源码中是没有使用到地图原点这个值的,但是在Proj4中这个值是使用的。

	if (this.options.origin) {
				this.transformation =
					new L.Transformation(1, -this.options.origin[0],
						-1, this.options.origin[1]);
			}

L.CRS.EPSG3395与L.Proj.CRS使用区别仅仅在于transformation这个函数。看下这个函数做什么用的

L.Transformation.prototype = {
	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;
	},

	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);
	}
};

简单说,就是将Mercator的坐标转化成像素坐标,再就是反转。其中的Zoom参数也很好理解,不同的比例尺下同一经纬度的像素坐标一定是不一样的。


那L.CRS.EPSG3395 使用的transformation是什么呢

 toTransformation(scale, 0.5, -scale, 0.5);

而L.Proj.CRS的transformation

L.Transformation(1, -this.options.origin[0],-1, this.options.origin[1]);

这两段代码参数不一样,计算方法就不一样么?往下看就知道根本就是一回事。
先定义一下长短轴,等会方便解释,我们来算一下:
Ma = 20037508.34279;
Mb = 15496570.73972;

this._scales[i] = 1 / this.options.resolutions[i];

从L.CRS.EPSG3395转L.Proj.CRS的transform
point.x = scale * (this._a * point.x + this._b);

point.x = 256×2 m (1/2Ma * point.x + 1/2)

point.x = 256×2 m/2Ma(1* point.x + Ma)

L.Proj.CRS的resolutions是256×2 m /2*Ma

point.x = 1 / this.options.resolutions[i] × (1* point.x + Ma)
看出来一样了吧。

因为两种投影方法 X轴是一样的,Y轴就有差异了

L.CRS.EPSG3395–>point.y = 256×2 m (1/2Ma * point.y + 1/2) 依然如此
L.Proj.CRS–>point.y = 256×2 m /2Ma × (1 point.y + Mb);

所以在L.Proj.CRS是比较完美的自定义地图的方法。

在Leaflet定义一个投影

var EPSG3396 = extend({}, Earth, {
	code: 'EPSG:3396',
	projection: Mercator,

	transformation: (function () {
		var scale = 0.5 / (Math.PI * Mercator.R);
		return toTransformation(scale, 0.5, -scale, 0.5*15496570.73972/20037508.34279);
	}())
});

其实不用一定要使用Proj4,可以直接在leaflet里面在定义一个投影方式,我们能用到的投影方式无外乎Mercator与Web Mercator这两种,在leaflet基础方法中就已经定义的很完善了,我们只需要在墨卡托坐标转像素坐标的时候转换正确就可以了。

贴一个leaflet中的Mercator计算

网上的Mercator精度太差了,贴一个leaflet定义的方法。

var Mercator = {
	R: 6378137,
	R_MINOR: 6356752.314245179,

	bounds: new 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 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 LatLng(phi * d, point.x * d / r);
	}
};

墨卡托投影
https://blog.csdn.net/mygisforum/article/details/13295223

比例尺,分辨率,级别,及其之间的转换
https://gaoyi2009.iteye.com/blog/1271014

转载自:https://blog.csdn.net/weixin_39279307/article/details/85115123

You may also like...

退出移动版