高德地图SDK加载WGS84坐标系瓦片的技术实现

引言

高德地图默认使用GCJ02坐标系(火星坐标系),而许多国际标准地图服务(如OpenStreetMap)使用WGS84坐标系。本文介绍如何通过高德地图SDK实现自定义瓦片图层加载,并解决WGS84与GCJ02坐标系转换的偏移问题。


一、核心原理

1. 坐标系差异

  • GCJ02:中国国家测绘局制定的加密坐标系,高德地图原生支持。

  • WGS84:国际通用的地理坐标系,需转换为GCJ02才能与高德地图叠加。

2. 实现思路

  1. 下载WGS84瓦片数据。

  2. 将瓦片边界坐标从GCJ02反向转换为WGS84。

  3. 计算瓦片偏移量并拼接至Canvas。

  4. 通过高德TileLayer.Flexible实现自定义图层渲染。


二、代码实现步骤

1. 基础坐标转换函数定义

	        // GCJ-02 到 WGS-84 的坐标转换方法
        function gcj02_To_wgs84(lng, lat) {
            const coord = transform(lng, lat);
            const lontitude = lng * 2 - coord.lng;
            const latitude = lat * 2 - coord.lat;
            return { lng: lontitude, lat: latitude };
        }

        // 坐标转换的辅助方法
        function transform(lng, lat) {
            const pi = 3.1415926535897932384626;
            const a = 6378245.0;
            const ee = 0.00669342162296594323;

            let dLat = transformLat(lng - 105.0, lat - 35.0);
            let dLng = transformLng(lng - 105.0, lat - 35.0);

            const radLat = (lat / 180.0) * pi;
            let magic = Math.sin(radLat);
            magic = 1 - ee * magic * magic;
            const sqrtMagic = Math.sqrt(magic);

            dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
            dLng = (dLng * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);

            const mgLat = lat + dLat;
            const mgLng = lng + dLng;

            return { lng: mgLng, lat: mgLat };
        }

        // 纬度转换的辅助方法
        function transformLat(x, y) {
            let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
            ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
            ret += (20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin(y / 3.0 * Math.PI)) * 2.0 / 3.0;
            ret += (160.0 * Math.sin(y / 12.0 * Math.PI) + 320 * Math.sin(y * Math.PI / 30.0)) * 2.0 / 3.0;
            return ret;
        }

        // 经度转换的辅助方法
        function transformLng(x, y) {
            let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
            ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
            ret += (20.0 * Math.sin(x * Math.PI) + 40.0 * Math.sin(x / 3.0 * Math.PI)) * 2.0 / 3.0;
            ret += (150.0 * Math.sin(x / 12.0 * Math.PI) + 300.0 * Math.sin(x / 30.0 * Math.PI)) * 2.0 / 3.0;
            return ret;
        }
	
		/**
	 * 将经纬度转换为瓦片坐标 (x, y)
	 * @param {number} lon - 经度
	 * @param {number} lat - 纬度
	 * @param {number} z - 瓦片层级 (缩放级别)
	 * @returns {Object} - 包含 x 和 y 坐标的对象
	 */
	function lonLatToTileXY(lon, lat, z) {
		// 计算每层的瓦片总数
		const n = Math.pow(2, z);

		// 将经度转换为瓦片的 x 坐标
		const x = Math.floor((lon + 180) / 360 * n);

		// 将纬度转换为瓦片的 y 坐标
		const latRad = lat * Math.PI / 180; // 将纬度转换为弧度
		const y = Math.floor((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n);

		return { x, y };
	}
	
	/**
	 * 将经纬度转换为全局像素坐标
	 * @param {number} lon - 经度
	 * @param {number} lat - 纬度
	 * @param {number} z - 缩放级别
	 * @returns {Object} - 包含全局像素坐标 (xp, yp) 的对象
	 */
	function lonLatToGlobalPixelXY(lon, lat, z) {
		const n = Math.pow(2, z) * 256; // 当前层级的全局像素范围

		// 计算全局像素坐标 xp
		const xp = Math.floor((lon + 180) / 360 * n);

		// 计算全局像素坐标 yp
		const latRad = lat * Math.PI / 180; // 纬度转换为弧度
		const yp = Math.floor((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n);

		return { xp, yp };
	}

2、定义一个取图函数

	function loadTile(url) {
		return new Promise((resolve, reject) => {
			const img = new Image();
			img.crossOrigin = "Anonymous";
			img.onload = () => resolve(img);
			img.onerror = () => reject(new Error(`Failed to load tile: ${url}`));
			img.src = url;
		});
	}

4. 自定义瓦片图层

通过AMap.TileLayer.Flexible实现动态加载:

var layer = new AMap.TileLayer.Flexible({
    zIndex: 200,
    createTile: function (x, y, z, success, fail) {
        const canvas = document.createElement('canvas');
        const ctx = canvas.getContext('2d');
        
        // 计算GCJ02瓦片边界并转换为WGS84
        const bounds = getTileBounds(x, y, z);
        const wgsTL = gcj02_To_wgs84(bounds.minLon, bounds.maxLat);
        const wgsRD = gcj02_To_wgs84(bounds.maxLon, bounds.minLat);
                // 下载并拼接所有相关的 WGS-84 瓦片
                const promises = [];
                for (let wgsX = tileTL.x; wgsX <= tileRD.x; wgsX++) {
                    for (let wgsY = tileTL.y; wgsY <= tileRD.y; wgsY++) {
                        const url = `https://xx/tile/${z}/${wgsX}/${wgsY}.png`;
                        promises.push(
                            loadTile(url)
                                .then(img => ({ img, wgsX, wgsY }))
                                .catch(() => null) // 忽略加载失败的瓦片
                        );
                    }
                }

        // 加载并拼接WGS84瓦片
        Promise.all(tilePromises).then(results => {
            results.forEach(({ img, wgsX, wgsY }) => {
                // 计算偏移量并绘制到Canvas
                                            // 计算当前 WGS-84 瓦片的范围
                            const wgsBounds = getTileBounds(wgsX, wgsY, z);
                            const tieCoord = gcj02_To_wgs84(bounds.minLon, bounds.maxLat);
							let picTL = lonLatToGlobalPixelXY(wgsBounds.minLon, wgsBounds.maxLat, z);
							let tileTL = lonLatToGlobalPixelXY(tieCoord.lng, tieCoord.lat, z);
							let offsetX = tileTL.xp - picTL.xp;
							let offsetY = tileTL.yp - picTL.yp;
                ctx.drawImage(img, -offsetX, -offsetY, 256, 256);
            });
            success(canvas);
        });
    }
});

// 添加至地图
map.add(layer);

5、完整代码

以下代码直接复制使用,修改地图链接和高德key即可

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Custom Tile Layer with AMap</title>
    <style>
        #map-container {
            width: 100%;
            height: 600px;
        }
    </style>
</head>
<body>
    <div id="map-container"></div>

    <!-- 引入高德地图 SDK -->
    <script src="https://webapi.amap.com/maps?v=2.0&key=xx"></script>
    <script>
        // 初始化地图
        const map = new AMap.Map('map-container', {
            zooms: [2,27], // 初始缩放级别
			zoom: 18,
			optimalZoom: 27,
            center: [104.15970804286191, 30.67776629150997], // 初始中心点坐标(北京)
        });

	// 引入 WGS84 到 GCJ02 的转换库
	// 可以使用 coordtransform 或其他开源库
	function wgs84ToGcj02(lng, lat) {
		// 这里使用 coordtransform 的转换逻辑
		// 具体实现可以参考:https://github.com/wandergis/coordtransform
		// 以下是简化的示例代码
		const pi = 3.1415926535897932384626;
		const a = 6378245.0;
		const ee = 0.00669342162296594323;

		function transformLat(x, y) {
			let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
			ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
			ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0;
			ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0;
			return ret;
		}

		function transformLon(x, y) {
			let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
			ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;
			ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0;
			ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0 * pi)) * 2.0 / 3.0;
			return ret;
		}

		let dLat = transformLat(lng - 105.0, lat - 35.0);
		let dLon = transformLon(lng - 105.0, lat - 35.0);
		const radLat = lat / 180.0 * pi;
		let magic = Math.sin(radLat);
		magic = 1 - ee * magic * magic;
		const sqrtMagic = Math.sqrt(magic);
		dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
		dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);
		const mgLat = lat + dLat;
		const mgLon = lng + dLon;
		return { lng: mgLon, lat: mgLat };
	}

	// 计算瓦片的范围
	function getTileBounds(x, y, z) {
		const n = Math.pow(2, z);
		const lon1 = (x / n) * 360.0 - 180.0;
		const lat1 = (Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 1) / n)))) * 180.0 / Math.PI;
		const lon2 = ((x + 1) / n) * 360.0 - 180.0;
		const lat2 = (Math.atan(Math.sinh(Math.PI * (1 - 2 * y / n)))) * 180.0 / Math.PI;
		return { minLon: lon1, minLat: lat1, maxLon: lon2, maxLat: lat2 };
	}
	// 下载瓦片
	function loadTile(url) {
		return new Promise((resolve, reject) => {
			const img = new Image();
			img.crossOrigin = "Anonymous";
			img.onload = () => resolve(img);
			img.onerror = () => reject(new Error(`Failed to load tile: ${url}`));
			img.src = url;
		});
	}
	
	        // GCJ-02 到 WGS-84 的坐标转换方法
        function gcj02_To_wgs84(lng, lat) {
            const coord = transform(lng, lat);
            const lontitude = lng * 2 - coord.lng;
            const latitude = lat * 2 - coord.lat;
            return { lng: lontitude, lat: latitude };
        }

        // 坐标转换的辅助方法
        function transform(lng, lat) {
            const pi = 3.1415926535897932384626;
            const a = 6378245.0;
            const ee = 0.00669342162296594323;

            let dLat = transformLat(lng - 105.0, lat - 35.0);
            let dLng = transformLng(lng - 105.0, lat - 35.0);

            const radLat = (lat / 180.0) * pi;
            let magic = Math.sin(radLat);
            magic = 1 - ee * magic * magic;
            const sqrtMagic = Math.sqrt(magic);

            dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);
            dLng = (dLng * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);

            const mgLat = lat + dLat;
            const mgLng = lng + dLng;

            return { lng: mgLng, lat: mgLat };
        }

        // 纬度转换的辅助方法
        function transformLat(x, y) {
            let ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
            ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
            ret += (20.0 * Math.sin(y * Math.PI) + 40.0 * Math.sin(y / 3.0 * Math.PI)) * 2.0 / 3.0;
            ret += (160.0 * Math.sin(y / 12.0 * Math.PI) + 320 * Math.sin(y * Math.PI / 30.0)) * 2.0 / 3.0;
            return ret;
        }

        // 经度转换的辅助方法
        function transformLng(x, y) {
            let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
            ret += (20.0 * Math.sin(6.0 * x * Math.PI) + 20.0 * Math.sin(2.0 * x * Math.PI)) * 2.0 / 3.0;
            ret += (20.0 * Math.sin(x * Math.PI) + 40.0 * Math.sin(x / 3.0 * Math.PI)) * 2.0 / 3.0;
            ret += (150.0 * Math.sin(x / 12.0 * Math.PI) + 300.0 * Math.sin(x / 30.0 * Math.PI)) * 2.0 / 3.0;
            return ret;
        }
	
		/**
	 * 将经纬度转换为瓦片坐标 (x, y)
	 * @param {number} lon - 经度
	 * @param {number} lat - 纬度
	 * @param {number} z - 瓦片层级 (缩放级别)
	 * @returns {Object} - 包含 x 和 y 坐标的对象
	 */
	function lonLatToTileXY(lon, lat, z) {
		// 计算每层的瓦片总数
		const n = Math.pow(2, z);

		// 将经度转换为瓦片的 x 坐标
		const x = Math.floor((lon + 180) / 360 * n);

		// 将纬度转换为瓦片的 y 坐标
		const latRad = lat * Math.PI / 180; // 将纬度转换为弧度
		const y = Math.floor((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n);

		return { x, y };
	}
	
	/**
	 * 将经纬度转换为全局像素坐标
	 * @param {number} lon - 经度
	 * @param {number} lat - 纬度
	 * @param {number} z - 缩放级别
	 * @returns {Object} - 包含全局像素坐标 (xp, yp) 的对象
	 */
	function lonLatToGlobalPixelXY(lon, lat, z) {
		const n = Math.pow(2, z) * 256; // 当前层级的全局像素范围

		// 计算全局像素坐标 xp
		const xp = Math.floor((lon + 180) / 360 * n);

		// 计算全局像素坐标 yp
		const latRad = lat * Math.PI / 180; // 纬度转换为弧度
		const yp = Math.floor((1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2 * n);

		return { xp, yp };
	}

	// 创建自定义瓦片图层
	var layer = new AMap.TileLayer.Flexible({
		cacheSize: 300,
		zIndex: 200,
		tileSize: 256,
		dataZooms:[2, 27],
		createTile: function (x, y, z, success, fail) {
			const canvas = document.createElement('canvas');
                canvas.width = canvas.height = 256;
                const ctx = canvas.getContext('2d');

                // 获取当前 GCJ-02 瓦片的范围
                const bounds = getTileBounds(x, y, z);

                // 将 GCJ-02 范围转换为 WGS-84 范围
                const wgsTL = gcj02_To_wgs84(bounds.minLon, bounds.maxLat);
                const wgsRD = gcj02_To_wgs84(bounds.maxLon, bounds.minLat);

                // 计算 WGS-84 瓦片的 x, y 范围
				let tileTL = lonLatToTileXY(wgsTL.lng, wgsTL.lat, z);
				let tileRD = lonLatToTileXY(wgsRD.lng, wgsRD.lat, z);

                // 下载并拼接所有相关的 WGS-84 瓦片
                const promises = [];
                for (let wgsX = tileTL.x; wgsX <= tileRD.x; wgsX++) {
                    for (let wgsY = tileTL.y; wgsY <= tileRD.y; wgsY++) {
                        const url = `https://xx/image/tile/${z}/${wgsX}/${wgsY}.png`;
                        promises.push(
                            loadTile(url)
                                .then(img => ({ img, wgsX, wgsY }))
                                .catch(() => null) // 忽略加载失败的瓦片
                        );
                    }
                }

                // 等待所有瓦片下载完成
                Promise.all(promises).then(results => {
                    results.forEach(result => {
                        if (result) {
                            const { img, wgsX, wgsY } = result;
                                        // 计算当前 WGS-84 瓦片的范围
                            const wgsBounds = getTileBounds(wgsX, wgsY, z);
                            const tieCoord = gcj02_To_wgs84(bounds.minLon, bounds.maxLat);
							let picTL = lonLatToGlobalPixelXY(wgsBounds.minLon, wgsBounds.maxLat, z);
							let tileTL = lonLatToGlobalPixelXY(tieCoord.lng, tieCoord.lat, z);
							let offsetX = tileTL.xp - picTL.xp;
							let offsetY = tileTL.yp - picTL.yp;

                            // 绘制瓦片到 Canvas 上
                            ctx.drawImage(img, -offsetX, -offsetY, 256, 256);
                        }
                    });
                    success(canvas);
                }).catch(fail);
            }
	});

	// 添加自定义瓦片图层
	map.add(layer);


    </script>
</body>
</html>

结语

本文实现了高德地图SDK加载WGS84坐标系瓦片的功能。开发者可基于此扩展自定义地图服务(如室内地图、专题图层)。完整代码已开源,欢迎在评论区交流讨论!


通过以上步骤,开发者可快速实现高德地图与WGS84坐标系的兼容,适用于需要混合多源地图数据的场景。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值