iT邦幫忙

0
鐵人賽 神助攻 HERE Technologies

快速建構地圖服務(四) - 當 Leaflet JS 遇見 Data Hub

WW 2020-09-11 14:50:427730 瀏覽
  • 分享至 

  • xImage
  •  

快速建構地圖服務(四) - 當 Leaflet JS 遇見 Data Hub

事前準備

首先,請登入 HERE Data Hub Console,建立一個新的 Token 並且把「readFeatures」的權限開給剛剛上傳的斷層分佈圖、順向坡分佈圖與土壤液化分佈圖。

HERE Data Hub Console:https://xyz.api.here.com/token-ui/

另外,也請記得在「DESCRIPTION」裡面留下註解,這樣以後才知道這個 Token 對應的是什麼用途,以方便管理。按下「Submit」來建立新 Token。

請保存好我們剛剛建立的新 Token 以及這三個 Space 的 ID,之後會用到。以我自己的為例:

  • Token: AMveoMAsRiKUc32sreLzIgA
  • 土壤液化潛勢圖資群組: YZ9MdErx
  • 活動斷層分佈圖: 19msLC2t
  • 順向坡分佈圖: q1gtCeWZ

接著就先使用 Leaflet JS 來建立一個空白地圖。

Leaflet JS 初體驗

Leaflet JS 官方網站:https://leafletjs.com/

Leaflet JS 是一個開放原始碼的地圖框架,使用 JavaScript 語言,使用起來非常簡便,也支援行動裝置的手勢操作,雖然它本身的功能比較陽春一點,但是社群很活躍,也有很多功力強大的網友寫出了各式外掛讓大家使用。

如果您有興趣,可以在官方網站找到教學、說明文件與外掛的資訊。

我們就先來寫一個空白的地圖網頁。請先用您慣用的文字編輯器,例如 Notepad++,或 Visual Studio Code,甚至記事本也可以,開啟一個空白文件,然後存檔成 . html 的網頁檔。

首先在 head 裡面,加入 Leaflet JS 需要的 CSS 與 JS 檔。

<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
    <!-- Load Leaflet from CDN -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
        integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
        crossorigin="" />
    <!-- Make sure you put this AFTER Leaflet's CSS -->
    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
        integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
        crossorigin=""></script>

    <style>
        #map {
            height: 100%;
        }
    </style>
</head>

接著在 body 裡面定義一個 div,id 是 map。

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

接著在 script 裡面,先定義一個地圖物件,center 是 23.773, 120.959,zoom 為 8,這樣剛好會把台灣放在地圖中心。

var map = L.map(
    'map', {
        center: [23.773, 120.959],
        zoom: 8,
    });

最後在地圖的左下角加上比例尺。

L.control.scale({
    position: 'bottomleft'
}).addTo(map);

完成的空白地圖會長的像這樣:

完整的原始碼是:

<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
    <!-- Load Leaflet from CDN -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
        integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
        crossorigin="" />
    <!-- Make sure you put this AFTER Leaflet's CSS -->
    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
        integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
        crossorigin=""></script>

    <style>
        #map {
            height: 100%;
        }
    </style>
</head>

<body>
    <div id="map"></div>
</body>
<script>
    var map = L.map(
        'map', {
            center: [23.773, 120.959],
            zoom: 8,
        });

    L.control.scale({
        position: 'bottomleft'
    }).addTo(map);
</script>

等等,可是地圖上沒有東西啊!這跟我們想像中的地圖有點不同?

認識更多 HERE 地圖 API

HERE Technologies 除了 Data Hub 之外,提供了非常多樣化的地圖 API 與 SDK,從簡單的網路地圖顯示、衛星影像、地址查詢、地點查詢、路徑規劃之外,也提供了更進階的物流用途,例如卡車路徑規劃、物流路徑計算、派車系統後台、地理圍籬、路徑點排序、GPS 軌跡分析、進階地圖屬性、通行費計算等等,這些服務通通都包裝成簡單易用的網路 API 來讓開發者使用。除了網路 API 之外也提供了 Android 跟 iOS 的地圖 SDK。

時至今日,HERE 在網路上發布的地圖服務相關產品,已經發展成一個大家族,滿足個人開發者、中小企業、大型企業、政府機關等不同的使用場景。

HERE 開發者網站:https://developer.here.com

例如以常見的物流送貨場景來說,我們可以結合 HERE 的網路 API 來搭建出自己的地圖服務,例如我們可以把一串地址轉換成經緯度,把這些經緯度用路徑點排序的 API 排出最佳順序,接著再算出每一個路徑點之間的詳細路徑,排好順序之後可以由控制中心派發地點給司機,並結合 HERE SDK 進行語音導航。

我們要做出一個住宅安全地圖,可能會需要以下功能:

  1. 底圖顯示,我們會需要一般地圖與衛星影像、地形圖的切換。
  2. 相關圖資顯示(在上一個課程已經做完了 Data Hub 的資料部署)
  3. 地址查詢功能,輸入地址之後回傳經緯度,並且查詢經緯度是否落在風險區域中。

以下我們就一步一步來實做。

HERE Freemium 免費帳號

在先前您申請 HERE 帳號的時候,並沒有要求您輸入信用卡,而您目前擁有的帳號是「Freemium」,「Freemium」如果沒有超過每個月的限量,是不需要付費的。相關的限制主要是:

  • 每個月 25 萬次的 API 傳輸
    • 例如轉換 1 筆地址就等於 1 次傳輸
    • 例如計算 1 次路線,就等於 1 次傳輸
    • 少部份的 API,則可能會有不同的計算方式
  • 每個月 5 千個 SDK 使用者
  • Studio 與 Data Hub 每個月 2.5GB 的傳輸量
  • Studio 與 Data Hub 每個月 5GB 的資料存儲量
  • 無法聯絡 HERE 客戶支援團隊取得服務。

因為計算的基準是「每個月」因此,在每個月第一天就會歸零開始重新計算,您如果精算過每個月的使用量不會超過的話,就可以持續的使用免費的服務,但是一旦超過的話帳號就會被停用,屆時可能要請您思考費用的問題了。

HERE 計價方式:https://developer.here.com/pricing

如果是 Pro 帳號,每個月費用是 449 歐元,而傳輸量提高到每個月 100 萬次,除了可以使用 Data Hub 的付費加值功能之外,也可以得到 HERE 客戶支援團隊的服務。但以個人開發者來說,一個月 25 萬次的限額,一天相當於超過 8000 次,以個人非商業用途來說已經相當夠用了。如果超過的話,或許代表您的服務相當的受歡迎,您也可以思考如何用這樣的服務來營利了。

產生您的 HERE API KEY

首先請您登入 https://developer.here.com/login

在這個畫面您會看到,您目前有一個專案,名稱是「Freemium 202x-xx-xx」,請點選開啟它。

點進去之後,會看到一個頁面,裡面有一些項目,包含專案資訊、每月使用量,以及 JS API/REST API/SDK 的開發金鑰管理。

您也會發現,您已經有一個 REST API 的 APP ID 了,因為您已經在 HERE Datahub 已經有資料,而那時候 HERE 後台已經幫您新開了一個 APP ID 了。

接著我們要進行一個動作,就是建立一個 API KEY,這個 API KEY 是一個獨一無二的金鑰,要先取得 API KEY 才能夠使用 HERE 的眾多 API,例如地址查詢、路徑計算等等。

請按下 REST 下方的「Create API key」,接著下方就會出現一串隱藏的字元,這就是 API KEY,我們可以按下眼睛圖示來顯示 API KEY,也可以按下「COPY」來複製到剪貼簿。

每個 APP ID 可以有兩個 API KEY,不需要的 API KEY 也可以刪除或停用。

認識 HERE Map Tile API

HERE Map Tile API 的功能,就是依照使用者需求的 X/Y/Z 座標位置回傳對應的圖片,可以說是所有網路地圖服務的基本元素。

HERE Map Tile API:https://developer.here.com/documentation/map-tile/

HERE Map Tile API 提供很多種風格的地圖,例如我們可以用這個方式取得一張「Normal Day」風格的地圖:

https://{SUB_DOMAIN}.{BASE_TYPE}.maps.ls.hereapi.com/maptile/2.1/{TILE_TYPE}/newest/{SCHEME}/{Z}/{X}/{Y}/{SIZE}/{FORMAT}?apiKey={API_KEY}

  • SUB_DOMAIN
    • 範圍是 1-4,在送出請求的時候隨機分散到 1-4 可以分散流量,縮短讀取時間。
  • BASE_TYPE
    • aerial:衛星影像圖
    • base:基本圖
    • traffic:即時路況圖
  • TILE_TYPE
    • alabeltile:行政區標籤
    • basetile:基本地圖(有街道、無標籤)
    • blinetile:基本地圖(無標籤、無建築)
    • labeltile:標籤地圖
    • linetile:道路線圖
    • mapnopttile:無大眾運輸地圖
    • maptile:一般地圖
    • streettile:道路線 + 標籤圖
    • trucknopttile:無大眾運輸卡車地圖
    • truckonlytile:卡車道路線圖
    • trucktile:卡車地圖
    • xbasetile:基本地圖(無街道、無標籤)
  • SCHEME(僅列出常用)
    • normal.day:一般日間
    • normal.day.grey:一般日間灰色調
    • normal.day.transit:一般日間大眾運輸
    • normal.night:一般夜間
    • normal.night.grey:一般夜間灰色調
    • pedestrian.day:行人日間
    • pedestrian.night:行人夜間
    • reduced.day:減色調日間
    • terrain.day:地形日間
    • satellite.day:衛星影像日間
    • hybrid.day:混合地圖日間
  • Z:Z 階層
  • X:X 座標
  • Y:Y 座標
  • SIZE
    • 256px
    • 512px
  • FORMAT
    • PNG
    • PNG8
    • JPG
  • API_KEY
    • 您的 API KEY

例如,我們可以呼叫一張衛星影像,以之前介紹 Data Hub API 所用的 X = 856、Y = 440、Z = 10 的區塊,大概是雪霸國家公園的位置:

https://1.aerial.maps.ls.hereapi.com/maptile/2.1/maptile/newest/satellite.day/10/856/440/256/png8?apiKey={API_KEY}

除了呼叫一張地圖之外,我們也有很多參數可以運用,例如常用的有:

  • lg/lg2:語言/次要語言
    • ara – Arabic
    • baq – Basque
    • cat – Catalan
    • chi – Chinese (simplified)
    • cht – Chinese (traditional)
    • cze – Czech
    • dan – Danish
    • dut – Dutch
    • eng – English
    • fin – Finnish
    • fre – French
    • ger – German
    • gle – Gaelic
    • gre – Greek
    • heb – Hebrew
    • hin – Hindi
    • ind – Indonesian
    • ita – Italian
    • mul – Multiple Languages
    • nor – Norwegian
    • per – Persian
    • pol – Polish
    • por – Portuguese
    • rus – Russian
    • sin – Sinhalese
    • spa – Spanish
    • swe – Swedish
    • tha – Thai
    • tur – Turkish
    • ukr – Ukrainian
    • urd – Urdu
    • vie – Vietnamese
    • wel – Welsh
  • ppi
    • 72:一般螢幕解析度
    • 250:行動裝置螢幕解析度
    • 320:更高解析度
    • 500:超高解析度
  • pois
    • 顯示興趣點與否(如觀光景點、政府機關等)

例如我們可以呼叫一張顯示正體中文的混合地圖,並顯示興趣點,支援更高解析度的 512px 大小的 png8 圖磚:

https://3.aerial.maps.ls.hereapi.com/maptile/2.1/maptile/newest/hybrid.day/16/54889/28057/512/png8?lg=cht&ppi=320&pois&apiKey={API_KEY}

在 Leaflet JS 加入底圖

一般而言我們很少手動使用 Map Tile API,而是透過其他的手段把地圖的圖磚拼起來顯示一個完整的地圖,例如我們可以在 Leaflet JS 中這麼做,把 HERE Map Tile 加入地圖中。請在剛剛編輯的網路地圖原始碼中加入這一段:

var hereApiKey = '您的 API KEY';
var hereNormal = L.tileLayer('https://{s}.base.maps.ls.hereapi.com/maptile/2.1/maptile/newest/normal.day/{z}/{x}/{y}/256/png8?lg=cht&ppi=72&pois&apiKey=' +
    hereApiKey, {
        attribution: '© 2020 HERE',
        subdomains: [1, 2, 3, 4]
    }).addTo(map); // Leaflet JS 預設使用 256px 大小的圖磚

重新開啟之後,就會發現台灣已經出現在地圖中央了。但我們可能會需要加入多種風格的底圖,例如我們需要一般地圖、地形圖與混合地圖,這樣就有三種,如果全部疊上去的話,最後只會顯示最上面的那一層,因此我們需要加入圖層控制的元件來切換不同種類的地圖。

剛剛我們已經把一般地圖的風格加入了,現在我們再加入混合地圖與地形圖。

var hereHybrid = L.tileLayer(
    'https://{s}.aerial.maps.ls.hereapi.com/maptile/2.1/maptile/newest/hybrid.day/{z}/{x}/{y}/256/png8?lg=cht&ppi=72&pois&apiKey=' +
    hereApiKey, {
        attribution: '© 2020 HERE',
        subdomains: [1, 2, 3, 4]
    }); // 衛星影像混合地圖

var hereTerrain = L.tileLayer(
    'https://{s}.aerial.maps.ls.hereapi.com/maptile/2.1/maptile/newest/terrain.day/{z}/{x}/{y}/256/png8?lg=cht&ppi=72&pois&apiKey=' +
    hereApiKey, {
        attribution: '© 2020 HERE',
        subdomains: [1, 2, 3, 4]
    }); // 地形圖

接著我們定義兩個 dictionary,分別是 baseLayers 與 overlays,baseLayers 中寫入剛剛加入的三個 tileLayer;overlays 則先留空。

var baseLayers = {
    'HERE 標準地圖': hereNormal,
    'HERE 衛星影像': hereHybrid,
    'HERE 地形圖': hereTerrain
};

var overlays = {

};

接著我們把圖層控制的功能(L.control.layers)加入地圖中:

L.control.layers(baseLayers, overlays, {collapsed: false}).addTo(map);

完成後,把滑鼠移到地圖右上角的圖層控制元件,就會出現我們建立好的三個圖層,可以自由切換。

Leaflet JS 加入 Data Hub 圖層

底圖建立好之後,我們就準備來加入 Data Hub 上面的內容,我們這邊要運用的是 Data Hub API。

這邊您或許也會好奇,為什麼 HERE 本身就提供 JavaScript API,功能甚至比 Leaflet JS 更強大,但為什麼我會選擇 Leaflet JS 而非 HERE 提供的 JavaScript API?其實主要的原因就是 Leaflet JS 提供的 UI 元件比較方便,做起來比較快,而且 Leaflet JS 原生對於 GeoJSON 物件的支援,也讓程式碼比較簡潔一點。

當然,如果您對於 HERE JavaScript API 提供的功能有興趣,也可以到這邊了解:https://developer.here.com/documentation/maps/

還記得之前提過的,我們要使用 Data Hub API,必備的參數有兩個:「Token」與「Space ID」。而把 Data Hub 的內容不透過搜尋的方式直接加到地圖上,有三個 API 可以操作。

  1. 使用 https://xyz.api.here.com/hub/spaces/{spaceId}/iterate 來直接把 Space 全部的內容下載下來,並加入地圖中。
    • 優點:只要呼叫一次,使用最簡單。
    • 缺點:如果資料量龐大,會耗費較長時間下載,影響使用者體驗。
  2. 使用 https://xyz.api.here.com/hub/spaces/{spaceId}/bbox ,但必須先定義東、南、西、北的邊界,並下載邊界內的 Space 內容,加入地圖中。
    • 優點:如果資料量極為龐大時,可以用這個方式下載一定範圍內資料就好。
    • 缺點:可能每一次移動地圖都要呼叫一次,影響使用者體驗。
  3. 使用 https://xyz.api.here.com/hub/spaces/{spaceId}/tile/{type}/{tileId} ,以圖磚的方式逐磚下載地圖。
    • 優點:使用圖磚的方式批次的下載小量資料,使用者體驗較為流暢。
    • 缺點:操作方式比較複雜。

因此,我們可以用資料量的大小來判斷用什麼方式把 Data Hub 的內容下載下來。

因此接著我們在實做的時候會採用以下方式:

  1. 活動斷層分佈圖,因為只有 66 個 Feature(圖徵),我們使用 https://xyz.api.here.com/hub/spaces/{spaceId}/iterate 。
  2. 土壤液化與順向坡分佈圖,Feature(圖徵)數量多,我們使用 https://xyz.api.here.com/hub/spaces/{spaceId}/tile/{type}/{tileId} 。

但是,在這之前,因為要把資料從 Data Hub API 下載下來,常用的方式有兩種,一種是自己寫 Ajax 的函數,一種用 jQuery,為了求方便我們這邊使用 jQuery 的方式實做。

jQuery 官方網站:https://jquery.com/

什麼是 jQuery?jQuery 是一套 JavaScript 函式庫,提供一系列方便的 API,們這邊要用的是他的 Ajax 部份。

首先把以下這段程式碼加入 head 之中,這樣網頁一開啟之後會從網路上面把 jQuery 下載下來。

<script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>

接著,我們先在地圖中加上三個 FeatureGroup,這麼做的原因是因為我們不只是要把這些 Feature(圖徵)放在地圖上,而還要控制它們的開關,用 FeatureGroup 的方式把這些圖徵放在同一個群組裡面,才能夠在 L.control.layers 用圖層的方式顯示出來。如果不這麼做,變成要控制每一個 Feature 的開關,非常不方便,實際上也不可行。

所以我們把待會要加上去的三個圖層,都先建立好 FeatureGroup。

var faultFeatureGroup = L.featureGroup(); //活動斷層圖層
var landLiquefactionFeatureGroup = L.featureGroup(); //土壤液化圖層
var dipSlopeFeatureGroup = L.featureGroup(); //順向坡圖層

接著準備好您具有讀取權限的 Token。

var dataHubReadToken = '您的 Data Hub Token';

然後我們用 jQuery 的 getJSON API 把活動斷層圖從 Data Hub 下載下來。

var faultLayer = $.getJSON(
    'https://xyz.api.here.com/hub/spaces/' + faultSpaceId + '/iterate?access_token=' + dataHubReadToken,
    value => {
        value.features.forEach(element => {
            L.geoJSON(element, {
                style: {
                    color: '#0032ff',
                    opacity: 0.8,
                    weight: 8,
                    fill: false
                }
            }).bindPopup(element.properties.Name).addTo(faultFeatureGroup);
        });
    });

以上這段程式碼,value 就是 Data Hub 回傳的結果,種類為 FeatureCollection 會直接被轉成 JSON 物件。接著我們要把 FeatureColleciton 裡面的每一個 Feature 都讀過一遍,轉成 L.geoJSON 物件,並且設定每個物件的 style,並且綁定一個 L.popup 物件來顯示名稱(element.properties.Name)。最後再把這些 L.geoJSON 物件加到 faultFeatureGroup 中。

然後我們把這個圖層加到 L.control.layers。

var overlays = {
    '活動斷層': faultFeatureGroup
};

如果一切順利,活動斷層會出現在選單中,可以自由開關。

而點在斷層線上面,也會出現名稱。

讀取 Data Hub 圖磚

使用 Leaflet KS如果您想要用圖磚的方式來讀取 Data Hub 的內容,會遇到一個問題:我要怎麼知道讀取哪一個圖磚?我們可以用兩種方式:

  1. 比較傳統的方式,用地圖畫面的四個邊加上 Z 階層來算出畫面中所有的圖磚 X 與 Y 座標,再逐一讀取它們。
  2. 用外掛。因為 Leaflet JS 原生的 L.tileLayer 只支援點陣圖(png, jpg),因此有強者開發出了外掛讓 L.tileLayer 也可以讀取 GeoJSON,並排列在地圖上:https://github.com/glenrobertson/leaflet-tilelayer-geojson/

第二個方式就交給您自行研究了。我們介紹介紹第一個方式,因為您可以學到如何捕捉地圖事件以及更了解圖磚的概念,這對於您之後想繼續深入會很有幫助的。

我們可以在每個畫面平移或縮放結束後,來計算一次圖面上目前覆蓋的所有圖磚編號,使用以下這個函數:

function getTileXYZ(bounds, zoom) {
    var min = map.project(bounds.getNorthWest(), zoom).divideBy(256).floor(),
        max = map.project(bounds.getSouthEast(), zoom).divideBy(256).floor(),
        coordsList = [];
    for (var i = min.x; i <= max.x; i++) {
        for (var j = min.y; j <= max.y; j++) {
            const coords = new L.Point(i, j);
            coords.z = zoom;
            console.log(coords); // 在 console 輸出結果
        }
    }
}

簡單來說,這個函數會需要兩個參數,第一個是 L.latLngBounds 物件,代表地圖畫面的四個邊界,可以透過 L.map.getBounds() 來取得;第二個則是 zoom 值,代表地圖目前的 Z 階層,用 L.map.getZoom() 來取得。

而呼叫的時機有兩個:

  1. 第一次的地圖讀取後,我們可以用「load」事件。但是要使用 load 事件,我們必須要改一下地圖載入的步驟。
  2. 每次地圖畫面移動結束後,我們可以用「moveend」事件。

首先把我們之前定義的:

var map = L.map(
    'map', {
        center: [23.773, 120.959],
        zoom: 8,
    });

改成:

var map = L.map('map'); // 建立 L.map 物件。

map.on('load', function () {
    getTileXYZ(map.getBounds(), map.getZoom());
}); // 先註冊一個「load」事件,等待接收第一次的地圖讀取

map.setView([23.773, 120.959], 8); // 設定地圖位置與 Z 階層,並讀取地圖

接著開啟 console,您就會看到輸出了一串圖磚的 X/Y/Z 座標資訊。

接著再把「moveend」事件註冊完就可以了。

map.on('moveend', function () {
    getTileXYZ(map.getBounds(), map.getZoom());
});

這樣地圖在第一次讀取的時候,以及每一次移動完都會計算一次圖面上面的圖磚座標。

既然已經可以捕捉到第一次讀取與每次移動的事件了,那我們就可以利用這兩個事件來讀取 Data Hub 上面的內容。在 Data Hub API 中,我們可以透過https://xyz.api.here.com/hub/spaces/{spaceId}/tile/{type}/{tileId 這個 API,而最重要的三個參數:

  1. spaceId:您的 Space ID。
  2. type:請填入「web」。
  3. tileId:請填入「z_x_y」,請把 z/x/y 都換成「getGeoJSONTiles」函數計算出來的 tile 座標值變數。

以下我們就來實做。請把剛剛的「getTileXYZ」函數改成「getGeoJSONTiles」,並修改內容:

function getGeoJSONTiles(bounds, zoom, spaceId, accessToken, featureGroup) {
    featureGroup.clearLayers();
    // 下載前先把 featureGroup 清空,以免重複的內容不斷疊加上去
    var min = map.project(bounds.getNorthWest(), zoom).divideBy(256).floor(),
        max = map.project(bounds.getSouthEast(), zoom).divideBy(256).floor();
    for (var i = min.x; i <= max.x; i++) {
        for (var j = min.y; j <= max.y; j++) {
            const coords = new L.Point(i, j);
            var x = coords.x,
                y = coords.y,
                z = zoom;
            $.getJSON('https://xyz.api.here.com/hub/spaces/' + spaceId + '/tile/web/' + z + '_' + x + '_' + y +
                '?access_token=' + accessToken, value => {
                    value.features.forEach(element => {
                        L.geoJSON(element).addTo(featureGroup);
                    });
                })
        }
    }
}

原本的「getTileXYZ」函數只接受 bounds 與 zoom,但為了同時對 Data Hub API 進行呼叫,我們在「getGeoJSONTiles」加上了三個參數,分別是 spaceId、accessToken 與輸出的featureGroup,這樣我們用同一段程式碼就可以處理多個 Space。

以上這段程式碼,會在計算出每一個圖磚的 X/Y/Z 座標之後,馬上就送出一個 Ajax 請求,並且把結果轉成 JSON 物件,並且回傳。

然後我們就可以把土壤液化的 Space ID 加進程式碼:

var landLiquefactionSpaceId = '{SPACE_ID}';

然後對「getGeoJSONTiles」進行呼叫。請修改「load」與「moveend」事件中的內容:

map.on('load', function () {
    getGeoJSONTiles(map.getBounds(), map.getZoom(), landLiquefactionSpaceId,
        dataHubReadToken, landLiquefactionFeatureGroup);
});

map.on('moveend', function () {
    getGeoJSONTiles(map.getBounds(), map.getZoom(), landLiquefactionSpaceId,
        dataHubReadToken, landLiquefactionFeatureGroup);
});

接著把「landLiquefactionFeatureGroup」加進圖層選單中。

var overlays = {
    '活動斷層': faultFeatureGroup,
    '土壤液化': landLiquefactionFeatureGroup
};

完成後測試,可以看到土壤液化的內容已經可以顯示在地圖上,而且每次移動地圖都會重新下載一份新的。

但是預設的樣式並不是我們想要的,因此我們要來定義樣式。請繼續改寫「getGeoJSONTiles」函數:

function getGeoJSONTiles(bounds, zoom, spaceId, accessToken, featureGroup) {
    featureGroup.clearLayers();
    var min = map.project(bounds.getNorthWest(), zoom).divideBy(256).floor(),
        max = map.project(bounds.getSouthEast(), zoom).divideBy(256).floor();
    for (var i = min.x; i <= max.x; i++) {
        for (var j = min.y; j <= max.y; j++) {
            const coords = new L.Point(i, j);
            var x = coords.x,
                y = coords.y,
                z = zoom;
            $.getJSON('https://xyz.api.here.com/hub/spaces/' + spaceId + '/tile/web/' + z + '_' + x + '_' + y +
                '?access_token=' + accessToken, value => {
                    value.features.forEach(element => {
                        var geoJSONObject = L.geoJSON(element);
                        if (element.properties['@ns:com:here:xyz'].space ==
                            landLiquefactionSpaceId) {
                            // 使用 properties 裡面的 '@ns:com:here:xyz' 裡面的 space 屬性來比對是否是我們要的土壤液化圖層
                            // 如果是的話就進行以下的動作,使用「分級」這個屬性來填入不同顏色
                            switch (element.properties.分級) {
                                case '低潛勢':
                                    geoJSONObject.setStyle({
                                        color: '#26ff00', // 綠色
                                        weight: 0
                                    });
                                    break;
                                case '中潛勢':
                                geoJSONObject.setStyle({
                                        color: '#ff9a03', // 橙色
                                        weight: 0
                                    });
                                    break;
                                case '高潛勢':
                                geoJSONObject.setStyle({
                                        color: '#dd00ff', // 紫色
                                        weight: 0
                                    });
                                    break;
                            }
                            geoJSONObject.bindPopup('土壤液化等級:' + element.properties.分級);
                            geoJSONObject.addTo(featureGroup);
                        }
                    });
                })
        }
    }
}

在這段程式碼裡面,我們從 Data Hub 上面下載了資料之後,多了一道比對的手續,比對這筆資料是不是我們要的土壤液化圖層,如果是的話,就依照土壤液化分級來填入不同顏色。

目前看似是完成了,但是我們在對地圖進行縮放的時候,會發現有些地方怪怪的。例如雲林嘉義的這塊區域,高潛勢的紫色區塊竟然出現了深淺不一的狀況!原因是因為使用圖磚方式下載的時候,如果一個 feature 跨過不同的圖磚,會被重複下載並繪製在地圖上,因此我們必須再加上一個參數「clip=true」來排除這個狀況。

請把

$.getJSON('https://xyz.api.here.com/hub/spaces/' + spaceId + '/tile/web/' + z + '_' + x + '_' + y + '?access_token=' + accessToken

改成

$.getJSON('https://xyz.api.here.com/hub/spaces/' + spaceId + '/tile/web/' + z + '_' + x + '_' + y + '?clip=true&access_token=' + accessToken

這樣就沒有出現深淺不一的狀況了,土壤液化圖層的新增到此算是完成了。

您可以試著用同樣的方法把順向坡的圖層加上去,大致上地圖部份就完成了!

全部的原始碼如下:

<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
    <!-- Load Leaflet from CDN -->
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
        integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
        crossorigin="" />
    <!-- Make sure you put this AFTER Leaflet's CSS -->
    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
        integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
        crossorigin=""></script>
    <script src="https://code.jquery.com/jquery-3.5.1.min.js"
        integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>

    <style>
        #map {
            height: 100%;
        }
    </style>
</head>

<body>
    <div id="map"></div>
</body>
<script>
    var hereApiKey = ''; // 您的 HERE APIKEY
    var dataHubReadToken = ''; // 您的 Data Hub Token
    var faultSpaceId = ''; // 活動斷層 Space ID
    var landLiquefactionSpaceId = ''; // 土壤液化 Space ID
    var dipSlopeSpaceId = ''; // 順向坡 Space ID

    var faultFeatureGroup = L.featureGroup(); //活動斷層圖層
    var landLiquefactionFeatureGroup = L.featureGroup(); //土壤液化圖層
    var dipSlopeFeatureGroup = L.featureGroup(); //順向坡圖層

    var map = L.map('map'); // 建立 L.map 物件。

    map.on('load', function () {
        getGeoJSONTiles(map.getBounds(), map.getZoom(), landLiquefactionSpaceId,
            dataHubReadToken, landLiquefactionFeatureGroup);
        getGeoJSONTiles(map.getBounds(), map.getZoom(), dipSlopeSpaceId,
            dataHubReadToken, dipSlopeFeatureGroup);
        faultFeatureGroup.addTo(map);
        landLiquefactionFeatureGroup.addTo(map);
        dipSlopeFeatureGroup.addTo(map);
    }); // 註冊 load 事件來監聽地圖第一次讀取完成

    map.on('moveend', function () {
        getGeoJSONTiles(map.getBounds(), map.getZoom(), landLiquefactionSpaceId,
            dataHubReadToken, landLiquefactionFeatureGroup);
        getGeoJSONTiles(map.getBounds(), map.getZoom(), dipSlopeSpaceId,
            dataHubReadToken, dipSlopeFeatureGroup);
    }); // 註冊 moveend 事件來監聽地圖每次移動結束

    map.setView([23.773, 120.959], 8); // 設定地圖位置與 Z 階層,並讀取地圖

    var hereNormal = L.tileLayer(
        'https://{s}.base.maps.ls.hereapi.com/maptile/2.1/maptile/newest/normal.day/{z}/{x}/{y}/256/png8?lg=cht&ppi=72&pois&apiKey=' +
        hereApiKey, {
            attribution: '© 2020 HERE',
            subdomains: [1, 2, 3, 4]
        }).addTo(map); // 一般地圖

    var hereHybrid = L.tileLayer(
        'https://{s}.aerial.maps.ls.hereapi.com/maptile/2.1/maptile/newest/hybrid.day/{z}/{x}/{y}/256/png8?lg=cht&ppi=72&pois&apiKey=' +
        hereApiKey, {
            attribution: '© 2020 HERE',
            subdomains: [1, 2, 3, 4]
        });

    var hereTerrain = L.tileLayer(
        'https://{s}.aerial.maps.ls.hereapi.com/maptile/2.1/maptile/newest/terrain.day/{z}/{x}/{y}/256/png8?lg=cht&ppi=72&pois&apiKey=' +
        hereApiKey, {
            attribution: '© 2020 HERE',
            subdomains: [1, 2, 3, 4]
        });

    var faultLayer = $.getJSON(
        'https://xyz.api.here.com/hub/spaces/' + faultSpaceId + '/iterate?access_token=' + dataHubReadToken,
        value => {
            value.features.forEach(element => {
                L.geoJSON(element, {
                    style: {
                        color: '#0032ff',
                        opacity: 0.8,
                        weight: 8,
                        fill: false
                    }
                }).bindPopup(element.properties.Name).addTo(faultFeatureGroup);
            });
        });

    function getGeoJSONTiles(bounds, zoom, spaceId, accessToken, featureGroup) {
        var loadedTileIds = [];
        featureGroup.clearLayers();
        var min = map.project(bounds.getNorthWest(), zoom).divideBy(256).floor(),
            max = map.project(bounds.getSouthEast(), zoom).divideBy(256).floor();
        for (var i = min.x; i <= max.x; i++) {
            for (var j = min.y; j <= max.y; j++) {
                const coords = new L.Point(i, j);
                var x = coords.x,
                    y = coords.y,
                    z = zoom;
                $.getJSON('https://xyz.api.here.com/hub/spaces/' + spaceId + '/tile/web/' + z + '_' + x + '_' + y +
                    '?clip=true&access_token=' + accessToken, value => {
                        value.features.forEach(element => {
                            if (!loadedTileIds.includes(element.id)) {
                                // 如果 id 並沒有在地圖上,就進行以下動作。
                                var geoJSONObject = L.geoJSON(element);
                                if (element.properties['@ns:com:here:xyz'].space ==
                                    landLiquefactionSpaceId) {
                                    // 使用 properties 裡面的 '@ns:com:here:xyz' 裡面的 space 屬性來比對是否是我們要的土壤液化圖層
                                    // 如果是的話就進行以下的動作,使用「分級」這個屬性來填入不同顏色
                                    switch (element.properties.分級) {
                                        case '低潛勢':
                                            geoJSONObject.setStyle({
                                                color: '#26ff00', // 綠色
                                                weight: 0
                                            });
                                            break;
                                        case '中潛勢':
                                            geoJSONObject.setStyle({
                                                color: '#ff9a03', // 橙色
                                                weight: 0
                                            });
                                            break;
                                        case '高潛勢':
                                            geoJSONObject.setStyle({
                                                color: '#dd00ff', // 紫色
                                                weight: 0
                                            });
                                            break;
                                    }
                                    geoJSONObject.bindPopup('土壤液化等級:' + element.properties.分級);
                                    geoJSONObject.addTo(featureGroup);
                                } else if (element.properties['@ns:com:here:xyz'].space ==
                                    dipSlopeSpaceId) {
                                    // 使用 properties 裡面的 '@ns:com:here:xyz' 裡面的 space 屬性來比對是否是我們要的順向坡圖層
                                    // 如果是的話就進行以下的動作,使用「分級」這個屬性來填入不同顏色
                                    geoJSONObject.setStyle({
                                        color: '#ff0051', // 紅色
                                        opacity: 0.3,
                                        weight: 1
                                    });
                                    geoJSONObject.bindPopup('順向坡坡度:' + element.properties.SLOPE_ANG);
                                    geoJSONObject.addTo(featureGroup);
                                }
                                loadedTileIds.push(element.id);
                            }
                        });
                    })
            }
        }
    }

    var baseLayers = {
        'HERE 標準地圖': hereNormal,
        'HERE 衛星影像': hereHybrid,
        'HERE 地形圖': hereTerrain
    };

    var overlays = {
        '活動斷層': faultFeatureGroup,
        '土壤液化': landLiquefactionFeatureGroup,
        '順向坡': dipSlopeFeatureGroup
    };

    L.control.layers(baseLayers, overlays, {
        collapsed: false
    }).addTo(map);

    L.control.scale({
        position: 'bottomleft'
    }).addTo(map);
</script>

「快速建構地圖服務」系列文章

快速建構地圖服務(一) - 認識 HERE Studio / Data Hub
快速建構地圖服務(二) - 認識 HERE Data Hub CLI / API
快速建構地圖服務(三) - 使用 QGIS 玩轉 HERE Data Hub
快速建構地圖服務(四) - 當 Leaflet JS 遇見 Data Hub
快速建構地圖服務(五) - 整合 HERE 地點搜尋 API
快速建構地圖服務(六)- HERE Waypoints Sequence 路徑最佳排序
快速建構地圖服務(七)- 認識 HERE Routing API - 路徑規劃
快速建構地圖服務(八)- 認識 Matrix Routing
快速建構地圖服務(九)- Isoline Routing
快速建構地圖服務(十)- HERE Tour Planning 物流路徑預排與成本精算
快速建構地圖服務(十一)- HERE Route Matching GPS 軌跡分析
快速建構地圖服務(十二)- HERE Custom Locations 地圖資料倉儲與查詢
快速建構地圖服務(十三)- HERE Geofencing 地理圍籬
快速建構地圖服務(十四)- HERE Custom Routes 自建路網 + Vector Tile 向量圖磚 + Map Image API 靜態地圖
快速建構地圖服務(十五)- HERE Positioning 網路定位服務


圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言