2024/1/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/1/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/1/8

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 人的一天

2023/12/25

Daemon Thread in java

Java 的 thread 有分為 User 與 Daemon Thread 兩種。

User Thread 的執行優先順序比較高,JVM會等所有的 User Thread 都結束工作後,才會完全地停止工作。

Daemon Thead 的 priority 比較低,通常是用來提供 service 給 user thread 使用。如果有資料 IO 的工作,不建議在 daemon thead 裡面實作。因為JVM 不需要等待 daemon thread,所以可以隨時中止 daemon thread,通常在 daemon thread 會用無窮迴圈實作,如果有 finally 部分的程式,在 daemon thread 並不會保證一定會被執行。

Daemon thead 可用在 garbage collection,釋放記憶體,釋放 cache 的工作。

Daemon thead 要在產生物件後,start 前,用 setDaemon 設定為 daemon thread

NewThread daemonThread = new NewThread();
daemonThread.setDaemon(true);
daemonThread.start();

// 用 isDaemon() 檢查是否為 Daemon Thread
daemonThread.isDaemon();

References

Daemon Threads in Java | Baeldung

JAVA并发编程——守护线程(Daemon Thread) - Luochengor - 博客园

2023/12/18

Fluent API

fluent API 是一種 OO API design 方式,可讓 API 的使用者,透過 chain method 的方式持續呼叫該物件的 method。在實作這種 API 介面時,method 必須要在最後回傳物件本身的 reference,以最常見的 set methods 來說,以往都是回傳 void,但要改為回傳 this。

在實作支援 Fluent API 這樣的類別時,要注意物件的 immutability 特性,如果該類別是修改物件內部資料的狀態,那麼就直接回傳 this 就好了,但也有可能是持續產生相同類別的新物件。

Difference Between Fluent Interface and Builder Pattern in Java | Baeldung 這個網頁提出了兩種不同的例子

以下是 User 的 Builder,可透過 set method 不斷地修改使用者的各個欄位資料,一直到最後,確認要產生 User 時,就呼叫 build

public static class Builder {
    private String firstName;
    private String lastName;
    private String email;
    private String username;
    private Long id;

    public Builder firstName(String firstName) {
        this.firstName = firstName;
        return this;
    }

    public Builder lastName(String lastName) {
        this.lastName = lastName;
        return this;
    }

    // other methods

    public User build() {
         return new User(firstName, lastName, email, username, id);
    }
}

在使用時

Builder userBuilder = new Builder();
User user = userBuilder
            .firstName("John")
            .lastName("Will")
            .build();

另一個要持續產生新的物件的例子是 Html,因為 html 裡面的 String content 是不能直接修改的,故必須要在 method 最後面都產生一個新的物件

public class HtmlDocument {
    private final String content;

    public HtmlDocument() {
        this("");
    }

    public HtmlDocument(String html) {
        this.content = html;
    }

    public String html() {
        return format("<html>%s</html>", content);
    }

    public HtmlDocument header(String header) {
        return new HtmlDocument(format("%s <h1>%s</h1>", content, header));
    }

    public HtmlDocument paragraph(String paragraph) {
        return new HtmlDocument(format("%s <p>%s</p>", content, paragraph));
    }
}

使用時

HtmlDocument document = new HtmlDocument()
  .header("header")
  .paragraph("paragraph 1")
  .paragraph("paragraph 2");
String html = document.html();

References

Fluent API: Practice and Theory | SIGPLAN Blog

Fluent programming style

Fluent API — 流畅API(基于Java介绍) | 桃子爱吃桃子