一張新型肺炎地區分佈地圖是怎麼製作的?
前言
2020年剛開始,各鐘不幸的訊息滿天飛。新型肺炎的蔓延,科比去世… 無時無刻讓我感到痛楚。為了不給國家添亂,新年幾天都在窩在家裡。時不時拿起手機,觀察一下現在病情蔓延情況。下面這張地圖就是一張典型的GIS應用。
每當我看到這張靜態圖時,很想要知道幾個資訊無法獲知。
- 我們能通過顏色和圖例比較一個省的確診人數範圍,看不到一個省具體患病人數。
- 由於是一張靜態圖,我們沒法獲市級資料。如果地圖可以拖動,放大縮小就簡單多了。
- 每次看到紅色,心裡都很焦慮。能換成其他顏色,我自己更加能接受點。
基於這兩個小功能,我準備介紹一下怎麼去製作一張地圖。我準備分兩個階段來做介紹。
- 先用最簡潔的程式碼來生成一張靜態圖片。通過這個階段,讓我們認識一下一般地圖應用開發的流程。
- 當我們瞭解流程以後,我們就把這個程式改造成地圖服務,讓她和知名的地圖前端庫
Leaflet
合併開發一個可互動的地圖,整合點有趣的功能。
這篇文章,我準備先從製作一張靜態圖片開始。
讓我們從環境開始
以前開發地圖應用軟體,可能需要掌握很多程式語言技能,才能勝任一個完整的專案。比如一個典型的GIS B/S應用一般會使用Java, C#或其他後端程式語言來開發後端,然後用JavaScript + HTML來開發前端展現。
今天用我們熟悉的JavaScript;即使是前端開發人員也可以開發後端地圖應用了。追求極簡開發環境的話,我們只需要2個工具。Node.js (推薦8以上,或者直接安裝最新版本都是相容的)和 vscode.
這篇文章照顧新手,寫的比較多。老鳥請自行過濾。勿噴。
建立工程,新增引用
接著,我們建立一個工程目錄。用以下命令就可以了。(我個人比較喜歡使用命令列,由於平時都是用macOS做日常使用機器。所以以下命令列都是macOS執行驗證的)。
# 建立專案目錄
cd [your workspace]
md nCoV-map
cd nCoV-map
# 建立功能,新增引用
npm init -y
npm i --save ginkgoch-map canvas lodash
# 新建一個檔案,這個將是我們寫程式碼的地方
touch tutorial-01.js
這裡引用了
canvas
庫,是因為Node.js
沒有提供繪圖API,我們只能引用一個第三方Node.js
庫來替代使用。
到這裡,我們的工程已經建立好了。
GIS資料
GIS應用裡面資料是很重要的。我把她分為靜態資料和動態資料。靜態資料就是我們的幾何圖形以及她們特定的特徵資料。如地區的名字等。動態資料就是我們實時關注的疫情變化。
一般靜態資料比較容易找到。百度搜尋中國地圖資料csv, json, shapefile都可以找到。這個專案裡面,我準備使用shapefile作為我的靜態資料。這裡你可以找到以下資料,我們一會兒會使用到。把上面資料下載下來以後,放到工程的data
目錄下面。
- chn/
- gadm36_CHN_1_3857.shp - 省級資料
- gadm36_CHN_2_3857.shp - 市級資料
- cntry02.shp - 世界國家資料
動態資料會麻煩點。我是寫了一個爬蟲,定時爬取。有興趣可以私聊。不過作為例子,我放上了幾份疫情資料在data/infected
目錄裡面以便做示例。
剩下的工作就很簡單了
疊加世界資料
首先,我們定義一個函式來建立一個地圖的圖層,一個資料來源即一個資料圖層,多個資料圖層疊加起來就可以構成我們期望的樣式。使用ginkgoch-map
,我們是這樣定義一個圖層的。
function createLayerWithDefaultStyle(filePath) {
// create a source with the specified shapefile file path
let source = new GK.ShapefileFeatureSource(path.resolve(__dirname, filePath));
// wrap the source as a world layer
let layer = new GK.FeatureLayer(source);
// set a style on the layer
layer.styles.push(new GK.FillStyle('#f0f0f0', '#636363', 1));
return layer;
}
有了layer
, 我們可以簡單檢視我們資料圖層的樣子。比如對於資料cntry02.shp
:
let worldLayer = createLayerWithDefaultStyle('../data/cntry02.shp');
await worldLayer.open();
let worldImage = await worldLayer.thumbnail(512, 512);
fs.writeFileSync(path.resolve(__dirname, './images/tutorial-01-world.png'), worldImage.toBuffer());
我們通過命令列執行下面的語句。我們可以找到圖片:
node tutorial-01.js
疊加中國資料
當然這個不是我們想要的樣子,我們還需要把省份的資料疊加在上面,才能看到我們中國詳細一點的資料。我們接著做。
const [imageWidth, imageHeight] = [512, 512];
// create a world layer with cntry02.shp
let worldLayer = createLayerWithDefaultStyle('../data/cntry02.shp');
// create a province layer with gadm36_CHN_1_3857.shp
let provinceLayer = createLayerWithDefaultStyle('../data/chn/gadm36_CHN_1_3857.shp');
let mapEngine = new GK.MapEngine(imageWidth, imageHeight);
mapEngine.srs = new GK.Srs('EPSG:900913');
mapEngine.pushLayer(worldLayer);
mapEngine.pushLayer(provinceLayer);
let image = await mapEngine.image();
fs.writeFileSync(path.resolve(__dirname, './images/tutorial-01-default.png'), image.toBuffer());
現在再開啟圖片tutorial-01-default.png
, 注意檢視中國的資料已經疊加成功了。
調整可視範圍
哦?太小了~ 好,我們調整下地圖的可視範圍。
await provinceLayer.open();
let chinaEnvelope = await provinceLayer.envelope();
chinaEnvelope = GK.ViewportUtils.adjustEnvelopeToMatchScreenSize(chinaEnvelope, imageWidth, imageHeight);
let image = await mapEngine.image(chinaEnvelope);
fs.writeFileSync(path.resolve(__dirname, './images/tutorial-01-china.png'), image.toBuffer());
連線動態資料
我們對靜態資料進行了渲染,同時對中國省份級別的邊框進行繪製。接下來,我們將連線動態資料,把動態的感染人數和地圖對應起來。我們怎麼做呢?
首先,我們先看下靜態資料的特徵資料。Shapefile的特徵資料存放在*.dbf檔案裡面。你可以選擇使用你常用的工具開啟dbf檔案。我個人一般使用的是shapefile viewer
來檢視。參考這裡獲取程式及使用說明。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-NUD1zDlD-1580548031579)(./tutorials/images/china-attributes.png)]
NL_NAME_1
就是我們需要的省份名字,然後我們來看看動態資料的一個片段。
[
{
"provinceName": "湖北省",
"provinceShortName": "湖北",
"confirmedCount": 4586,
"suspectedCount": 0,
"curedCount": 90,
"deadCount": 162,
"comment": "待明確地區:治癒 30",
"cities": [
{
"cityName": "武漢",
"confirmedCount": 2261,
"suspectedCount": 0,
"curedCount": 54,
"deadCount": 129
},
{
"cityName": "黃岡",
"confirmedCount": 496,
"suspectedCount": 0,
"curedCount": 5,
"deadCount": 12
},
...
有趣的是,動態資料也包含省份的名字provinceShortName
;以及感染,疑似,治癒以及死亡的人數。現在,我們要做的就是通過靜態資料的NL_NAME_1
和動態資料的provinceShortName
相等的資料相關聯。在ginkgoch-map
裡面是這樣做的。
首先,我們定義一個函式來幫助我們定義一個關係。
/**
* field - the dynamic field value to return.
* infectedData - the infected data in JSON format.
*/
function _getDynamicFieldForProvince(field, infectedData) {
return {
name: field, fieldsDependOn: ['NL_NAME_1'], mapper: feature => {
const fullName = feature.properties.get('NL_NAME_1');
const infectionInfo = _.find(infectedData, d => {
return fullName.includes(d.provinceShortName);
});
if (infectionInfo === undefined) {
return undefined;
} else {
return infectionInfo[field];
}
}
};
}
然後建立4個列的對映函式。
function connectDynamicData(layer) {
// load dynamic data
let dynamicData = fs.readFileSync(path.resolve(__dirname, '../data/infected/1580376765333.json')).toString();
dynamicData = JSON.parse(dynamicData);
// connect 4 dynamic attribute fields to the source.
const source = layer.source;
source.dynamicFields.push(_getDynamicFieldForProvince('confirmedCount', dynamicData));
source.dynamicFields.push(_getDynamicFieldForProvince('suspectedCount', dynamicData));
source.dynamicFields.push(_getDynamicFieldForProvince('curedCount', dynamicData));
source.dynamicFields.push(_getDynamicFieldForProvince('deadCount', dynamicData));
}
最後,我們呼叫這個函式進行對映。
//...省略前後重複的程式碼
let provinceLayer = createLayerWithDefaultStyle('../data/chn/gadm36_CHN_1_3857.shp');
connectDynamicData(provinceLayer);
至此,我們可以認為provinceLayer
已經包含了動態資料。她將在需要的時候動態的去通過對映關係找到需要的資料來使用。
樣式化地圖
做到這裡,大家可以去休息一下。迎接最後一步:地圖樣式化。我們把感染人數分成幾個等級,根據等級渲染不同的顏色來表示嚴重程度。比較好的是,ginkgoch-map
提供了對應的API來渲染。
我們先定義個函式來建立樣式化物件Style
.
function _getClassBreakStyle(field) {
const strokeColor = '#636363';
const strokeWidth = 1;
let countStops = [1, 10, 50, 100, 300, 500, 750, 1000, Number.MAX_SAFE_INTEGER];
let activePallette = ['#fff5f0', '#fee0d2', '#fcbba1', '#fc9272', '#fb6a4a', '#ef3b2c', '#cb181d', '#67000d']
let style = new GK.ClassBreakStyle(field);
for (let i = 1; i < countStops.length; i++) {
style.classBreaks.push({ minimum: countStops[i - 1], maximum: countStops[i], style: new GK.FillStyle(activePallette[i - 1], strokeColor, strokeWidth) });
}
return style;
}
再應用到layer
上即可看到效果。
let confirmedCountStyle = _getClassBreakStyle('confirmedCount');
provinceLayer.styles.push(confirmedCountStyle);
// 順便我們把文字渲染上去,即可完成。
let provinceLabelStyle = new GK.TextStyle('[NL_NAME_1]', 'black', 'Arial 12px');
provinceLabelStyle.lineWidth = 2;
provinceLabelStyle.strokeStyle = 'white';
provinceLabelStyle.location = 'interior';
provinceLayer.styles.push(provinceLabelStyle);
是不是很有趣?我們現在可以隨意替換調色盤,讓她變成藍色系的。替換這一句即可。
// let activePallette = ['#fff5f0', '#fee0d2', '#fcbba1', '#fc9272', '#fb6a4a', '#ef3b2c', '#cb181d', '#67000d'];
let activePallette = ['#f7fbff', '#deebf7', '#c6dbef', '#9ecae1', '#6baed6', '#4292c6', '#2171b5', '#08519c'];
寫在最後
最終的程式碼可以在這裡下載:https://github.com/ginkgoch/nCoV-map/tree/develop/tutorials
看起來很多,大多數程式碼都是業務程式碼,和對樣式的設定。瞭解起來還是挺簡單的。今天就到這裡,後面有時間,我再寫一篇搭建一個地圖服務,製作一個可以互動的地圖應用。有問題可以隨時聯絡我, ginkgoch@outlook.com。
Happy Mapping!
相關文章
- 怎麼製作地圖分佈圖,要製作電子地圖怎麼做地圖
- 地圖網點分佈圖怎麼做,如何製作地圖資料分佈圖地圖
- 如何製作網點地圖,網點分佈圖怎麼製作地圖
- 新冠確診病例軌跡地圖怎麼畫?小區疫情分佈地圖製作!地圖
- 業務分佈地圖怎麼做,用地圖製作客戶分佈圖地圖
- 可以繪製地圖的軟體,公司區域分佈圖怎麼做地圖
- 商場分佈圖是怎麼做,地圖資料分佈圖怎麼做地圖
- 室內地圖怎麼製作,室內電子地圖是如何繪製的地圖
- 怎麼製作區域分佈圖?區域網格分佈圖怎麼做?
- 小區確診病例動態分佈圖,怎麼定製防疫地圖?地圖
- 場所位置圖怎麼製作,怎樣製作自己需要的區域地圖地圖
- 怎麼自己製作地圖,簡單的地圖繪製軟體地圖
- 智慧化園區電子地圖專業製作公司,智慧園區電子地圖怎麼製作?地圖
- 位置分佈圖怎麼畫,怎麼做地圖網點分佈圖地圖
- 如何製作位置分佈圖,如何在地圖上畫出區域地圖
- 網點分佈圖怎麼做,如何製作商場內部的導航地圖地圖
- 怎麼自己製作地圖?如何快速實現簡單地圖繪製?地圖
- 智慧園區三維電子地圖繪製平臺,智慧園區導航地圖怎麼製作?地圖
- 怎麼製作位置分佈圖?什麼軟體可以做區域分佈圖?
- 地圖免費標註公司位置,怎麼做業務分佈地圖地圖
- 室內地圖怎麼製作?哪裡有可以定製室內地圖?地圖
- 室內地圖製作軟體有哪些?怎樣製作自己需要的地圖?地圖
- 地圖製作地圖
- 網站地圖怎麼做?dedecms網站地圖製作方法聽語音網站地圖
- 智慧園區視覺化地圖製作,如何做一個園區的導航地圖?視覺化地圖
- 《塞爾達傳說:荒野之息》的大世界地圖是怎麼製作的?地圖
- 室內地圖怎麼畫,樓層平面索引圖製作地圖索引
- 小區確診病例實時地圖,怎麼繪製疫情視覺化地圖?地圖視覺化
- 【高德地圖API】如何製作自己的旅遊地圖?地圖API
- 如何製作地圖?有沒有什麼超好用的地圖軟體?地圖
- 如何製作室內地圖,室內地圖繪製工具地圖
- 園區裡適合使用什麼的導航地圖,哪裡可以製作智慧園區的導航地圖?地圖
- 室內導航地圖怎麼做,室內導航電子地圖製作平臺地圖
- 分佈圖用什麼軟體製作,用什麼軟體做區域分佈圖
- 區域分佈圖怎麼做,怎麼做區域網格分佈圖
- 如何製作室內地圖?停車場效果圖怎麼做出來的?地圖
- 商場電子地圖怎麼做出來的,商場導購圖製作地圖
- 絕地求生大逃殺新地圖資源分佈圖 絕地求生新地圖資源分佈一覽地圖