顯示具有 javascript 標籤的文章。 顯示所有文章
顯示具有 javascript 標籤的文章。 顯示所有文章

2026/02/09

Cytoscape.js

Cytoscape.js 是一個處理資料視覺化的 javascript library,當我們要對資料關係進行可視化顯示時,例如社交網路關係或網路拓樸圖時,Cytoscape.js 是個不錯的選擇。

Cytoscape 和 Cytoscape.js 是兩個完全獨立不同的軟體

  • Cytoscape

    • 使用 Java 語言編寫的用於網絡可視化的桌面應用程序

    • 需要安裝 Java SDK 才能使用

    • 用於大型網絡分析和可視化的高性能應用程序

  • Cytoscape.js

    • 用於網絡可視化的 javascript library,本身不是一個完整的 Web Application

    • 可以在大多數瀏覽器上使用

    • 不需要 plugin 即可運行

    • 需要編寫程式來建構 Web Application

    • 支援 Extensions

    • 基於 CSS 將資料映射到元件屬性

sample1

建立一個 圓形排列 的 4 個節點 (A, B, C, D),節點之間有箭頭連線,點擊節點會有事件

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>test1</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
            display: block;
        }
    </style>
</head>
<body>
    <h2>test1</h2>
    <div id="cy"></div>

    <script>
        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'a', label: '節點 A' } },
                { data: { id: 'b', label: '節點 B' } },
                { data: { id: 'c', label: '節點 C' } },
                { data: { id: 'd', label: '節點 D' } },
                { data: { id: 'ab', source: 'a', target: 'b' } },
                { data: { id: 'bc', source: 'b', target: 'c' } },
                { data: { id: 'cd', source: 'c', target: 'd' } },
                { data: { id: 'da', source: 'd', target: 'a' } }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'background-color': '#0074D9',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#0074D9'
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 3,
                        'line-color': '#AAAAAA',
                        'target-arrow-color': '#AAAAAA',
                        'target-arrow-shape': 'triangle',
                        'curve-style': 'bezier',
                    }
                }
            ],

            layout: {
                name: 'circle'
            }
        });

        // 點擊事件
        cy.on('tap', 'node', function(evt) {
            let node = evt.target;
            console.log('你點了節點: ' + node.id());
        });
    </script>
</body>
</html>

sample2

流程圖,layout 調整為 dagre extension。

使用時要引用 dagre library 及 Cytoscape.js 的 extension

dagre 正是 Cytoscape.js 常用來畫flowchart或 directed graph 的 layout。適合做 flowchart, network topology, workflow

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>Cytoscape.js Flowchart</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <script src="https://unpkg.com/dagre/dist/dagre.min.js"></script>
    <script src="https://unpkg.com/cytoscape-dagre/cytoscape-dagre.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
    <h2>flowchart</h2>
    <div id="cy"></div>

    <script>
        cytoscape.use(cytoscapeDagre);

        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'start', label: '開始' } },
                { data: { id: 'step1', label: '步驟 1' } },
                { data: { id: 'step2', label: '步驟 2' } },
                { data: { id: 'decision', label: '判斷 ?' } },
                { data: { id: 'end', label: '結束' } },
                { data: { id: 's1', source: 'start', target: 'step1' } },
                { data: { id: 's2', source: 'step1', target: 'step2' } },
                { data: { id: 's3', source: 'step2', target: 'decision' } },
                { data: { id: 's4', source: 'decision', target: 'end' } },
                { data: { id: 's5', source: 'decision', target: 'step1' } }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'shape': 'round-rectangle',
                        'background-color': '#28a745',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#28a745'
                    }
                },
                {
                    selector: 'node[id="decision"]',
                    style: {
                        'shape': 'diamond',
                        'background-color': '#ffc107',
                        'text-outline-color': '#ffc107'
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 2,
                        'line-color': '#555',
                        'target-arrow-color': '#555',
                        'target-arrow-shape': 'triangle',
                        'curve-style': 'bezier',
                    }
                }
            ],

            layout: {
                name: 'dagre',
                // rankDir: 'TB'  // top-to-bottom
                rankDir: 'LR' // 由左到右 排列
            }
        });
    </script>
</body>
</html>

sample3

鐵路模擬,增加火車在鐵軌上移動的動畫

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>Cytoscape.js Railway</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
    <h2>Railway</h2>
    <div id="cy"></div>

    <script>
        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'station1', label: '車站 1' } },
                { data: { id: 'station2', label: '車站 2' } },
                { data: { id: 'station3', label: '車站 3' } },
                { data: { id: 'checkpoint4', label: '檢查點 4' } },
                { data: { id: 'checkpoint5', label: '檢查點 5' } },
                { data: { id: 'checkpoint6', label: '檢查點 6' } },
                { data: { id: 's1', source: 'station1', target: 'station2' } },
                { data: { id: 's2', source: 'station2', target: 'station3' } },
                { data: { id: 's3', source: 'station2', target: 'checkpoint4' } },
                { data: { id: 's4', source: 'checkpoint4', target: 'checkpoint5' } },
                { data: { id: 's5', source: 'checkpoint5', target: 'station3' } },
                { data: { id: 's6', source: 'checkpoint5', target: 'checkpoint6' } },

                // 列車節點
                { data: { id: 'train1', label: '🚆' }, classes: 'train' }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'shape': 'ellipse',
                        'background-color': '#0074D9',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#0074D9'
                    }
                },
                {
                    selector: 'node[id^="station"]',
                    style: {
                        'shape': 'round-rectangle',
                        'background-color': '#17a2b8',
                        'text-outline-color': '#17a2b8'
                    }
                },
                {
                    selector: 'node.train',
                    style: {
                        'background-color': 'red',
                        'shape': 'ellipse',
                        'label': 'data(label)',
                        'font-size': 24,
                        'width': 30,
                        'height': 30
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 2,
                        'line-color': '#555',
                        'target-arrow-color': '#555',
                        'target-arrow-shape': 'triangle'
                    }
                }
            ],

            layout: {
                name: 'breadthfirst',
                directed: true,
                padding: 20
            }
        });

        function moveAlongEdge(train, fromNode, toNode, duration, callback) {
            const start = fromNode.position();
            const end = toNode.position();
            const startTime = performance.now();

            function animate(now) {
                const elapsed = now - startTime;
                const t = Math.min(elapsed / duration, 1); // 0~1
                const x = start.x + (end.x - start.x) * t;
                const y = start.y + (end.y - start.y) * t;
                train.position({ x, y });

                if (t < 1) {
                    requestAnimationFrame(animate);
                } else if (callback) {
                    callback();
                }
            }

            requestAnimationFrame(animate);
        }

        function moveTrain(path) {
            let i = 0;
            const train = cy.getElementById('train1');

            function step() {
                if (i >= path.length - 1) return;
                const fromNode = cy.getElementById(path[i]);
                const toNode = cy.getElementById(path[i + 1]);

                moveAlongEdge(train, fromNode, toNode, 2000, () => {
                    i++;
                    step();
                });
            }

            step();
        }

        // 定義路徑
        const route = ['station1', 'station2', 'checkpoint4', 'checkpoint5', 'station3'];

        // 初始化列車位置
        cy.getElementById('train1').position(cy.getElementById(route[0]).position());

        // 2 秒後啟動列車
        setTimeout(() => moveTrain(route), 2000);
    </script>
</body>
</html>

2025/10/13

如何使用 vue3-draggable-next esm module

vue3-draggable-next 是 draggable 套件的 vue3 版本,預設在 dist 裡面,只提供 umd 及 commonjs module,如果要在 browser 裡面,透過 esm 的方式 import module,必須要先自己製作一個簡單的 esm js

vuedraggable.umd.js 檔案,就跟 vuedraggable.umd.min.js 放在同一個目錄

// 確保 UMD 先載入(它會掛在 window.vuedraggable)
import "./vuedraggable.umd.min.js";
// 把全域變數 export 出去,提供 ESM 的 default
export default window.vuedraggable;

然後在 html 裡面,先以 importmap 方式列出 import list

因為 draggable 有使用到 sortablejs,故這邊要先放到 import list

另外在下面的 module 裡面,要先將 Vue 及 Sortable 掛載到全域變數裡面,因為 draggable 是這樣直接呼叫 Vue 跟 Sortable,所以必須這樣掛載

    <script type="importmap">
    {
        "imports": {
            "vue": "../js/lib/vue-3.5.13/vue.esm-browser.prod.min.js",
            "vuedraggable": "../js/lib/vue3-draggable-next-4.1.4/vuedraggable.esm.js",
            "sortablejs": "../js/lib/sortable-1.15.6/sortable.esm.js"
        }
    }
    </script>

    <script type="module">
        import * as Vue from 'vue';
        window.Vue = Vue; // 讓 draggable UMD 找得到 Vue.defineComponent

        import Sortable from 'sortablejs';
        window.Sortable = Sortable;
    </script>

最後可製作 App

  <script type="module">
    import { createApp, reactive } from "vue";
    import Draggable from "vuedraggable";

    const state = reactive({
      rows: [
        { id: 1, name: "Item A" },
        { id: 2, name: "Item B" },
        { id: 3, name: "Item C" }
      ]
    });

    createApp({
      components: { Draggable },
      setup() {
        return { state };
      }
    }).mount("#app");
  </script>

html 的部分,可放在 tbody 裡面。

這邊注意 #item="{ element, index } #item 裡面,只能用 element, index,不能改成其他變數名稱。否則會一直遇到 undefined 物件的問題。

<div id="app">
    <table border="1">
      <thead>
        <tr>
          <th>Drag</th>
          <th>Name</th>
        </tr>
      </thead>
      <!-- draggable tbody -->
      <draggable
        tag="tbody"
        v-model="state.rows"
        handle=".drag-handle"
      >
        <template #item="{ element, index }">
          <tr :key="element.id">
            <td class="drag-handle" style="cursor: grab;">☰</td>
            <td>{{ element.name }}</td>
          </tr>
        </template>
      </draggable>
    </table>
    <pre>{{ state.rows }}</pre>
  </div>

vuedraggable 是 SortableJS 的包裝,所以大部分事件都對應到 SortableJS events,最常用的是:

  • @start 拖曳開始

  • @end 拖曳結束

  • @add 新元素被加入(跨清單)

  • @remove 元素被移除

  • @update 同一清單內順序改變

  • @change 綜合事件(新增、刪除、移動都會觸發)

實際上操作時

  • 拖曳開始到結束:會觸發 @start 與 @end

  • 順序有變動:會觸發 @update 和 @change

  • 跨清單拖曳:會觸發 @add 與 @remove

2025/09/22

vue3 runtime

如果要使用 vue3 runtime,也就是 vue.runtime.esm-browser.js,而不使用 vue.esm-browser.js。這兩個版本的差異是使用後者,有支援 template,可直接將 template 字串寫在 component 裡面,但如果使用 runtime library,因為這個檔案大小比較小,缺少了動態編譯 template 的功能,必須改寫為使用 render function。

實例

以下是可以直接在 browser 執行的範例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title></title>
</head>
<body>
<div id="app"></div>

<script type="importmap">
    {
        "imports": {
            "vue": "https://unpkg.com/vue@3.5.13/dist/vue.runtime.esm-browser.js",
            "vue-i18n": "https://unpkg.com/vue-i18n@11.1.1/dist/vue-i18n.runtime.esm-browser.js"
        }
    }
</script>

<script type="module">
    import { createApp, h } from 'vue';
    import { createI18n, useI18n } from 'vue-i18n';

    const messages = {
        en: { greeting: 'Hello!' },
        zh: { greeting: '你好!' }
    };

    const i18n = createI18n({
        legacy: false,
        locale: 'en',
        messages
    });

    const App = {
        setup() {
            const { t, locale } = useI18n(); // 正確取得 t() 函數

            const toggleLang = () => {
                locale.value = locale.value === 'en' ? 'zh' : 'en';
            };

            return () =>
                h('div', [
                    h('h1', t('greeting')),
                    h('button', { onClick: toggleLang }, '🌐 Switch Language')
                ]);
        }
    };

    createApp(App).use(i18n).mount('#app');
</script>
</body>
</html>

compiler-dom

vue3 官方提供了一個 compiler-dom,可以將 template 字串轉換為 render function。

安裝首先要安裝 compiler-dom

npm i @vue/compiler-dom

撰寫一個 convert.js

// https://www.npmjs.com/package/@vue/compiler-dom
// npm i @vue/compiler-dom

const fs = require('fs');
const path = require('path');
const { compile } = require('@vue/compiler-dom');

const file = process.argv[2];

if (!file || !file.endsWith('.template.html')) {
  console.error('請提供一個 .template html 檔案,例如:node convert.js MyComponent.html');
  process.exit(1);
}

const filePath = path.resolve(process.cwd(), file);

const template = fs.readFileSync(filePath, 'utf-8');

// 編譯 template 成 render 函數
const { code } = compile(template, {
  mode: 'module',
  prefixIdentifiers: true, // 避免 with()
  filename: file
});

// 產出 JS 檔案名稱
const baseName = path.basename(file, '.template.html');
const outputFile = `${baseName}.template.render.js`;

// // 包裝為可匯入的模組
const outputContent = `
${code}
`;
// 寫入檔案
fs.writeFileSync(outputFile, outputContent.trim());

console.log(`Render function 已輸出為:${outputFile}`);

使用方法

把 template 的部分,改為獨立的 test.template.html 檔案

<div>
    <h1> {{ t("greeting") }} </h1>
    <button @click="toggleLang()">🌐 Switch Language</button>
</div>

透過 nodejs 將 template 轉換為 render function

import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", null, [
    _createElementVNode("h1", null, _toDisplayString(_ctx.$t("greeting")), 1 /* TEXT */),
    _createElementVNode("button", {
      onClick: $event => (_ctx.toggleLang())
    }, "🌐 Switch Language", 8 /* PROPS */, ["onClick"])
  ]))
}

改寫原本的測試網頁

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title></title>
</head>
<body>
<div id="app"></div>

<script type="importmap">
    {
        "imports": {
            "vue": "https://unpkg.com/vue@3.5.13/dist/vue.runtime.esm-browser.js",
            "vue-i18n": "https://unpkg.com/vue-i18n@11.1.1/dist/vue-i18n.runtime.esm-browser.js",

            "render": "./test.template.render.js"
        }
    }
</script>

<script type="module">
    import { createApp, h } from 'vue';
    import { createI18n, useI18n } from 'vue-i18n';
    import {render} from 'render';

    const renderFn = render;

    const i18n = createI18n({
        legacy: false,
        locale: 'en',
        fallbackLocale: "en",
        messageCompiler: null,
        messages: {
            "en": {
                "greeting": 'Hello!'
            },
            "zh": {
                "greeting": '你好!'
            },
        },
    });

    const App = {
        setup() {
            const { t, locale } = useI18n();

            const toggleLang = () => {
                locale.value = locale.value === 'en' ? 'zh' : 'en';
            };

            return {
                t, locale, toggleLang
            };
        },
        render: renderFn,
        methods: {
        }
    };

    createApp(App).use(i18n).mount('#app');
</script>
</body>
</html>

note

點擊切換語言時,網頁 console 會出現這樣的警告訊息

[intlify] the message that is resolve with key 'greeting' is not supported for jit compilation

不影響網頁操作,但還不知道為什麼會出現這個 warning

2024/02/05

在網頁使用 sqlite

SQLite compiled to JavaScript 透過 WASM,可在網頁直接載入 sqlite db,使用 SQL 指令操作資料庫。WebAssembly或稱wasm是一個低階程式語言,讓開發者能運用自己熟悉的程式語言(最初以C/C++作為實作目標)編譯,再藉虛擬機器引擎在瀏覽器內執行。透過 WebAssembly 可以讓一些 C/C++ 開發的函示庫,移動到網頁裡面運作。sql.js 就是用這種方式,讓網頁可以直接使用 sqlite 資料庫。

使用sql.js要先初始化資料庫物件

引用 javascript

<script src='https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.min.js'></script>

接下來有兩種方式初始化資料庫

方法 1: fetch

    async function initdb() {
        let config = {
            locateFile: filename => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.wasm`
        };
        const sqlPromise = initSqlJs(config);
        const dataPromise = fetch("csv/townsnote.db").then(res => res.arrayBuffer());
        const [SQL, buf] = await Promise.all([sqlPromise, dataPromise])
        const sqlitedb = new SQL.Database(new Uint8Array(buf));
        window.sqlitedb = sqlitedb;
    };

    initdb();

方法 2: XMLHttpRequest

    let config = {
        locateFile: filename => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.wasm`
    };
    initSqlJs(config).then(function(SQL){
        const xhr = new XMLHttpRequest();

        xhr.open('GET', 'csv/townsnote.db', true);
        xhr.responseType = 'arraybuffer';

        xhr.onload = e => {
            const uInt8Array = new Uint8Array(xhr.response);
            const db = new SQL.Database(uInt8Array);

            window.sqlitedb = db;

            // const contents = db.exec("SELECT * FROM towns");
            // contents is now [{columns:['col1','col2',...], values:[[first row], [second row], ...]}]
            // console.log("contents=",contents);
        };
        xhr.send();
    });

初始化資料庫後,就可以直接使用資料庫,執行 SQL 查詢指令

let contents = window.sqlitedb.exec("SELECT * FROM towns where id="+id);

以下是載入 sqlite db,執行一個 SQL 查詢的範例

<!DOCTYPE html>
<html lang="zh-tw">
<head>
    <meta charset="utf-8">
    <title>test</title>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.min.js'></script>
    <script>
    async function initdb() {
        let config = {
            locateFile: filename => `https://cdnjs.cloudflare.com/ajax/libs/sql.js/1.9.0/sql-wasm.wasm`
        };
        const sqlPromise = initSqlJs(config);
        const dataPromise = fetch("csv/townsnote.db").then(res => res.arrayBuffer());
        const [SQL, buf] = await Promise.all([sqlPromise, dataPromise])
        const sqlitedb = new SQL.Database(new Uint8Array(buf));
        window.sqlitedb = sqlitedb;
    };

    initdb();

    function get_town_by_id() {
        if(!window.sqlitedb) return;
        let id = document.getElementById('id').value;
        let contents = window.sqlitedb.exec("SELECT * FROM towns where id="+id);
        console.log("contents=", contents);

        var jsonArray = JSON.parse(JSON.stringify(contents))
        console.log("jsonArray=", jsonArray);
        document.getElementById('result').value = JSON.stringify(contents);
    };
    </script>

</head>
<body>
    <input type="text" value="9007010" id="id"></input>
    <button onclick="get_town_by_id();">query</button>
    <br/><br/>
    <textarea id="result" rows="20" cols="50"></textarea>
</body>
</html>

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

2022/02/21

ES6 語法 note

JavaScript ES6 的一些新的語法的筆記

var, let, const

  • var 的作用域為 function。
  • let 與 const 作用域為 block,也就是 大括號 { } 裡面
  • const 宣告的變數無法被 re-assigned

Arrow Function

// 傳統 function 寫法
var call = function(someone) {
  return someone + ' is called';
}

var plus = function(A, B) {
  return A + B;
}

// arrow function
var call = someone => someone + ' is called';

var plus = (A, B) => A + B;

arrow function 的 this 是外面的 this,跟原本 function 的 this 不同

展開符號 ...

展開運算子 (Spread Operator) / 其餘運算子 (Rest Operator)

合併數個陣列

let groupA = ['小明', '杰倫', '阿姨'];
let groupB = ['老媽', '老爸'];
let groupAll = [...groupA, ...groupB];
console.log(groupAll);
// ['小明', '杰倫', '阿姨','老媽', '老爸']

複製陣列(淺複製)

用展開符號來複製 A 陣列,則會是 call-by-value 的複製,修改 B 不會影響到 A。

如果直接 let groupB = groupA 會是 call-by-reference

let groupA = ['小明', '杰倫', '阿姨'];
let groupB = [...groupA] // 新陣列
groupB.push('阿明');
console.log(groupA); // 不會有阿明

類陣列

querySelectorAll() 取出來的 NodeList 會以類陣列的形式呈現所有被選取的節點,但它卻不是真正的陣列。透過展開符號可以複製類陣列中的內容,藉此將類陣列轉為真正的陣列。

let doms = document.querySelectorAll('li'); // doms 為類陣列
let newDoms = [...doms]; // newDoms 為陣列

函式內建的 arguments 物件也是類陣列,arguments 會包含所有傳入函式的參數。

其餘參數

如果不確定要傳入函式的參數數量,可以在函式的最後一個參數前面加上展開符號(該參數就會變成其餘參數),就能把多傳入的參數值以陣列的形式,容納在其餘參數中。注意,函式裡只能有一個其餘參數,而且一定要放在最右邊。

function moreMoney(name, ...money) {
  console.log(name, money);
  // "Grace" [100, 200, 300, 400, 500]
}
moreMoney('Grace', 100, 200, 300, 400, 500);

ES Module 與 import 、 export

可將每個 JS 檔案當作一個獨立模組,A.js 匯出 export,在 B.js 匯入 import

// A.js
export const aString = "A String";

export function aFunction() {
    console.log("aFunction");
}

export const aObject = {a:1};


// B.js
import {aString, aFunction, aObject} from './A.js';

console.log(aString);
console.log(aObject);
aFunction();


// test.html
<!DOCTYPE html>
<html lang="en">

<head>
</head>

<body>
  <script type="module" src="./A.js"></script>
  <script type="module" src="./B.js"></script>

</body>
</html>

String Template

以往要在

  • 裡面加上圖片

    // HTML
    <ul class="list">
      <li>
        <img src="logo.png">
      </li>
    </ul>
    // JS
    // 用 const 選取 DOM
    const list = document.querySelector('.list')
    // 用 const 儲存要放入的圖片路徑
    const imgURL = 'logo.png';
    
    // 舊的寫法,要用字串連接
    list.innerHTML = '<li><img src='+ imgURL +'></li>'
    
    // 新的寫法,可用 `` 裡面放入 ${變數}
    list.innetHTML = `<li><img src=" ${imgURL} "></li>`

    可搭配陣列輸出

    • // 陣列
      const people = [
        {
          name: '小明',
          friends: 2
        },
        {
          name: '阿姨',
          friends: 999
        },
        {
          name: '杰倫',
          friends: 0
        }
      ]
      
      let newUl = `
        <ul> ${people.map(person =>
          `<li>我叫做 ${person.name}</li>`)
          .join('')}
        </ul>`

      Destructing Assignment

      將陣列或物件的資料直接拆開對應賦值

      const user = {
          id: 42,
          name: 'juice',
          fullname: {
              first: 'orange',
              last: 'juice'
          }
      };
      
      
      const {id, name, fullname} =  user;
      
      console.log(id);
      console.log(name);
      console.log(fullname);
      const num = [1,2,3,4,5];
      
      const [x,y] = num;
      
      //1
      console.log(x);
      //2
      console.log(y);

      常用陣列function

      forEach()

      ​ 對每一個元素運算一次 fuction

      map()

      ​ 運算後,回傳新的陣列

      reduce()

      find()

      ​ 只回傳第一個符合條件的元素

      filter()

      ​ 回傳符合條件的元素的新陣列

      every()

      ​ 所有元素都符合條件才回傳 true

      some()

      ​ 有部分元素符合條件就回傳 true

      Promise

      為解決非同步的問題,提供 Promise 物件

      成功時要呼叫 resolve,失敗時呼叫 reject。

      用 then 綁定 fullfilled 或 rejected 時,分別要執行的 function

      // 宣告 promise 建構式
      let newPromise = new Promise((resolve, reject) => {
        // 傳入 resolve 與 reject,表示資料成功與失敗
        let ran = parseInt(Math.random() * 2); // 隨機成功或失敗
        console.log('Promise 開始')
        if (ran) {
          setTimeout(function(){
            // 3 秒時間後,透過 resolve 來表示完成
            resolve('3 秒時間(fulfilled)');
          }, 3000);
        } else {
          // 回傳失敗
          reject('失敗中的失敗(rejected)')
        }
      
      });
      
      newPromise.then((data)=> {
        // 成功訊息 (需要 3 秒)
        console.log(data);
      }).catch((err)=> {
        // 失敗訊息 (立即)
        console.log(err)
      });

      async, await

      let runPromise = (someone, timer, success = true) => {
        console.log(`${someone} 開跑`);
        return new Promise((resolve, reject) => {
          // 傳入 resolve 與 reject,表示資料成功與失敗
          if (success) {
            setTimeout(function () {
              // 3 秒時間後,透過 resolve 來表示完成
              resolve(`${someone} 跑 ${timer / 1000} 秒時間(fulfilled)`);
            }, timer);
          } else {
            // 回傳失敗
            reject(`${someone} 跌倒失敗(rejected)`)
          }
        });
      }
      
      // 此段函式並不會影響其它函示的執行
      runPromise('小明', 3000).then(someone => {
        console.log('小明', someone)
      });
      // 以下這段 console 會在 promise 結束前就執行
      console.log('這裡執行了一段 console');

      await 會等待 promise 的處理結果

      // 此段函示會中斷其它函式的運行
      let mingRun = await runPromise('小明', 2000)
      console.log('跑完了:', mingRun);
      let auntieRun = await runPromise('漂亮阿姨', 2500);
      console.log('跑完了:', auntieRun);

      async 類似 promise,但會將 await 包裝在裡面

      const asyncRun = async () => {
        let mingRun = await runPromise('小明', 2000);
        let auntieRun = await runPromise('漂亮阿姨', 2500);
        return `${mingRun}, ${auntieRun}`
      }
      asyncRun().then(string => {
        console.log(string)
      }).catch(response => {
        console.log(string)
      })

      References

      [JS] JavaScript ES6 常用語法筆記

      JavaScript Await 與 Async