搞懂Leaflet的L.geoJSON

去年有參與開發一個 leaflet 圖台,那時匆匆忙忙再加上沒經驗,很多地方都沒有搞懂,程式能動就怎麼幹。今年圖台要新增兩個圖層,剛好我可以再回頭來研究一下 leaflet 的 L.geoJSON()。

甚麼是geoJSON

維基百科的解釋如下:

geoJSON是一種基於JSON的地理空間數據交換格式,它定義了幾種類型的JSON物件以及它們組合在一起的方法,以表示有關地理要素、屬性和它們的空間範圍的數據。

geoJSON 定義的地理數據類型(type)有以下幾種:

  • 基本幾何圖形
    • 點(Point)
    • 線(LineString)
    • 面(Polygon)
  • 複合幾何圖形
    • 多點(MultiPoint)
    • 多線(MultiLineString)
    • 多面(MultiPolygon)

geoJSON 的資料結構十分簡單,它是一個物件,typecoordinates 這2個屬性,type定義了點、線、面的資料類型,’coordinates’ 則是一個座標陣列,要注意的是經度在前,緯度在後

點(Point):

{
    "type": "Point", 
    "coordinates": [30, 10]
}

線(LineString)

{
    "type": "LineString", 
    "coordinates": [
        [30, 10], [10, 30], [40, 40]
    ]
}

多邊形(Polygon)

{
    "type": "Polygon", 
    "coordinates": [
        [[30, 10], [40, 40], [20, 40], [10, 20], [30, 10]]
    ]
}

挖洞的多邊形

因為第二個陣列裡的座標位置在第一個陣列的座標裡面,所以畫出來面被挖了一個洞。

{
    "type": "Polygon", 
    "coordinates": [
        [[35, 10], [45, 45], [15, 40], [10, 20], [35, 10]], 
        [[20, 30], [35, 35], [30, 20], [20, 30]]
    ]
}

以上是geoJSON的簡單說明,實際的結構可以更複雜。下面將針對如何在 leaflet 中使用 L.geoJSON 來將資料展現在地圖上做說明。

L.geoJSON 如何使用

在leaflet中可以使用它提供的 L.geoJSON 方法將點、線、面畫到地圖上。

L.geoJSON ()的第一個參數是 GeoJSON 物件,定義了空間資訊;第二個參數也是一個物件,可以放入它提供的 style、pointToLayer、onEachFeature及filter選項,這部分稍後說明。

const highBuilding = L.geoJSON(GeoJSON空間資料, {
  style: 線段及面的樣式
}).addTo(map);

如何劃一條線

首先簡單定義一個簡單的線段 geoJSON 物件,type 是 LineString,coordinates為一個陣列,裡面有三個經緯度座標的陣列。

const lineJSON = {
  type: "LineString",
  coordinates: [
    [121.56, 25.033],
    [121.58, 25.015],
    [121.61, 25.043]
  ]
};

然後定義線段的顏色

const lineStyle = {
  color: "#ff0000",
  // weight 控制線的粗細
  weight: 8,
  // opacity 控制透明度
  opacity: 0.65
};

最後使用L.geoJSON方法把線段展在地圖上,第一個參數是 上面定義的lineJSON物件,第二個參數物件裡面放了 linesStyle 物件,定義了線段的顏色。

const highBuilding = L.geoJSON(line, {
  style: lineStyle
}).addTo(map);

範例

如果你想要畫二條以上的線,可以改變一下geoJSON 物件的資料,改成陣列包住線段物件。

const lineJSON = [{
    "type": "LineString",
    "coordinates": [[121.54, 25.033], [121.56, 25.033], [121.57, 25.033]]
}, {
    "type": "LineString",
    "coordinates": [[121.54, 25.030], [121.56, 25.030], [121.57, 25.030]]
}];

範例

如何畫一個面

依樣定義一個面的 geoJSON 物件,type 是 Polygon,coordinates 是一個三個階層的陣列,第二層的陣列中放入至少3個的經緯度座標陣列。

const polygonJSON = {
  type: "Polygon",
  coordinates: [
    [
      [121.56, 25.033],
      [121.58, 25.015],
      [121.61, 25.043],
      [121.56, 25.043]
    ],
     [
      [121.52, 25.033],
      [121.48, 25.015],
      [121.48, 25.043],
      [121.52, 25.043]
    ]
  ]
};

定義面的樣式

const polygonStyle = {
  color: "#ff0000",
  // weight 控制線的粗細
  weight: 8,
  // opacity 控制透明度
  opacity: 0.65,
  // 如果沒有fillColor選項,則由 color 控制填色
  fillColor: "#B0DE5C",
  fillOpacity: 0.8
};

最後使用L.geoJSON方法把面展在地圖上,第一個參數是 上面定義的 polygonJSON 物件,第二個參數物件裡面放了 polygonStyle 物件,定義了面的顏色。

const polygons = L.geoJSON(polygonJSON, {
  style: polygonStyle
}).addTo(map);

範例

如何畫一個點

最後來談談如何展一個點,一樣先定義 geoJSON 資料,

const pointJSON = {
    "type": "Point", 
    "coordinates": [121.5647238,25.0336659]
}

如果要自定義樣式,要使用 pointToLayer 方法,在這裡我們先不使用 pointToLayer 方法,而是使用預設的 marker。

直接使用L.geoJSON方法把面展在地圖上,第一個參數是 上面定義的 polygonJSON 物件,沒有第二個參數物件。

	const highBuilding = L.geoJSON(pointJSON).addTo(map);

範例

如果要自定義點的樣式就要在L.geoJSON的第二個參數物件使用它所提供的 pointToLayer 方法,回傳 L.circleMarker 或 L.marker。

const highBuilding = L.geoJSON(taipei101, {
  pointToLayer(feature, latlng) {
    return L.circleMarker(latlng, {
      // 控制點的大小
      radius: 8,
      //  ORANGE
      fillColor: "#ff7800",
      //         線框的顏色
      color: "#000",
      //         線框的粗細
      weight: 1,
      opacity: 1,
      fillOpacity: 0.8
    });
  }
}).addTo(map);

範例

更複雜多樣的geoJSON

geoJSON 也可以是以下的結構,外層的 type 設成 Feature ,在 geometry 放入 原本的 type (這裡的type類型為 Point、LineString及Polygon) 及 coordinates 屬性,還有一個 properties 屬性可以放入 style 及 要放在 popup 的內容。

const campus = {
    "type": "Feature",
    "properties": {
        "popupContent": "This is the Auraria West Campus",
        "style": {
            weight: 2,
            color: "#999",
            opacity: 1,
//           GREEN
            fillColor: "#FF0000",
            fillOpacity: 0.8
        }
    },
    "geometry": {
        "type": "MultiPolygon",
        "coordinates": [
            [
                [
                    [121.519708, 25.035685],
                    [121.523249, 25.035685],
                    [121.523249, 25.033586],
                    [121.519708, 25.033586],                    
                ],
            ]
        ]
    }
};

也可以將外層的 type 設為 FeatureCollection,在內層的 features 陣列,放入有 geometry 、properites 及將內層type 設為 feature

const bicycleRental = {
    "type": "FeatureCollection",
    // "features" 為陣列,裡面放的物件們為點、線、面等屬性
    "features": [
        {
            "geometry": {
                "type": "Point",
                "coordinates": [
                    121.5647238,25.0336659
                ]
            },
            "type": "Feature",
            "properties": {
                "popupContent": "台北i101"
            },
            "id": 51
        },
    ]
};

當 type 設為 FeatureCollection 的時候,可以同時包含各種點、線及面的空間資訊。L.geoJSON 會自動判斷是點還是面。

const bicycleRental = {
    "type": "FeatureCollection",
    // "features" 為陣列
    "features": [
        {
            "geometry": {
                "type": "Point",
                "coordinates": [
                    121.5647238,25.0336659
                ]
            },
            "type": "Feature",
            "properties": {
                "popupContent": "台北i101"
            },
            "id": 51
        },
        {
            "geometry": {
                "type": "Point",
                "coordinates": [
                    121.5848238,25.0337659
                ]
            },
            "type": "Feature",
            "properties": {
                "popupContent": "This is a B-Cycle Station. Come pick up a bike and pay by the hour. What a deal!"
            },
            "id": 52
        },
        {
            "geometry": {
                "type": "Polygon",
                "coordinates":  [[
            [121.55, 25.04],
            [121.6,  25.05],
            [121.6,  25.055],
            [121.55, 25.055],
        ]]
            },
            "type": "Feature",
            "properties": {
                "popupContent": "This is a B-Cycle Station. Come pick up a bike and pay by the hour. What a deal!"
            },
            "id": 54
        },
       
    ]
};

範例

L.geoJSON 所提供的四種選項方法

搭配這4種選項方法,geoJSON的資料屬性除了type、coordinates之外,也可以加入其他屬性使用。

style

用來定義線跟面的樣式,作為 L.geoJSON() 第二個參數的物件屬性。

以上面的 Polygon 為例:

const polygonStyle = {
  color: "#ff0000",
  // weight 控制線的粗細
  weight: 8,
  // opacity 控制透明度
  opacity: 0.65,
  // 如果沒有fillColor選項,則由 color 控制填色
  fillColor: "#B0DE5C",
  fillOpacity: 0.8
};

const polygons = L.geoJSON(polygonJSON, {
  style: polygonStyle
}).addTo(map);

也可以將 style 當成方法來使用,以下為官網範例:

var states = [{
    "type": "Feature",
    "properties": {"party": "Republican"},
    "geometry": {
        "type": "Polygon",
        "coordinates": [[
            [-104.05, 48.99],
            [-97.22,  48.98],
            [-96.58,  45.94],
            [-104.03, 45.94],
            [-104.05, 48.99]
        ]]
    }
}, {
    "type": "Feature",
    "properties": {"party": "Democrat"},
    "geometry": {
        "type": "Polygon",
        "coordinates": [[
            [-109.05, 41.00],
            [-102.06, 40.99],
            [-102.03, 36.99],
            [-109.04, 36.99],
            [-109.05, 41.00]
        ]]
    }
}];

L.geoJSON(states, {
    style: function(feature) {
        switch (feature.properties.party) {
            case 'Republican': return {color: "#ff0000"};
            case 'Democrat':   return {color: "#0000ff"};
        }
    }
}).addTo(map);

onEachFeature

onEachFeature是一個函數,在每一個空間資訊要被加入 geoJSON 圖層的時候會被調用。常見於要將 properties 中所帶的資訊呈現在 popup 的時候(官網範例):

function onEachFeature(feature, layer) {
    // does this feature have a property named popupContent?
    if (feature.properties && feature.properties.popupContent) {
        layer.bindPopup(feature.properties.popupContent);
    }
}

var geojsonFeature = {
    "type": "Feature",
    "properties": {
        "name": "Coors Field",
        "amenity": "Baseball Stadium",
        "popupContent": "This is where the Rockies play!"
    },
    "geometry": {
        "type": "Point",
        "coordinates": [-104.99404, 39.75621]
    }
};

L.geoJSON(geojsonFeature, {
    onEachFeature: onEachFeature
}).addTo(map);

pointToLayer

如果要在將 geoJSON 中的點資訊畫在地圖上,而不使用預設的 marker ,必須使用 pointToLayer 這個方法(官網範例)。

var geojsonMarkerOptions = {
    radius: 8,
    fillColor: "#ff7800",
    color: "#000",
    weight: 1,
    opacity: 1,
    fillOpacity: 0.8
};

L.geoJSON(someGeojsonFeature, {
    pointToLayer: function (feature, latlng) {
        return L.circleMarker(latlng, geojsonMarkerOptions);
    }
}).addTo(map);

filter

可以用來過濾 geoJSON 的屬性,常常用來控制圖徵地可見性。(官網範例)

Busch Field 因為 show_on_map 屬性為 false,所以不會顯示在地圖上。

var someFeatures = [{
    "type": "Feature",
    "properties": {
        "name": "Coors Field",
        "show_on_map": true
    },
    "geometry": {
        "type": "Point",
        "coordinates": [-104.99404, 39.75621]
    }
}, {
    "type": "Feature",
    "properties": {
        "name": "Busch Field",
        "show_on_map": false
    },
    "geometry": {
        "type": "Point",
        "coordinates": [-104.98404, 39.74621]
    }
}];

L.geoJSON(someFeatures, {
    filter: function(feature, layer) {
        return feature.properties.show_on_map;
    }
}).addTo(map);

參考資料:

https://leafletjs.com/examples/geojson/