2024/01/22

Leaflet 基本使用方法

大部分想到地圖 API 直覺都是 Google Map,但在商業用途上,Google Map API 有使用量要收費的問題。目前可以透過 Leaflet 使用 OpenStreepMap,這部分在商業用途是可以免費使用的。

建立地圖

使用 leaflet 要先 include css 及 js,一開始要先指定地圖的中心點,以下範例定位在台中市政府。過程是用 L.map 建立地圖物件,然後加入 OpenStreetMap 這個 tile layer

建立地圖的範例

<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
    integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
    crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
    integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
    crossorigin=""></script>

<style>
    #map { height: 450px; };
</style>

</head>
<body>
<div id="map"></div>
</body>

<script>
var map = null;
function create_map() {

    let zoom = 16; // 0 - 18
    let taichung_cityhall = [24.1635657,120.6486657]; // 中心點座標
    let maptemp = L.map('map',{renderer: L.canvas()}).setView(taichung_cityhall, zoom);
    map = maptemp;
    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        attribution: '© OpenStreetMap', // 商用時必須要有版權出處
        zoomControl: true, // 是否顯示 - + 按鈕
        zoomAnimation:false,
        markerZoomAnimation: false,
        fadeAnimation: false,
    }).addTo(map);

    // 強制用 resize window 將所有 tiles 載入
    setTimeout(function () {
        window.dispatchEvent(new Event('resize'));
    }, 1000);

};

create_map();

</script>

</html>

當我們把程式整合到比較複雜的網頁中,會發現地圖的區塊在一開始載入後,會出現灰色的區塊,這一段程式碼是修正這個問題,用意是強制用 js resize window,讓 leaflet 能夠載入所有的地圖區塊。

// 強制用 resize window 將所有 tiles 載入
    setTimeout(function () {
        window.dispatchEvent(new Event('resize'));
    }, 1000);

建立 Marker,放上 Tooltip

因為要使用 bootstrap icon 測試,要先引用 bootstrap-icons,然後加上兩個 css class: icon1, icon2

<link href="
https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css
" rel="stylesheet">

<style>
#map { height: 450px; }

.icon1 {
  color: #e41a1c;
}
.icon2 {
  color: #377eb8;
}
</style>

這邊加上兩個 Marker 地圖標記,分別在台中市政府及火車站。

我們使用 L.divIcon ,這裡要注意,不能直接在 html 裡面寫上 style 調整 icon 顏色,只能指定 className 套用 css class。css class 很簡單,就只是修改 icon 的顏色

過程是用 L.marker 產生 marker,然後 bindTooltip,再將 marker 加入地圖中

function test_marker() {
    console.log("test_marker")
    // ref: https://gis.stackexchange.com/questions/291901/leaflet-divicon-how-to-hide-the-default-square-shadow
    // 不要寫 style   要用 className
    const icon1 = L.divIcon({
        html: '<i class="bi bi-geo-alt-fill"></i>',
        iconSize: [20, 20],
        className: 'icon1'
    });
    let taichung_cityhall = [24.1635657,120.6486657];
    let taichung_station = [24.136941,120.685056];
    let taichung_station2 = [24.1360,120.685056];
    let marker1 = L.marker(taichung_cityhall, {
        icon: icon1
    });

    marker1.bindTooltip("test1", {
        direction: 'bottom', // right、left、top、bottom、center。default: auto
        sticky: false, // true 跟著滑鼠移動。default: false
        permanent: false, // 是滑鼠移過才出現,還是一直出現
        opacity: 1.0
    }).openTooltip();
    marker1.addTo(map);
    // remove marker
    // marker1.remove();

    const icon2= L.divIcon({
        html: '<i class="bi bi-geo-alt-fill"></i>',
        iconSize: [20, 20],
        className: 'icon2'
    });
    let marker2 =  L.marker(taichung_station2, {
        icon: icon2
    });
    marker2.bindTooltip("test2", {
        direction: 'bottom',
        sticky: false,
        permanent: false,
        opacity: 1.0
    }).openTooltip();
    marker2.addTo(map);
};

test_marker();

Layer Group, Layer Control

多個 Marker 可以組合成一個 Layer,放到 Layer Group 裡面。再透過右上角 Layer Control,可將該 layer 的標記切換 on/off

function test_polyline() {
    console.log("test_polyline");
    const icon1 = L.divIcon({
        html: '<i class="bi bi-geo-alt-fill"></i>',
        iconSize: [20, 20],
        className: 'icon1'
    });

    const layerControl = L.control.layers(null).addTo(map);

    var test1_geos = [
        [24.136941,120.68],
        [24.136941,120.685056],
        [24.1360,120.6850]
    ];
    var test1_markers = [];
    for (let i = 0; i < test1_geos.length; i++) {
        let marker = L.marker([test1_geos[i][0], test1_geos[i][1]], {
            icon: icon1
        });
        marker.bindTooltip("test1", {
            direction: 'bottom',
            sticky: false,
            permanent: false,
            opacity: 1.0
        }).openTooltip();
        test1_markers.push(marker);
    }
    // marker 座標的連線
    var polyline1 = L.polyline(test1_geos, {color: '#e41a1c'});
    var test1LayerGroup = L.layerGroup(test1_markers).addLayer(polyline1);

    map.addLayer(test1LayerGroup);
    layerControl.addOverlay(test1LayerGroup, "test1");

    map.panTo(test1_geos[1]);
};
test_polyline();

搭配 vue3 使用的問題

ref: https://www.cnblogs.com/hjyjack9563-bk/p/16856014.html

在測試過程中,常會在 console 發生類似這樣的錯誤

Cannot read properties of null (reading '_latLngToNewLayerPoint')

這是因為 vue3 搭配 leaflet 才會遇到的問題,解決方式是在使用到 map 時,都要用 toRaw() 轉回原本的物件,所有用到 addlayer,removeLayer,clearLayers的方法 都應該用 toRaw(this.map)

ex:

// add layer control
this.layerControl = L.control.layers(null).addTo(toRaw(this.map));

References

OSM + Leaflet 學習筆記 1:建地圖、marker、事件、換圖層 - Front-End - Let's Write

Quick Start Guide - Leaflet - a JavaScript library for interactive maps

2024/01/15

webcam 即時影像

有一種特殊的 MIME 類型,稱為 multipart/x-mixed-replace,這是由 Netscape 在1995年引入的。當伺服器想向客戶端持續發送資料時,瀏覽器可以在一個 http 通道中,持續接收這些改變的資料。它可以用於串流傳輸圖像的 webcam。

通常 http protocol 會需要 content-length 這個 header,讓接收資料的一端,明確知道這個 http 封包的資料有多少,但 multipart/x-mixed-replace 刻意在 client 打開一個 http 連線後,不回傳 response 的 content length,反而是直接用 multipart 的格式,持續對 client 發送資料。browser 在收到這些 content 時,可用來持續更新畫面。

最常見的應用是 webcam,在高速公路、氣象局的網頁,有即時影像的區塊,就是用這種方式實作的。

response 的資料可以是這種格式,這是持續發送圖片

HTTP/1.1 200 OK
Content-Type: multipart/x-mixed-replace; boundary=frame

--frame
Content-Type: image/jpeg

<1.jpg>

--frame
Content-Type: image/jpeg

<2.jpg>

--frame
Content-Type: image/jpeg

<3.jpg>

也可以是這種格式

HTTP/1.1 200 OK
Content-Type: multipart/x-mixed-replace; boundary=frame

--frame
Content-Type: text/html

<html><body>0</body></html>

--frame
Content-Type: text/html

<html><body>1</body></html>

--frame
Content-Type: text/html

<html><body>2</body></html>

每個 multipart 區塊,可以定義告知 content-type

在氣象局的即時影像網頁,每一次取得影像,都會在 30s 以後,關閉這個連線,使用者必須要重新點一次,才會再取得一次 webcam 資料。

References

使用 multipart/x-mixed-replace 实现 http 实时视频流

Albert 的筆記本: 高速公路路況即時影像的作法

iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天

推播技術 - 維基百科,自由的百科全書

2024/01/08

HSTS(HTTP Strict Transport Security)

RFC 6797 規定了網際網路瀏覽安全機制 HSTS(HTTP Strict Transport Security) ,宣告瀏覽器與伺服器之間通訊方式必須強制採用 TLS/SSL。

只要從伺服器端送出一個 Strict-Transport-Security 標頭 (Header) 給瀏覽器,即可告知瀏覽器於未來的某段時間內一律使用 SSL 來和該網站連接。一旦發生憑證失效情況,使用者將無法再瀏覽該網站,如此一來便可大幅減少中間人攻擊的問題發生。

換句話說:HSTS Header 就是負責將 http 強制轉為 https。

設定方式是 Server 回傳的 html response,必須要有 Strict-Transport-Security header,內容為

Strict-Transport-Security: max-age=<expire-time>
Strict-Transport-Security: max-age=<expire-time>; includeSubDomains
Strict-Transport-Security: max-age=<expire-time>; preload

max-age 是讓 browser 儲存該設定多久的時間,以秒為單位,通常是設定 31536000 (1年) 以上

includeSubDomains 是套用到所有子網域

preload 不是標準的規格的設定值,是 Preloading Strict Transport Security 的意思,不一定要寫

所以設定值為

Strict-Transport-Security: max-age=31536000; includeSubDomains

維持 HSTS 設定一年,且包含子網域

References

經得起原始碼資安弱點掃描的程式設計習慣培養(五)_Missing HSTS Header

HSTS設定 - iT 邦幫忙::一起幫忙解決難題,拯救 IT 人的一天