墨卡托投影瓦片转换成WGS84投影瓦片

墨卡托,投影,瓦片,转换成,wgs84 · 浏览次数 : 19

小编点评

**图中两级附近是很色的原因是:** 由于 Mercator 投影无数据完整,所以图中两级附近的地图区域的经度范围在 > 85° 或者 < -85°。这些区域的经度值不在 Mercator 投影范围内,导致图中这些区域无法得到正常渲染。

正文

如题,相信任何一个GIS引擎开发者都会遇到的问题,要解决这个问题,首先要了解两者的区别(mercator投影).WGS84投影

如图所示,是一张Mercator投影的地图瓦片,这种投影将地球投影成 W 与 H 相等的图片,是一个正方形(宽度与高度相等)

经纬度方式:-180°,-85°,+180°,+85°, 两极数据超过85°的地方无数据。

下图所示WGS84投影瓦片,图中宽度 = 2倍高度,这种投影比较容易理解

经纬度方式:-180°,-90°,+180°,+90°, 数据无丢失。

目前主流GIS球体都内部都采用WGS84投影(例如 Google Earth,Arg Earth,FastEarth OsgEarth,Cesuim),这种投影更加容易的被球体适配,所以作为主流使用方式。

二维地图则更多的使用Mercaror投影(例如 Google Map,Baidu,Bing,Arc Map),对于两种投影的数据都在大规模的使用,如何能在三维球体上也能使用Mercator投影的

地图数据,就是本文所要解决的问题。

常见的解决方式有如下几种:

1.  实时CPU计算,用CPU方式实时的将mercator瓦片转换成 wgs84瓦片(代表作: FastEarth),也是本文的实现方式

  优点: 通用性强

  缺点:    速度相比GPU要慢一点,因为墨卡托投影 范围  - 180°,+180°, -85°,+ 85°,对于两极数据 没有数据输出,转换后数据丢失。

  处理流程如下图所示:

2.  实时GPU计算,利用GPU机型实时计算,该方式速度快(代表作:cesium)。

3.  非实时计算,与第一种类似,提前计算转换便存储计算结果。

4.  参考代码如下:

计算经纬度范围函数,输入的是瓦片的Id,输出瓦片的经纬度范围

        /// <summary>
        /// 获取瓦片编号
        /// </summary>
        aabb2dr getRange(const int3& tileKey) override
        {
            unsigned    xTiles      =   2 << (tileKey.z);
            unsigned    yTiles      =   1 << (tileKey.z);

            double      xTileWidth  =   TWO_PI / xTiles;
            double      yTileHeight =   PI / yTiles;
            double      dLon0       =   tileKey.x * xTileWidth -PI;
            double      dLat0       =   HALF_PI - tileKey.y * yTileHeight;
            aabb2dr     abbb;
            abbb.setExtents(dLon0,dLat0 - yTileHeight,dLon0 + xTileWidth,dLat0);
            
            return  abbb;
        }

根据经纬度范围计算Mercator瓦片Id

       aabb2di   calcMercatorRange(aabb2dr wgsAabb)
      { wgsAabb._minimum *= RAD2DEG; wgsAabb._maximum *= RAD2DEG; double resX = wgsAabb.getSize().x / tileWidth; double resY = wgsAabb.getSize().y / tileHeight; real2 pxOffset(resX,resY); /// 2. 根据经纬度范围查询mercator id list; int2 tileStart = merctor.getTileId(wgsAabb._minimum + pxOffset,lev); int2 tileEnd = merctor.getTileId(wgsAabb._maximum - pxOffset,lev); int minX = std::min(tileStart.x,tileEnd.x); int minY = std::min(tileStart.y,tileEnd.y); int maxX = std::max(tileStart.x,tileEnd.x); int maxY = std::max(tileStart.y,tileEnd.y);
       return      aabb2di(minX,minY,maxX,maxY);
}

 

合成大的瓦片

FEImage     toImage(aabb2di box,int tileWidth,int tileHeight)
        {
            int         width       =   (box._maximum.x - box._minimum.x + 1) * tileWidth;
            int         height      =   (box._maximum.y - box._minimum.y + 1) * tileHeight;

            FEImage     srcImage;
            FEImage     tmpImage;
            srcImage.create(width,height,FEImage::FORMAT_RGB);
            srcImage.fillColor(Rgba(0,0,0,0));

            tmpImage.create(tileWidth,tileHeight,FEImage::FORMAT_RGB);
            
            aabb2dr     srcBox;

            /// 3. 加载所有mercator id list
            /// 4. 合并image 生成一个大的 image;
            for (int c = minX ; c <= maxX;++ c)
            {
                for (int r = minY ; r <= maxY;++ r)
                {
                    FEObject*   ptr     =   grp->readObject(app,int3(c,r,lev),user,userArg,&tmpImage);
                 
                    if(ptr == nullptr)
                        continue;

                    byte*       pBuffer =   (byte*)srcImage.dataPtr((c - minX) * tileWidth, (r - minY) * tileHeight);
                    byte*       pSrc    =   (byte*)tmpImage.data();

                    for (int h = 0; h < tileHeight; ++h)
                    {
                        memcpy(pBuffer,pSrc,tmpImage.pitchSize());

                        pBuffer +=  srcImage.pitchSize();
                        pSrc    +=  tmpImage.pitchSize();
                    }
                    aabb2dr box =   merctor.getRange(int3(c,r,lev));
                    srcBox.merge(box);
                }
            }
        }

萃取函数:

bool    convert(  const FEImage& srcImage
                , FEImage& dstImage
                , const aabb2dr& srcBox
                , aabb2dr& dstBox
                , int dstW =256
                , int dstH = 256)
{
    OGRSpatialReference srcSRS;
    srcSRS.importFromEPSG(3857);

    OGRSpatialReference dstSRS;
    dstSRS.importFromEPSG(4326);

    char*     srcPrj  =   nullptr;
    char*     dstPrj  =   nullptr;

    srcSRS.exportToWkt(&srcPrj);
    dstSRS.exportToWkt(&dstPrj);

    /// image 数据转换成 gdal数据源
    uint            w           =   srcImage.width();
    uint            h           =   srcImage.height();
    GDALDriver*     poDriver    =   GetGDALDriverManager()->GetDriverByName("MEM");
    GDALDataset*    poSrcDS     =   poDriver->Create( "MEM", w, h,3, GDT_Byte,nullptr);
    byte*           pData       =   (byte*)srcImage.data();

    auto            rBand       =   (GDALRasterBand*)GDALGetRasterBand(poSrcDS, 1 );
    auto            gBand       =   (GDALRasterBand*)GDALGetRasterBand(poSrcDS, 2 );
    auto            bBand       =   (GDALRasterBand*)GDALGetRasterBand(poSrcDS, 3 );

    GDALRasterIOExtraArg exterArg;
    INIT_RASTERIO_EXTRA_ARG(exterArg);
    exterArg.eResampleAlg   =   GDALRIOResampleAlg::GRIORA_Cubic;

    rBand->RasterIO(GF_Write, 0, 0, w, h, pData + 0, w,h, GDT_Byte, 3, w * 3,&exterArg);
    gBand->RasterIO(GF_Write, 0, 0, w, h, pData + 1, w,h, GDT_Byte, 3, w * 3,&exterArg);
    bBand->RasterIO(GF_Write, 0, 0, w, h, pData + 2, w,h, GDT_Byte, 3, w * 3,&exterArg);

    double  srcTrans[6] =   {0};
    double  resX        =   srcBox.getSize().x / w;
    double  resY        =   srcBox.getSize().y / h;
    /// min longitude
    srcTrans[0]         =   srcBox._minimum.x; 
    srcTrans[1]         =   resX;
    srcTrans[2]         =   0;
    /// max latitude
    srcTrans[3]         =   srcBox._maximum.y; 
    srcTrans[4]         =   0;
    srcTrans[5]         =   -resY;

    poSrcDS->SetGeoTransform(srcTrans);
    poSrcDS->SetProjection(srcPrj);

    double  dstTrans[6] =   {0};
    double  dstResX     =   dstBox.getSize().x / dstW;
    double  dstResY     =   dstBox.getSize().y / dstH;

    /// min longitude
    dstTrans[0]         =   dstBox._minimum.x; 
    dstTrans[1]         =   dstResX;
    dstTrans[2]         =   0;
    /// max latitude    
    dstTrans[3]         =   dstBox._maximum.y; 
    dstTrans[4]         =   0;
    dstTrans[5]         =   -dstResY;

    /// 做转换
    GDALDataset*    dstDS   =   extractDataset(poSrcDS,dstTrans,dstW,dstH,dstPrj);
    if (dstDS == nullptr)
    {
        GDALClose(poSrcDS);
        return  false;
    }

    byte*           pDstBuf =   (byte*)dstImage.data();
    auto            rDst    =   (GDALRasterBand*)GDALGetRasterBand(dstDS, 1 );
    auto            gDst    =   (GDALRasterBand*)GDALGetRasterBand(dstDS, 2 );
    auto            bDst    =   (GDALRasterBand*)GDALGetRasterBand(dstDS, 3 );

    rDst->RasterIO(GF_Read, 0, 0, dstW, dstH, pDstBuf + 0, dstW, dstH, GDT_Byte, 3, dstW * 3);
    gDst->RasterIO(GF_Read, 0, 0, dstW, dstH, pDstBuf + 1, dstW, dstH, GDT_Byte, 3, dstW * 3);
    bDst->RasterIO(GF_Read, 0, 0, dstW, dstH, pDstBuf + 2, dstW, dstH, GDT_Byte, 3, dstW * 3);

    GDALClose(dstDS);

    return  true;
}

 

 结果展示:

图中两级附近是很色的,原因即纬度在 > 85° 或者 < -85°,Mercator投影无数据

完整代码因依赖工程,有需要的小伙伴可以微信联系我,或者留下邮箱

微信号:JingWeiKuaiTu

 

与墨卡托投影瓦片转换成WGS84投影瓦片相似的内容:

墨卡托投影瓦片转换成WGS84投影瓦片

如题,相信任何一个GIS引擎开发者都会遇到的问题,要解决这个问题,首先要了解两者的区别(mercator投影).WGS84投影 如图所示,是一张Mercator投影的地图瓦片,这种投影将地球投影成 W 与 H 相等的图片,是一个正方形(宽度与高度相等) 经纬度方式:-180°,-85°,+180°,

研发三维GIS系统笔记/实现wgs84投影-001

1. 工作内容,改造引擎,支持wgs84投影 改造原因:目前投影是墨卡托投影(与Google Map一致) 目前的GIS系统是二维的采用这个坐标系是没有问题的 但不支持wgs84瓦片数据以及高程数据,工作中很多数据是wgs84格式的,尤其很多三维GIS都是采用wgs84投影 wgs84 与merca

我有点想用JDK17了

大家好呀,我是summo,JDK版本升级的非常快,现在已经到JDK20了。JDK版本虽多,但应用最广泛的还得是JDK8,正所谓“他发任他发,我用Java8”。 其实我也不太想升级JDK版本,感觉投入高,收益小,不过有一次我看到了一些使用JDK17新语法写的代码,让我改变了对升级JDK的看法,因为这些

[转帖]墨菲定律——你越害怕的事越会发生的

https://baijiahao.baidu.com/s?id=1736226498427322139&wfr=spider&for=pc 关注 车站里,队伍总是排成长龙,你仔细计算着办理速度,换到了可能最快的一队排上,却没想到前边的人发生了争执,办理一度停滞;工作中,常常是马上就到了汇报环节突然

我不应该用JWT的!

一、前言 大家好呀,我是summo,之前有自学过Shrio框架,网上一搜就有SpringBoot整合Shrio+ JWT的文章,我是在学习Shrio框架的时候顺带学的JWT。后来我还看见有很多博主专门写文章介绍JWT,说这个东西的优点很多,安全性好、去中心化、方便啥的,我就把JWT也应用在我们自己的

使用中台 Admin.Core 实现了一个Razor模板的通用代码生成器

前言 前面使用 Admin.Core 的代码生成器生成了通用代码生成器的基础模块 分组,模板,项目,项目模型,项目字段的基础功能,本篇继续完善,实现最核心的模板生成功能,并提供生成预览及代码文件压缩下载 准备 首先清楚几个模块的关系,如何使用,简单画一个流程图 前面完成了基础的模板组,模板管理,项目

基于 .net core 8.0 的 swagger 文档优化分享-根据命名空间分组显示

之前也分享过 Swashbuckle.AspNetCore 的使用,不过版本比较老了,本次演示用的示例版本为 .net core 8.0,从安装使用开始,到根据命名空间分组显示,十分的有用

《优化接口设计的思路》系列:第十一篇—表格的导入导出接口优化

一、前言 大家好!我是sum墨,一个一线的底层码农,平时喜欢研究和思考一些技术相关的问题并整理成文,限于本人水平,如果文章和代码有表述不当之处,还请不吝赐教。 作为一名从业已达六年的老码农,我的工作主要是开发后端Java业务系统,包括各种管理后台和小程序等。在这些项目中,我设计过单/多租户体系系统,

中台框架模块开发实践-用 Admin.Core 代码生成器生成通用代码生成器的模块代码

之前分享中台 Admin.Core 的模块代码生成器,陆续也结合群友们的反馈,完善了一些功能和模板上的优化,而本篇将基于此代码生成器生成一个通用代码生成器模块的基本代码 后续再在此代码的基础上进行完善,制作一个通用的代码生成器

【BUG记录】Cause: java.sql.SQLException: Incorrect string value: '\xF0\x9F\x90\xA6' for column 'name' at row 1

大家好呀,我是summo,这次的文章标题是一个Mysql数据库的SQL错误,遇到的同学自然懂,没遇到的同学希望你永远也不要遇到。 一、错误说明 Cause: java.sql.SQLException: Incorrect string value: '\xF0\x9F\x90\xA6' for c