天天看點

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

interpolate是mapboxgl地圖樣式中用于插值的表達式,能對顔色和數字進行插值。

它的應用場景有兩類:1、對地圖資料進行顔色拉伸渲染。2、在地圖縮放時對圖形屬性進行插值。

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

目錄

  • 一、前言
  • 二、文法
  • 三、對地圖顔色進行拉伸渲染
    • 1. 熱力圖
    • 2. 軌迹圖
    • 2. 模型網格渲染
  • 四、随着地圖縮放對圖形屬性進行插值
  • 五、interpolate的高階用法
  • 六、總結

一、前言

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. 熱力圖

實作效果:

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

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

代碼為:

//添加圖層
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

表達式對資料進行顔色分類渲染。

顔色分類渲染的實作方式在上面示例的代碼中就有,隻是被注釋了,可以把代碼下載下傳下來自行嘗試。

實作效果如下:

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

2. 軌迹圖

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

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

它是用線的

line-gradient

屬性來實作的,其中用到了插值表達式

interpolate

和線進度表達式

line-progress

interpolate

表達式在這裡的作用依舊是對屬性值進行顔色拉伸渲染,代碼如下:

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)

    輸出

    綠色

實作效果如下:

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

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

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

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

如何能讓局部的顔色也平滑起來呢?

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

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

實作思路:

每條線段的屬性中有

開始速度

結束速度

,根據顔色和速度的對應關系,可以插值出每條線段的

開始顔色

結束顔色

,前一條線段的

開始顔色

和後一條線段的

結束顔色

為同一個顔色,每條線段中間的顔色通過使用

line-gradient

實作從

開始顔色

結束顔色

的漸變。

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

實作方法:

按照這個思路需要進行兩次插值,第一次插值是插值出每個線段的

開始顔色

結束顔色

,第二次是插值出每個線段上每個像素的顔色

本來是想在mapboxgl中,通過多個表達式的嵌套來實作此功能,這樣代碼會比較簡潔,但多次嘗試發現行不通,原因是,因為mapboxgl對

line-gradient

line-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

的判斷參數,這樣就能産生和多個圖層同樣的效果,同時隻需要建立一個圖層。

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

最終實作效果如下:

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

示例線上位址: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 中插值表達式的應用場景

四、随着地圖縮放對圖形屬性進行插值

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
]);
           

效果圖:

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

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

相關代碼:

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);
           

效果圖:

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

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

五、interpolate的高階用法

前面介紹插值工具

interpolate

的文法時,暫時沒有介紹

插值類型

這個選項,這一節我們好好說說它。

前面的多數示例中,

插值類型

選項我們都是使用的

['linear']

這個類型,意思是線性插值。

除了線性插值外,

插值類型

還支援

["exponential",base]

指數插值和

["cubic-bezier", x1, y1, x2, y2]

三次貝賽爾曲線插值。

它們的文法為:

  • ["linear"]

    線性插值,沒有其它參數。
  • ["exponential",base]

    指數插值,

    base

    參數為指數值。
  • ["cubic-bezier",x1,y1,x2,y2]

    三次貝塞爾曲線插值,

    x1

    y1

    x2

    y2

    4個參數用于控制貝塞爾曲線的形态。

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

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

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級,建築物不透明度會勻速的增加,直到完全顯示。

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

指數插值:

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

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

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

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

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

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

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

三次貝塞爾曲線插值:

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

還是通過函數圖像來幫助了解,指數插值的圖像隻能向一個方向彎曲,指數在0-1之間時曲線向上彎曲,大于1時曲線向下彎曲。

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

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

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

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個參數

x1

y1

x2

y2

的值分别為:0、 0.5、 1、 0.5。

它的函數圖像為:

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

通過上圖可以看出,函數輸出的速度是 先快 再慢 最後又快,結合海洋深度的示例,當深度在

200

米和

9000

米附近時,顔色變化較快,深度在中間時,顔色變化比較平緩。下面兩張圖是線性插值和三次貝塞爾曲線插值的對比:

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

上圖使用

["linear"]

線性插值,顔色勻速輸出,能看出深淺變化,但是‘塊狀感’明顯

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

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

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

這三種插值方法所代表的函數都可以在坐标軸中畫出來,無論畫出來是直線還是各種曲線,我們都不需要去糾結這個線條是如何畫的,因為這一步我們可以借助工具來完成,需要關心的是這條線它輸出速度的快慢,這才和我們

"interpolate"

表達式的意義平滑過渡相關。

六、總結

  1. interpolate

    是mapboxgl地圖樣式中用于插值的表達式,能對顔色和數字進行插值,可以讓地圖實作平滑的過渡效果。
  2. 它的應用場景有兩類,一類是對地圖資料進行顔色拉伸渲染,如:熱力圖、軌迹圖、模型網格渲染等。
  3. 另一類是在地圖縮放時對圖形屬性進行插值,如:随着地圖的縮放實作建築物高度的緩慢變化、圖形顔色的平滑切換等效果。
  4. interpolate

    插值工具提供了三種插值方式,線性插值、指數插值、三次貝塞爾曲線插值,它們的差別在于控制插值輸出快慢的不同。

* * *

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

歡迎關注《GIS兵器庫》

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