openlayers6 解决调用百度地图之瓦片偏移、坐标偏移、无限拖动裂缝偏移问题

说在前面

  最近接了一个恶心的活,客户需求是国内、国外都要进行地图展示货运车辆位置及路线图,国外没啥好说的,本身客户国内用的百度API使用百度地图,国外使用谷歌API调用谷歌地图,这不能忍啊,同一套业务整两套代码这怎么维护呢,完全不符合设计原则,于是果断建议国内国外均使用百度地图API(主要是没花钱想最小代价解决问题。。。),国外仍然用百度API调用谷歌地图,于是乎,花了一天时间实在解决不了谷歌地图在百度地图里的偏移问题。
  没办法只能建议用户使用ol重写,保证一套代码同时满足国内国外需求,本来想着ol有以前的积累很快搞定,国内果断使用高德地图,国外使用谷歌地图。结果一提交,客户提出新的问题,国内用户有国外业务,仍然需要在国内查看国外地图,而高德地图在国外没有道路数据。。。这就很扯了,看来是避不过去百度地图这个大坑了,只能一边问候当初设计bd-09的人一边开搞。。。
  果不其然,遇到各种问题,期间也网上找了不少资料、咨询了同行,总算解决了,特记录下来,备无奈使用百度地图的同行参考~

开发环境

  简单说下开发环境:vue+openlayers6

第一个问题:调用百度地图缩放时瓦片产生偏移

  废话不多说,直接上正确调用百度地图栅格瓦片代码:
PS:如下方式并不能完美解决,完美解决方法详见第五个问题

let resolutions = [];
        for (let i = 0; i < 19; i++) {
          resolutions[i] = Math.pow(2, 18 - i);
        }
        let tilegrid = new TileGrid({
          origin: [0, 0],
          resolutions: resolutions,
        });

        let baidu_source = new TileImage({
          projection: "EPSG:3857",
          tileGrid: tilegrid,
          tileUrlFunction: function (tileCoord, pixelRatio, proj) {
            if (!tileCoord) {
              return "";
            }
            let z = tileCoord[0];
            let x = tileCoord[1];
            let y = -tileCoord[2] - 1;//ol6需要此处减一,否则缩放有偏移

            if (x < 0) {
              x = "M" + -x;
            }
            if (y < 0) {
              y = "M" + -y;
            }
            return "https://online3.map.bdimg.com/onlinelabel/?qt=tile&x=" + x + "&y=" + y + "&z=" + z + "&styles=pl&udt=20151021&scaler=1&p=1";
          }
        });
        mapLayer = new TileLayer({
          source: baidu_source
        })

引用:

import TileLayer from "ol/layer/Tile"
import TileImage from 'ol/source/TileImage';
import TileGrid from 'ol/tilegrid/TileGrid';

  问题主要出在这一句:let y = -tileCoord[2] - 1;
  openlayers6之前不减一可以正常显示,但是ol6这个版本机制改变了,至于改成啥了,咱也没看源码,按网上大神解决方式解决了就ok,以后有时间再研究吧(大概率很久很久了。。。)。

第二个问题:判断坐标是否在国内,常用方式不准确

  由于客户的货车全国各地均有,在使用网上常用方式:

return (lng < 72.004 || lng > 137.8347) || ((lat < 18.033 || lat > 55.8271) || false);//粗略判断

  进行国内国外判断是并不准确,翻了下网上博客,有好几种精确判定的方式,各有优劣,选了一个相对准确且运算速度快的,代码如下:

//计算四至范围
    Rectangle(latitude1, longitude1, latitude2, longitude2) {
        let reJ = {
            West: null,
            North: null,
            East: null,
            South: null,
        }
        reJ.West = Math.min(longitude1, longitude2);
        reJ.North = Math.max(latitude1, latitude2);
        reJ.East = Math.max(longitude1, longitude2);
        reJ.South = Math.min(latitude1, latitude2);
        return reJ
    },

    /**
     * 精确判断是否在中国
     * @param longitude
     * @param latitude
     * @returns {boolean}
     * @constructor
     */
    IsInsideChina(longitude, latitude) {
        //大陆
        let region = [
            this.Rectangle(49.220400, 79.446200, 42.889900, 96.330000),
            this.Rectangle(54.141500, 109.687200, 39.374200, 135.000200),
            this.Rectangle(42.889900, 73.124600, 29.529700, 124.143255),
            this.Rectangle(29.529700, 82.968400, 26.718600, 97.035200),
            this.Rectangle(29.529700, 97.025300, 20.414096, 124.367395),
            this.Rectangle(20.414096, 107.975793, 17.871542, 111.744104),
        ]
        //台湾省
        let exclude = [
            this.Rectangle(25.398623, 119.921265, 21.785006, 122.497559),
            this.Rectangle(22.284000, 101.865200, 20.098800, 106.665000),
            this.Rectangle(21.542200, 106.452500, 20.487800, 108.051000),
            this.Rectangle(55.817500, 109.032300, 50.325700, 119.127000),
            this.Rectangle(55.817500, 127.456800, 49.557400, 137.022700),
            this.Rectangle(44.892200, 131.266200, 42.569200, 137.022700),
        ]
        for (let i = 0; i < region.length; i++) {
            if (this.InRectangle(region[i], longitude, latitude)) {
                for (let j = 0; j < exclude.length; j++) {
                    if (this.InRectangle(exclude[j], longitude, latitude)) {
                        return false;
                    }
                }
                return true;
            }
        }
        return false;
    },
    /**
     * 判断是否在范围内
     * @param rect
     * @param longitude
     * @param latitude
     * @returns {boolean}
     * @constructor
     */
    InRectangle(rect, longitude, latitude) {
        return rect.West <= longitude && rect.East >= longitude && rect.North >= latitude && rect.South <= latitude;
    }

  整体原理比较简单,就是将原来的一个四至范围剖分出很多小的四至范围,增加判定准确度,这里面有个坑,把台湾省排除出去了,具体还没测试台湾省的范围内是否用gcj02偏移了,后面有数据可以验证下~

第三个问题:百度地图国内部分,存在坐标偏移问题

  问题解决到这,再次问候当初设计bd-09的人。。。根据以前的经验(5、6年前)百度地图是原始经纬度先gcj02偏移,然后bd-09偏移,然后再转web墨卡托,但是一通操作下来,坐标飞了,这特么奇了怪了,当年设计bd-09的人又换人了?此处再问候新的设计此机制的人。。。。
  网上搜了下,发现现在百度地图长能耐了,人家叫百度经纬度和百度墨卡托。。。。这是看谷歌搞了个web墨卡托,表示不服吗,你特么不符你用同一套标准椭球体行吗,非要自己玩自己的是吧,怪不得市场份额跟百度的市值一样缩的惨不忍睹。
  转换思路:wgs84(原始经纬度)转gcj02,然后转bd-09经纬度,最后百度经纬度坐标再转百度墨卡托。
  解决核心代码如下:

var coordinateTransform = {
    //定义一些常量
    PI: 3.1415926535897932384626,
    x_PI: 3.14159265358979324 * 3000.0 / 180.0,
    a: 6378245.0,
    ee: 0.00669342162296594323,
    EARTHRADIUS: 6370996.81,
    MCBAND: [12890594.86, 8362377.87, 5591021, 3481989.83, 1678043.12, 0],
    LLBAND: [75, 60, 45, 30, 15, 0],
    MC2LL: [[1.410526172116255e-8, 0.00000898305509648872, -1.9939833816331, 200.9824383106796, -187.2403703815547, 91.6087516669843, -23.38765649603339, 2.57121317296198, -0.03801003308653, 17337981.2], [-7.435856389565537e-9, 0.000008983055097726239, -0.78625201886289, 96.32687599759846, -1.85204757529826, -59.36935905485877, 47.40033549296737, -16.50741931063887, 2.28786674699375, 10260144.86], [-3.030883460898826e-8, 0.00000898305509983578, 0.30071316287616, 59.74293618442277, 7.357984074871, -25.38371002664745, 13.45380521110908, -3.29883767235584, 0.32710905363475, 6856817.37], [-1.981981304930552e-8, 0.000008983055099779535, 0.03278182852591, 40.31678527705744, 0.65659298677277, -4.44255534477492, 0.85341911805263, 0.12923347998204, -0.04625736007561, 4482777.06], [3.09191371068437e-9, 0.000008983055096812155, 0.00006995724062, 23.10934304144901, -0.00023663490511, -0.6321817810242, -0.00663494467273, 0.03430082397953, -0.00466043876332, 2555164.4], [2.890871144776878e-9, 0.000008983055095805407, -3.068298e-8, 7.47137025468032, -0.00000353937994, -0.02145144861037, -0.00001234426596, 0.00010322952773, -0.00000323890364, 826088.5]],
    LL2MC: [[-0.0015702102444, 111320.7020616939, 1704480524535203, -10338987376042340, 26112667856603880, -35149669176653700, 26595700718403920, -10725012454188240, 1800819912950474, 82.5], [0.0008277824516172526, 111320.7020463578, 647795574.6671607, -4082003173.641316, 10774905663.51142, -15171875531.51559, 12053065338.62167, -5124939663.577472, 913311935.9512032, 67.5], [0.00337398766765, 111320.7020202162, 4481351.045890365, -23393751.19931662, 79682215.47186455, -115964993.2797253, 97236711.15602145, -43661946.33752821, 8477230.501135234, 52.5], [0.00220636496208, 111320.7020209128, 51751.86112841131, 3796837.749470245, 992013.7397791013, -1221952.21711287, 1340652.697009075, -620943.6990984312, 144416.9293806241, 37.5], [-0.0003441963504368392, 111320.7020576856, 278.2353980772752, 2485758.690035394, 6070.750963243378, 54821.18345352118, 9540.606633304236, -2710.55326746645, 1405.483844121726, 22.5], [-0.0003218135878613132, 111320.7020701615, 0.00369383431289, 823725.6402795718, 0.46104986909093, 2351.343141331292, 1.58060784298199, 8.77738589078284, 0.37238884252424, 7.45]],
    
    _transformlat(lng, lat) {
        var ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng));
        ret += (20.0 * Math.sin(6.0 * lng * this.PI) + 20.0 * Math.sin(2.0 * lng * this.PI)) * 2.0 / 3.0;
        ret += (20.0 * Math.sin(lat * this.PI) + 40.0 * Math.sin(lat / 3.0 * this.PI)) * 2.0 / 3.0;
        ret += (160.0 * Math.sin(lat / 12.0 * this.PI) + 320 * Math.sin(lat * this.PI / 30.0)) * 2.0 / 3.0;
        return ret
    },

    _transformlng(lng, lat) {
        var ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng));
        ret += (20.0 * Math.sin(6.0 * lng * this.PI) + 20.0 * Math.sin(2.0 * lng * this.PI)) * 2.0 / 3.0;
        ret += (20.0 * Math.sin(lng * this.PI) + 40.0 * Math.sin(lng / 3.0 * this.PI)) * 2.0 / 3.0;
        ret += (150.0 * Math.sin(lng / 12.0 * this.PI) + 300.0 * Math.sin(lng / 30.0 * this.PI)) * 2.0 / 3.0;
        return ret
    },
        _getLoop(lng, min, max) {
        while (lng > max) {
            lng -= max - min;
        }
        while (lng < min) {
            lng += max - min;
        }
        return lng;
    },

    _getRange(lat, min, max) {
        if (min != null) {
            lat = Math.max(lat, min);
        }
        if (max != null) {
            lat = Math.min(lat, max);
        }
        return lat;
    }
}
/**
 * WGS转百度经纬
 * @param lon
 * @param lat
 * @returns {*[]}
 */
coordinateTransform.WGS2BD = function (lon, lat) {
    //先由经纬转火星
    var coor = this.WGS2GCJ(lon, lat);
    //再将火星转百度
    coor = this.GCJ2BD(coor[0], coor[1]);
    return coor;
}
/**
 * WGS84转GCj02
 * @param lon
 * @param lat
 * @returns {*[]}
 */
coordinateTransform.WGS2GCJ = function (lon, lat) {
    if (this._out_of_china(lon, lat)) {
        return [lon, lat]
    } else {
        var dlat = this._transformlat(lon - 105.0, lat - 35.0);
        var dlon = this._transformlng(lon - 105.0, lat - 35.0);
        var radlat = lat / 180.0 * this.PI;
        var magic = Math.sin(radlat);
        magic = 1 - this.ee * magic * magic;
        var sqrtmagic = Math.sqrt(magic);
        dlat = (dlat * 180.0) / ((this.a * (1 - this.ee)) / (magic * sqrtmagic) * this.PI);
        dlon = (dlon * 180.0) / (this.a / sqrtmagic * Math.cos(radlat) * this.PI);
        var mglat = lat + dlat;
        var mglon = lon + dlon;
        return [mglon, mglat];
    }
}
/**
 * 火星坐标系 (GCJ-02) 与百度坐标系 (BD-09) 的转换
 * 即谷歌、高德 转 百度
 * @param lon
 * @param lat
 * @returns {*[]}
 */
coordinateTransform.GCJ2BD = function (lon, lat) {
    var z = Math.sqrt(lon * lon + lat * lat) + 0.00002 * Math.sin(lat * this.x_PI);
    var theta = Math.atan2(lat, lon) + 0.000003 * Math.cos(lon * this.x_PI);
    var bd_lon = z * Math.cos(theta) + 0.0065;
    var bd_lat = z * Math.sin(theta) + 0.006;
    return [bd_lon, bd_lat];
}
/**
 * 百度经纬度转百度墨卡托
 * @param lng
 * @param lat
 * @returns {*[]}
 */
coordinateTransform.BD_WGS2MKT = function (lng, lat) {
    var cF = null;
    lng = this._getLoop(lng, -180, 180);
    lat = this._getRange(lat, -74, 74);
    for (var i = 0; i < this.LLBAND.length; i++) {
        if (lat >= this.LLBAND[i]) {
            cF = this.LL2MC[i];
            break;
        }
    }
    if (cF != null) {
        for (var i = this.LLBAND.length - 1; i >= 0; i--) {
            if (lat <= -this.LLBAND[i]) {
                cF = this.LL2MC[i];
                break;
            }
        }
    } 
    lng = cF[0] + cF[1] * Math.abs(lng);
    var cC = Math.abs(lat) / cF[9];
    lat = cF[2] + cF[3] * cC + cF[4] * cC * cC + cF[5] * cC * cC * cC + cF[6] * cC * cC * cC * cC + cF[7] * cC * cC * cC * cC * cC + cF[8] * cC * cC * cC * cC * cC * cC;
    lng *= (lng < 0 ? -1 : 1);
    lat *= (lat < 0 ? -1 : 1);
    return [lng, lat];
}

  BD_WGS2MKT这个方法里的代码参考网上大神,git地址:https://github.com/FreeGIS/CoordinateTransform
  写到这里想着终于完事了,于是选了全球的数据进行展示测试,对比了百度地图和谷歌地图上的数据,国内都基本在同一位置,国外澳大利亚和美国的都飞到海里了,我特么的,不是说国外部分是wgs84的未加偏移坐标吗?又是哪个百度的高人整的活,此处问候+1。。。

第四个问题:百度地图国外部分,存在坐标偏移问题

  接第三个问题,明明东南亚地区坐标按上面的转换方式没有便宜,为何澳大利亚和美国都飞了呢,试了好几次发现北半球、东半球均可以正常转换且无偏移,南半球、西半球均会产生偏移,仔细看了下大神的代码,发现转换所用的参数LLBAND只有0-75,那坐标要是负值的就直接报错了。。。
  想了半天解决方式无非两种,要么调用百度转换API,要么自己写转换算法,第一种调用百度API会有次数限制,花钱是不可能花钱的,第二种自己写算法也没那个精力 (本事)。。。
  是咱建议客户换了百度地图API的,不甘心就这样打脸,于是想想有没有什么好办法可以解决的,最开始想照着大神的参数把负值加上,但是没那个精力 (本事)去计算转换参数,想破了脑袋,试试能不能取个巧,遇到负值先转成正值,转换之后再变成负值,写完试验了下,结果和谷歌的叠加看没什么问题。。。
  最后总结下转换路径:百度地图叠加国外的数据,不进行gcj02转换,需要用原始经纬度(应该在国外与百度经纬度一致)直接转换为百度墨卡托才行。
  核心代码(其实就是做了个负值转换)如下:

/**
 * 百度经纬度转百度墨卡托(南半球、西半球均适用)
 * @param lng
 * @param lat
 * @returns {*[]}
 */
coordinateTransform.BD_WGS2MKT_NORTH = function (lng, lat) {
    let isNorth = false
    let isWest = false
    if(lat<0){
        lat=Math.abs(lat)
        isNorth = true
    }
    if(lng<0){
        lng=Math.abs(lng)
        isWest = true
    }
    var cF = null;
    lng = this._getLoop(lng, -180, 180);
    lat = this._getRange(lat, -74, 74);
    for (var i = 0; i < this.LLBAND.length; i++) {
        if (lat >= this.LLBAND[i]) {
            cF = this.LL2MC[i];
            break;
        }
    }
    if (cF != null) {
        for (var i = this.LLBAND.length - 1; i >= 0; i--) {
            if (lat <= -this.LLBAND[i]) {
                cF = this.LL2MC[i];
                break;
            }
        }
    } else {
        console.dir('未转成功',[lng, lat])
        return [lng, lat];
    }
    lng = cF[0] + cF[1] * Math.abs(lng);
    var cC = Math.abs(lat) / cF[9];
    lat = cF[2] + cF[3] * cC + cF[4] * cC * cC + cF[5] * cC * cC * cC + cF[6] * cC * cC * cC * cC + cF[7] * cC * cC * cC * cC * cC + cF[8] * cC * cC * cC * cC * cC * cC;
    lng *= (lng < 0 ? -1 : 1);
    lat *= (lat < 0 ? -1 : 1);
    if(isNorth){
        lat = -lat;
    }
    if(isWest){
        lng = -lng;
    }
    return [lng, lat];
}

第五个问题:百度地图开启wrapX,存在裂缝问题

  第五个问题属于遗留问题,现补充如下:
  虽然第一个问题解决了缩放时瓦片偏移的问题,但是需要地图左右无限拖动时,打开wrapX:true,会出现连接处裂缝,且缩放时瓦片仍然偏移。
裂缝问题截图
在这里插入图片描述

  经研究发现由于坐标系不一致导致此问题,以前百度瓦片采用3857加载的,虽然加载到3857的map上单个看不出问题,但是实际由于计算问题会导致边缘瓦片的偏移,故需要将百度瓦片使用BD09的坐标系进行加载,这里做了一个测试,百度瓦片用BD09,map仍然用3857,此时可以完美贴合,也不用将前面坐标转换再处理一次了,这样可以保证在国外仍然使用标准的3857坐标。
  核心代码如下:
第一步:自定义BD09坐标系

import {get as ProjGet, addProjection, addCoordinateTransforms} from 'ol/proj'

//BD:09
getBD09ProjByCode: function (projCode) {
    let projection = ProjGet(projCode)
    if (projection) {
      return projection
    }
    // proj4.defs("BD:09", "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=crs")
    /**
     *  定义百度投影,这是实现无偏移加载百度地图离线瓦片核心所在。
     *  网上很多相关资料在用OpenLayers加载百度地图离线瓦片时都认为投影就是EPSG:3857(也就是Web墨卡托投影)。
     *  事实上这是错误的,因此无法做到无偏移加载。
     *  百度地图有自己独特的投影体系,必须在OpenLayers中自定义百度投影,才能实现无偏移加载。
     *  百度投影实现的核心文件为bd09.js,在迈高图官网可以找到查看这个文件。
     */
    let projBD09 = new Projection({
      code: mapConf.curBaiDuProj, // mapConf.curBaiDuProj值为'BD:09'
      // extent: [-20037726.37, -11708041.66, 20037726.37, 12474104.17],//原始的百度范围
      // extent: [-20037508.34, -20037508.34, 20037508.34, 20037508.34],
      units: 'm',
      axisOrientation: 'neu',
      global: false
    });
    addProjection(projBD09);
    // addCoordinateTransforms('EPSG:4326', projBD09, projzh.ll2bmerc, projzh.bmerc2ll);//这里使用的转换算法跟我调用的开源代码一样,只能处理北半球东半球的坐标数据
    // addCoordinateTransforms('EPSG:3857', projBD09, projzh.smerc2bmerc, projzh.bmerc2smerc);
    return projection
  }

第二步:定义百度瓦片时,使用BD09坐标系

 // 自定义分辨率和瓦片坐标系
                let rlProject = [];

                // 计算百度使用的分辨率
                for (let i = 0; i <= 19; i++) {
                    rlProject[i] = Math.pow(2, 18 - i);
                }
                let _tilegrid = new TileGrid({
                    origin: [0, 0],    // 设置原点坐标
                    resolutions: rlProject // 设置分辨率
                });
                /**
                 * 加载百度地图离线瓦片不能用ol.source.XYZ,ol.source.XYZ针对谷歌地图(注意:是谷歌地图)而设计,
                 * 而百度地图与谷歌地图使用了不同的投影、分辨率和瓦片网格。
                 * 因此这里使用ol.source.TileImage来自行指定投影、分辨率、瓦片网格。
                 */
                let baiduSource = new TileImage({
                    projection: mapConf.curBaiDuProj,//值为'BD:09'
                    tileGrid: _tilegrid,
                    tilePixelRatio: 2,
                    wrapX: true,//允许左右无限移动
                    tileUrlFunction: (tileCoord, pixelRatio, proj) => {
                        if (!tileCoord) {
                            return "";
                        }
                        let z = tileCoord[0];
                        // let x: number | string = tileCoord[1];
                        let x = tileCoord[1];
                        // 注意,在openlayer3中由于载地图的方式是右上递增
                        // let y: number | string = tileCoord[2];
                        // 而openlayer6中是右下递增,所以y的值需要注意
                        // 6版本需要取负值,同时注意要减一,否则缩放有问题
                        // let y: number | string = -tileCoord[2] - 1;
                        let y = -tileCoord[2] - 1;

                        // 百度瓦片服务url将负数使用M前缀来标识
                        if (x < 0) {
                            x = 'M' + (-x);
                        }
                        if (y < 0) {
                            y = 'M' + (-y);
                        }
                        // online3的3是用来分流的,还可以是其他路径
                        // udt应该表示的是地图发布日期。p表示地图上的信息,scaler表示缩放
                        // 0表示不显示地名等标注信息,只单纯的地图地图,1表示显示地名信息
                        // return `http://online3.map.bdimg.com/onlinelabel/?qt=tile&x=${x}&y=${y}&z=${z}&styles=pl&udt=20220426&scaler=2&p=1`;//20190426
                        return "https://maponline3.bdimg.com/tile/?qt=vtile&x=" + x + "&y=" + y + "&z=" + z + "&styles=pl&udt=20151021&scaler=1&p=1";
                    }
                });
                mapLayer = new TileLayer({
                    source: baiduSource
                })
                break;

第三步:主map页面里初始化时,进行新坐标系的加载,一般放在new Map之前

    //bd-09不在默认的proj里面,需要单独新增
    getBD09ProjByCode(mapConf.curBaiDuProj);//值为'BD:09'

加载效果如下
在这里插入图片描述

  写到这里,所有问题才算全部解决,只能感叹,百度地图没落从一开始就注定了,虽说高德有阿里这个金主爸爸不担心营收压力可以全力做技术的优势,但是你百度也不差啊,广告的钱还是很好赚的呀,搜索不干人事,其他地方干点人事也行啊,没看见各种路子富起来的都做点慈善吗,**平衡知道不,不然ALL IN AI就能翻身了???

写在后面

  好久不写博客了,也好久没有做GIS开发了,没想到几年时间变化那么多,一直以为大家都按OGC标准来玩,没想到只有基础是可以复制的,代码时间长了就不见得能复用了,还是得常学常用,共勉。

评论 28
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值