mapboxgl 中插值表示式的應用場景

GIS兵器庫發表於2022-05-10

一、前言

interpolate是mapboxgl地圖樣式中用於插值的表示式,能對顏色和數字進行插值。

它的應用場景有兩類:

  1. 對地圖資料進行顏色拉伸渲染。常見的應用場景有:熱力圖、軌跡圖、模型網格渲染等。
  2. 在地圖縮放時對圖形屬性進行插值。具體為,隨著地圖的縮放,在改變圖示大小、建築物高度、圖形顏色等屬性時,對屬性進行插值,從而實現平滑的過渡效果。

這篇文章就把 mapboxgl 中interpolate插值工具的常見應用場景介紹一下。


二、語法

先看一下interpolate插值工具的語法。

interpolate表示式要求至少有5個引數,分別是表示式名稱插值型別輸入值判斷值輸出值

["interpolate",		//表示式名稱
    interpolation: ["linear"] | ["exponential", base] | ["cubic-bezier", x1, y1, x2, y2 ],  //插值型別
    input: number,	//輸入值
    stop_input_1: number, stop_output_1: OutputType,		//一組判斷值和輸出值
    stop_input_n: number, stop_output_n: OutputType, ...	//一組判斷值和輸出值
]: OutputType (number, array<number>, or Color)		//返回插值完的結果

其中插值型別會在後面詳細介紹,這裡先不多說。

判斷值輸出值是“一組”的關係,它們必須兩兩出現。

還有一點需要注意,就是判斷值必須遵循升序規則。

下面我們結合實際場景理解起來會更容易一些,先說第一類應用場景:對地圖資料進行顏色拉伸渲染。


三、對地圖顏色進行拉伸渲染

這個和ArcGIS中對柵格資料進行顏色拉伸渲染是一個意思。

地圖顏色拉伸渲染的本質,是根據網格的屬性值為網格設定顏色,當網格足夠小、足夠密時,就容易產生顏色平滑過渡的效果。

前面說到,常見的應用場景有:熱力圖、軌跡圖、模型網格渲染等。

在mapboxgl中,熱力圖和軌跡圖它們雖然看上去不像是由網格組成的,但在計算機圖形學的框架下,任何在螢幕上顯示的內容,都是由畫素來呈現的,而畫素是規律排列的網格,所以可以把熱力圖和軌跡也看成是由網格組成的。

這一點在WebGL開發時尤為明顯,因為需要自己寫片元著色器定義每個畫素的顏色。

mapboxgl提供了熱力圖和軌跡圖的畫素屬性值計算工具:

  • 熱力圖中為heatmap-density表示式,用來計算熱力圖上每個畫素的熱力值。

  • 軌跡線中為line-progress表示式,用來計算在當前線段上每個畫素的行進百分比。

模型網格渲染時,網格需要自己生成,網格中的屬性值也需要自己計算,通常在專案上這些是由模型完成的,如:EFDC水動力模型、高斯煙羽大氣汙染擴散模型等。

模型輸出的結果就是帶屬性值的網格,interpolate表示式的任務仍然是根據網格的屬性值為網格設定顏色。

1. 熱力圖

實現效果:

資料使用的是北京市公園綠地無障礙設施數量。

程式碼為:

//新增圖層
map.addLayer({
    "id": "park",
    "type": "heatmap",
    "minzoom": 0,
    "maxzoom": 24,
    "source": "park",
    "paint": {
        "heatmap-weight": 1,
        "heatmap-intensity": 1,
        'heatmap-opacity':0.4,
        'heatmap-color': [//熱力圖顏色
            'interpolate',
            ['linear'],
            ['heatmap-density'],
            0,'rgba(255,255,255,0)',
            0.2,'rgb(0,0,255)',
            0.4, 'rgb(117,211,248)',
            0.6, 'rgb(0, 255, 0)',
            0.8, 'rgb(255, 234, 0)',
            1, 'rgb(255,0,0)',
        ]
    }
});

上述程式碼中,使用interpolate表示式進行線性插值,輸入值是heatmap-density熱力圖密度,熱力圖密度的值在0-1之間,輸出值是熱力圖中各個畫素的顏色。

'heatmap-color': [
    'interpolate',
    ['linear'],
    ['heatmap-density'],
    0,'rgba(255,255,255,0)',
    0.2,'rgb(0,0,255)',
    0.4, 'rgb(117,211,248)',
    0.6, 'rgb(0, 255, 0)',
    0.8, 'rgb(255, 234, 0)',
    1, 'rgb(255,0,0)',
]

表示式詳解:

  • 密度為0或小於0,輸出顏色'rgba(255,255,255,0)'
  • 密度為0-0.2,輸出顏色在'rgba(255,255,255,0)''rgb(0,0,255)'之間
  • 密度為0.2,輸出顏色'rgb(0,0,255)'
  • 密度為0.2-0.4,輸出顏色在'rgb(0,0,255)''rgb(117,211,248)'之間
  • 密度為0.4,輸出顏色'rgb(117,211,248)'
  • 密度為0.4-0.6,輸出顏色在'rgb(117,211,248)''rgb(0, 255, 0)'之間
  • 密度為0.6,輸出顏色'rgb(0, 255, 0)'
  • 密度為0.6-0.8,輸出顏色在'rgb(0, 255, 0)''rgb(255,0,0)'之間
  • 密度為0.8,輸出顏色'rgb(255, 234, 0)'
  • 密度為0.8-1,輸出顏色在'rgb(255, 234, 0)''rgb(255,0,0)'之間
  • 密度為1或大於1,輸出顏色'rgb(255,0,0)'

線上示例:http://gisarmory.xyz/blog/index.html?demo=mapboxglStyleInterpolate1

和顏色拉伸渲染對應的另一種渲染方式,是使用step表示式對資料進行顏色分類渲染。

顏色分類渲染的實現方式在上面示例的程式碼中就有,只是被註釋了,可以把程式碼下載下來自行嘗試。

實現效果如下:

2. 軌跡圖

mapboxgl官網上提供了一個示例,是用顏色來表達軌跡行進的進度,效果圖如下:

它是用線的line-gradient屬性來實現的,其中用到了插值表示式interpolate和線進度表示式line-progressinterpolate表示式在這裡的作用依舊是對屬性值進行顏色拉伸渲染,程式碼如下:

map.addLayer({
    type: 'line',
    source: 'line',
    id: 'line',
    paint: {
        'line-color': 'red',
        'line-width': 14,
        // 'line-gradient' 必須使用 'line-progress' 表示式實現
        'line-gradient': [    //
            'interpolate',
            ['linear'],
            ['line-progress'],
            0, "blue",
            0.1, "royalblue",
            0.3, "cyan",
            0.5, "lime",
            0.7, "yellow",
            1, "red"
        ]
    },
    layout: {
        'line-cap': 'round',
        'line-join': 'round'
    }
});

在實際專案中,這種用顏色表達軌跡進度的場景相對少見,更多時候我們需要用顏色來表示軌跡的速度。

用顏色表示軌跡速度:

我們準備了一條騎行軌跡資料,軌跡由多個線段組成,每個線段上包含開始速度、結束速度和平均速度屬性,相鄰的兩條線段,前一條線段的結束點和下一條線段的開始點,它們的經緯度和速度相同。

//line資料中的單個線段示例
{
    "type": "Feature",
        "properties": {
            "startSpeed": 8.301424026489258, //開始速度
            "endSpeed": 9.440339088439941, //結束速度
            "speed": 8.8708815574646 //平均速度
        },
        "geometry": {
            "coordinates": [
                [
                    116.29458653185719,
                    40.08948061960585
                ],
                [
                    116.29486002031423,
                    40.08911413450488
                ]
            ],
                "type": "LineString"
        }
}

最簡單的實現方式就是,根據線段的平均速度,給每條線段設定一個顏色。

實現方式仍然是使用interpolate表示式,用它來根據軌跡中線段的速度對顏色進行插值。

核心程式碼如下:

//新增圖層
map.addLayer({
    type: 'line',
    source: 'line',
    id: 'line',
    paint: {
        'line-color': [
            'interpolate',//表示式名稱
            ["linear"],//表示式型別,此處是線性插值
            ["get", "speed"],//輸入值,此處是屬性值speed
            0,'red',//兩兩出現的判斷值和輸出值
            8,'yellow',
            10,'lime'
        ],
        'line-width': 6,
        'line-blur': 0.5
    },
    layout: {
        'line-cap': 'round'
    }
});

上面程式碼中,interpolate表示式的意思是:

  • 0km/h及以下(含0km/h)輸出紅色
  • 0-8km/h輸出紅到黃之間的顏色
  • 8km/h輸出黃色
  • 8-10km/h輸出黃到綠之間的顏色
  • 10km/h及以上(含10km/h)輸出綠色

實現效果如下:

示例線上地址:http://gisarmory.xyz/blog/index.html?demo=mapboxglStyleInterpolate2

整體看上去還不錯,但放大地圖時會發現,顏色是一段一段的,過渡不夠平滑,如下圖:

如何能讓區域性的顏色也平滑起來呢?

要是能讓兩個線段間的顏色平滑過渡就好了。

想到這裡,我們又想起了前面那個用顏色表示軌跡進度的官方示例,如果把兩種方式結合一下或許能實現想要的效果。

實現思路:

每條線段的屬性中有開始速度結束速度,根據顏色和速度的對應關係,可以插值出每條線段的開始顏色結束顏色,前一條線段的開始顏色和後一條線段的結束顏色為同一個顏色,每條線段中間的顏色通過使用line-gradient實現從開始顏色結束顏色的漸變。

這樣就能實現兩個線段間顏色的平滑過渡了。

實現方法:

按照這個思路需要進行兩次插值,第一次插值是插值出每個線段的開始顏色結束顏色,第二次是插值出每個線段上每個畫素的顏色

本來是想在mapboxgl中,通過多個表示式的巢狀來實現此功能,這樣程式碼會比較簡潔,但多次嘗試發現行不通,原因是,因為mapboxgl對line-gradientline-progress在的使用上的一些限制,所以第一次插值的邏輯需要自己動手實現。

第一步,自己動手寫個顏色插值函式,插值出每個線段的開始顏色結束顏色,實現方式註釋裡面已經寫的比較清楚了。

//通過canvas獲取開始顏色和結束顏色:
//原理是利用canvas建立一個線性漸變色物件,再通過計算顏色所在的位置去用getImageData獲取顏色,最後返回這個顏色
//1.建立canvas
var canvas = document.createElement("canvas");
canvas.width = 101;
canvas.height = 1;
var ctx = canvas.getContext('2d');
//2.建立線性漸變的函式,該函式會返回一個線性漸變物件,引數0,1,101,1分別指:漸變的起始點x,y和漸變的終止點x,y
var grd = ctx.createLinearGradient(0,1,101,1) 
//3.給線性漸變物件新增顏色點的函式,引數分別是停止點、顏色
grd.addColorStop(0,'red');
grd.addColorStop(0.8,'yellow');
grd.addColorStop(1,'lime');
//4.給canvas填充漸變色
ctx.fillStyle = grd;
ctx.fillRect(0, 0, 101, 1);
//5.返回漸變色的函式
function getInterpolateColor(r) {
    //6.這裡是漸變色的精細度,我將canvas分成101份來取值,每一份都會有自己對應的顏色
    //宣告的變數x就是我們需要的顏色在漸變物件上的位置
    let x =  parseInt(r * 100);
    x>100?x=100:x=x
    //7.傳入插值顏色所在的位置x,通過canvas的getImageData方法獲取顏色
    var colorData = ctx.getImageData(x, 0, 1, 1).data;
    //8.返回這個顏色
    return `rgba(${colorData[0]},${colorData[1]},${colorData[2]},${colorData[3]})`
}

第二步,每個線段設定為一個圖層,每個圖層呼叫第一步的方法獲取線段的開始顏色結束顏色,然後使用line-gradient屬性設定線段中間的顏色。

//allFeatures是line資料中單個線段組成的集合
allFeatures.map((item,index)=>{
    //通過上面的漸變色函式獲取開始顏色和結束顏色
    let startColor = getInterpolateColor(item.properties.startSpeed/10)
    let endColor = getInterpolateColor(item.properties.endSpeed/10)
    //迴圈新增圖層
    map.addLayer({
        type: 'line',
        source: 'line',
        id: 'line'+index,
        paint: {
            'line-width': 6,
            'line-blur': 0.5,
            'line-gradient': [
                'interpolate',
                ['linear'],
                ['line-progress'],
                0, startColor,
                1, endColor
            ]
        },
        layout: {
            'line-cap': 'round',
        },
        'filter': ['==', "$id", index]
    });
})

每個線段設定為一個圖層,最後可能會有上千個圖層,這樣不容易管理。

這裡提供另一種思路,可以將所有線段合併為一條折線,然後計算出折線上每個節點的速度、顏色和佔整個軌跡的百分比,佔整個軌跡的百分比通過節點距離起點和終點的長度來計算。

將所有節點的百分比和顏色兩兩對應作為line-gradient的判斷引數,這樣就能產生和多個圖層同樣的效果,同時只需要建立一個圖層。

這種方式的缺點是需要處理資料,具體適合用哪種可以根據實際情況來定。

最終實現效果如下:

示例線上地址:http://gisarmory.xyz/blog/index.html?demo=mapboxglStyleInterpolate3

2. 模型網格渲染

這種模式下,網格資料主要來自模型輸出結果,在輸出結果的基礎上,只需要用interpolate插值工具,根據網格屬性值插值出網格顏色就ok。

下面的程式碼和效果圖,是用EFDC模型的輸出結果做的示例,這個網格相對比較大一些,但中間部分的過渡還算自然。

程式碼:

//圖層
{
    "id": "waterTN",
    "type": "fill",
    "source": "efdc",
    "paint": {
        "fill-color": [
            "interpolate",
            ["linear"],
            ["get", "TN"],//輸入值是屬性TN
            0, "#36D1DC",
            15, "#6666ff",
            20, "#4444FF"
        ]
    }
}

效果圖:


四、隨著地圖縮放對圖形屬性進行插值

mapboxgl官網給出了兩個相關示例:

一個是按照縮放級別改變建築顏色,裡面同時對建築物的顏色和透明度進行了插值。

相關程式碼:

//對顏色插值
map.setPaintProperty('building', 'fill-color', [
    "interpolate",
    ["exponential", 0.5],
    ["zoom"],
    15,
    "#e2714b",
    22,
    "#eee695"
]);
//對透明度插值
map.setPaintProperty('building', 'fill-opacity', [
    "interpolate",
    ["exponential", 0.5],
    ["zoom"],
    15,
    0,
    22,
    1
]);

效果圖:

縮放改變顏色3

另一個是按照地圖縮放級別去改變建築物顯示高度,裡面對建築物的高度和建築物距離地圖的高度進行了插值。

相關程式碼:

map.addLayer({
    'id': '3d-buildings',
    'source': 'composite',
    'source-layer': 'building',
    'filter': ['==', 'extrude', 'true'],
    'type': 'fill-extrusion',
    'minzoom': 15,
    'paint': {
        'fill-extrusion-color': '#aaa',
        'fill-extrusion-height': [
            "interpolate", ["linear"],
            ["zoom"],
            15, 0,
            15.05, ["get", "height"]
        ],
        'fill-extrusion-base': [
            "interpolate", ["linear"],
            ["zoom"],
            15, 0,
            15.05, ["get", "min_height"]
        ],
        'fill-extrusion-opacity': .6
    }
}, labelLayerId);

效果圖:

縮放改變高度

同理,我們還可以對地圖圖示的大小進行插值,比如縮放級別越大圖示越大,縮放級別越小圖示越小等。


五、interpolate的高階用法

前面介紹插值工具interpolate的語法時,暫時沒有介紹插值型別這個選項,這一節我們好好說說它。

前面的多數示例中,插值型別選項我們都是使用的['linear']這個型別,意思是線性插值。

除了線性插值外,插值型別還支援["exponential",base]指數插值和["cubic-bezier", x1, y1, x2, y2]三次貝賽爾曲線插值。

它們的語法為:

  • ["linear"]線性插值,沒有其它引數。
  • ["exponential",base]指數插值,base引數為指數值。
  • ["cubic-bezier",x1,y1,x2,y2]三次貝塞爾曲線插值,x1y1x2y24個引數用於控制貝塞爾曲線的形態。

聽上去可能有點抽象,我們舉個例子:

下面這段的程式碼是根據地圖縮放級別改變建築物的透明度:

map.setPaintProperty('building', 'fill-opacity', [
   "interpolate", 
    ["linear"],
    ["zoom"],
    15,0,
    22,1
]);

意思為:

  • 當縮放級別小於15時,透明度為0。

  • 當縮放級別大於等於22時,透明度為1。

  • 當縮放級別在15到22之間時,使用線性插值方式自動計算透明度的值,介於0到1之間。

線性插值:

如果把縮放級別設定為x,透明度為y,限定x的值在15到22之間,則線性插值的方程式為:

y=(x-15)/(22-15)

從下面的函式影像上可以直觀的看出,它就是一條直線,這意味著地圖放大時,從15級開始到22級,建築物不透明度會勻速的增加,直到完全顯示。

指數插值

指數插值的方程式線上性插值方程式的基礎上增加了指數值,這個值我們用z來表示,方程式為:

y=((x-15)/(22-15))^z

通過z值來我們可以調整函式影像的形態,如:分別取z值為0.1、0.5、1、2、10這5個值,畫成圖如下:

以上圖中指數為10次方的紫色線為例,當地圖從15級放大到19級時,會一直都看不到建築物,因為建築物的透明度一直為0。

繼續放大,從19級放大到22級時,建築物會快速的顯現直到完全顯示。

這就是指數插值和線性插值的區別,它提供給了我們一個可以控制插值輸出快慢的方式。

三次貝塞爾曲線插值:

三次貝塞爾曲線插值和上面的指數插值是一個意思,都是為了能夠更靈活的控制插值輸出的快慢。

還是通過函式影像來幫助理解,指數插值的影像只能向一個方向彎曲,指數在0-1之間時曲線向上彎曲,大於1時曲線向下彎曲。

而三次貝塞爾曲線插值則可以讓曲線雙向彎曲。

mapboxgl官網提供了一個海洋深度的示例,裡面有用到三次貝塞爾曲線插值。

示例中使用三次貝塞爾曲線對錶示海洋深度的顏色進行插值,效果如下圖:

相關程式碼如下:

{
    'id': '10m-bathymetry-81bsvj',
    'type': 'fill',
    'source': '10m-bathymetry-81bsvj',
    'source-layer': '10m-bathymetry-81bsvj',
    'layout': {},
    'paint': {
    'fill-outline-color': 'hsla(337, 82%, 62%, 0)',
    'fill-color': [
        'interpolate',
        ['cubic-bezier', 0, 0.5, 1, 0.5],
        ['get', 'DEPTH'],
        200,'#78bced',
        9000,'#15659f'
        ]
    }
},

上面程式碼中,三次貝塞爾曲線插值的4個引數x1y1x2y2的值分別為:0、 0.5、 1、 0.5。

它的函式影像為:

通過上圖可以看出,函式輸出的速度是 先快 再慢 最後又快,結合海洋深度的示例,當深度在200米和9000米附近時,顏色變化較快,深度在中間時,顏色變化比較平緩。下面兩張圖是線性插值和三次貝塞爾曲線插值的對比:

上圖使用["linear"]線性插值,顏色勻速輸出,能看出深淺變化,但是‘塊狀感’明顯

下圖使用 ['cubic-bezier', 0, 0.5, 1, 0.5]三次貝塞爾曲線插值,顏色輸出先快再慢最後又快,既能看出深淺變化,又能實現自然過渡的平滑效果,會讓人感覺更柔和。

推薦文章一篇通俗易懂的三次貝塞爾曲線講解可以瞭解三次貝塞爾曲線是怎麼畫出來的,還有一個工具網站可以自己畫點幫助理解。

這三種插值方法所代表的函式都可以在座標軸中畫出來,無論畫出來是直線還是各種曲線,我們都不需要去糾結這個線條是如何畫的,因為這一步我們可以藉助工具來完成,需要關心的是這條線它輸出速度的快慢,這才和我們"interpolate"表示式的意義平滑過渡相關。

六、總結

  1. interpolate是mapboxgl地圖樣式中用於插值的表示式,能對顏色和數字進行插值,可以讓地圖實現平滑的過渡效果。
  2. 它的應用場景有兩類,一類是對地圖資料進行顏色拉伸渲染,如:熱力圖、軌跡圖、模型網格渲染等。
  3. 另一類是在地圖縮放時對圖形屬性進行插值,如:隨著地圖的縮放實現建築物高度的緩慢變化、圖形顏色的平滑切換等效果。
  4. interpolate插值工具提供了三種插值方式,線性插值、指數插值、三次貝塞爾曲線插值,它們的區別在於控制插值輸出快慢的不同。

* * *

原文地址:http://gisarmory.xyz/blog/index.html?blog=mapboxglStyleInterpolate

歡迎關注《GIS兵器庫

本文章採用 知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議 進行許可。歡迎轉載、使用、重新發布,但務必保留文章署名《GIS兵器庫》(包含連結: http://gisarmory.xyz/blog/),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。

相關文章