Leaflet-輕量且易懂易用的互動地圖

最後修改日期|Aug 04, 2021
撰文時版本 |1.7.1

DowYuu言

最近滿常用到Leaflet,順手來記錄一下。

Leaflet

地圖初始化

官方文件 Map

1
2
3
4
5
6
7
8
const openstreet = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});
let map = L.map('map', {
  layers: [openstreet],
  center: [23.604799, 120.7976256], // 台灣中心點
  zoom: 8
});

移除地圖

移除後即可重新進行初始化動作。

1
map.remove();

經緯位置資訊 LatLng

地圖中所有的經緯度位置都可以任意切換成如下的格式:

  • [lat, lon]
  • { lat: lat, lon: lon }
  • { lon: lon, lat: lat }
  • L.latLng(lat, lon)

PS. Latitude 是緯度,Longitude 是經度。

設置視圖

同時設置經緯位置與縮放等級。

1
map.setView([23.604799, 120.7976256], 8);

只設置經緯位置資訊

只要設置經緯位置,延伸自setView

1
map.panTo([23.604799, 120.7976256]);

只設置縮放等級(Zoom)

只設置縮放等級,延伸自setView

1
map.setZoom(8);

取得地圖資訊狀態

官方文件 Methods for Getting Map State

取得地圖中心點座標

1
map.getCenter(); // 回傳中心點座標

取得地圖縮放級別

1
map.getZoom(); // 回傳數字

取得地圖邊界點

會回傳東北點及西南點的座標。

1
map.getBounds(); // 回傳東北點及西南點的座標

地圖事件

點擊地圖取得該點資訊

1
2
3
4
5
6
7
map.on('click', function(e){
  let coord = e.latlng,
      lat = coord.lat,
      lng = coord.lng;
  console.log('latitude(緯度)', lat);
  console.log('longitude(經度)', lng);
});

開始地圖拖動

1
2
3
map.on('movestart', function (){
  console.log('你開始地圖拖動了');
});

結束地圖拖動

1
2
3
map.on('moveend', function (){
  console.log('你結束地圖拖動了');
});

圖層群組

官方文件 LayerGroup

將圖層群組加至地圖中

1
2
let markersLayer = new L.layerGroup();
markersLayer.addTo(map);

移除圖層群組

1
map.removeLayer(markersLayer);

清除圖層群組

會將此圖層內所有圖層(例如你加到此群組中的Marker)清除。

1
markersLayer.clearLayers();

圖層控制器

官方文件 Control.layers

1
2
3
4
5
6
7
8
let baseLayers = {
  '開放街圖': baseOpenStreet,
  '臺灣通用電子地圖': baseEMAP
};
let overlays = {
  '地標': markersLayer
};
let controler = L.control.layers(baseLayers, overlays).addTo(map);

注意原本map有設定layers還是不能拿掉,layers設定中的圖層會成為預設載入圖層。

1
2
3
4
5
let map = L.map('map', {
  layers: [openstreet], // 不能拿掉,這個會是預設載入圖層
  center: [23.604799, 120.7976256],
  zoom: 8
});

底圖圖層baseLayers會被設置為radio供使用者點選,疊加圖層overlays會被設置為checkbox供使用者點選。

該底圖圖層在不支援的縮放等級時,會被設置disabled

像下方範例,目前在縮放等級20,中間三個底圖圖層是支援到20的,其餘上下兩個底圖圖層不支援,radio就會被設置disabled

該底圖圖層在不支援的縮放等級時,會被設置disabled

顯示圖層名稱

顯示的圖層名稱可以包含HTML

1
2
3
4
5
6
let baseLayers = {
  '<i class="bi bi-map-fill"></i><span>開放街圖</span>': baseOpenStreet
};
let overlays = {
  '<i class="bi bi-show"></i><span>餐廳</span>': markersLayer
};

新增底圖圖層baseLayer

要注意參數是先layer再圖層名稱。

1
controler.addBaseLayer(basePHOTO2, '正射影像圖');

新增疊加圖層overlay

要注意參數是先layer再圖層名稱。

1
controler.addOverlay(busStopLayer, '公車站點');

地圖比例尺

官方文件 Control.Scale

1
L.control.scale().addTo(map);

預設公制單位(m/km)和英制單位(mi/ft)都會顯示,可在option中設置。

1
2
3
4
L.control.scale({
  metric: true,   // 控制公制單位(m/km)顯示
  imperial: true  // 控制英制單位(mi/ft)顯示
}).addTo(map);

瀏覽器支援、版本判斷

官方文件 Browser

Leaflet也有提供瀏覽器支援、版本判斷方法,有是否支援某元素(如是否支援svg),或是瀏覽器版本判斷(是否為ie),相當方便。

使用上就是用L.Browser.<Properties>,回傳皆為boolean值,<Properties>列表與回傳值代表含意請詳見官方文件 Browser.Properties

1
2
3
if(L.Browser.ie){
  alert('兄Day,換個先進點的瀏覽器好不?');
}

Marker標誌

官方文件 Marker

1
2
3
4
5
let m = new L.Marker([23.604799, 120.7976256], {markerName: '我是臺灣的中心啦'}).on('click', function(e){
  console.log(e.target.options); // 可以看到options設置的東西
  console.log(e.target); // e.target是此L.Marker
});
markersLayer.addLayer(m);

這邊範例是把Marker加到圖層群組markersLayer中,當然你要直接加到地圖裡也可以。

1
map.addLayer(m);

Icon

官方文件 Icon

預設的Marker會是一個藍色的icon,這邊也可以用圖片自訂。

1
2
3
4
5
6
7
8
9
10
11
let greenIcon = L.icon({
    iconUrl: 'leaf-green.png',
    shadowUrl: 'leaf-shadow.png',

    iconSize:     [38, 95], // icon 寬, 長
    shadowSize:   [50, 64], // 陰影 寬, 長
    iconAnchor:   [22, 94], // icon 中心偏移
    shadowAnchor: [4, 62],  // 陰影 中心偏移
    popupAnchor:  [-3, -76] // 綁定popup 中心偏移
});
let m = L.marker([23.604799, 120.7976256], {icon: greenIcon});

要用類似預設標點但是帶icon圖示的話,可以參考Leaflet.awesome-markers

Popup彈出窗

官方文件 Popup

簡易設定:

1
m.bindPopup('Hello');

進階設定:

1
2
3
4
let p = new L.Popup({ className: 'station-popup'}) // 一些設定
                    .setContent('<div>HELLO</div>') // popup內容HTML
                    .setLatLng([23.604799, 120.7976256]);
m.bindPopup(p).openPopup(); // 將popup綁定至marker並開啟popup

綁定至Marker、開啟Popup、關閉Popup

Popup的開關是取決於綁定的Marker上。

1
2
3
4
5
m.bindPopup(p); // 將popup綁定至marker

m.openPopup(); // marker開啟綁定的popup

m.closePopup(); // marker關閉綁定的popup

讓Popup的開啟時不會關掉其他Popup

預設點了一個Popup就會關掉原本開啟的Popup

所以這邊可以設置autoClose: falsecloseOnClick: false,這樣Popup的開關就不會影響到其他的Popup

1
2
3
4
let p = new L.Popup({ className: 'station-popup', autoClose: false, closeOnClick: false})
                    .setContent('<div>HELLO</div>')
                    .setLatLng([23.604799, 120.7976256]);
m.bindPopup(p).openPopup();

讓Popup維持顯示無法關閉

可以用CSS把Popup上的關閉按鈕隱藏,並且用javascript設置點擊Marker時會把Popup關閉,這樣關掉後觸發反向就會又自動開起來。

1
2
3
.leaflet-popup-close-button{
  display: none;
}
1
2
3
let m = new L.Marker([22, 120], {markerName: 'OO公車站'}).on('click', function(e){
  e.target.options.closePopup(); // 多加這行
});

免費地圖圖資

※ 這只是個人整理,使用上勞煩自行查證細讀各圖資使用之版權、授權條款,有糾紛不負責RRR

這邊放上OpenStreetMap開放街圖國土測繪中心 國土測繪圖資服務雲、以及地理資訊科學研究專題中心所提供的部分圖資,下方也有簡介與介接說明。

OpenStreetMap開放街圖

OpenStreetMap開放街圖 圖資

是自由而且開源的全球地圖。

要注意使用上要參照OpenStreetMap版權與授權條款,必須標註圖資作者© OpenStreetMap contributors

1
2
3
4
// OpenStreetMap開放街圖
const baseOpenStreet = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});

國土測繪中心圖資

國土測繪中心圖資介接說明是寫縮放層級到19,但實測都有到20。要注意Leaflet預設最高縮放層級只到18,所以使用上記得設定最大限制maxNativeZoom: 20maxZoom: 20

網站上寫了很多次,所以特別附註上來:請注意不可以將圖資大量的用 cache 的方式儲存於伺服器上再提供服務,這將構成「著作權法」第3條的「重製」及「發行」,屬嚴重侵權的違法行為。

這邊只放幾個自己常用的。

臺灣通用電子地圖

臺灣通用電子地圖 圖資

優點是有臺灣的門牌號碼(於縮放層級19與20)。

1
2
3
4
5
// 國土測繪中心 臺灣通用電子地圖
const baseEMAP = L.tileLayer('https://wmts.nlsc.gov.tw/wmts/EMAP/default/GoogleMapsCompatible/{z}/{y}/{x}', {
  maxNativeZoom: 20,
  maxZoom: 20
});

臺灣通用電子地圖(高DPI文字)

臺灣通用電子地圖(高DPI文字) 圖資

背景同通用電子地圖,沒有門牌且只有圖資臺灣地區,但是圖資上的文字變得比較清晰銳利也變大了,拿來看街道名稱更易讀一點

相同位置與Zoom情況下,臺灣通用電子地圖 與 臺灣通用電子地圖(高DPI文字) 比較

縮放等級拉到6以下地圖就會消失,所以使用上記得設定最小限制minZoom: 6

1
2
3
4
5
6
// 國土測繪中心 臺灣通用電子地圖(高DPI文字)
const baseEMAPHighDPI = L.tileLayer('https://wmts.nlsc.gov.tw/wmts/EMAP96/default/GoogleMapsCompatible/{z}/{y}/{x}', {
  maxNativeZoom: 20,
  maxZoom: 20,
  minZoom: 6
});

正射影像圖

正射影像圖 圖資

實拍照,縮放等級拉到7以下臺灣就會消失,所以使用上記得設定最小限制minZoom: 7

1
2
3
4
5
6
// 國土測繪中心 正射影像圖
const basePHOTO2 = L.tileLayer('https://wmts.nlsc.gov.tw/wmts/PHOTO2/default/GoogleMapsCompatible/{z}/{y}/{x}', {
  maxNativeZoom: 20,
  maxZoom: 20,
  minZoom: 7
});

圖資介接

圖資介接說明,進網頁後點選「WMTS」並點選「列表」,進到「WMTS列表」畫面後點擊介接網址處的類別網址,會下載下xml說明檔案。

進網頁後點選「WMTS」並點選「列表」

進到「WMTS列表」畫面後點擊介接網址處的類別網址,會下載下xml說明檔案

打開說明檔案,找到該圖層ResourceURL中的template網址,{Style}對應到該圖層的Style底下的ows:Identifier設定值,{TileMatrixSet}對應到該圖層的TileMatrixSet設定值,把{TileMatrix}換成{z}{TileRow}換成{y}{TileCol}換成{x}即可。

國土測繪中心 圖資介接說明

地理資訊科學研究專題中心圖資

說明文件中的TileMatrixSet部分也有提到,本單位的縮放等級最大到17,到17以上地圖就會消失,所以使用上記得設定最大限制maxZoom: 17

這邊只放「福衛二號衛星影像」。

福衛二號衛星影像

福衛二號衛星影像 圖資

實拍照,影像範圍只有臺灣周圍,不過每個縮放層級的周圍範圍不太一樣,有點奇妙XD

1
2
3
4
// 福衛二號衛星影像
const baseF2 = L.tileLayer('http://gis.sinica.edu.tw/tgos/file-exists.php?img=F2IMAGE_W-png-{z}-{x}-{y}', {
  maxZoom: 17
});

圖資介接

圖資介接說明,找到該圖層ResourceURL中的template網址,把{TileMatrix}換成{z}{TileCol}換成{x}{TileRow}換成{y}即可。

地理資訊科學研究專題中心圖資 圖資介接說明

常用套件

Leaflet.awesome-markers-讓Marker附帶icon且支援多色

撰文時版本|2.0.2

Leaflet.awesome-markers 展示

Leaflet.awesome-markers,可在原Marker上加上icon,並且支持多種顏色。

1
2
3
4
5
6
7
let redMarker = L.AwesomeMarkers.icon({
  icon: 'arrow-alt-circle-right',
  prefix: 'fa',
  markerColor: 'red'
});

let m = L.marker([23.604799, 120.7976256], {icon: redMarker});

目前似乎無法使用目前常見的svg icon,只能用Font Icon型式的icon。

icon參數就是各套件中,icon的個別名稱。
要注意Ionicons,因為本身有分樣式類型,所以在icon頁面看到的名稱記得要加上樣式類型字首(ios風格就加上’ios-‘,Material風格就加上’md-‘)。

prefix參數是icon的字首,以下為官方說明有提到的icon套件與其CDN連結頁面,可自己挑看選看:

套件 prefix CDN 頁面 備註
Bootstrap Icons bi CDN 頁面
Bootstrap3 Glyphicons glyphicon CDN 頁面 預設值
Font Awesome fa CDN 頁面
Ionicons ion CDN 頁面 Font Icon只到版本4.5。
因為Ionicons本身有分樣式類型,所以在icon頁面看到的名稱記得要加上樣式類型字首(ios風格就加上'ios-',Material風格就加上'md-')。

markerColor參數目前版本(2.0.1)有red、darkred、lightred、orange、beige、green、darkgreen、lightgreen、blue、darkblue、lightblue、purple、darkpurple、pink、cadetblue、white、gray、lightgray、black。

其他參數還有:

iconColor 就icon的顏色,預設是白色。

spin 當使用Font Awesome,設為true即可使該icon旋轉。

extraClasses 可加上自訂的class

Leaflet.markercluster-群聚Markers展開縮起

撰文時版本|1.5.0

Leaflet.markercluster 展示

Leaflet.markercluster,當部分Marker太過密集,就會自動縮起並顯示縮起的Marker數目,不同數目級距也會顯示不同顏色。

1
let markersLayer = new L.MarkerClusterGroup();

裡面可以設置參數但是我都沒用過,基本上不設定就很好用了(?)

使用上就是把Marker加到MarkerClusterGroup群組中,就會自動展開縮起了。

可以和前面提到的Leaflet.awesome-markers併用。

leaflet-velocity-繪製風場

撰文時版本|1.9.0

leaflet-velocity 展示

leaflet-velocity,將json格式(格式請參考grib2json)的風場資料視覺化。

1
2
3
4
5
6
7
8
9
let windLayer = L.velocityLayer({
  displayValues: true, // 若為true,會顯示目前滑鼠游標處的風場資料
  displayOptions: { // displayValues: true時顯示資料的設定
    velocityType: "台灣海域風",
    displayPosition: "bottomleft",
    displayEmptyString: "無資料"
  },
  data: windData // 資料不一定要在此時放入,也可用下方setData方法
});

若要初始化後再設置設定值或風場資料,可以用setOptionssetData

1
windLayer.setData(windData);

Leaflet-Ruler-地圖量測

撰文時版本|無

Leaflet-Ruler 展示

Leaflet-Ruler,簡易的地圖量測工具。

這邊有修改樣式以及語言顯示,如果不需要修改就不用加上rulerOption這段了,樣式部分也同樣不必要,但只是換成自己比較喜歡的模樣。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let rulerOption = {
                    position: 'topright',
                    circleMarker: {
                      color: '#ac1515',
                      radius: 2
                    },
                    lineStyle: {
                      color: 'rgba(0, 0, 0, 0.4)',
                      dashArray: '1,6'
                    },
                    lengthUnit: {
                      display: 'km',
                      decimal: 2,
                      factor: null,
                      label: '總距離:'
                    },
                    angleUnit: {
                      display: '&deg;',
                      decimal: 2,
                      factor: null,
                      label: '方位角:'
                    }
                  };
L.control.ruler(rulerOption).addTo(map);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* 移動時即時資料顯示區塊 */
.moving-tooltip{
  border: 0;
  background-color: rgba(0, 0, 0, 0.8);
  color: #FFF;
  letter-spacing: 0.5px;
  line-height: 1.2;
  opacity: 1;
}
.moving-tooltip.leaflet-tooltip-left::before{
  border-left-color: rgba(0, 0, 0, 0.8);
}
.moving-tooltip.leaflet-tooltip-right::before{
  border-right-color: rgba(0, 0, 0, 0.8);
}

/* 計算結果顯示區塊 */
.result-tooltip{
  border: 0;
  background-color: rgba(0, 0, 0, 0.6);
  color: #FFF;
  letter-spacing: 0.5px;
  line-height: 1.2;
}
.result-tooltip.leaflet-tooltip-left::before{
  border-left-color: rgba(0, 0, 0, 0.6);
}
.result-tooltip.leaflet-tooltip-right::before{
  border-right-color: rgba(0, 0, 0, 0.6);
}

/* 量測狀態時的開關按鈕 */
.leaflet-ruler-clicked{
  border-color: #d81111 !important;
}

Leaflet.Photo-照片標點

撰文時版本|無

Leaflet.Photo 展示

Leaflet.Photo,好看的地圖照片標點,支援群聚展開縮起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 預設資料格式
let data = [
  {
    lat: 22.61828436781493, // 緯度
    lng: 120.29206931591035, // 經度
    url: 'img/0.jfif', // 原圖url
    caption: '<div class="caption">高雄海洋流行音樂中心</div><div class="photo-by">Photo By Nick  Valmores</div>', // 照片標題
    thumbnail: 'thumbnail/0.jfif', // 縮圖url
    video: '' // 影片檔
  },
  {
    lat: 25.033821475206487,
    lng: 121.56453609466554,
    url: 'img/1.jpeg',
    caption: '<div class="caption">臺北101</div><div class="photo-by">Photo By Timo Volz</div>',
    thumbnail: 'thumbnail/1.jpeg',
    video: ''
  },
  ...
];

// 程式

// 照片圖層事件綁定
// 事件綁定內容可自行修改,此處放的是官方範例(點擊會跳出Popup)
let photoLayer = L.photo.cluster().on('click', function(evt){
  let photo = evt.layer.photo, // 該圖片資料物件
      template = '<img src="{url}" class="img-preview"/>{caption}';

  // 若有設置video則會使用影片,我個人沒試過影片
  if (photo.video && (!!document.createElement('video').canPlayType('video/mp4; codecs=avc1.42E01E,mp4a.40.2'))) {
    template = '<video autoplay controls poster="{url}" class="video-preview"><source src="{video}" type="video/mp4"/></video>';
  };

  evt.layer.bindPopup(L.Util.template(template, photo), {
    className: 'leaflet-popup-photo',
    minWidth: 300
  }).openPopup();

});

// 清除照片圖層中的所有Layer
photoLayer.clear();

// 將資料加入照片圖層,並加入地圖
photoLayer.add(data).addTo(map);

Leaflet.Sync-地圖同步

撰文時版本|0.2.4

Leaflet.Sync 展示

Leaflet.Sync,方便的地圖定位同步工具。

只要設定定位同步,當map1定位變動時,map2也會跟著一起定位變動。但要注意,此時map2移動,map1不會跟著移動(互相同步請看下個範例)。

1
2
3
4
5
6
7
8
let option = { // 可省略
  noInitialSync: true, // 禁用地圖的初始同步
  syncCursor: true, // 當滑鼠在地圖上滑動,同步的地圖上也會顯示一個圓形標點來標記滑鼠的點位
  offsetFn: function (center, zoom, refMap, tgtMap){ // 計算中心偏移量的函數
    return center;
  }
};
map1.sync(map2, option);

如果要互相定位同步(當map1移動map2會跟著移動,當map2移動map1會跟著移動),則要綁定兩次:

1
2
map1.sync(map2);
map2.sync(map1);

以下是範例圖片的程式:

HTML:

1
2
<div id="map1"></div>
<div id="map2"></div>

js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let center = [23.604799, 120.7976256];
let layer1 = L.tileLayer('https://wmts.nlsc.gov.tw/wmts/EMAP/default/GoogleMapsCompatible/{z}/{y}/{x}', {
  maxNativeZoom: 20,
  maxZoom: 20
});
let layer2 = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});
let map1 = L.map('map1', {
    layers: [layer1],
    center: center,
    zoom: 7
});
let map2 = L.map('map2', {
    layers: [layer2],
    center: center,
    zoom: 7
});

map1.sync(map2);
map2.sync(map1);