WebGIS前端瓦片地圖顯示原理及實現


  目前,有很多WebGIS開發包,ArcGIS API for JS、OpenLayers、LeafLetjs等為我們從事WebGIS開發的人封裝了很多強大的功能。我們很方便的使用這些庫的時候,也讓我們忽略了很多原理性的東西。

比如說,我之前一直在被一個問題困擾,就是如何將一個點正確的顯示在瀏覽器屏幕的正確的位置,即經緯度坐標和屏幕坐標的轉換問題。直到我看到一位大牛的博客(點擊學習),里面對WebGIS的原理進行了深入的講解。看了他的文章后一直覺得,我寫這篇文章是多余的。但是大神的文章里面並沒有詳細講解原理的代碼實現。個人覺得還是很有必要通過實現相應功能的方式了解其原理,而且實現時還是遇到了不少的問題,所以還是寫了這篇文章。

在線地圖及參數

 Arcgis online上的瓦片地圖為例,服務中有幾個比較關鍵的使用到的參數。

  • Height、Weight:每個瓦片的寬度和高度
  • Resolution:每一個縮放級別下1像素代表的地圖單位(投影坐標)
  • Initial Extent:瓦片地圖的范圍

獲取地圖瓦片

通過觀察arcgis地圖的瓦片組織方式,

http://cache1.arcgisonline.cn/ArcGIS/rest/services/ChinaOnlineCommunityOnlyENG/MapServer/tile/縮放等級/行號/列號

通過python程序將一定縮放等級的瓦片保存到本地 我只抓取了0-5級別的瓦片,並按照arcgis瓦片的保存方式存儲。

# -*- coding:utf-8 -*-
import urllib2
import urllib
import os
import math
def GetPage(geturl):
    req = urllib2.Request(geturl)
    user_agent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 ' \
                 '(KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36'
    req.add_header('User-Agent', user_agent)
    response = urllib2.urlopen(req, timeout=10)
    page = response.read()
    return page
for level in range(0,6):
    try:
        newdir = "MapTitles/"+str(level)
        os.makedirs(newdir.decode("utf-8"))
    except:
        pass
    for row in range(0,int(math.pow(2,level))):
        try:
            newdir = "MapTitles/"+str(level)+"/"+str(row)
            os.makedirs(newdir.decode("utf-8"))
        except:
            pass
        for col in range(0,int(math.pow(2,level))):
            f = open("MapTitles/"+str(level)+"/"+str(row)+"/"+str(col)+'.jpg', 'wb')
            dataurl = "http://cache1.arcgisonline.cn/ArcGIS/rest/services/ChinaOnlineCommunityOnlyENG/MapServer/tile/"+str(level)+"/"+str(row)+"/"+str(col)
            data = GetPage(dataurl)
            f.write(data)
            f.close()
            pass
        pass
    pass
View Code

 展示頁面

展會頁面只含有一個canvas元素作為地圖容器。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>顯示瓦片地圖</title>
</head>
<body>
<canvas width="1000px" height="700px" id="mapcv" style="margin: 10px"></canvas>
</body>
<script src="Libs/jquery-1.9.1.min.js"></script>
<script src="Scripts/config.js"></script>
<script src="Scripts/init.js"></script>
</html>
View Code

配置信息

在config.js里面保存了相關的配置信息

 

var MapConfig={
    RootDir:'MapTitles/',
    ViewHeight:700,
    ViewWidth:1000,
    TitlePix:256,
    Resolution:[156543.033928,78271.5169639999,39135.7584820001,19567.8792409999,
                9783.93962049996,4891.96981024998,2445.98490512499,1222.99245256249,
                611.49622628138,305.748113140558,152.874056570411,76.4370282850732,
                38.2185141425366,19.1092570712683,9.55462853563415,4.77731426794937,
                2.38865713397468,1.19432856685505],
    Scale:[ 591657527.591555,295828763.795777,147914381.897889,73957190.948944,
            36978595.474472,18489297.737236,9244648.868618,4622324.434309,2311162.217155,
            1155581.108577,577790.554289,288895.277144,144447.638572,72223.819286,
            36111.909643,18055.954822,9027.977411,4513.988705],
    FullExtent:{
        xmin : -20037507.0672,
        ymin : -20036018.7354,
        xmax : 20037507.0672,
        ymax : 20102482.4102,
        spatialReference : {
            wkid : 102100
        }
    }
};
View Code

 

功能實現 init.js

上面只是把代碼列了出來,這一部分才是我要講的終點(才到重點☺)

① 確定戰士的地圖中心點坐標,以及縮放級別
② 計算當前窗口顯示的地圖范圍

我們可以根據窗口的中心點坐標,窗口大小,以及當前縮放級別的Resolution可以很容易通過計算得到,當前窗口你可以看到的地圖范圍。

//當前窗口顯示的范圍
minX=centerGeoPoint.x-(MapConfig.Resolution[level]*MapConfig.ViewWidth/2);
maxX=centerGeoPoint.x+(MapConfig.Resolution[level]*MapConfig.ViewWidth/2);
minY=centerGeoPoint.y-(MapConfig.Resolution[level]*MapConfig.ViewHeight/2);
maxY=centerGeoPoint.y+(MapConfig.Resolution[level]*MapConfig.ViewHeight/2);

此處要注意一下地圖的行列號的起點在左上角,但是地圖左上角的投影坐標x是最小的,y是最大的。也就會說行列號的起點在左上角,投影坐標的起點在左下角。

③ 計算左上角起始行列號

//左上角開始的行列號
leftTopTitleRow = Math.floor(Math.abs(maxY-MapConfig.FullExtent.ymax)/MapConfig.Resolution[level]/MapConfig.TitlePix);
leftTopTitleCol = Math.floor(Math.abs(minX-MapConfig.FullExtent.xmin)/MapConfig.Resolution[level]/MapConfig.TitlePix);

④ 計算實際地理范圍

//實際地理范圍
realMinX = MapConfig.FullExtent.xmin+leftTopTitleCol*MapConfig.TitlePix*MapConfig.Resolution[level];
realMaxY = MapConfig.FullExtent.ymax-leftTopTitleRow*MapConfig.TitlePix*MapConfig.Resolution[level];

我們都知道,我們獲取到的瓦片的范圍一定是大於顯示窗口的范圍。否則在窗口內顯示的地圖是不完整的

 

⑤ 計算左上角偏移像素

在將地圖瓦片拼接到窗口內是需要考慮到實際地理范圍與顯示地理范圍的偏移

//計算左上角偏移像素
offSetX = (realMinX-minX)/MapConfig.Resolution[level];
offSetY = (maxY-realMaxY)/MapConfig.Resolution[level];

⑥ 計算瓦片個數

獲得瓦片個數后就可以根據瓦片個數以及偏移后的起始瓦片位置,將每一個瓦片拼接到canvas相應的位置上

//計算瓦片個數
xClipNum = Math.ceil((MapConfig.ViewHeight+Math.abs(offSetY))/MapConfig.TitlePix);
yClipNum = Math.ceil((MapConfig.ViewWidth+Math.abs(offSetX))/MapConfig.TitlePix);

⑦ 前端繪制瓦片

var mapcv = document.getElementById("mapcv");
    var myctx = mapcv.getContext("2d");
    for(var i=0;i<xClipNum;i++){
        for(var j=0;j<yClipNum;j++){
            var beauty = new Image();
            beauty.src = MapConfig.RootDir+level+"/"+(leftTopTitleRow+i)+"/"+(leftTopTitleCol+j)+".jpg";
            var TitleImg={
                img:null,
                x:0,
                y:0
            };
            TitleImg.img=beauty;
            TitleImg.x=offSetX+(j*MapConfig.TitlePix);
            TitleImg.y=offSetY+(i*MapConfig.TitlePix);
            TitlesArry.push(TitleImg);
            myctx.drawImage(TitleImg.img, TitleImg.x, TitleImg.y);
        }
    }

全部代碼

里面涉及到了一個經緯度換投影坐標的函數,詳情參考本人的另一篇關於百度坐標、WGS-84、火星坐標,以及投影坐標與經緯度的轉換的文章(點擊跳轉

 

$(document).ready(function(){
    moveX = 0;
    moveY = 0;
    TitlesArry=[];
    //設置將要現實的地圖中心點
    centerGeoPoint={
        x:116.337737,
        y:39.912465
    };
    centerGeoPoint=lonlatTomercator(centerGeoPoint);
    level = 5;
    //當前窗口顯示的范圍
    minX=centerGeoPoint.x-(MapConfig.Resolution[level]*MapConfig.ViewWidth/2);
    maxX=centerGeoPoint.x+(MapConfig.Resolution[level]*MapConfig.ViewWidth/2);
    minY=centerGeoPoint.y-(MapConfig.Resolution[level]*MapConfig.ViewHeight/2);
    maxY=centerGeoPoint.y+(MapConfig.Resolution[level]*MapConfig.ViewHeight/2);
    //左上角開始的行列號
    leftTopTitleRow = Math.floor(Math.abs(maxY-MapConfig.FullExtent.ymax)/MapConfig.Resolution[level]/MapConfig.TitlePix);
    leftTopTitleCol = Math.floor(Math.abs(minX-MapConfig.FullExtent.xmin)/MapConfig.Resolution[level]/MapConfig.TitlePix);
    //實際地理范圍
    realMinX = MapConfig.FullExtent.xmin+leftTopTitleCol*MapConfig.TitlePix*MapConfig.Resolution[level];
    realMaxY = MapConfig.FullExtent.ymax-leftTopTitleRow*MapConfig.TitlePix*MapConfig.Resolution[level];

    //計算左上角偏移像素
    offSetX = (realMinX-minX)/MapConfig.Resolution[level];
    offSetY = (maxY-realMaxY)/MapConfig.Resolution[level];
    //計算瓦片個數
    xClipNum = Math.ceil((MapConfig.ViewHeight+Math.abs(offSetY))/MapConfig.TitlePix);
    yClipNum = Math.ceil((MapConfig.ViewWidth+Math.abs(offSetX))/MapConfig.TitlePix);
    //右下角行列號
    rightBottomTitleRow = leftTopTitleRow+xClipNum-1;
    rightBottomTitleCol = leftTopTitleCol+yClipNum-1;
    realMaxX = MapConfig.FullExtent.xmin+(rightBottomTitleCol+1)*MapConfig.TitlePix*MapConfig.Resolution[level];
    realMinY = MapConfig.FullExtent.ymax-(rightBottomTitleRow+1)*MapConfig.TitlePix*MapConfig.Resolution[level];
    var mapcv = document.getElementById("mapcv");
    var myctx = mapcv.getContext("2d");
    for(var i=0;i<xClipNum;i++){
        for(var j=0;j<yClipNum;j++){
            var beauty = new Image();
            beauty.src = MapConfig.RootDir+level+"/"+(leftTopTitleRow+i)+"/"+(leftTopTitleCol+j)+".jpg";
            var TitleImg={
                img:null,
                x:0,
                y:0
            };
            TitleImg.img=beauty;
            TitleImg.x=offSetX+(j*MapConfig.TitlePix);
            TitleImg.y=offSetY+(i*MapConfig.TitlePix);
            TitlesArry.push(TitleImg);
            myctx.drawImage(TitleImg.img, TitleImg.x, TitleImg.y);
        }
    }
});
function lonlatTomercator(lonlat) {
    var mercator={x:0,y:0};
    var x = lonlat.x *20037508.34/180;
    var y = Math.log(Math.tan((90+lonlat.y)*Math.PI/360))/(Math.PI/180);
    y = y *20037508.34/180;
    mercator.x = x;
    mercator.y = y;
    return mercator ;
}
View Code

 

總結

希望對WebGIS的初學者理解瓦片地圖顯示原理能有幫助

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM