iT邦幫忙

0
鐵人賽 神助攻 HERE Technologies

快速建構地圖服務(五) - 整合 HERE 地點搜尋 API

WW 2020-09-11 14:50:524066 瀏覽

快速建構地圖服務(五) - Leaflet JS 整合 HERE 地點搜尋 API

事前準備

大部分的地圖服務網站都會有一個輸入框,讓使用者輸入想要搜尋的目標,按下「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

HERE Geocoding and Search API 提供了搜尋地址與地點的功能,這個 API 主要提供幾種功能:

  1. Discover:興趣點(POI/Point of Interest)查詢
  2. Geocode:地址查詢
  3. Autosuggest:根據輸入的字串提供建議
  4. Browse:設定過濾條件進行查詢
  5. Lookup by ID:使用地點的 ID 來查詢詳情
  6. Reverse Geocode:經緯度反查地址或興趣點
Geocoding and Search API:https://developer.here.com/documentation/geocoding-search-api/

所以我們可以規劃出的流程是:

  1. 使用者輸入字串。
  2. 把使用者輸入的字串傳到 Autosuggest 功能,取得建議後回傳顯示在地圖上。
  3. 使用者如果選擇了建議的項目,則使用 Lookup by ID 功能來查詢詳情,取得結果之後移動地圖、顯示在地圖上。
  4. 使用者如果沒有選擇建議項目,但按下「Enter」,則進行一個 Discover 查詢,取得結果之後移動地圖、顯示在地圖上。
  5. 如果使用者在地圖上按右鍵,則使用地圖上的經緯度來反查地址,並顯示在地圖上。

如此一來,大部分的功能都有使用到。

實做 Autosuggest 功能

首先我們先來看看 Autosuggest,這個api的網址是:https://autosuggest.search.hereapi.com/v1/autosuggest? ,並且接受這些主要的參數:

  1. q:輸入的字串。
  2. at:定義一個查詢的中心點,但不設半徑。
  3. in:定義一個查詢的範圍,可以是國家或是定義好的圓形、方形區域。
  4. limit:限制回傳的筆數。
  5. lang:定義回傳的語言,例如英文是 en,台灣正體中文是 zh-TW。
  6. apikey:之前課程提過的,HERE API 使用 apikey 作為認證。

我們現在就來試試看,設定地點為台北市政府(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 官方網站:http://easyautocomplete.com/

首先,我們先把 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 這個元件

新增完畢之後,我們可以開始測試,如果成功的話會像這樣,輸入字串,取得建議,點選建議項目之後會移動地圖到項目的位置:

實做 Discover 功能

但是如果使用者沒有選取建議的項目,而是直接按下「Enter」的話,要怎麼處理呢?我們就要用另外一個功能:Discover,也就是搜尋地點。

Discover 的 API 網址是:https://discover.search.hereapi.com/v1/discover? 並且常用的參數有:

  1. q:輸入的字串。
  2. at:定義一個查詢的中心點,但不設半徑。
  3. in:定義一個查詢的範圍,可以是國家或是定義好的圓形、方形區域。
  4. limit:限制回傳的筆數。
  5. lang:定義回傳的語言,例如英文是 en,台灣正體中文是 zh-TW。
  6. apikey:之前課程提過的,HERE API 使用 apikey 作為認證。

其實跟 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。

結合搜尋功能與 Data Hub 查詢功能

根據之前的課程提過的,我們可以用中心點加上半徑的方式來進行地理搜尋,例如我們試過的這個搜尋(查詢台北車站(經度: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? ,並接受以下參數:

  1. at:經緯度位置。
  2. limit:回傳資料的筆數上限。
  3. limit:限制回傳的筆數。
  4. lang:定義回傳的語言,例如英文是 en,台灣正體中文是 zh-TW。

例如我們可以使用台北車站的經緯度,來看看會回傳什麼資料: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 的內容,最後顯示結果在地圖上。

我們的住宅安全地圖就到此差不多完成了,但是在使用的過程中,您一定會發現一些問題,例如:

  1. 如果搜尋範圍內有好幾個活動斷層,似乎不一定會顯示最接近的那一個?
  2. 活動斷層的線條被另外兩個圖層蓋住了,要怎麼調整圖層的順序?
  3. 如何增加流暢度?減少重複下載?

這些就留給您思考要怎麼解決嘍!不過其實在網路上搜尋一下都可以找到解法的,只是要把它們實做出來而已。

您也可以想想有什麼可以改進或加入的新功能或資料集,或許您也可以發想出其他更多有趣的新地圖應用,也歡迎到 HERE 開發者網站探索更多 HERE 地圖 API/SDK 的使用方式。

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

這個專案的完整原始碼如下:

<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 網路定位服務


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

尚未有邦友留言

立即登入留言