iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 22
0
Modern Web

《你的地圖會說話? WebGIS與JavaScript的情感交織》系列 第 22

[8-1] 展點多到爆?那就試試看 Leaflet MarkerCluster吧!

展點,把一堆點資料展在地圖上。
如果今天點的數量很多很多,除了圖台效能受到影響外,
視覺上也不美觀,那要怎麼樣呈現才能解決點多到爆的問題呢?

那就用群聚吧!我們讓聚集的點用同一個圖標來表示,
並且用圖標的數字、大小、顏色,讓視覺化上更加鮮明,
讓人一眼就能看出哪裡是比較密集的地區!
今天要來介紹Leaflet API plugins的markercluster! (Leaflet Plugins)
讓展點通通群聚吧!


初始化地圖

久違的Leaflet API,幫大家複習一下初始化地圖!

↓ 引入leaflet的css與js

    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>

↓ css 讓地圖滿板,沒有邊界

        #lmap {
            height: 100%;
        }

        html,
        body {
            height: 100%;
            margin: 0;
            padding: 0;
        }

↓ 新增一個存放地圖的div

    <div id="lmap"></div>

↓ 初始化地圖

        var LMap = L.map(document.getElementById('lmap'), {
            center: [23.5, 121],
            zoom: 7,
            crs: L.CRS.EPSG3857,
        });
        L.tileLayer('https://api.tiles.mapbox.com/v4/{id}/{z}/{x}/{y}.png?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw', {
            maxZoom: 18,
            id: 'mapbox.streets'
        }).addTo(LMap);

亂數產生點

由於要測試群聚,因此寫一個亂數點產生器,來產生無數的點。

↓ 隨機在最小值(min)與最大值(max)之間產生數值

        function random(min, max) {
            return Math.random() * (max - min) + min;
        }

Math.random()會隨機產生介於0~1的小數點,做為縮放比例。再乘以變量(最大減最小),最後再加上最小值,就能取得範圍中的任意數。

↓ 新增 CreatePoint 在固定範圍內產生隨機經緯度的點,並存進陣列中。

        let arr = [];
        function CreatePoint(count) {  // count為產生的點數量
            for (let i = 0; i < count; i++) {
                let longitude = random(120.5, 121.4);  // 經度介於120.5~121.4
                let latitude = random(23, 24.6);  // 緯度介於23~24.6

                arr.push({ x: longitude, y: latitude });
            }
        }

↓ 呼叫,產生1500個點。

        CreatePoint(1500);
        console.log(arr);

↓ 結果
https://ithelp.ithome.com.tw/upload/images/20201007/20130604QqeSI4wCAt.jpg

↓ 將arr陣列跑迴圈,把所有點秀在地圖上。

         arr.map(item => L.marker(new L.LatLng(item.y, item.x)))
             .forEach(item => LMap.addLayer(item));

↓ 結果
https://ithelp.ithome.com.tw/upload/images/20201007/20130604elvRvgsODp.jpg
密密麻麻的!密集恐懼症的別看~/images/emoticon/emoticon06.gif

Leaflet MarkerCluster

  • 群聚

↓ 引入 Leaflet MarkerCluster的css與js

    <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css" />
    <script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js"></script>

↓ 首先,先新增一個L.markerClusterGroup,它可以存入許多座標點,並且讓它們群聚起來!

        var markers = L.markerClusterGroup();

↓ 將arr陣列跑迴圈,這次把marker加入L.markerClusterGroup中,而不是直接加入地圖!

arr.map(item => L.marker(new L.LatLng(item.y, item.x))  // 新增Marker
    .bindPopup(`<p>經度: ${item.x}</p><p>緯度: ${item.y}</p>`))  // 資訊視窗
    .forEach(item => markers.addLayer(item));  // 把marker加入 L.markerClusterGroup中

↓ 將剛剛存放所有點的L.markerClusterGroup加入地圖中

        LMap.addLayer(markers);

↓ 結果
https://ithelp.ithome.com.tw/upload/images/20201007/20130604yGWxU9r6Ba.jpg

  • 用css自定義群聚icon

↓ MarkerCluster的css要把剛剛引入的MarkerCluster.Default.css換掉,改引入MarkerCluster.css
unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css

    <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css" />

↓ 將L.markerClusterGroup的參數加入iconCreateFunction,並且用cluster.getChildCount()計算群聚點數量。

        var markers = L.markerClusterGroup({
            iconCreateFunction: function (cluster) {
                const number = cluster.getChildCount();
                return L.divIcon({ html: number, className: 
                    'cluster cluster-yellow', iconSize: L.point(25, 25) });
            }
        });

↓ 可以用css自己刻群聚圖標的樣式,懂SCSS或其他預處理器的人也可以用它們刻。

        .cluster {
            border: 2px solid grey;
            border-radius: 35px;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .cluster.cluster-green {
            background-color: green;
        }

        .cluster.cluster-yellow {
            background-color: yellow;
        }

        .cluster.cluster-red {
            background-color: red;
        }

↓ 結果
https://ithelp.ithome.com.tw/upload/images/20201007/20130604F0zrCadPoG.jpg
換成自己刻的icon了!

↓ 可是要依照不同的數量給予icon不同的大小及顏色,因此加入自定義的icon邏輯。

        function IconLogic(number) {  // 數量
            let className = 'cluster';
            let point;

            if (number < 100) {
                className += ' cluster-green';
                point = L.point(25, 25);
            } else if (number < 200) {
                className += ' cluster-yellow';
                point = L.point(30, 30);
            } else {
                className += ' cluster-red';
                point = L.point(35, 35);
            }

            return {
                className: className,
                point: point
            }
        }

↓ 將群聚icon的參數改為變數,加入剛剛寫好的icon邏輯。

        var markers = L.markerClusterGroup({
            iconCreateFunction: function (cluster) {
                const number = cluster.getChildCount();
                let icon = IconLogic(number);

                return L.divIcon({ html: number, className: icon.className
                                        , iconSize: icon.point });
        });

↓ 結果
https://ithelp.ithome.com.tw/upload/images/20201007/20130604onc2Zksn8A.jpg

↓ 放大
https://ithelp.ithome.com.tw/upload/images/20201007/20130604mbR6eTrCJF.jpg

↓ 再放大成一般icon後點擊會顯示資訊視窗
https://ithelp.ithome.com.tw/upload/images/20201007/20130604O357Enx2bW.jpg

  • 事件

↓ 在L.markerClusterGroup物件上綁定clusterclick,可以幫群聚點新增點擊事件。

        markers.on('clusterclick', function (e) {
            const number = e.layer.getAllChildMarkers().length;  

            console.log('群聚數量: ' + number);
        });

callback後的e.layer.getAllChildMarkers()可以取得該群聚點數量。

↓ console 群聚點數量
https://ithelp.ithome.com.tw/upload/images/20201007/20130604uvxw5cDTXZ.jpg

↓ e.layer.getConvexHull()可以取得群聚點計算該區塊群聚數量的邊界。

        var polySelected;
        markers.on('clusterclick', function (e) {
            const number = e.layer.getAllChildMarkers().length;

            if (polySelected) {  // 如果有選取的邊界存在,先清除
                LMap.removeLayer(polySelected);
            }
            
            polySelected = L.polygon(e.layer.getConvexHull());  // 繪出邊界
            LMap.addLayer(polySelected);
            
            console.log(e.layer.getConvexHull());
            console.log('群聚數量: ' + number);
        });

↓ console 邊界座標
https://ithelp.ithome.com.tw/upload/images/20201007/20130604ddlgxfHefj.jpg

↓ 點擊群聚點後繪出邊界。
https://ithelp.ithome.com.tw/upload/images/20201007/201306047CwCw4qtam.jpg

  • 蜘蛛網

密集恐懼症請左轉!

e.layer.spiderfy()可以劃出群聚點與群聚內各個點的連線,俗稱蜘蛛網。

        var polySelected;
        markers.on('clusterclick', function (e) {
            const number = e.layer.getAllChildMarkers().length;

            if (number < 100) {  // 群聚數量小於100才繪製蜘蛛網
                e.layer.spiderfy();
            }

            if (polySelected) {
                LMap.removeLayer(polySelected);

            }

            polySelected = L.polygon(e.layer.getConvexHull())
            LMap.addLayer(polySelected);

            console.log(e.layer.getConvexHull());
            console.log('群聚數量: ' + number);
        });

↓ 結果
https://ithelp.ithome.com.tw/upload/images/20201007/20130604J4RGCz9mT4.jpg

https://ithelp.ithome.com.tw/upload/images/20201007/20130604A0vfJQ3Onu.jpg

https://ithelp.ithome.com.tw/upload/images/20201007/20130604QNGsAmrnGn.jpg

https://ithelp.ithome.com.tw/upload/images/20201007/20130604o3oDc7IztH.jpg

L.markerClusterGroup其他設定

  • showCoverageOnHover: 滑鼠移至群聚點時,顯示群聚區塊的面圖徵
  • zoomToBoundsOnClick: 點擊時放大至群聚點
  • spiderfyOnMaxZoom: 放到最大時顯示蜘蛛網
  • removeOutsideVisibleBounds: 群聚點及點座標在可視範圍外不操作
  • spiderLegPolylineOptions: 設定蜘蛛網樣式,預設為{ weight: 1.5, color: '#222', opacity: 0.5 }

今天介紹了有趣的Leaflet MarkerCluster,
明天再來繼續玩玩看其他有趣的Leaflet Plugins吧!

大家看到那個蜘蛛網是覺得很噁想吐?還是覺得很潮呢? (笑
/images/emoticon/emoticon39.gif


上一篇
還在ES6?那你有聽過ES7、ES8嗎?
下一篇
[8-2] heatmap.js 熱區- 以Leaflet地圖實作
系列文
《你的地圖會說話? WebGIS與JavaScript的情感交織》30

尚未有邦友留言

立即登入留言