-1.不想看背景、設(shè)計過程觉痛、編碼思路的同學(xué)薪棒,直接移步頁尾GayHub鏈接钉鸯。
0.背景
啊唠雕,大家好钞脂,我是一頭老王啊冰啃,前天閑著沒事,在工位上刷知乎净薛。
MD被組長發(fā)現(xiàn)了肃拜。
——“老王士聪,很閑嘛~”
——“沒有沒有剥悟,剛剛看一下知乎而已……”
——“很閑就給你個任務(wù)~”
——“真沒有真沒有区岗,就看剛看……”
——“那你這么閑的話就做一下離線地圖服務(wù)吧慈缔,有個項目是部署在政務(wù)內(nèi)網(wǎng)的赂韵,不能訪問外網(wǎng)肄满,所以要離線的地圖服務(wù),Leaflet要能用哦~”
——“…………MMP……”
人在座上坐瞬测,鍋從天上來月趟。我這個小開發(fā)仔還能怎么辦孝宗?穷躁?问潭?做唄狡忙。
看看Leaflet梳虽,用的是TMS服務(wù)(地圖瓦片服務(wù))窜觉。啊北专,老王我突然靈光一現(xiàn)禀挫,只要能下載瓦片特咆,然后部署在Tomcat/Jetty/Nginx上不就得了?啥繁?
Google一下下載瓦片的軟件:
又查了十幾個捣辆,水經(jīng)注,什么地圖下載器什么的此迅,我就沒見過可以用的汽畴,其中一個上來還就要收費(fèi)?人家去Piao都還是先體驗耸序,后付費(fèi)的H绦!騙錢能不能走點(diǎn)心坎怪?0瞻印?搅窿!
累覺不愛……
我老王好歹也是常年爬人家窗戶的人嘁酿,這么點(diǎn)小挫折能難倒我?痹仙??我自己寫一個總行了吧=开仰。=
1.設(shè)計
事實證明,人都是逼出來的=众弓。=
被組長、搜索引擎谓娃,還有百度網(wǎng)盤逼迫的我,設(shè)想出一個服務(wù)滨达,這個服務(wù)需要完成:
- 輸入經(jīng)緯度矩形、地圖等級锌订、地圖類型,后端自行下載瓦片辆飘,存放入相應(yīng)的位置。
- 下載瓦片的同時蜈项,還需要記錄下后端擁有哪些類型续挟、哪些層級紧卒、哪些范圍的瓦片庸推,用以前端調(diào)起。
- 提供地圖服務(wù)以及下載記錄贬媒。
- 依賴盡量少。
OK坡倔,開始編碼。
2.后端
2.0 算法基礎(chǔ)
若對Web墨卡托罪塔,Google Web墨卡托投蝉,墨卡托有疑惑的同學(xué)征堪,請移步,熟悉理論知識:https://blog.csdn.net/mygisforum/article/details/7582449
2.1 經(jīng)緯度轉(zhuǎn)Google Web墨卡托
首先佃蚜,因為瓦片地圖用的都是Google Web墨卡托的坐標(biāo),那么我們需要先將經(jīng)緯度的矩形換算成Google Web墨卡托的坐標(biāo)熟尉。
OK,Google一個經(jīng)緯度轉(zhuǎn)Web墨卡托的斤儿,我再改寫一下恐锦,轉(zhuǎn)成Google Web墨卡托:
public static LatLngInfo lonLatToMercator(double lon, double lat)
{
double toX = lon * 20037508.34D / 180.0D;
double toY = Math.log(Math
.tan((90.0D + lat) * 3.141592653589793D / 360.0D)) / 0.0174532925199433D;
toY = toY * 20037508.34D / 180.0D;
return new LatLngInfo(toY, toX);
}
解釋一下這里的魔術(shù)數(shù)字往果,話說網(wǎng)上這個轉(zhuǎn)換代碼寫的是真的蛋疼一铅,滿篇充斥著魔術(shù)數(shù)字……
- 20037508.34 :Web墨卡托投影的長或?qū)挼囊话耄瑔挝粸镸馅闽;
- 0.0174532925199433:一角度為多少弧度馍迄;
- 90,180攀圈,360:都是角度;
針對原來經(jīng)緯度轉(zhuǎn)Web墨卡托坐標(biāo)的源碼赘来,我改寫時只加了 第一行toX中的 “+ 20037508.34D”,以及return前一行 “toY = 20037508.34D - toY”嗦篱。
因為Google Web墨卡托的坐標(biāo)原點(diǎn)在經(jīng)緯度(180,90)或者說(-180灸促,90)處,也就是整個Web墨卡托投影地圖的左上角浴栽;
Web墨卡托的在(0,0)處典鸡,也就是整個Web墨卡托投影地圖的中心,因此需回歸原點(diǎn)萝玷。
2.2 使用Google Web墨卡托坐標(biāo)計算需要請求的瓦片序列號
public static LevelInfo getLevelInfo(Integer level, Double left,
Double right, Double top, Double bottom)
{
LevelInfo levelInfo = new LevelInfo();
LatLngInfo leftTopMocPoint = CoodUtils.lonLatToGoogleMercator(left, top);
LatLngInfo rightBottomMocPoint = CoodUtils.lonLatToGoogleMercator(right, bottom);
Double perTileWidth = WEB_MOC_HALF_WIDTH * 2 / Math.pow(2, level);
levelInfo.setZ(level);
levelInfo.setxL((int)(leftTopMocPoint.getLongitude() / perTileWidth));
levelInfo.setxR((int)(rightBottomMocPoint.getLongitude() / perTileWidth));
levelInfo.setyT((int)(leftTopMocPoint.getLatitude() / perTileWidth));
levelInfo.setyB((int)(rightBottomMocPoint.getLatitude() / perTileWidth));
return levelInfo;
}
首先,得到perTileWidth亦渗,這個變量的含義為該地圖等級下,每個切片代表的實際距離為多少法精,單位與Google Web墨卡托一致痴突,為米搂蜓。
前面說了辽装,因為Google Web墨卡托的坐標(biāo)原點(diǎn)為左上角,那么拾积,得到Web墨卡托坐標(biāo)之后,就可以直接用Google Web墨卡托坐標(biāo)除以當(dāng)前等級每個切片的寬度拓巧,得到瓦片序列號的X,Y值傻唾。
Z值為地圖等級,至此冠骄,X,Y凛辣,Z值全部得知锁荔,可以進(jìn)行瓦片URL構(gòu)建蟀给。
2.3 瓦片URL構(gòu)建
// 待匹配的URL
private static String Google_Satellite_Url = "http://mt0.google.cn/vt/lyrs=y&hl=zh-CN&hl=zh-CN&gl=CN&x={x}&y={y}&z={z}&s=Gali";
private static String Google_Image_Url = "http://mt0.google.cn/vt/lyrs=m&hl=zh-CN&hl=zh-CN&gl=CN&x={x}&y={y}&z={z}&s=Gali";
private static String Google_Terrain_Url = "http://mt0.google.cn/vt/lyrs=p&hl=zh-CN&hl=zh-CN&gl=CN&x={x}&y={y}&z={z}&s=Gali";
private static String AMap_Satellite_Url = "http://webst02.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}";
private static String AMap_Cover_Url = "http://webst02.is.autonavi.com/appmaptile?x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1&style=8";
private static String AMap_Image_Url = "http://webrd03.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}";
private static String TianDiTu_Satellite_Url = "http://t1.tianditu.cn/DataServer?T=img_w&X={x}&Y={y}&L={z}";
private static String TianDiTu_Image_Url = "http://t1.tianditu.cn/DataServer?T=vec_w&X={x}&Y={y}&L={z}";
private static String TianDiTu_Cover_Url = "http://t1.tianditu.cn/DataServer?T=cva_w&X={x}&Y={y}&L={z}";
上各個地圖跋理,打開Chrome的控制臺择克,看他們請求的URL是什么前普,將X,Y拭卿,Z值替換成{x} , {y} , {z}。
隨后構(gòu)建URL時峻厚,調(diào)用字符串替換即可。
2.4 循環(huán)請求
得到了左上角到右下角的瓦片X浦夷,Y,Z劈狐,剩下就是循環(huán)請求的事了。
請求的量比較大肥缔,我們開個多線程:
public static Boolean getLevelPic(BackgroundType type, Integer level, Double left,
Double right, Double top, Double bottom)
{
LevelInfo levelInfo = getLevelInfo(level, left, right, top, bottom);
// 開多線程請求圖片
Vector<Thread> threads = new Vector<>();
for(int x = levelInfo.getxL() ; x <= levelInfo.getxR() ; x ++)
{
for(int y = levelInfo.getyT(); y <= levelInfo.getyB() ; y ++)
{
final int fX = x;
final int fY = y;
Thread threadGetPic = new Thread(
() -> getPic(type, fX, fY, levelInfo.getZ()));
threads.add(threadGetPic);
threadGetPic.start();
}
}
// 等待所有線程執(zhí)行完畢
for (Thread perThread : threads)
{
try
{
perThread.join();
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
return true;
}
這里的getPic方法包含了請求和請求后的保存汹来。
2.5 其余功能
記錄地圖請求過的參數(shù)、返回該參數(shù)收班。
返回地圖瓦片這些常規(guī)操作,直接下載看Gayhub的代碼吧闺阱。
3 前端
3.1 依賴
依賴AntDesign舵变,Leaflet,React纪隙,使用TypeScript。
{
"@types/leaflet": "^1.2.7",
"antd": "^3.6.3",
"axios": "^0.18.0",
"leaflet": "^1.3.1",
"react": "^16.4.1",
"react-dom": "^16.4.1",
"react-scripts-ts": "2.16.0"
}
3.2 思路
首先需要一個矩形的框绵咱,用來輸入地圖范圍,然后再填入地圖等級、地圖類型住涉,鏈?zhǔn)秸埱蠛蠖耍尯蠖瞬粩嗟墨@取瓦片舆声。
3.3 效果
點(diǎn)擊下載瓦片后即可得到地圖服務(wù)柳爽。
測試頁面會得到已有地圖服務(wù)的類型。
Q&A頁面是常見問題與API的描述磷脯。
4.GayHub地址:
后端:https://github.com/Icemap/MapServer
前端:https://github.com/Icemap/MapServer-Front
Release版本:https://github.com/Icemap/MapServer/releases
兩個倉庫的README都有服務(wù)的使用方法。
Release版本使用:需求Java1.8+環(huán)境打毛,直接“java -jar {jarFileName}”,{jarFileName}替換為你下載的Release版本的Jar包名稱。訪問localhost:2018 即可隘冲。