# Google Map Api & Google Firebase Hosting ###### tags: vue firebase google map Demo : https://scfdroutemap.web.app/ ## :memo: 啟用Google Map Api 服務 --- 獲得金鑰及使用權限 - [ ] 前往 google cloud platform > 控制台 (console) - [ ] 新增專案 > 綁定銀行帳號 (會試刷VISA) > 建立完成 ![](https://i.imgur.com/M62X81u.png) - [ ] 選取左上角 導覽總覽 > API和服務 > 建立憑證 ![](https://i.imgur.com/35ACsPV.png) - [ ] 複製金鑰到記事本(妥善保管)![](https://i.imgur.com/rqhqtbO.png) - [ ] API和服務 > 已啟用的API和服務 > + 啟用API和服務 - [ ] Geocoding Api (地址轉地圖座標) - [ ] Maps JavaScript Api(地圖資源 利用座標去標記)![](https://i.imgur.com/ii2bvmK.png) ## :memo: 使用於Node.js --- 獲取api回應的資訊 ### :rocket: Step 1: 建立 data 資料夾 及 放置原始資料 data.json ![](https://i.imgur.com/MIocEHb.png) ### :rocket: Step 2: 建立 dataFormator.js ---- 訪問Geocoding Api - dataFormator.js: 外部可呼叫 node dataFormator.js,即以 async function dataFormator() {} 為進入點 ```javascript=16 var request = require("request"); //取用 request 資源 用來訪問api async function dataFormator() { const json = require("./data/data.json"); // 取得原始資料 把裡面的地址 轉換為座標 加進去原始資料 for (let index = 0; index < json.length; index++) { var dangerRoad = json[index]; // dangerRoad = 第index筆原始資料 var roadInfo = numberGrabber(dangerRoad.The_width_and_length); // 字串找其中數字 以得知寬度、長度 dangerRoad.width = roadInfo.width; dangerRoad.long = roadInfo.long; dangerRoad.location = await FindByKeyWord( dangerRoad.District + dangerRoad.Location ); //每一筆都寫進 新的 width,long,location(座標) console.log(dangerRoad); } // 上述迴圈執行完,node js 已經獲取轉換後的地圖座標、道路長寬 // 存成檔案 var fs = require("fs"); var data = JSON.stringify(json); fs.writeFile("./data/test.json", data, function (err) { if (err) { console.log(err); } }); } function numberGrabber(str) { var roadInfo = { width: 0, long: 0, }; var numArr = str.match(/\d+(\.\d+)?/g); // 找出字串中的數字 存成[] roadInfo.width = numArr[0]; roadInfo.long = numArr[2] || numArr[1]; return roadInfo; // 原始資料 的道路資訊在同一格資料裡 // ex: 寬度約:3.2~5公尺,長度約:100公尺 // ex 回傳值 : // { // width: 3.4, // long: 200, // } } async function FindByKeyWord(str) { var location = { lat: 0, lng: 0, }; var API_KEY = "AIzaSyAUix8SWrqOyR5XSpuGQk_62AdR0idfW9o"; var BASE_URL = "https://maps.googleapis.com/maps/api/geocode/json?address="; var address = encodeURI(str); // 地址中文 需改變 編碼方式 console.log(typeof address, address); var url = BASE_URL + address + "&key=" + API_KEY; console.log("url:", url); return new Promise(function (resolve, reject) { request(url, function (error, response, results) { if (!error && response.statusCode == 200) { var data = JSON.parse(results); var resultNum = data.results.length; if (resultNum === 0) { location.lat = 0; location.lng = 0; resolve(location); } else { location.lat = data.results[0].geometry.location.lat; location.lng = data.results[0].geometry.location.lng; resolve(location); } } else { // The request failed, handle it console.log("The request failed", error); reject(error); } }); }); } dataFormator(); ``` - Geocoding Results: ```json= results[]: { types[]: string, formatted_address: string, address_components[]: { short_name: string, long_name: string, postcode_localities[]: string, types[]: string }, partial_match: boolean, place_id: string, postcode_localities[]: string, geometry: { location: LatLng, location_type: GeocoderLocationType viewport: LatLngBounds, bounds: LatLngBounds } } ``` :::info :bulb: **Hint:** google 官方還有其他使用方式 詳細可參考 此網址::arrow_upper_left: [https://developers.google.com/maps/documentation/javascript/geocoding](https://) **const geocoder = new google.maps.Geocoder();** > geocoder > .geocode({ location: latlng }) > .then(()=>{}) ==此種引用方式,無法在node js 下使用== ::: ## :memo: 使用於Vue --- 將座標資料 繪製在地圖中 > 第一步 :+1: : 初始化 google map , mount 上 Vue ```javascript=16 <template> <div id="map" class="text-dark" style="width: 100%; height: 80vh" /> </template> <script> import { Loader } from "@googlemaps/js-api-loader"; import { toRaw, watch, computed, ref, onMounted, onUnmounted, reactive, } from "vue"; import targetData from "../data/test.json"; export default { props: ["passInLocation"], setup(props) { const isInit = ref(false); const { coords } = useGeolocation(); const currPos = computed(() => ({ lat: coords.value.latitude, lng: coords.value.longitude, })); const states = reactive({ google: null, map: null, markers: null, center: null, clientTarget: [], trackMode: true, trackCountDown: 0, }); const clientTarget = ref(null); const isLoading = false; var timeOut = window.setTimeout(() => { states.trackMode = true; }, 5000); onMounted(async () => { await initMap(); handleLoadMarkers(); }); onUnmounted(async () => { if (timeOut) clearListeners(timeOut); }); const initMap = async () => { const mapDiv = document.getElementById("map"); // 拿到要放map進去的 DOM const loader = new Loader({ apiKey: process.env.VUE_APP_GOOGLE_MAP_API_KEY, version: "weekly", language: "zh-TW", }); // loader Option : https://googlemaps.github.io/js-api-loader/interfaces/LoaderOptions.html states.google = await loader.load(); // 存取到GOOGLE 之後才能GOOGLE.map.Map() states.map = new states.google.maps.Map(document.getElementById("map"), { center: { lat: 25.05825, lng: 121.49093 }, zoom: 12, mapTypeControl: true, // 是否開啟地圖選擇:衛星、地形 fullscreenControl: true, scaleControl: true, minZoom: 11, // 鎖住到新北市的範圍 mapTypeControlOptions: { position: google.maps.ControlPosition.TOP_CENTER, }, fullscreenControlOptions: { position: google.maps.ControlPosition.TOP_RIGHT, }, scaleControlOptions: { position: google.maps.ControlPosition.RIGHT_BOTTOM, }, streetViewControlOptions: { position: google.maps.ControlPosition.LEFT_BOTTOM, }, zoomControlOptions: { position: google.maps.ControlPosition.RIGHT_BOTTOM, }, // 控制UI按鍵的位置 }); // new map 出來 states.map.addListener("center_changed", () => { states.trackMode = false; clearTimeout(timeOut); timeOut = window.setTimeout(() => { states.trackMode = true; }, 5000); }); isInit.value = true; console.log("init finished"); }; // 底下Hint有 Listener 不同的Events const handleLoadMarkers = async () => { var prev_infowindow = false; targetData.forEach((i) => { var bannedVehical = ""; var iconUrlLink; // 不同的路段要給 不同的 標語 跟 分級 if (i.width >= 3.5) { iconUrlLink = "https://maps.google.com/mapfiles/kml/pal3/icon45.png"; bannedVehical = "請注意,有可能道路過窄"; } else if (3.5 >= i.width && i.width > 2.2) { iconUrlLink = "https://maps.google.com/mapfiles/kml/pal4/icon15.png"; bannedVehical = "救災車輛無法通行"; } else { iconUrlLink = "https://maps.google.com/mapfiles/kml/shapes/caution.png"; bannedVehical = "救護車輛無法通行"; } var contentString = `<div id="content" style="color: black"><div id="siteNotice"></div><h1 id="firstHeading" class="firstHeading">${i.Location}</h1><div id="bodyContent"><p><b>地址:${i.District}${i.Location}</b></br><b>道路資訊:${i.The_width_and_length}</b></br><b>所屬轄區:${i.Fire_Branch}</b></br><b style="color:red">警告:${bannedVehical}</b></div></div>`; var infowindow = new states.google.maps.InfoWindow({ content: contentString, }); // new 出 標點點擊後的訊息視窗 var marker = new states.google.maps.Marker({ position: { lat: i.location.lat, lng: i.location.lng }, map: states.map, draggable: false, icon: { url: iconUrlLink, size: new states.google.maps.Size(30, 30), scaledSize: new states.google.maps.Size(30, 30), // 縮放地圖時icon大小 }, }); states.google.maps.event.addListener(marker, "click", function () { if (prev_infowindow) { prev_infowindow.close(); } states.map.setZoom(20); // 聚焦 states.center = new states.google.maps.LatLng( i.location.lat, i.location.lng ); states.map.panTo(states.center); // 移至該座標 prev_infowindow = infowindow; infowindow.open({ anchor: marker, map, shouldFocus: true, }); }); states.google.maps.event.addListener( infowindow, "closeclick", function () { states.map.setZoom(16); //removes the marker // then, remove the infowindows name from the array } ); }); }; return { currPos, isLoading, states, followClient }; }, }; </script> ``` :::info :bulb: **EventListener** * bounds_changed * center_changed * click * contextmenu * dblclick * drag * dragend * dragstart * heading_changed * idle * maptypeid_changed * mousemove * mouseout * mouseover * projection_changed * resize * rightclick * tilesloaded * tilt_changed * zoom_changed [https://developers.google.com/maps/documentation/javascript/events](https://) ::: ## :memo: 新增自動定位功能 才可追蹤 Client 位置 > 第一步 :+1: : 利用 HTML5 的定位功能,製作一個watcher,不斷更新 當前座標 > The Geolocation method watchPosition() method is used to register a handler function that will be called automatically each time the position of the device changes. You can also, optionally, specify an error handling callback function. ```javascript= import { onUnmounted, onMounted, ref } from "vue"; export function useGeolocation() { const coords = ref({ latitude: 122, longitude: 25 }); // 台灣座標 const isSupported = "navigator" in window && "geolocation" in navigator; // 檢查是否支援 geolocation var watchoptions = { enableHighAccuracy: true, }; // 三種 OPTIONS 於下方HINT let watcher = null; onMounted(() => { if (isSupported) { watcher = navigator.geolocation.watchPosition( (position) => (coords.value = position.coords), consoleErr, watchoptions ); } else { alert("此瀏覽器不支援定位"); } }); onUnmounted(() => { if (watcher) navigator.geolocation.clearWatch(watcher); }); return { coords, isSupported }; } function consoleErr(err) { alert(err.code); alert(err.message); } ``` :::info :bulb: **Hint:** HTML5 Geolacation : :arrow_upper_left: [https://developer.mozilla.org/en-US/docs/Web/API/Geolocation/watchPosition](https://) **options Optional** An optional object including the following parameters: **maximumAge** A positive long value indicating the maximum age in milliseconds of a possible cached position that is acceptable to return. If set to 0, it means that the device cannot use a cached position and must attempt to retrieve the real current position. If set to Infinity the device must return a cached position regardless of its age. Default: 0. **timeout** A positive long value representing the maximum length of time (in milliseconds) the device is allowed to take in order to return a position. The default value is Infinity, meaning that getCurrentPosition() won't return until the position is available. **enableHighAccuracy** A boolean value that indicates the application would like to receive the best possible results. If true and if the device is able to provide a more accurate position, it will do so. Note that this can result in slower response times or increased power consumption (with a GPS chip on a mobile device for example). On the other hand, if false, the device can take the liberty to save resources by responding more quickly and/or using less power. Default: false. ==warning : 須確保 專案內所有request 都 follow Https protocol 否則 browser 將不提供此功能== ::: ![](https://i.imgur.com/ze9G3ch.png) > 第二步 :+1: : 在 Vue 中用 Computed 不斷更新位置 ```javascript= import { useGeolocation } from "../useGeolocation"; const clientTarget = ref(null); const { coords } = useGeolocation(); const currPos = computed(() => ({ lat: coords.value.latitude, lng: coords.value.longitude, })); const followClient = () => { if (clientTarget.value != null && states.trackMode == true) { toRaw(clientTarget.value).setMap(null); // 將舊的自身標記從地圖上消除 } if (isInit.value == true && states.trackMode == true) { states.center = new states.google.maps.LatLng( currPos.value.lat, currPos.value.lng ); // 使用panTo()時,須建構google.maps LatLng() 不可用{lat:,lng:} states.map.panTo(states.center); // 移動地圖至.... states.map.setZoom(16); // 放大倍率至.... console.log("出發找Client"); var marker = new states.google.maps.Marker({ position: { lat: currPos.value.lat, lng: currPos.value.lng }, map: states.map, draggable: false, icon: { url: "https://maps.google.com/mapfiles/kml/pal3/icon28.png", size: new states.google.maps.Size(30, 30), scaledSize: new states.google.maps.Size(30, 30), // 縮放地圖時Icon大小 }, }); clientTarget.value = marker; } }; watch(currPos, () => { followClient(); }); ``` :::info :bulb: **刪除Marker** If you wish to manage a set of markers, you should create an array to hold the markers. Using this array, you can then call setMap() on each marker in the array in turn when you need to remove the markers. **You can delete the markers by removing them from the map and then setting the array's length to 0,** which removes all references to the markers. ::: --- ## :memo: firebase hosting :::success :bulb: **須先自行創建firebase 帳號 並新增好專案** ![](https://i.imgur.com/nyGDzEm.png) [https://console.firebase.google.com/](https://) :bulb: **並且先 npm run build 出 dist** ::: 1. 在**本機中**安裝 firebase tool `npm install -g firebase-tools` 1. 進入專案目錄 連結上firebase 專案 `firebase login` 1. 啟動firebase `firebase init hosting` ![](https://i.imgur.com/wp0J6Gq.png) ![](https://i.imgur.com/qkiFmz1.png) :::warning 在項目初始化期間,請按照Firebase CLI 提示執行以下操作: 選擇一個Firebase 項目以與您的本地項目目錄關聯。 選定的Firebase 項目是本地項目目錄的“默認”Firebase 項目。要將其他Firebase 項目關聯到本地項目目錄,請設置項目別名。 指定用作公共根目錄的目錄。 此目錄包含您公開提供的所有靜態文件,包括 index.html 文件以及要部署到Firebase Hosting 的所有其他資產。 公共根目錄的默認名稱為public。 您可以立即指定公共根目錄,也可以稍後指定(在您的 firebase.json 配置文件中)。 如果您選擇默認值並且還沒有名為 public 的目錄,Firebase 會為您創建該目錄。 如果您的公共根目錄中不存在有效的 index.html 文件或 404.html 文件,Firebase 會為您創建一個。 為您的網站選擇配置。 如果您選擇製作一個單頁應用,Firebase 會自動為您添加重寫配置。 ::: 4. 部署 ![](https://i.imgur.com/Rz1mhIl.png) `firebase deploy --only hosting` ```sequence ``` [ToC]