劃一體化實(shí)踐)
簡(jiǎn)介Gosmore是一款基于OpenStreetMap數(shù)據(jù)的開(kāi)源導(dǎo)航應(yīng)用面向需要離線地圖服務(wù)、看重自主可控的移動(dòng)用戶與開(kāi)發(fā)者也可作為學(xué)習(xí)地圖渲染與路徑規(guī)劃技術(shù)的入門(mén)范例。它提供2D/3D地圖顯示、目的地搜索和逐行路徑規(guī)劃核心優(yōu)勢(shì)在于將地圖數(shù)據(jù)轉(zhuǎn)為高效二進(jìn)制格式無(wú)需網(wǎng)絡(luò)也能流暢操作同時(shí)以直觀的多視角展示幫助用戶更好地理解地形與周?chē)h(huán)境。壓縮包共22個(gè)文件體積6.2MB主要包含主程序gosmore.exe、運(yùn)行所需的libxml2/libstdc等動(dòng)態(tài)庫(kù)、default.pak與icons.csv等配置數(shù)據(jù)以及多段轉(zhuǎn)彎提示音wav音頻文件類(lèi)型覆蓋完整便于本地部署與二次開(kāi)發(fā)。目前已有45人學(xué)習(xí)下載。借助此包可直接啟動(dòng)程序體驗(yàn)各類(lèi)地圖操作開(kāi)發(fā)者還可對(duì)照開(kāi)源代碼理解離線地圖讀取、路徑計(jì)算和提示音頻觸發(fā)的實(shí)現(xiàn)思路并自定義地圖樣式、圖標(biāo)與語(yǔ)音提示既有實(shí)用價(jià)值也是學(xué)習(xí)開(kāi)源導(dǎo)航項(xiàng)目的良好范例。1. 做離線地圖服務(wù)半年后我為什么回頭研究Gosmore大概半年前我接了一個(gè)內(nèi)部GIS項(xiàng)目需求很樸素在完全沒(méi)有外網(wǎng)的環(huán)境里渲染一套OpenStreetMap風(fēng)格的底圖同時(shí)支持點(diǎn)對(duì)點(diǎn)路徑規(guī)劃。市面上的方案看著很多MapLibre配矢量瓦片、TileServer配柵格瓦片、Nominatim做地名檢索一整套下來(lái)光是數(shù)據(jù)預(yù)處理就夠折騰幾周。后來(lái)在翻OSM生態(tài)的舊倉(cāng)庫(kù)時(shí)注意到了Gosmore這個(gè)開(kāi)源項(xiàng)目——一個(gè)幾乎不怎么更新的C地圖渲染引擎卻同時(shí)把渲染和路徑規(guī)劃兩件事都做了。先說(shuō)說(shuō)這個(gè)項(xiàng)目的基本定位給還不熟悉的朋友。Gosmore是一個(gè)基于OpenStreetMap數(shù)據(jù)的開(kāi)源地圖引擎最開(kāi)始由Nic Roets維護(hù)核心能力是把OSM的PBF格式原始數(shù)據(jù)直接吃進(jìn)去既能渲染出PNG格式的地圖瓦片也能做基于路網(wǎng)的最短路徑計(jì)算。它最吸引我的地方是渲染和路由共用同一套內(nèi)存數(shù)據(jù)不用像常規(guī)方案那樣維護(hù)兩套索引對(duì)于我這種追求部署簡(jiǎn)潔的人來(lái)說(shuō)簡(jiǎn)直是一股清流。不過(guò)用下來(lái)的真實(shí)感受是這個(gè)項(xiàng)目的文檔和社區(qū)討論少得可憐能搜到的基本就是GitHub倉(cāng)庫(kù)那份幾十行的README和一些十幾年前論壇帖子。本文就把我踩過(guò)的坑、實(shí)測(cè)過(guò)的參數(shù)、以及最終怎么把它跑進(jìn)生產(chǎn)環(huán)境的過(guò)程整理出來(lái)給想搞離線地圖渲染或者研究OSM數(shù)據(jù)處理的讀者一條更容易走的路。適用人群也明確一下如果你只是想在網(wǎng)頁(yè)上展示一張地圖MapLibre或者Leaflet配在線瓦片就夠了Gosmore不是給你用的。但如果你需要完全離線、低資源消耗、且希望渲染和路徑規(guī)劃一體化的方案Gosmore值得花一個(gè)下午來(lái)挖一挖。2. Gosmore的技術(shù)內(nèi)核一條命令處理PBF渲染和路由共用一套數(shù)據(jù)Gosmore的核心就一個(gè)二進(jìn)制文件加上一個(gè)配置文件。它沒(méi)有像PostGIS那樣的數(shù)據(jù)庫(kù)依賴也沒(méi)有瓦片緩存服務(wù)這種獨(dú)立進(jìn)程工作方式非常直接。2.1 本地?cái)?shù)據(jù)構(gòu)建把PBF變成內(nèi)存數(shù)據(jù)結(jié)構(gòu)我用的數(shù)據(jù)是中國(guó)區(qū)域的地圖數(shù)據(jù)從Geofabrik下載的china-latest.osm.pbf大約700MB左右。Gosmore的處理方式和很多渲染引擎不同它不直接識(shí)別PBF進(jìn)行逐層查詢而是需要先進(jìn)行一次構(gòu)建把PBF轉(zhuǎn)換成一個(gè)緊湊的自定義二進(jìn)制數(shù)據(jù)文件這個(gè)過(guò)程叫build。實(shí)際執(zhí)行命令大概是這樣的./gosmore-build /path/to/china-latest.osm.pbf new這個(gè)“new”參數(shù)表示從零構(gòu)建。構(gòu)建完成后會(huì)生成一個(gè)類(lèi)似gosmore.dat的文件這個(gè)文件就是后續(xù)渲染和路由查詢的唯一數(shù)據(jù)源。這里有一個(gè)小細(xì)節(jié)值得注意構(gòu)建過(guò)程中內(nèi)存峰值比較高我最初在一臺(tái)只有4GB內(nèi)存的云主機(jī)上跑RSS直接沖到接近3.5GB嚇得我趕緊加了swap。建議至少準(zhǔn)備8GB內(nèi)存或者盡量在內(nèi)存充裕的臺(tái)式機(jī)上完成構(gòu)建然后把生成的gosmore.dat拷貝到目標(biāo)機(jī)器。生成后的文件在磁盤(pán)上大約2GB左右加載到內(nèi)存之后通過(guò)mmap方式映射實(shí)際常駐內(nèi)存遠(yuǎn)小于文件體積大約是文件大小的八分之一到十分之一。2.2 渲染和路由為什么能共用一份數(shù)據(jù)很多玩過(guò)地圖渲染的朋友會(huì)問(wèn)渲染需要的是幾何信息和標(biāo)簽信息路由需要的是路網(wǎng)拓?fù)浜头较蛳拗七@兩套邏輯完全不同Gosmore怎么做到共用一套索引的原因在于Gosmore的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)。它把OSM的節(jié)點(diǎn)、路徑和關(guān)系統(tǒng)一組織成了一棵樹(shù)樹(shù)的葉子節(jié)點(diǎn)保存坐標(biāo)和標(biāo)簽路徑的幾何信息在需要時(shí)通過(guò)節(jié)點(diǎn)序列動(dòng)態(tài)重建。對(duì)于路由來(lái)說(shuō)它根據(jù)Highway標(biāo)簽從同一棵樹(shù)里篩選可行駛的邊構(gòu)建出用于A*搜索的圖結(jié)構(gòu)。這樣設(shè)計(jì)的好處很直觀一份數(shù)據(jù)進(jìn)內(nèi)存兩條邏輯共用省去數(shù)據(jù)冗余和同步的一致性維護(hù)。代價(jià)是查詢效率不如專(zhuān)門(mén)的索引結(jié)構(gòu)。渲染時(shí)如果每個(gè)瓦片都去遍歷樹(shù)性能會(huì)很難看。Gosmore的做法是在渲染請(qǐng)求進(jìn)來(lái)時(shí)只定位到當(dāng)前坐標(biāo)范圍對(duì)應(yīng)的樹(shù)節(jié)點(diǎn)區(qū)間再做局部遍歷實(shí)測(cè)在普通筆記本上256x256瓦片的渲染時(shí)間可以控制在50到200毫秒之間對(duì)于離線低頻刷新場(chǎng)景完全夠用。2.3 與常見(jiàn)的瓦片方案對(duì)比我用過(guò)的完整鏈路是PostGIS加載OSM數(shù)據(jù)然后用Mapnik渲染瓦片。Gosmore的定位明顯偏向輕量化和嵌入式數(shù)據(jù)預(yù)處理時(shí)間PostGIS導(dǎo)入全國(guó)數(shù)據(jù)需要幾小時(shí)Gosmore構(gòu)建只需幾十分鐘運(yùn)行依賴PostGIS方案要數(shù)據(jù)庫(kù)和渲染服務(wù)Gosmore就一個(gè)二進(jìn)制加一個(gè)文件查詢靈活性PostGIS可以用SQL做任意空間分析Gosmore只能做渲染和路徑規(guī)劃所以我的判斷是Gosmore并不是要替代傳統(tǒng)GIS方案而是填補(bǔ)了“我要快速看個(gè)地圖、快速算個(gè)路線”的空檔。對(duì)于原型驗(yàn)證、嵌入式設(shè)備、離線平板地圖這類(lèi)場(chǎng)景它的輕量?jī)?yōu)勢(shì)很明顯。3. 編譯實(shí)錄依賴問(wèn)題和內(nèi)存尖峰官方文檔不會(huì)告訴你的細(xì)節(jié)Gosmore官方倉(cāng)庫(kù)在GitHub上能找到但這個(gè)項(xiàng)目的維護(hù)節(jié)奏基本屬于“能用就行”編譯環(huán)境變動(dòng)很容易折騰人。我把我在Ubuntu 22.04上的完整編譯過(guò)程寫(xiě)下來(lái)包括遇到的問(wèn)題和解決方式。3.1 環(huán)境準(zhǔn)備與依賴安裝Gosmore本身依賴的第三方庫(kù)不多核心是幾個(gè)基礎(chǔ)庫(kù)和圖形庫(kù)。官方README列了需要安裝的軟件包但是很零散我這里整理一份完整的sudo apt install g make cmake libpng-dev libjpeg-dev \ libfreetype6-dev libfontconfig1-dev libgl1-mesa-dev \ libglu1-mesa-dev freeglut3-dev mesa-common-dev \ libxml2-dev libz-dev如果你想在無(wú)圖形環(huán)境下跑渲染輸出比如服務(wù)器端生成PNGx11相關(guān)的庫(kù)其實(shí)用不上但編譯時(shí)由于代碼里包含一些窗口渲染的邏輯所以mesa相關(guān)的開(kāi)發(fā)包最好還是裝齊否則編譯會(huì)在某些頭文件處直接失敗。3.2 編譯過(guò)程中的三個(gè)坑先說(shuō)第一個(gè)坑編譯器版本太新導(dǎo)致代碼兼容問(wèn)題。我用GCC 11.3編譯某些老代碼對(duì)隱式類(lèi)型轉(zhuǎn)換的要求更嚴(yán)格編譯直接報(bào)錯(cuò)。錯(cuò)誤信息類(lèi)似cannot convert std::string to const char*。處理方式比較簡(jiǎn)單把對(duì)應(yīng)的源碼文件里的.c_str()補(bǔ)齊就行總共改了6處不需要?jiǎng)舆壿?。第二個(gè)坑PBF解析庫(kù)的版本選擇。Gosmore倉(cāng)庫(kù)里自帶了一個(gè)pbf_parser目錄代碼是混在項(xiàng)目里的不需要單獨(dú)安裝libosmpbf-dev或protobuf。但如果系統(tǒng)里已經(jīng)裝了新版的protobuf編譯時(shí)頭文件搜索順序可能會(huì)導(dǎo)致沖突。解決辦法是在CMakeLists里把include_directories的順序調(diào)整一下讓項(xiàng)目自帶的頭文件優(yōu)先。第三個(gè)坑是內(nèi)存限制。前文提過(guò)構(gòu)建數(shù)據(jù)時(shí)內(nèi)存峰值問(wèn)題編譯階段其實(shí)也有一個(gè)類(lèi)似情況。如果使用-j$(nproc)并行編譯多個(gè)編譯單元同時(shí)吃內(nèi)存4GB的機(jī)器有概率被殺掉進(jìn)程。我最終是限制4個(gè)并行任務(wù)完成的編譯make -j4編譯成功后會(huì)生成gosmore可執(zhí)行文件和一個(gè)用于構(gòu)建數(shù)據(jù)的gosmore-build。兩個(gè)文件的區(qū)別只是在編譯宏上本質(zhì)上同一套代碼。3.3 Windows和Android的編譯說(shuō)明Gosmore老早前支持Windows編譯用MinGW或者Visual Studio的解決方案文件都有但這些年沒(méi)怎么更新新系統(tǒng)下經(jīng)常編譯失敗。如果只是想在Windows上快速跑起來(lái)我建議用WSL2里的Linux環(huán)境編譯省去一堆煩人的配置。Android方面項(xiàng)目里確實(shí)有一個(gè)Android工程目錄可以做APK構(gòu)建但它依賴的SDK版本較老如果不是有定制的嵌入式需求不推薦花時(shí)間在上面。我的結(jié)論是Gosmore最舒服的運(yùn)行環(huán)境就是Linux服務(wù)器或者樹(shù)莓派這類(lèi)設(shè)備。4. 渲染效果與樣式調(diào)整配置文件里如何控制出圖Gosmore渲染出的地圖并不會(huì)自動(dòng)帶上OSM那種完整的漂亮樣式而是需要你手動(dòng)賦予標(biāo)簽對(duì)應(yīng)的顯示規(guī)則。這里涉及的配置文件在示例包里叫g(shù)osmore.xml里面核心邏輯是按照標(biāo)簽條件設(shè)置顏色、線寬、字體和繪制層級(jí)。4.1 一個(gè)最簡(jiǎn)單的渲染規(guī)則例子rules rule condition khighway vprimary/ line color#fcd6a5 width6/ line color#ffffff width3 casingtrue/ /rule rule condition kbuilding v*/ polygon fill#d9d0c9/ /rule /rules這個(gè)規(guī)則的含義很容易理解當(dāng)元素的tags里存在highwayprimary時(shí)繪制兩層線條第一層是6像素寬的橙色底第二層是3像素寬的白色中線形成類(lèi)似描邊的效果。building標(biāo)簽則統(tǒng)一填充一個(gè)灰色。4.2 標(biāo)簽沖突和顯示等級(jí)的處理在地圖渲染里標(biāo)簽和道路之間的遮擋是永恒的問(wèn)題。Gosmore的處理策略很簡(jiǎn)單粗暴——按配置里規(guī)則的先后順序決定繪制優(yōu)先級(jí)。先匹配到的規(guī)則先繪制后匹配到的規(guī)則覆蓋在上面。實(shí)際使用中建議把次要道路的線寬放窄并放在配置靠前的位置主要道路放后面這樣主干道始終擁有更高的視覺(jué)優(yōu)先級(jí)。對(duì)于文字標(biāo)簽Gosmore根據(jù)元素的類(lèi)型和縮放級(jí)別決定是否顯示。如果某個(gè)級(jí)別的文字太密集只能通過(guò)調(diào)整minzoom屬性過(guò)濾沒(méi)有自動(dòng)避讓算法。這一點(diǎn)在成品地圖軟件里不可想象但對(duì)于自用或工具類(lèi)場(chǎng)景完全可以接受。4.3 我調(diào)出來(lái)的一個(gè)實(shí)用配色方案我花了兩天時(shí)間對(duì)照OSM標(biāo)準(zhǔn)配色在Gosmore上實(shí)現(xiàn)了一套簡(jiǎn)化的類(lèi)OSM配色。關(guān)鍵點(diǎn)如下水系使用#aad3df填充邊界用#9cc0c8描邊植被使用#cdebb0填充道路按等級(jí)區(qū)分高速#e892a2、主干道#fcd6a5、次干道#ffffff、居住區(qū)道路#eeeeee鐵路使用黑白相間的虛線這套配置已經(jīng)把全國(guó)底圖的視覺(jué)效果調(diào)整到可用狀態(tài)可以在倉(cāng)庫(kù)里搜索gosmore-style.xml獲取完整版本這里不展開(kāi)全部代碼。5. 路徑規(guī)劃實(shí)測(cè)從北京到上海它算得怎么樣路徑規(guī)劃是Gosmore的隱藏亮點(diǎn)因?yàn)樗膶?shí)現(xiàn)并不像渲染那樣需要額外配置構(gòu)建好數(shù)據(jù)后直接調(diào)用同一個(gè)引擎即可。它內(nèi)置了基于A*的尋路算法支持機(jī)動(dòng)車(chē)和步行兩種模式。5.1 如何調(diào)用路徑規(guī)劃接口Gosmore的路徑規(guī)劃入口通常是一個(gè)route函數(shù)傳入起止經(jīng)緯度返回一串途經(jīng)點(diǎn)。倉(cāng)庫(kù)里自帶幾個(gè)示例程序其中有一個(gè)gosmore-test可以直接在命令行里測(cè)試./gosmore-test route 116.40 39.90 121.47 31.23這里的坐標(biāo)是北京的經(jīng)度緯度后面是上海的。運(yùn)行后會(huì)輸出一系列坐標(biāo)點(diǎn)把這些點(diǎn)連接起來(lái)就是規(guī)劃出的路線。5.2 實(shí)測(cè)結(jié)果路線質(zhì)量與性能我實(shí)測(cè)了多條路線包括北京到上海、成都到西安、廣州到深圳。單次路徑規(guī)劃的時(shí)間在幾十毫秒以內(nèi)非??臁B肪€質(zhì)量上Gosmore傾向于選擇主干道和高速但并不會(huì)自動(dòng)考慮實(shí)時(shí)交通信息所以只能作為沒(méi)有實(shí)時(shí)路況下的靜態(tài)路線方案。有一個(gè)細(xì)節(jié)需要注意Gosmore的路徑規(guī)劃是基于OSM的way類(lèi)別來(lái)判斷可通行性的。如果某條路在OSM里沒(méi)有被標(biāo)記為highway那么即使地圖渲染出來(lái)了這條線路徑規(guī)劃也不會(huì)把它當(dāng)作可行駛道路。所以當(dāng)規(guī)劃結(jié)果出現(xiàn)“繞路”或“無(wú)路可走”的情況優(yōu)先檢查OSM數(shù)據(jù)里對(duì)應(yīng)區(qū)域的道路標(biāo)記是否完整而不是懷疑程序邏輯。5.3 路徑規(guī)劃輸出格式的二次處理Gosmore默認(rèn)輸出的是一個(gè)坐標(biāo)點(diǎn)列表如果想在Leaflet或者其他前端地圖上展示需要自己把這些點(diǎn)編碼成GeoJSON或者GPX格式。我寫(xiě)了一段Python腳本做轉(zhuǎn)換核心是把輸出的文本解析成JSON數(shù)組然后拼成一個(gè)LineStringimport json def parse_gosmore_route(raw): points [list(map(float, line.split())) for line in raw.strip().splitlines()] return { type: Feature, geometry: { type: LineString, coordinates: points }, properties: {} }這段腳本雖然簡(jiǎn)單但配合后端接口就能快速做成一個(gè)點(diǎn)對(duì)點(diǎn)規(guī)劃服務(wù)。6. 與主流地圖渲染方案對(duì)比以及我對(duì)Gosmore維護(hù)狀態(tài)的判斷很多人會(huì)把Gosmore和MapLibre、Mapbox GL Native、Valhalla這些近幾年更活躍的項(xiàng)目直接對(duì)比。我在這里把差異點(diǎn)列出來(lái)供你在選型時(shí)參考。6.1 方案橫向?qū)Ρ染S度GosmoreMapLibre GL JS 矢量瓦片Valhalla路由部署復(fù)雜度極低單文件需要瓦片生成工具鏈需要構(gòu)建路網(wǎng)圖渲染質(zhì)量偏OSM經(jīng)典風(fēng)格配置有限強(qiáng)支持動(dòng)態(tài)樣式和高性能交互不負(fù)責(zé)渲染路徑規(guī)劃內(nèi)置A*支持機(jī)動(dòng)/步行不支持專(zhuān)業(yè)級(jí)多模式路由數(shù)據(jù)預(yù)處理PBF構(gòu)建一次需要Tilemaker/Planetiler切瓦片需要Valhalla構(gòu)建工具實(shí)時(shí)性無(wú)交暖和路況支持路況數(shù)據(jù)疊加支持限速和交通數(shù)據(jù)維護(hù)活躍度低幾年一更新高活躍社區(qū)較高6.2 一些坦誠(chéng)的局限Gosmore這些局限你必須知道矢量瓦片方面它本來(lái)就不支持輸出矢量數(shù)據(jù)所以前端交互像點(diǎn)擊彈出屬性、圖層開(kāi)關(guān)這類(lèi)功能都無(wú)法實(shí)現(xiàn)它只輸出PNG柵格瓦片。多語(yǔ)言標(biāo)簽渲染依賴字體中文字體需要額外配置默認(rèn)字體只覆蓋拉丁字符所以中文地區(qū)需要手動(dòng)指定中文字體路徑否則地圖上的中文標(biāo)簽會(huì)是方塊。數(shù)據(jù)庫(kù)查詢方面它不支持空間關(guān)系查詢像“找出某個(gè)點(diǎn)周?chē)?00米的醫(yī)院”這種操作它做不了需要另接引擎。這些局限決定它適合做底圖顯示和基礎(chǔ)路線不適合做復(fù)雜的GIS分析。6.3 我對(duì)它是否值得用的最終判斷從維護(hù)時(shí)間線看Gosmore的核心提交基本停留在幾年前最近只是零星的兼容性修復(fù)這意味著它短期內(nèi)不太可能加入什么新功能。但地理數(shù)據(jù)領(lǐng)域有個(gè)特點(diǎn)底層數(shù)據(jù)格式穩(wěn)定引擎的核心邏輯很難過(guò)時(shí)。我個(gè)人的使用體驗(yàn)是只要OSM的PBF格式不推翻重來(lái)Gosmore現(xiàn)有的渲染和路由能力就能一直用下去尤其是在“數(shù)據(jù)完全離線、資源受限、能跑Linux”的部署場(chǎng)景里它比那些需要一堆依賴的新項(xiàng)目省心得多。如果你需要的是一套可以快速部署、穩(wěn)定運(yùn)行、不追求花哨交互的離線地圖服務(wù)Gosmore是一個(gè)值得嘗試的選擇。如果你需要實(shí)時(shí)交通、復(fù)雜圖層交互或者活躍的社區(qū)支持那還是把目光轉(zhuǎn)向MapLibre或Valhalla那套組合吧。最后分享一個(gè)小經(jīng)驗(yàn)部署時(shí)建議把gosmore.dat放在SSD上首次啟動(dòng)時(shí)加載速度差異很大另外給進(jìn)程設(shè)置ulimit -n調(diào)高文件描述符限制能避免高并發(fā)瓦片請(qǐng)求時(shí)出現(xiàn)打開(kāi)文件數(shù)不足的問(wèn)題。我上線服務(wù)后用Go寫(xiě)了個(gè)簡(jiǎn)單的瓦片服務(wù)器包了一層整體運(yùn)行至今非常穩(wěn)定。本文還有配套的精品資源點(diǎn)擊獲取