(相關的代碼可以從https://github.com/goldenhawking/mercator.qtviewer.git直接克隆)
git
咱們現在是準備作一個C/S架構的地圖顯示控件。就一定牽扯到座標系和UI的界面控制。github
眼下osm採用墨卡託投影,這個投影的原理可以用一個假想實驗解釋。windows
若是地球是一個透明的球體。在球體的球心有一個光源。咱們把一張幕布沿着赤道捲起來。使之與地球內切,地球上的一個點在這塊幕布上的投影就是其墨卡託投影位置。架構
上圖中,地球半徑是R=6378137米,可想而知,圓柱頂面周長爲 2 pi R。咱們以0度經線投影爲中軸,用剪刀沿着180度經線投影剪開,就能夠展開造成地圖面。這個地圖面的中心與地理位置 (0,0)重合;X軸是赤道,長度爲 2 * Pi * R,取值範圍 -pi R 到 pi R。即 -20037508 ~ + 20037508 米。Y軸是本初子午線投影。點光源直接照耀球迷質點造成的影子具備拉伸特性,高緯度地區拉伸很嚴重。其拉伸效果是 y = R ln (tan (pi/4 + lat/2)),在南極、北極存在奇點。框架
對通常的瓦片地圖而言,爲了方便計算機處理。通常y的取值範圍也是 -20037508 ~ + 20037508 米,反推回去,相應緯度範圍僅僅能表示到 -85度~85度。函數
上面所說的墨卡託投影完畢了從地球上的一點到虛擬圓柱上一點的映射。然而,爲了使用計算機存儲、訪問地圖,就必須引入採樣。所謂的採樣,即便用離散的柵格像素表示連續的地理空間數據。咱們眼下所見的OpenStreetMap採用了19層比例尺,標號爲 0 ~ 18.post
在0級,整個世界地圖被縮略爲一塊 256x256 的位圖。在1級。咱們把分辨率提升一倍。地圖由4塊256x256的瓦片組成;在二級,規模擴到 16塊,以此類推。下圖顯示的是這樣的層次關係:this
可以簡單推算一下各級比例尺下,完整圖幅的大小。柵格化後的座標左上角是0,0,右下角是 size-1, size-1
spa
級別 | 瓦片行/列數 | 圖幅長/寬(size) | 粗略像素分辨率 |
0 | 1 | 256 | 156千米 |
1 | 2 | 512 | 78千米 |
2 | 4 | 1024 | 39千米 |
3 | 8 | 2048 | 19千米 |
4 | 16 | 4096 | 9千米 |
5 | 32 | 8192 | 5千米 |
6 | 64 | 16384 | 2.5千米 |
7 | 128 | 32768 | 1.3千米 |
8 | 256 | 65536 | 611米 |
9 | 512 | 131072 | 305米 |
10 | 1024 | 262144 | 152米 |
11 | 2048 | 524288 | 76米 |
12 | 4096 | 1048576 | 38米 |
13 | 8192 | 2097152 | 19米 |
14 | 16384 | 4194304 | 9米 |
15 | 32768 | 8388608 | 4.5米 |
16 | 65536 | 16777216 | 2.2米 |
17 | 131072 | 33554432 | 1.1米 |
18 | 262144 | 67108864 | 0.5米 |
這些瓦片被編號爲行、列,加上比例尺,一個瓦片的索引即爲 (level, x, y)。即比例尺、所在列號、所在行號。咱們僅僅要這三個參數,就能夠從openstreetmap瓦片server上下載瓦片位圖。插件
如:
http://c.tile.openstreetmap.org/0/0/0.png
http://c.tile.openstreetmap.org/2/2/1.png
需要注意的是。OSM瓦片server速度很是慢,當中國的鏡像位置有很多,比方
http://120.52.72.79/c.tile.openstreetmap.org/c3pr90ntcsf0/2/2/1.png
建議使用 FireFox 查看頁面元素,得到使用的瓦片真實地址。
視圖在這裏可簡單理解爲一個窗體,具備有限的像素大小。
視圖控制包含顯示、漫遊、縮放等操做。這些操做的關鍵是從全局座標(瓦片墨卡托地圖)到視圖座標(通常左上角是0,0,右下角是 width-1。height-1) 的相互映射。
咱們可以記錄當前窗體左上角、右下角的全局座標,從而實現窗體像素和全局像素的換算。然而,考慮到對於各個比例尺而言,圖幅是不斷變化的。且記錄左上角、右下角座標在比例尺變化後。相應的全局座標必須刷新。咱們決定不這麼作。
可以採用更簡單的方式——記錄中心相對百分比座標和當前比例尺來實現一樣功能,進而,百分比做爲第一種全局座標系被創建起來,最好仍是稱之爲百分比座標 。
百分比座標是一個等效的尺度無關座標。記錄了當前視圖中心位置相應的摩卡託座標百分比。
//Center Lat,Lon double m_dCenterX; //percentage, -0.5~0.5 double m_dCenterY; //percentage, -0.5~0.5 int m_nLevel; //0-18
在第一章的投影座標中。X.Y座標定義域均爲 [-piR , piR],而百分比座標即爲摩卡託座標與2piR的比值,記錄了當前中心實際偏離全圖中心的的比例,實質是歸一化。
設 px,py爲百分比座標, mx,my爲摩卡託投影座標。兩者關係爲
px = mx / 2piR
py = - my / 2piR
百分比座標的優勢是尺度無關。在各類比例尺下,一個固定的地理位置相應的百分比座標不變。
需要注意的是,百分比座標的Y軸取反。以便在興許轉換中與設備座標在度量、座標方向上取得一致。
百分比座標是一個浮點值。還沒法相應到當前比例尺圖幅上去。
咱們在第二章已經介紹了另一種全局座標系,即全局像素座標系。
全局像素座標即當前比例尺下。一個位置相應的像素位置。第二章的表格裏。記錄了每個比例尺下的圖幅大小。這個座標就是地理位置相應當前比例尺圖幅上的像素點位置。當前圖幅左上角爲(0,0),右下角爲 (size-1, size-1)。 有了全局像素座標。就能夠計算需要的像素位於哪一個瓦片上。因爲所有的瓦片都是256x256大小。瓦片位置直接等於 Xw / 256, Yw/256。同一時候。基於3.1, 3.2的工做,依據當前窗體的尺寸,就能夠立馬計算窗體中隨意一點的全局像素座標。代碼是這種:
計算窗體位置(dX,dY)相應的全局像素座標(px,py)bool tilesviewer::CV_DP2World(qint32 dX, qint32 dY, double * px, double * py) { if (!px||!py) return false; //!1.Current World Pixel Size, connected to nLevel int nCurrImgSize = (1<<m_nLevel)*256; //!2.current DP according to center double dx = dX-(width()/2.0); double dy = dY-(height()/2.0); //!3.Percentage -0.5 ~ 0.5 coord double dImgX = dx/nCurrImgSize+m_dCenterX; double dImgY = dy/nCurrImgSize+m_dCenterY; //!4.Calculat the World pixel coordinats *px = dImgX * nCurrImgSize + nCurrImgSize/2; *py = dImgY * nCurrImgSize + nCurrImgSize/2; return true; }
上圖中,黑色爲全局像素座標,紅色爲百分比座標,綠色爲窗體像素座標。
全局像素是由瓦片拼接而成的。咱們用3.2節的世界座標系可方便求取瓦片像素座標。
瓦片行 = wy /256, 瓦片列 = wx /256
瓦片像素: (wx % 256, wy %256)
上圖中。藍色爲瓦片座標與瓦片分割線。相應8x8。爲比例尺 3 時的情形。
有了上述幾種座標系,咱們可以爲用戶給定的 中心百分比座標 m_dCenterX, m_dCenterY,結合窗體大小。直接得到需要的瓦片索引。以及他們粘貼在當前視窗上的像素偏移。
/*! \brief When the tileviewer enter its paint_event function, this callback will be called. \fn layer_tiles::cb_paintEvent \param pImage the In-mem image for paint . */ void layer_tiles::cb_paintEvent( QPainter * pPainter ) { if (!m_pViewer || m_bVisible==false) return; //!1,We should first calculate current windows' position, centerx,centery, in pixcel. double nCenter_X ,nCenter_Y; //!2,if the CV_PercentageToPixel returns true, painting will begin. if (true==m_pViewer->CV_Pct2World( m_pViewer->centerX(), m_pViewer->centerY(), &nCenter_X,&nCenter_Y)) { int sz_whole_idx = 1<<m_pViewer->level(); //!2.1 get current center tile idx, in tile count.(tile is 256x256) int nCenX = nCenter_X/256; int nCenY = nCenter_Y/256; //!2.2 calculate current left top tile idx int nCurrLeftX = floor((nCenter_X-m_pViewer->width()/2)/256.0); int nCurrTopY = floor((nCenter_Y-m_pViewer->height()/2)/256.0); //!2.3 calculate current right bottom idx int nCurrRightX = ceil((nCenter_X+m_pViewer->width()/2)/256.0); int nCurrBottomY = ceil((nCenter_Y+m_pViewer->height()/2)/256.0); //!2.4 a repeat from tileindx left to right. for (int col = nCurrLeftX;col<=nCurrRightX;col++) { //!2.4.1 a repeat from tileindx top to bottom. for (int row = nCurrTopY;row<=nCurrBottomY;row++) { QImage image_source; int req_row = row, req_col = col; if (row<0 || row>=sz_whole_idx) continue; if (col>=sz_whole_idx) req_col = col % sz_whole_idx; if (col<0) req_col = (col + (1-col/sz_whole_idx)*sz_whole_idx) % sz_whole_idx; //!2.4.2 call getTileImage to query the image . if (true==this->getTileImage(m_pViewer->level(),req_col,req_row,image_source)) { //bitblt int nTileOffX = (col-nCenX)*256; int nTileOffY = (row-nCenY)*256; //0,0 lefttop offset int zero_offX = int(nCenter_X+0.5) % 256; int zero_offY = int(nCenter_Y+0.5) % 256; //bitblt cood int tar_x = m_pViewer->width()/2-zero_offX+nTileOffX; int tar_y = m_pViewer->height()/2-zero_offY+nTileOffY; //bitblt pPainter->drawImage(tar_x,tar_y,image_source); } } } } }
拖動、漫遊相應的是鼠標消息。鼠標消息中的座標全部都是視窗像素。咱們僅僅要把視窗像素換算爲百分比,把音響施加到中心座標下,就能夠完畢動做。
縮放是指改變比例尺 m_nLevel,無需別的操做。m_nLevel改變後,立馬重繪窗體,一切皆本身主動計算——這得益於咱們控制視圖的參數是尺度無關的歸一化座標。
咱們以拖動爲例, 首先,在鼠標按鍵按下時。記錄起始位置:
見bool layer_tiles::cb_mousePressEvent(QMouseEvent*event)
if (event->button()==Qt::LeftButton) { this->m_nStartPosX = event->pos().x(); this->m_nStartPosY = event->pos().y(); }
if (event->button()==Qt::LeftButton) { int nOffsetX = event->pos().x()-this->m_nStartPosX; int nOffsetY = event->pos().y()-this->m_nStartPosY; if (!(nOffsetX ==0 && nOffsetY==0)) { m_pViewer->DragView(nOffsetX,nOffsetY); this->m_nStartPosX = this->m_nStartPosY = -1; res = true; } }
void tilesviewer::DragView(int nOffsetX,int nOffsetY) { if (nOffsetX==0 && nOffsetY == 0) return; int sz_whole_idx = 1<<m_nLevel; int sz_whole_size = sz_whole_idx*256; double dx = nOffsetX*1.0/sz_whole_size; double dy = nOffsetY*1.0/sz_whole_size; this->m_dCenterX -= dx; this->m_dCenterY -= dy;
本章介紹了視圖的控制。爲了簡單方便,咱們創建了一個百分比座標系。歸一化的參數避免在縮放過程當中改動視窗的全局座標,且很便於計算。
固然,上述座標系僅僅是顯示瓦片需要的座標系。假設還要和經緯度打交道,那就必須引入經緯度座標、墨卡託座標。做爲一個插件化的project,咱們但願這些座標轉化全部由主框架公佈功能,供插件使用,在下一章節,咱們就介紹基於Qt插件的圖層架構設計。