大部分的地圖服務網站都會有一個輸入框,讓使用者輸入想要搜尋的目標,按下「Enter」之後,就會進行搜尋的動作,並把結果顯示在地圖上,我們接下來就要實做這個部份。
首先,請先在 body 裡面新增一個 div,裡面放一個 input (文字輸入框)。
<body>
<div id="map"></div>
<div id="searchbar" style=" position: absolute; top: 20px; z-index: 1000; left: 20px; ">
<input id="inputbox" size="20"> </div>
</body>
結果輸入框是新增了,但是也跟地圖縮放的按鈕疊在一起了,我們稍微對地圖版面微調一下:把縮放按鈕放到左下角,把比例尺放到右下角。首先先把建立地圖物件的宣告加個選項,把預設的縮放按鈕關掉:
var map = L.map('map', {zoomControl:false}); // 建立 L.map 物件。
接著修改 L.control.scale 到右下角,以及加入一個 L.control.zoom 到左下角:
L.control.scale({
position: 'bottomright'
}).addTo(map);
L.control.zoom({
position: 'bottomleft'
}).addTo(map);
這樣感覺好多了,接下來我們就要來實做搜尋的功能。除了搜尋之外,我們想要實做的,也包括搜尋建議的功能。
HERE Geocoding and Search API 提供了搜尋地址與地點的功能,這個 API 主要提供幾種功能:
所以我們可以規劃出的流程是:
如此一來,大部分的功能都有使用到。
首先我們先來看看 Autosuggest,這個api的網址是:https://autosuggest.search.hereapi.com/v1/autosuggest? ,並且接受這些主要的參數:
我們現在就來試試看,設定地點為台北市政府(25.0378862,121.5645032),輸入字串為「咖哩」,語言設定為台灣正體中文,限制回傳五筆。網址為:https://autosuggest.search.hereapi.com/v1/autosuggest?at=25.0378862,121.5645032&limit=5&lang=zh-TW&q=咖哩&apikey={API_KEY}
回傳的結果為:
{
"items": [
{
"title": "咖哩大王",
"id": "here:pds:place:158cpf7c-1fc78995f8910fa21177f2ffa189be35",
"resultType": "place",
"address": {
"label": "台灣110台北市信義區松高路19號號咖哩大王"
},
"position": {
"lat": 25.03933,
"lng": 121.56703
},
"access": [
{
"lat": 25.03909,
"lng": 121.56701
}
],
"distance": 241,
"categories": [
{
"id": "100-1000-0000",
"name": "餐廳",
"primary": true
}
],
"highlights": {
"title": [
{
"start": 0,
"end": 2
}
],
"address": {
"label": [
{
"start": 18,
"end": 20
}
]
}
}
},
{
"title": "咖哩王子",
"id": "here:pds:place:158wsqqq-0be352797cf64262b484c60ac596971c",
"resultType": "place",
"address": {
"label": "咖哩王子, No. 1, 臨江街, 大安區, 台北市, 106, 台灣"
},
"position": {
"lat": 25.02955,
"lng": 121.55708
},
"access": [
{
"lat": 25.02946,
"lng": 121.55705
}
],
"distance": 1242,
"categories": [
{
"id": "100-1000-0001",
"name": "休閒餐飲",
"primary": true
},
{
"id": "100-1000-0000",
"name": "餐廳"
},
{
"id": "100-1000-0006",
"name": "熟食店"
}
],
"references": [
{
"supplier": {
"id": "yelp"
},
"id": "ZBsIqUpMZdM4Rg7y82_eig"
}
],
"foodTypes": [
{
"id": "203-000",
"name": "日式",
"primary": true
},
{
"id": "200-000",
"name": "亞洲"
},
{
"id": "201-049",
"name": "中式 - 台菜"
}
],
"highlights": {
"title": [
{
"start": 0,
"end": 2
}
],
"address": {
"label": [
{
"start": 0,
"end": 2
}
]
}
}
},
{
"title": "咖哩王國",
"id": "here:pds:place:158wsqqq-bf6227954ca943a49cbf723d266ad138",
"resultType": "place",
"address": {
"label": "台灣106台北市大安區光復南路240巷11號咖哩王國"
},
"position": {
"lat": 25.04057,
"lng": 121.55696
},
"access": [
{
"lat": 25.04074,
"lng": 121.55696
}
],
"distance": 945,
"categories": [
{
"id": "100-1000-0000",
"name": "餐廳",
"primary": true
}
],
"foodTypes": [
{
"id": "201-000",
"name": "中式",
"primary": true
},
{
"id": "203-000",
"name": "日式"
}
],
"highlights": {
"title": [
{
"start": 0,
"end": 2
}
],
"address": {
"label": [
{
"start": 22,
"end": 24
}
]
}
}
},
{
"title": "卡里咖哩",
"id": "here:pds:place:158wsqqq-96d2889bf2074c5f9c72fc0c4c7c3210",
"resultType": "place",
"address": {
"label": "台灣110台北市信義區松壽路9號卡里咖哩"
},
"position": {
"lat": 25.03602,
"lng": 121.56658
},
"access": [
{
"lat": 25.03585,
"lng": 121.56657
}
],
"distance": 188,
"categories": [
{
"id": "100-1000-0000",
"name": "餐廳",
"primary": true
}
],
"references": [
{
"supplier": {
"id": "core"
},
"id": "1119349173"
},
{
"supplier": {
"id": "tripadvisor"
},
"id": "6152045"
}
],
"foodTypes": [
{
"id": "800-073",
"name": "小酒館",
"primary": true
},
{
"id": "800-066",
"name": "創意料理"
}
],
"highlights": {
"title": [
{
"start": 2,
"end": 4
}
],
"address": {
"label": [
{
"start": 18,
"end": 20
}
]
}
}
},
{
"title": "茄子咖哩",
"id": "here:pds:place:158wsqqq-2e6fed8a873b44aea2929c0ef4b7ff89",
"resultType": "place",
"address": {
"label": "台灣110台北市信義區松壽路12號茄子咖哩"
},
"position": {
"lat": 25.0357,
"lng": 121.566
},
"access": [
{
"lat": 25.03587,
"lng": 121.566
}
],
"distance": 204,
"categories": [
{
"id": "100-1000-0000",
"name": "餐廳",
"primary": true
}
],
"foodTypes": [
{
"id": "201-000",
"name": "中式",
"primary": true
},
{
"id": "203-000",
"name": "日式"
}
],
"highlights": {
"title": [
{
"start": 2,
"end": 4
}
],
"address": {
"label": [
{
"start": 19,
"end": 21
}
]
}
}
}
],
"queryTerms": []
}
先從 Autosuggest 功能開始,我們把搜尋的功能加到地圖上。我們這邊要使用一個非常方便的 jQuery 外掛:EasyAutocomplete。
首先,我們先把 EasyAutocomplete 的 JS 與 CSS 加到 head 裡面。
<script src="https://cdnjs.cloudflare.com/ajax/libs/easy-autocomplete/1.3.5/jquery.easy-autocomplete.min.js"
integrity="sha512-Z/2pIbAzFuLlc7WIt/xifag7As7GuTqoBbLsVTgut69QynAIOclmweT6o7pkxVoGGfLcmPJKn/lnxyMNKBAKgg=="
crossorigin="anonymous"></script>
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/easy-autocomplete/1.3.5/easy-autocomplete.min.css"
integrity="sha512-TsNN9S3X3jnaUdLd+JpyR5yVSBvW9M6ruKKqJl5XiBpuzzyIMcBavigTAHaH50MJudhv5XIkXMOwBL7TbhXThQ=="
crossorigin="anonymous" />
然後加入這段程式碼:
var options = { // 定義 EasyAutocomplete 的選取項目來源
url: function (phrase) {
return 'https://autosuggest.search.hereapi.com/v1/autosuggest?' + // Autosuggest 的 API URL
'q=' + phrase + // 接收使用者輸入的字串做搜尋
'&limit=5' + // 最多限定五筆回傳
'&lang=zh_TW' + // 限定台灣正體中文
'&at=' + map.getCenter().lat + ',' + map.getCenter().lng + // 使用目前地圖的中心點作為搜尋起始點
'&apikey=' + hereApiKey; // 您的 HERE API KEY
},
listLocation: 'items', // 使用回傳的 item 作為選取清單
getValue: 'title', // 在選取清單中顯示 title
list: {
onClickEvent: function () { // 按下選取項目之後的動作
var data = $("#inputbox").getSelectedItemData();
var northWest = L.latLng(data.mapView.north, data.mapView.west), // 選取項目的西北角
southEast = L.latLng(data.mapView.south, data.mapView.east); // 選取項目的東南角
map.fitBounds([northWest, southEast]); // 把地圖移動到選取項目
}
},
requestDelay: 100, // 延遲 100 毫秒再送出請求
placeholder: '搜尋地點' // 預設顯示的字串
};
$('#inputbox').easyAutocomplete(options); // 啟用 EasyAutocomplete 到 inpupbox 這個元件
新增完畢之後,我們可以開始測試,如果成功的話會像這樣,輸入字串,取得建議,點選建議項目之後會移動地圖到項目的位置:
但是如果使用者沒有選取建議的項目,而是直接按下「Enter」的話,要怎麼處理呢?我們就要用另外一個功能:Discover,也就是搜尋地點。
Discover 的 API 網址是:https://discover.search.hereapi.com/v1/discover? 並且常用的參數有:
其實跟 Autosuggest 接收的參數差不多,我們現在就來試試看,設定地點為台北市政府(25.0378862,121.5645032),輸入字串為「咖哩」,語言設定為台灣正體中文,限制回傳五筆。網址為:https://discover.search.hereapi.com/v1/discover?at=25.0378862,121.5645032&limit=5&lang=zh-TW&q=咖哩&apikey={API_KEY}
回傳的資料如下:
{
"items": [
{
"title": "茄子咖哩",
"id": "here:pds:place:158wsqqq-2e6fed8a873b44aea2929c0ef4b7ff89",
"resultType": "place",
"address": {
"label": "台灣110台北市信義區松壽路12號茄子咖哩",
"countryCode": "TWN",
"countryName": "台灣",
"county": "台北市",
"city": "台北市",
"district": "信義區",
"street": "松壽路",
"postalCode": "110",
"houseNumber": "12"
},
"position": {
"lat": 25.0357,
"lng": 121.566
},
"access": [
{
"lat": 25.03587,
"lng": 121.566
}
],
"distance": 287,
"categories": [
{
"id": "100-1000-0000",
"name": "餐廳",
"primary": true
}
],
"foodTypes": [
{
"id": "201-000",
"name": "中式",
"primary": true
},
{
"id": "203-000",
"name": "日式"
}
],
"contacts": [
{
"phone": [
{
"value": "+886227236989"
}
]
}
]
},
{
"title": "卡里咖哩",
"id": "here:pds:place:158wsqqq-96d2889bf2074c5f9c72fc0c4c7c3210",
"resultType": "place",
"address": {
"label": "台灣110台北市信義區松壽路9號卡里咖哩",
"countryCode": "TWN",
"countryName": "台灣",
"county": "台北市",
"city": "台北市",
"district": "信義區",
"street": "松壽路",
"postalCode": "110",
"houseNumber": "9"
},
"position": {
"lat": 25.03602,
"lng": 121.56658
},
"access": [
{
"lat": 25.03585,
"lng": 121.56657
}
],
"distance": 295,
"categories": [
{
"id": "100-1000-0000",
"name": "餐廳",
"primary": true
}
],
"references": [
{
"supplier": {
"id": "core"
},
"id": "1119349173"
},
{
"supplier": {
"id": "tripadvisor"
},
"id": "6152045"
}
],
"foodTypes": [
{
"id": "800-073",
"name": "小酒館",
"primary": true
},
{
"id": "800-066",
"name": "創意料理"
}
],
"contacts": [
{
"phone": [
{
"value": "+886227292200"
}
]
}
]
},
{
"title": "咖哩大王",
"id": "here:pds:place:158cpf7c-1fc78995f8910fa21177f2ffa189be35",
"resultType": "place",
"address": {
"label": "台灣110台北市信義區松高路19號號咖哩大王",
"countryCode": "TWN",
"countryName": "台灣",
"county": "台北市",
"city": "台北市",
"district": "信義區",
"street": "松高路",
"postalCode": "110",
"houseNumber": "19號"
},
"position": {
"lat": 25.03933,
"lng": 121.56703
},
"access": [
{
"lat": 25.03909,
"lng": 121.56701
}
],
"distance": 301,
"categories": [
{
"id": "100-1000-0000",
"name": "餐廳",
"primary": true
}
]
},
{
"title": "東京咖哩",
"id": "here:pds:place:158wsqqq-7d3ea6d6a64744429a19705d56dc89c3",
"resultType": "place",
"address": {
"label": "台灣110台北市信義區58號東京咖哩",
"countryCode": "TWN",
"countryName": "台灣",
"county": "台北市",
"city": "台北市",
"district": "信義區",
"street": "5",
"postalCode": "110",
"houseNumber": "8"
},
"position": {
"lat": 25.0408,
"lng": 121.56499
},
"access": [
{
"lat": 25.04112,
"lng": 121.565
}
],
"distance": 327,
"categories": [
{
"id": "100-1000-0000",
"name": "餐廳",
"primary": true
}
],
"foodTypes": [
{
"id": "203-000",
"name": "日式",
"primary": true
}
],
"contacts": [
{
"phone": [
{
"value": "+886227251859"
}
]
}
],
"openingHours": [
{
"text": [
"星期一-星期日: 11:00 - 21:30"
],
"isOpen": true,
"structured": [
{
"start": "T110000",
"duration": "PT10H30M",
"recurrence": "FREQ:DAILY;BYDAY:MO,TU,WE,TH,FR,SA,SU"
}
]
}
]
},
{
"title": "加油添醋",
"id": "here:pds:place:158t4x7z-ac81a291346c083e87c0d2b4b465520d",
"resultType": "place",
"address": {
"label": "台灣110台北市信義區吳興街71號加油添醋",
"countryCode": "TWN",
"countryName": "台灣",
"county": "台北市",
"city": "台北市",
"district": "信義區",
"street": "吳興街",
"postalCode": "110",
"houseNumber": "71"
},
"position": {
"lat": 25.03082,
"lng": 121.56061
},
"access": [
{
"lat": 25.03076,
"lng": 121.56053
}
],
"distance": 878,
"categories": [
{
"id": "100-1000-0000",
"name": "餐廳",
"primary": true
},
{
"id": "100-1000-0001",
"name": "休閒餐飲"
},
{
"id": "100-1000-0004",
"name": "食品市場/攤位"
},
{
"id": "600-6900-0000",
"name": "日常必需品"
},
{
"id": "600-6900-0247",
"name": "市場"
}
],
"foodTypes": [
{
"id": "201-000",
"name": "中式",
"primary": true
},
{
"id": "101-003",
"name": "美式 - 燒烤/南部"
},
{
"id": "200-000",
"name": "亞洲"
},
{
"id": "203-000",
"name": "日式"
},
{
"id": "800-085",
"name": "麵食"
}
],
"contacts": [
{
"phone": [
{
"value": "+886227365556"
},
{
"value": "+886933204228"
}
],
"mobile": [
{
"value": "+886917050303"
}
]
}
]
}
]
}
為了要實做按下「Enter」進行搜尋的功能,請加入以下的程式碼:
$('#inputbox').on('keypress', function (e) {
if (e.which == 13) { // 監聽使用者是否按下「Enter」
var phrase = $('#inputbox').val(); // 取得使用者輸入的字串
$.getJSON('https://discover.search.hereapi.com/v1/discover?' + // Discover 的 API URL
'q=' + phrase + // 接收使用者輸入的字串做搜尋
'&limit=1' + // 最多限定一筆回傳
'&lang=zh-TW' + // 限定台灣正體中文
'&at=' + map.getCenter().lat + ',' + map.getCenter().lng + // 使用目前地圖的中心點作為搜尋起始點
'&apikey=' + hereApiKey, value => {
value.items.forEach(data => {
if (data.mapView) { // 如果回傳的是地址,就進行這個動作
var northWest = L.latLng(data.mapView.north, data.mapView
.west), // 選取項目的西北角
southEast = L.latLng(data.mapView.south, data.mapView
.east); // 選取項目的東南角
map.flyToBounds([northWest, southEast]); // 把地圖移動到選取項目
} else if (data.position) { // 如果回傳的是興趣點,就進行這個動作
map.flyTo(L.latLng(data.position), 16); // 把地圖移到選取項目的地點
}
})
})
}
});
接著我們就可以測試輸入字串後,不選取建議項目而直接按下「Enter」會有什麼效果:
這樣基本的搜尋功能就完成了,我們結合了兩個 API:Autosuggest 與 Discover。接下來我們要想辦法得知我們搜尋的目標是否位在土壤液化區、順向坡或是斷層帶附近,我們這邊要使用一個經緯度來搜尋這三個 Space。
根據之前的課程提過的,我們可以用中心點加上半徑的方式來進行地理搜尋,例如我們試過的這個搜尋(查詢台北車站(經度:121.5170534/緯度:25.0478554)週邊一公里(1000 公尺)內所有的醫事機構):
https://xyz.api.here.com/hub/spaces/{SPACE_ID}/spatial?lon=121.5170534&lat=25.0478554&radius=1000&access_token={TOKEN}
我們可以把同樣的作法運用在土壤液化區、順向坡與斷層帶三個 Space。把上面這個查詢的 SPACE_ID 換成土壤液化區的 Space ID,一樣查詢台北車站週邊,但是半徑換成 10 公尺,您會得到以下這個結果:
可以看到回傳的資訊是,台北車站實際上位在一個土壤液化中潛勢區域,因此我們可以在地圖上整合這樣的搜尋,使用我們剛剛用 Autosuggest 或 Discover 的結果取得的經緯度。
我們先在網頁原始碼中加入以下這個函數「dataHubSpatialSearch」。這個函數接收緯度、經度、半徑、Space ID 與 Data Hub Token,不難看出這是用來搜尋 Data Hub 上面的內容。
function dataHubSpatialSearch(lat, lng, radius, spaceId, accessToken) {
return new Promise(function (resolve, reject) {
var url = 'https://xyz.api.here.com/hub/spaces/' + spaceId + '/spatial?lon=' + lng +
'&lat=' + lat + '&radius=' + radius + '&access_token=' + accessToken;
$.getJSON(url, value => {
var result;
if (value.features.length > 0) {
result = value.features[0]; // 如果找到結果,就回傳第一個 Feature
} else {
result = null; // 如果找不到結果,就回傳 null
}
resolve(result); // 把回傳結果交給下一步處理
})
});
}
接著,我們先建立一個變數,這個變數之後會容納一個地圖 marker,但目前就先留空。
var mapResultMarker;
之後,在地圖上加入以下這個函數「getDataHubResults」,接收經緯度之後,分別會對土壤液化區、順向坡與斷層帶三個 Space 進行查詢,因為是非同步請求,因此會等前一次查詢找到結果後才進行下一次。最後三次查詢都完成後,建立一個新的 L.marker,加入 L.popup 來顯示文字,最後加到地圖上。至於 label 參數則是用來顯示地址或地點名稱用。
function getDataHubResults(lat, lng, label) {
if (mapResultMarker) {
mapResultMarker.remove(); // 如果地圖上已經有 marker,就從地圖上移除
}
var landLiquefactionInfo = '土壤液化:無',
faultInfo = '活動斷層:無',
dipSlopeInfo = '順向坡:無'; // 先定義如果沒查到結果,就顯示「無」。
dataHubSpatialSearch(lat, lng, 100, landLiquefactionSpaceId, dataHubReadToken).then(
function onFulfilled( // 查詢是否 100 公尺內有土壤液化帶,如果有就顯示出來
result) {
if (result) {
landLiquefactionInfo = '土壤液化:' + result.properties.分級;
}
dataHubSpatialSearch(lat, lng, 100, dipSlopeSpaceId, dataHubReadToken).then(
function onFulfilled( // 查詢是否 100 公尺內有順向坡,如果有就顯示出來
result) {
if (result) {
dipSlopeInfo = '順向坡:坡度' + result.properties.SLOPE_ANG + '度'
}
dataHubSpatialSearch(lat, lng, 20000, faultSpaceId, dataHubReadToken).then(
function onFulfilled( // 查詢是否 20 公里內有活動斷層,如果有就顯示出來
result) {
if (result) {
faultInfo = '活動斷層:' + result.properties.Name;
}
mapResultMarker = L.marker(L.latLng(lat, lng)).bindPopup(label + '</br>' +
landLiquefactionInfo + '</br>' + dipSlopeInfo + '</br>' + faultInfo
); // 定義一個新的 markerp
mapResultMarker.addTo(map); // 把 marker 加到地圖上
mapResultMarker.openPopup();
})
})
})
}
接著,我們在每一次獲得地點查詢的結果後,不管是用 Autosuggest 取得建議,還是直接按下「Enter」搜尋,都在取得結果後呼叫「getDataHubResults」來搜尋。把呼叫加在「map.flyToBounds()」與「map.flyTo()」下方即可。
最後完成效果會像這樣!
Leaflet JS 提供了許多物件的互動與事件通知/監聽功能,我們這裡想做的是在地圖上按下右鍵,之後可以回傳這個地點的地址,以及如同上一個部份的查詢相關的 Data Hub 圖層。
我們這裡要使用的 API 是 Reverse Geocode,通常又被稱為「地址反查」。HERE Reverse Geocode API 的網址為:https://revgeocode.search.hereapi.com/v1/revgeocode? ,並接受以下參數:
例如我們可以使用台北車站的經緯度,來看看會回傳什麼資料:https://revgeocode.search.hereapi.com/v1/revgeocode?at=25.0478554,121.5170534&limit=1&lang=zh-TW&apikey={API_KEY}
{
"items": [
{
"title": "台北火車站",
"id": "here:pds:place:158wsqqm-97384e3eacc34dbdaf32a6bf341e50c9",
"resultType": "place",
"address": {
"label": "台灣100台北市中正區台北火車站",
"countryCode": "TWN",
"countryName": "台灣",
"county": "台北市",
"city": "台北市",
"district": "中正區",
"postalCode": "100"
},
"position": {
"lat": 25.04775,
"lng": 121.51716
},
"access": [
{
"lat": 25.04828,
"lng": 121.51725
}
],
"distance": 17,
"categories": [
{
"id": "400-4100-0042",
"name": "巴士車站",
"primary": true
}
]
}
]
}
嗯,它確實回傳了「台北火車站」,雖然不是回傳地址,但也差強人意了。
Leaflet LS 監聽使用者在地圖上按下滑鼠右鍵的事件是「contextmenu」,因此我可以用很簡單的方式去呼叫看看。請把以下程式碼加入網頁中:
map.on('contextmenu', event => {
console.log(event); // 把事件顯示在 console
})
打開 console,並且在地圖上按下右鍵,會顯示出這個 event 的內容。可以看到裡面包含了一個 latlng 的屬性,這就是我們要的經緯度資訊。
拿到經緯度資訊之後,我們就可以開始開發了,其實作法跟地點查詢有點像,就是把經緯度放進查詢的 url 去呼叫 Reverse Geocode API,接收回傳結果並且查詢 Data Hub,最後顯示在地圖上。
把剛剛那一段原始碼修改一下:
map.on('contextmenu', event => {
if (mapResultMarker) {
mapResultMarker.remove(); // 如果地圖上已經有 marker,就從地圖上移除
}
var reverseGeocodeUrl = 'https://revgeocode.search.hereapi.com/v1/revgeocode?at=' + event.latlng.lat +
',' + event.latlng.lng + '&limit=1&lang=zh-TW&apikey=' + hereApiKey
$.getJSON(reverseGeocodeUrl, value => {
if (value.items.length > 0) {
getDataHubResults(event.latlng.lat, event.latlng.lng, value.items[0].title);
}
})
})
再加上另外一個事件監聽,就是在地圖上如果點一下滑鼠左鍵,則會把 marker 移除掉。
map.on('click', () => {
if (mapResultMarker) {
mapResultMarker.remove(); // 如果地圖上已經有 marker,就從地圖上移除
}
})
完成後,在地圖上面按下右鍵,會進行 Reverse Geocode 並且查詢 Data Hub 的內容,最後顯示結果在地圖上。
我們的住宅安全地圖就到此差不多完成了,但是在使用的過程中,您一定會發現一些問題,例如:
這些就留給您思考要怎麼解決嘍!不過其實在網路上搜尋一下都可以找到解法的,只是要把它們實做出來而已。
您也可以想想有什麼可以改進或加入的新功能或資料集,或許您也可以發想出其他更多有趣的新地圖應用,也歡迎到 HERE 開發者網站探索更多 HERE 地圖 API/SDK 的使用方式。
這個專案的完整原始碼如下:
<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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/easy-autocomplete/1.3.5/jquery.easy-autocomplete.min.js"
integrity="sha512-Z/2pIbAzFuLlc7WIt/xifag7As7GuTqoBbLsVTgut69QynAIOclmweT6o7pkxVoGGfLcmPJKn/lnxyMNKBAKgg=="
crossorigin="anonymous"></script>
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/easy-autocomplete/1.3.5/easy-autocomplete.min.css"
integrity="sha512-TsNN9S3X3jnaUdLd+JpyR5yVSBvW9M6ruKKqJl5XiBpuzzyIMcBavigTAHaH50MJudhv5XIkXMOwBL7TbhXThQ=="
crossorigin="anonymous" />
<style>
#map {
height: 100%;
}
</style>
</head>
<body>
<div id="map"></div>
<div id="searchbar" style=" position: absolute; top: 20px; z-index: 1000; left: 20px; ">
<input id="inputbox" size="20"> </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', {
zoomControl: false
}); // 建立 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) {
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 => {
// 如果 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);
}
});
})
}
}
}
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: 'bottomright'
}).addTo(map);
L.control.zoom({
position: 'bottomleft'
}).addTo(map);
function dataHubSpatialSearch(lat, lng, radius, spaceId, accessToken) {
return new Promise(function (resolve, reject) {
var url = 'https://xyz.api.here.com/hub/spaces/' + spaceId + '/spatial?lon=' + lng +
'&lat=' + lat + '&radius=' + radius + '&access_token=' + accessToken;
$.getJSON(url, value => {
var result;
if (value.features.length > 0) {
result = value.features[0]; // 如果找到結果,就回傳第一個 Feature
} else {
result = null; // 如果找不到結果,就回傳 null
}
resolve(result); // 把回傳結果交給下一步處理
})
});
}
var mapResultMarker;
function getDataHubResults(lat, lng, label) {
if (mapResultMarker) {
mapResultMarker.remove(); // 如果地圖上已經有 marker,就從地圖上移除
}
var landLiquefactionInfo = '土壤液化:無',
faultInfo = '活動斷層:無',
dipSlopeInfo = '順向坡:無'; // 先定義如果沒查到結果,就顯示「無」。
dataHubSpatialSearch(lat, lng, 100, landLiquefactionSpaceId, dataHubReadToken).then(
function onFulfilled( // 查詢是否 100 公尺內有土壤液化帶,如果有就顯示出來
result) {
if (result) {
landLiquefactionInfo = '土壤液化:' + result.properties.分級;
}
dataHubSpatialSearch(lat, lng, 100, dipSlopeSpaceId, dataHubReadToken).then(
function onFulfilled( // 查詢是否 100 公尺內有順向坡,如果有就顯示出來
result) {
if (result) {
dipSlopeInfo = '順向坡:坡度' + result.properties.SLOPE_ANG + '度'
}
dataHubSpatialSearch(lat, lng, 20000, faultSpaceId, dataHubReadToken).then(
function onFulfilled( // 查詢是否 20 公里內有活動斷層,如果有就顯示出來
result) {
if (result) {
faultInfo = '活動斷層:' + result.properties.Name;
}
mapResultMarker = L.marker(L.latLng(lat, lng)).bindPopup(label + '</br>' +
landLiquefactionInfo + '</br>' + dipSlopeInfo + '</br>' + faultInfo
); // 定義一個新的 markerp
mapResultMarker.addTo(map); // 把 marker 加到地圖上
mapResultMarker.openPopup();
})
})
})
}
var options = { // 定義 EasyAutocomplete 的選取項目來源
url: function (phrase) {
return 'https://autosuggest.search.hereapi.com/v1/autosuggest?' + // Autosuggest 的 API URL
'q=' + phrase + // 接收使用者輸入的字串做搜尋
'&limit=10' + // 最多限定五筆回傳
'&lang=zh-TW' + // 限定台灣正體中文
'&at=' + map.getCenter().lat + ',' + map.getCenter().lng + // 使用目前地圖的中心點作為搜尋起始點
'&apikey=' + hereApiKey; // 您的 HERE API KEY
},
listLocation: 'items', // 使用回傳的 items 作為選取清單
getValue: function (element) {
if (element.mapView || element.position) {
return element.title;
} else {
return '';
}
}, // 在選取清單中顯示 title
list: {
onClickEvent: function () { // 按下選取項目之後的動作
var data = $("#inputbox").getSelectedItemData();
if (data.mapView) { // 如果回傳的是地址,就進行這個動作
var northWest = L.latLng(data.mapView.north, data.mapView.west), // 選取項目的西北角
southEast = L.latLng(data.mapView.south, data.mapView.east); // 選取項目的東南角
map.flyToBounds([northWest, southEast]); // 把地圖移動到選取項目
getDataHubResults(data.position.lat, data.position.lng, data.title);
} else if (data.position) { // 如果回傳的是興趣點,就進行這個動作
map.flyTo(L.latLng(data.position), 16); // 把地圖移到選取項目的地點
getDataHubResults(data.position.lat, data.position.lng, data.title);
}
}
},
requestDelay: 100, // 延遲 100 毫秒再送出請求
placeholder: '搜尋地點' // 預設顯示的字串
};
$('#inputbox').easyAutocomplete(options); // 啟用 EasyAutocomplete 到 inpupbox 這個元件
$('#inputbox').on('keypress', function (e) {
if (e.which == 13) { // 監聽使用者是否按下「Enter」
var phrase = $('#inputbox').val(); // 取得使用者輸入的字串
$.getJSON('https://discover.search.hereapi.com/v1/discover?' + // Discover 的 API URL
'q=' + phrase + // 接收使用者輸入的字串做搜尋
'&limit=1' + // 最多限定一筆回傳
'&lang=zh-TW' + // 限定台灣正體中文
'&at=' + map.getCenter().lat + ',' + map.getCenter().lng + // 使用目前地圖的中心點作為搜尋起始點
'&apikey=' + hereApiKey, value => {
value.items.forEach(data => {
if (data.mapView) { // 如果回傳的是地址,就進行這個動作
var northWest = L.latLng(data.mapView.north, data.mapView
.west), // 選取項目的西北角
southEast = L.latLng(data.mapView.south, data.mapView
.east); // 選取項目的東南角
map.flyToBounds([northWest, southEast]); // 把地圖移動到選取項目
getDataHubResults(data.position.lat, data.position.lng, data.title);
} else if (data.position) { // 如果回傳的是興趣點,就進行這個動作
map.flyTo(L.latLng(data.position), 16); // 把地圖移到選取項目的地點
getDataHubResults(data.position.lat, data.position.lng, data.title);
}
})
})
}
});
map.on('contextmenu', event => {
if (mapResultMarker) {
mapResultMarker.remove(); // 如果地圖上已經有 marker,就從地圖上移除
}
var reverseGeocodeUrl = 'https://revgeocode.search.hereapi.com/v1/revgeocode?at=' + event.latlng.lat +
',' + event.latlng.lng + '&limit=1&lang=zh-TW&apikey=' + hereApiKey
$.getJSON(reverseGeocodeUrl, value => {
if (value.items.length > 0) {
getDataHubResults(event.latlng.lat, event.latlng.lng, value.items[0].title);
}
})
})
map.on('click', () => {
if (mapResultMarker) {
mapResultMarker.remove(); // 如果地圖上已經有 marker,就從地圖上移除
}
})
</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 網路定位服務