當專案需要用到地圖時,應該首選都是 Google Map 提供的 API 服務。本文會先從 API 的介紹開始,慢慢進入如何應用到專案,接著詳細介紹在專案中使用到的 API,最後以一個概論作結。

Google Maps JavaScript API V3 介紹

因應不同的需求,主要可以分成七大類 API:

API 說明 範例
❶Maps 顧名思義就是呼喚出那張地圖時所要使用的。自訂地圖樣式等。
❷Drawing on the map 想在地圖上顯示出指定地點的 Marker 或甚至點擊 Marker 後要跳出一個 Info Window 嗎?內容都在這裡。
❸Street View 街景服務
❹Places 取得地點詳細資訊、經緯度變換地點都得用這組 API,也是本次會重點介紹的項目 詳見文章以下介紹
❺Routes 導航路線相關。
❻Local Context (beta) Local Context 將地圖、路徑規劃、地點 (Maps, Routes, Places) 功能,透過一支 API 全部整合,一次提供 3 種功能,似乎很讚,但還在 beta 中。
❼Journey Sharing (beta) 顧名思義就是呼喚出那張地圖時所要使用的。自訂地圖樣式等。

在之前接觸的專案中,主要使用的主要是 ❶、❷,也就是使用 Google 地圖,並把相關店家的資訊(例如車咕嚕中洗車場地點)放置 Marker 在地圖上,點擊後會跳出店家詳細資訊及預約的 Info Window。 而本文主要想著重介紹的是 ❹,也是最近偉士牌需求中的購車頁優化,會需要依照使用者的 input 去移動到對應的城市,並顯示城市的名稱,其實就是模擬 Google Map 的搜尋功能,但實際做起來需要熟悉 API 的混用。

需求

  1. 輸入 Enter 後將地圖定位至該縣市/區域並在列表顯示該搜尋區域所在縣市的名稱及所有分店。
  2. 移動地圖至其他縣市時,地圖:顯示可見地區的分店地標;列表:顯示地圖中心所在縣市的所有分店

我們來拆解一下這個需求的實作步驟看看。

需求 ❶ 拆解

  1. 使用者輸入欲搜尋的區域,例如「豐原」、「綠島」
  2. 把這個 input 傳給 API,得到 response
  3. response 應該就有郵遞區號,直接用這個去 mapping 城市表找出豐原是在臺中市;綠島是在臺東縣
  4. 地圖上呈現以豐原為中心的畫面、列表上印出臺中市

需求 ❷ 拆解

  1. 使用者從 A 縣市拖曳到 B 縣市
  2. 監聽 DragEnd,發現地圖中心有改變就取得現在中心的經緯度丟到 API
  3. 從 response 整理出目前是在哪一個縣市
  4. 畫面改變

實作

首先,遵循 guide 取得一組 API Key。 由於 Google Maps Platform 只提供原生 JS 或 TS,我們可以直接使用好心人士包裝成 React 專用的套件

基礎建設

※ 程式碼會省略 input 的 component

import React, { useState } from 'react'
import { GoogleMap, useLoadScript } from '@react-google-maps/api'

const CustomGoogleMap = () => {
  // 載入 Places API 所需要的 libraries
  const [libraries] = useState(['places'])

  const { isLoaded, loadError } = useLoadScript({
    googleMapsApiKey: GOOGLE_MAP_API_KEY,
    libraries,
  })

  // 定義地圖的 style、各種控制等
  const renderMap = () => {
    const options = {
      disableDefaultUI: true,
      zoomControl: true,
      scaleControl: true,
      styles: mapStyle,
    }

   //todo1 處理使用者輸入的資料,實現需求❶
   //todo2:處理拖曳結束後的事件,實現需求❷

    return (
      <>
        <GoogleMap
          mapContainerStyle={{
            width: '100%',
            height: '320px',
          }}
          center={center}
          zoom={12}
          options={options}
          onLoad={handleLoad}
          onDragEnd={handleCenterChanged}
        />
      </>
    )
  }

  if (loadError) {
    return <h1>Map cannot be loaded right now, sorry.</h1>
  }

  return isLoaded ? renderMap() : null
}

需求 ❶

做好前置作業後,來實作需求 ❶ 吧!這邊我們需要運用到 Places 裡 Autocomplete() 來取得預測值,接著再將這個值丟給 Geocoder() 轉換成經緯度,才能夠將中心點設為 input 的區域。如下圖 1:

// 處理使用者輸入的資料,實現需求❶
const handleKeyPress = event => {
	if (event.key === 'Enter') {
		const maps = window.google.maps;
		const sessionToken = new maps.places.AutocompleteSessionToken();
		const service = new maps.places.AutocompleteService();
		const request = {
			input: userInput,
			sessionToken,
			language: 'zh-TW', // 限定回傳語言為臺灣繁體中文
			types: [
				'administrative_area_level_1',
				'administrative_area_level_2',
				'administrative_area_level_3',
			], // 限定回傳區域為 1~3 級行政區
		};
		service.getPlacePredictions(request, predictions => {
			const geocoder = new window.google.maps.Geocoder();
			geocoder.geocode({ placeId: predictions[0].place_id }, responses => {
				// 取得 input data 的經緯度後將地圖中間設為該值
				setCenter({
					lat: responses[0].geometry.location.lat(),
					lng: responses[0].geometry.location.lng(),
				});

				// 取得郵遞區號以用來 mapping 城市,例如 110 則對應到臺北市信義區
				setZipCode(responses[0].address_components.slice(-1)[0].long_name);
			});
		});
		// 使用 webview 開啟時需再按下 enter 後使用 blur() 以讓 portable device 鍵盤自動收起
		event.target.blur();
	}
};

這樣第一個需求就完成囉~可以回顧上面的影片。

需求 ❷

做完第一項之後,這個就很好理解了!直接看 code。

const handleCenterChanged = () => {
	// ❶將拖曳結束後的中心點放到 Geocoder() 取得經緯度
	// ❷將資料處理後得到郵遞區號去做城市 mapping
};

//....

<GoogleMap onDragEnd={handleCenterChanged}>

🎉🎉🎉🎉🎉 完成囉! 🎉🎉🎉🎉🎉

可以再優化的部分

  • 當 response 沒有回傳郵遞區號時,該如何去找到對應的城市?
  • 如果使用者輸入非 1~3 級行政區(例如:陽明山),是否需提示 Alert?

結語

當初這個功能其實摸索了很久,主要在於 Google 很有商業頭腦地把每一隻 API 回傳的 data 區分的很細,導致你要去組合兩三隻才能夠得到你要的結果。

每一隻 API 都是分開計費的!如有專案需求可能會需要在開發前與 PM 討論,確認客戶接不接受收費方式,畢竟如果每一個拖曳都要打 API,即使有豐沛的免費額度也不得不注意~

參考資料

Google Maps JavaScript API V3 Reference > Places Autocomplete Service > Geocoder > @react-google-maps/api

PS: 以上程式碼如果凌亂或不符合大家的 code 標準請多多指教 🙏