สร้างเครื่องมือระบุตำแหน่งร้านค้าแบบฟูลสแต็กด้วย Google Maps Platform และ Google Cloud

1. บทนำ

บทคัดย่อ

สมมติว่าคุณมีสถานที่หลายแห่งที่ต้องการใส่ในแผนที่ และต้องการให้ผู้ใช้เห็นว่าสถานที่เหล่านี้อยู่ที่ใดและระบุสถานที่ที่ต้องการไป ตัวอย่างที่พบบ่อย ได้แก่

  • เครื่องระบุตำแหน่งร้านค้าในเว็บไซต์ของผู้ค้าปลีก
  • แผนที่สถานที่ลงคะแนนสำหรับการเลือกตั้งที่กำลังจะมาถึง
  • ไดเรกทอรีของสถานที่เฉพาะทาง เช่น ภาชนะสำหรับรีไซเคิลแบตเตอรี่

สิ่งที่คุณจะสร้าง

ใน Codelab นี้ คุณจะได้สร้างเครื่องระบุตำแหน่งที่ดึงข้อมูลจากฟีดข้อมูลสดของสถานที่เฉพาะทาง และช่วยให้ผู้ใช้ค้นหาสถานที่ที่อยู่ใกล้จุดเริ่มต้นมากที่สุด เครื่องมือระบุตำแหน่งแบบฟูลสแต็กนี้รองรับสถานที่จำนวนมากกว่าเครื่องมือระบุตำแหน่งร้านค้าแบบง่ายได้มาก ซึ่งจำกัดไว้ที่ 25 แห่งหรือน้อยกว่า

2ece59c64c06e9da.png

สิ่งที่คุณจะได้เรียนรู้

Codelab นี้ใช้ชุดข้อมูลแบบเปิดเพื่อจำลองข้อมูลเมตาที่สร้างไว้ล่วงหน้าเกี่ยวกับสถานที่ตั้งของร้านค้าจำนวนมาก เพื่อให้คุณมุ่งเน้นไปที่การเรียนรู้แนวคิดทางเทคนิคที่สำคัญได้

  • Maps JavaScript API: แสดงสถานที่ตั้งจำนวนมากบนเว็บแมปที่กำหนดเอง
  • GeoJSON: รูปแบบที่จัดเก็บข้อมูลเมตาเกี่ยวกับสถานที่
  • Place Autocomplete: ช่วยให้ผู้ใช้ระบุจุดเริ่มต้นได้เร็วขึ้นและแม่นยำมากขึ้น
  • Go: ภาษาโปรแกรมที่ใช้ในการพัฒนาแบ็กเอนด์ของแอปพลิเคชัน แบ็กเอนด์จะโต้ตอบกับฐานข้อมูลและส่งผลการค้นหากลับไปยังฟรอนต์เอนด์ในรูปแบบ JSON
  • App Engine: สำหรับโฮสต์เว็บแอป

ข้อกำหนดเบื้องต้น

  • ความรู้พื้นฐานเกี่ยวกับ HTML และ JavaScript
  • บัญชี Google

2. ตั้งค่า

ในขั้นตอนที่ 3 ของส่วนต่อไปนี้ ให้เปิดใช้ Maps JavaScript API, Places API และ Distance Matrix API สำหรับ Codelab นี้

เริ่มต้นใช้งาน Google Maps Platform

หากคุณยังไม่เคยใช้ Google Maps Platform มาก่อน ให้ทำตามคู่มือการเริ่มต้นใช้งาน Google Maps Platform หรือดูเพลย์ลิสต์การเริ่มต้นใช้งาน Google Maps Platform เพื่อทำตามขั้นตอนต่อไปนี้

  1. สร้างบัญชีสำหรับการเรียกเก็บเงิน
  2. สร้างโปรเจ็กต์
  3. เปิดใช้ Google Maps Platform API และ SDK (แสดงอยู่ในส่วนก่อนหน้า)
  4. สร้างคีย์ API

เปิดใช้งาน Cloud Shell

ในโค้ดแล็บนี้ คุณจะได้ใช้ Cloud Shell ซึ่งเป็นสภาพแวดล้อมบรรทัดคำสั่งที่ทำงานใน Google Cloud และให้สิทธิ์เข้าถึงผลิตภัณฑ์และทรัพยากรที่ทำงานใน Google Cloud เพื่อให้คุณโฮสต์และเรียกใช้โปรเจ็กต์จากเว็บเบราว์เซอร์ได้อย่างสมบูรณ์

หากต้องการเปิดใช้งาน Cloud Shell จาก Cloud Console ให้คลิกเปิดใช้งาน Cloud Shell 89665d8d348105cd.png (ระบบจะจัดสรรและเชื่อมต่อกับสภาพแวดล้อมภายในเวลาไม่กี่นาที)

5f504766b9b3be17.png

ซึ่งจะเปิดเชลล์ใหม่ในส่วนล่างของเบราว์เซอร์หลังจากที่อาจแสดงโฆษณาคั่นระหว่างหน้าเบื้องต้น

d3bb67d514893d1f.png

ยืนยันโปรเจ็กต์

เมื่อเชื่อมต่อกับ Cloud Shell แล้ว คุณควรเห็นว่าคุณได้รับการตรวจสอบสิทธิ์แล้ว และระบบได้ตั้งค่าโปรเจ็กต์เป็นรหัสโปรเจ็กต์ที่คุณเลือกไว้ในระหว่างการตั้งค่าแล้ว

$ gcloud auth list
Credentialed Accounts:
ACTIVE  ACCOUNT
  *     <myaccount>@<mydomain>.com
$ gcloud config list project
[core]
project = <YOUR_PROJECT_ID>

หากไม่ได้ตั้งค่าโปรเจ็กต์เนื่องด้วยเหตุผลบางประการ ให้เรียกใช้คำสั่งต่อไปนี้

gcloud config set project <YOUR_PROJECT_ID>

เปิดใช้ App Engine Flex API

คุณต้องเปิดใช้ AppEngine Flex API จาก Cloud Console ด้วยตนเอง การดำเนินการนี้ไม่เพียงแต่จะเปิดใช้ API แต่ยังสร้างบัญชีบริการของ App Engine Flexible Environment ซึ่งเป็นบัญชีที่ได้รับการตรวจสอบสิทธิ์ที่จะโต้ตอบกับบริการของ Google (เช่น ฐานข้อมูล SQL) ในนามของผู้ใช้ด้วย

3. Hello, World

แบ็กเอนด์: Hello World ใน Go

ในอินสแตนซ์ Cloud Shell คุณจะเริ่มต้นด้วยการสร้างแอป Go App Engine Flex ซึ่งจะเป็นพื้นฐานสำหรับส่วนที่เหลือของโค้ดแล็บ

ในแถบเครื่องมือของ Cloud Shell ให้คลิกปุ่มเปิดตัวแก้ไขเพื่อเปิดโปรแกรมแก้ไขโค้ดในแท็บใหม่ ตัวแก้ไขโค้ดบนเว็บนี้ช่วยให้คุณแก้ไขไฟล์ในอินสแตนซ์ Cloud Shell ได้อย่างง่ายดาย

b63f7baad67b6601.png

จากนั้นคลิกไอคอนเปิดในหน้าต่างใหม่เพื่อย้ายเอดิเตอร์และเทอร์มินัลไปยังแท็บใหม่

3f6625ff8461c551.png

สร้างไดเรกทอรี austin-recycling ใหม่ในเทอร์มินัลที่ด้านล่างของแท็บใหม่

mkdir -p austin-recycling && cd $_

จากนั้นคุณจะสร้างแอป Go App Engine ขนาดเล็กเพื่อให้แน่ใจว่าทุกอย่างทำงานได้ Hello World!

austin-recycling ไดเรกทอรีควรปรากฏในรายการโฟลเดอร์ของเอดิเตอร์ทางด้านซ้ายด้วย ในไดเรกทอรี austin-recycling ให้สร้างไฟล์ชื่อ app.yaml ใส่เนื้อหาต่อไปนี้ในไฟล์ app.yaml

app.yaml

runtime: go
env: flex

manual_scaling:
  instances: 1
resources:
  cpu: 1
  memory_gb: 0.5
  disk_size_gb: 10

ไฟล์กำหนดค่านี้จะกำหนดค่าแอป App Engine ให้ใช้รันไทม์ Go Flex ดูข้อมูลพื้นฐานเกี่ยวกับความหมายของรายการการกำหนดค่าในไฟล์นี้ได้ที่เอกสารประกอบเกี่ยวกับสภาพแวดล้อมมาตรฐานของ Google App Engine Go

จากนั้นสร้างไฟล์ main.go ไว้ข้างไฟล์ app.yaml

main.go

package main

import (
        "fmt"
        "log"
        "net/http"
        "os"
)

func main() {
        http.HandleFunc("/", handle)
        port := os.Getenv("PORT")
        if port == "" {
                port = "8080"
        }
        log.Printf("Listening on port %s", port)
        if err := http.ListenAndServe(":"+port, nil); err != nil {
                log.Fatal(err)
        }
}

func handle(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path != "/" {
                http.NotFound(w, r)
                return
        }
        fmt.Fprint(w, "Hello world!")
}

เราควรหยุดสักครู่เพื่อทำความเข้าใจว่าโค้ดนี้ทำอะไร อย่างน้อยก็ในระดับสูง คุณได้กำหนดแพ็กเกจ main ที่เริ่มต้นเซิร์ฟเวอร์ HTTP ซึ่งรับฟังที่พอร์ต 8080 และลงทะเบียนฟังก์ชันตัวแฮนเดิลสำหรับคำขอ HTTP ที่ตรงกับเส้นทาง "/"

ฟังก์ชันแฮนเดิล ซึ่งเรียกอย่างสะดวกว่า handler จะเขียนสตริงข้อความ "Hello, world!" ระบบจะส่งต่อข้อความนี้กลับไปยังเบราว์เซอร์ของคุณ ซึ่งคุณจะอ่านได้ ในขั้นตอนต่อๆ ไป คุณจะสร้างตัวแฮนเดิลที่ตอบกลับด้วยข้อมูล GeoJSON แทนสตริงที่ฮาร์ดโค้ดแบบง่าย

หลังจากทำตามขั้นตอนเหล่านี้ คุณควรมีเครื่องมือแก้ไขที่มีลักษณะดังนี้

2084fdd5ef594ece.png

ทดสอบ

หากต้องการทดสอบแอปพลิเคชันนี้ คุณสามารถเรียกใช้เซิร์ฟเวอร์การพัฒนา App Engine ภายในอินสแตนซ์ Cloud Shell ได้ กลับไปที่บรรทัดคำสั่ง Cloud Shell แล้วพิมพ์คำสั่งต่อไปนี้

go run *.go

คุณจะเห็นเอาต์พุตบันทึกบางบรรทัดที่แสดงว่าคุณกำลังเรียกใช้เซิร์ฟเวอร์การพัฒนาในอินสแตนซ์ Cloud Shell จริงๆ โดยเว็บแอป Hello World จะรับฟังในพอร์ต 8080 ของ localhost คุณเปิดแท็บเว็บเบราว์เซอร์ในแอปนี้ได้โดยกดปุ่มแสดงตัวอย่างเว็บ แล้วเลือกรายการเมนูแสดงตัวอย่างบนพอร์ต 8080 ในแถบเครื่องมือ Cloud Shell

4155fc1dc717ac67.png

การคลิกรายการเมนูนี้จะเปิดแท็บใหม่ในเว็บเบราว์เซอร์พร้อมข้อความ "Hello, world!" ที่แสดงจากเซิร์ฟเวอร์การพัฒนา App Engine

ในขั้นตอนถัดไป คุณจะเพิ่มข้อมูลการรีไซเคิลของเมืองออสตินลงในแอปนี้ และเริ่มแสดงภาพข้อมูล

4. รับข้อมูลปัจจุบัน

GeoJSON ซึ่งเป็นภาษากลางของโลก GIS

ขั้นตอนก่อนหน้านี้ระบุว่าคุณจะสร้างตัวแฮนเดิลในโค้ด Go ที่แสดงข้อมูล GeoJSON ในเว็บเบราว์เซอร์ แต่ GeoJSON คืออะไร

ในโลกของระบบสารสนเทศทางภูมิศาสตร์ (GIS) เราต้องสามารถสื่อสารความรู้เกี่ยวกับหน่วยงานทางภูมิศาสตร์ระหว่างระบบคอมพิวเตอร์ได้ แผนที่เหมาะสำหรับมนุษย์ในการอ่าน แต่โดยทั่วไปแล้วคอมพิวเตอร์จะชอบข้อมูลในรูปแบบที่ย่อยง่ายกว่า

GeoJSON เป็นรูปแบบสำหรับการเข้ารหัสโครงสร้างข้อมูลทางภูมิศาสตร์ เช่น พิกัดของสถานที่ทิ้งขยะรีไซเคิลในออสติน รัฐเท็กซัส GeoJSON ได้รับการกำหนดมาตรฐานในมาตรฐาน Internet Engineering Task Force ที่เรียกว่า RFC7946 GeoJSON ได้รับการกำหนดในรูปแบบ JSON ซึ่งเป็น JavaScript Object Notation และได้รับการกำหนดมาตรฐานใน ECMA-404 โดยองค์กรเดียวกันกับที่กำหนดมาตรฐาน JavaScript นั่นคือ Ecma International

สิ่งสำคัญคือ GeoJSON เป็นรูปแบบการส่งข้อมูลที่รองรับอย่างกว้างขวางสำหรับการสื่อสารความรู้ทางภูมิศาสตร์ Codelab นี้ใช้ GeoJSON ในลักษณะต่อไปนี้

  • ใช้แพ็กเกจ Go เพื่อแยกวิเคราะห์ข้อมูลออสตินเป็นโครงสร้างข้อมูล GIS ภายในที่เฉพาะเจาะจง ซึ่งคุณจะใช้เพื่อกรองข้อมูลที่ขอ
  • จัดรูปแบบข้อมูลที่ขอเพื่อส่งระหว่างเว็บเซิร์ฟเวอร์กับเว็บเบราว์เซอร์
  • ใช้ไลบรารี JavaScript เพื่อแปลงการตอบกลับเป็นเครื่องหมายบนแผนที่

ซึ่งจะช่วยให้คุณไม่ต้องพิมพ์โค้ดเป็นจำนวนมาก เนื่องจากไม่จำเป็นต้องเขียนตัวแยกวิเคราะห์และตัวสร้างเพื่อแปลงสตรีมข้อมูลแบบเรียลไทม์เป็นตัวแทนในหน่วยความจำ

ดึงข้อมูล

พอร์ทัลข้อมูลแบบเปิดของเมืองออสติน รัฐเท็กซัสเผยแพร่ข้อมูลเชิงพื้นที่เกี่ยวกับแหล่งข้อมูลสาธารณะเพื่อให้สาธารณชนได้ใช้งาน ในโค้ดแล็บนี้ คุณจะได้แสดงภาพชุดข้อมูลสถานที่ทิ้งขยะรีไซเคิล

คุณจะแสดงภาพข้อมูลด้วยเครื่องหมายบนแผนที่ ซึ่งแสดงผลโดยใช้เลเยอร์ข้อมูลของ Maps JavaScript API

เริ่มต้นด้วยการดาวน์โหลดข้อมูล GeoJSON จากเว็บไซต์ของเมืองออสตินลงในแอป

  1. ในหน้าต่างบรรทัดคำสั่งของอินสแตนซ์ Cloud Shell ให้ปิดเซิร์ฟเวอร์โดยพิมพ์ [CTRL] + [C]
  2. สร้างไดเรกทอรี data ภายในไดเรกทอรี austin-recycling แล้วเปลี่ยนเป็นไดเรกทอรีนั้น
mkdir -p data && cd data

ตอนนี้ใช้ curl เพื่อดึงข้อมูลสถานที่รีไซเคิล

curl "https://data.austintexas.gov/resource/qzi7-nx8g.geojson" -o recycling-locations.geojson

สุดท้าย ให้เปลี่ยนกลับไปที่ไดเรกทอรีระดับบน

cd ..

5. ทำแผนที่สถานที่

ก่อนอื่น ให้อัปเดตไฟล์ app.yaml เพื่อแสดงแอปพลิเคชันที่แข็งแกร่งยิ่งขึ้น ซึ่ง "ไม่ใช่แค่แอป Hello World อีกต่อไป" ที่คุณกำลังจะสร้าง

app.yaml

runtime: go
env: flex

handlers:
- url: /
  static_files: static/index.html
  upload: static/index.html
- url: /(.*\.(js|html|css))$
  static_files: static/\1
  upload: static/.*\.(js|html|css)$
- url: /.*
  script: auto

manual_scaling:
  instances: 1
resources:
  cpu: 1
  memory_gb: 0.5
  disk_size_gb: 10

การกำหนดค่า app.yaml นี้จะนำคำขอสำหรับ /, /*.js, /*.css และ /*.html ไปยังชุดไฟล์แบบคงที่ ซึ่งหมายความว่าคอมโพเนนต์ HTML แบบคงที่ของแอปจะได้รับการแสดงผลโดยตรงจากโครงสร้างพื้นฐานการแสดงไฟล์ของ App Engine ไม่ใช่แอป Go ซึ่งจะช่วยลดภาระของเซิร์ฟเวอร์และเพิ่มความเร็วในการแสดงผล

ตอนนี้ก็ถึงเวลาสร้างแบ็กเอนด์ของแอปพลิเคชันใน Go แล้ว

สร้างแบ็กเอนด์

คุณอาจสังเกตเห็นว่าสิ่งหนึ่งที่น่าสนใจซึ่งไฟล์ app.yaml ไม่ได้ทำคือการแสดงไฟล์ GeoJSON เนื่องจากแบ็กเอนด์ Go ของเราจะประมวลผลและส่ง GeoJSON ซึ่งจะช่วยให้เราสร้างฟีเจอร์ที่น่าสนใจในขั้นตอนต่อๆ ไปได้ เปลี่ยนไฟล์ main.go ให้มีลักษณะดังนี้

main.go

package main

import (
        "fmt"
        "log"
        "net/http"
        "os"
        "path/filepath"
)

var GeoJSON = make(map[string][]byte)

// cacheGeoJSON loads files under data into `GeoJSON`.
func cacheGeoJSON() {
        filenames, err := filepath.Glob("data/*")
        if err != nil {
                log.Fatal(err)
        }

        for _, f := range filenames {
                name := filepath.Base(f)
                dat, err := os.ReadFile(f)
                if err != nil {
                        log.Fatal(err)
                }
                GeoJSON[name] = dat
        }
}

func main() {
        // Cache the JSON so it doesn't have to be reloaded every time a request is made.
        cacheGeoJSON()


        // Request for data should be handled by Go.  Everything else should be directed
        // to the folder of static files.
        http.HandleFunc("/data/dropoffs", dropoffsHandler)
        http.Handle("/", http.FileServer(http.Dir("./static/")))

        // Open up a port for the webserver.
        port := os.Getenv("PORT")
        if port == "" {
                port = "8080"
        }
        log.Printf("Listening on port %s", port)

        if err := http.ListenAndServe(":"+port, nil); err != nil {
                log.Fatal(err)
        }
}

func helloHandler(w http.ResponseWriter, r *http.Request) {
        // Writes Hello, World! to the user's web browser via `w`
        fmt.Fprint(w, "Hello, world!")
}

func dropoffsHandler(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-type", "application/json")
        w.Write(GeoJSON["recycling-locations.geojson"])
}

แบ็กเอนด์ของ Go มีฟีเจอร์ที่มีประโยชน์คือ อินสแตนซ์ AppEngine จะแคชตำแหน่งทั้งหมดทันทีที่เริ่มทำงาน ซึ่งจะช่วยประหยัดเวลาเนื่องจากแบ็กเอนด์ไม่ต้องอ่านไฟล์จากดิสก์ทุกครั้งที่ผู้ใช้ทุกคนรีเฟรช

สร้างฟรอนท์เอนด์

สิ่งแรกที่เราต้องทำคือสร้างโฟลเดอร์เพื่อเก็บชิ้นงานแบบคงที่ทั้งหมด สร้างโฟลเดอร์ static จากโฟลเดอร์หลักของโปรเจ็กต์

mkdir -p static && cd static

เราจะสร้างไฟล์ 3 ไฟล์ในโฟลเดอร์นี้

  • index.html จะมี HTML ทั้งหมดสำหรับแอปเครื่องมือระบุตำแหน่งร้านค้าแบบหน้าเดียว
  • style.css จะมีการจัดรูปแบบตามที่คุณคาดไว้
  • app.js จะมีหน้าที่ดึงข้อมูล GeoJSON, เรียกใช้ Maps API และวางเครื่องหมายบนแผนที่ที่กำหนดเอง

สร้างไฟล์ทั้ง 3 นี้และตรวจสอบว่าได้วางไว้ใน static/

style.css

html,
body {
  height: 100%;
  margin: 0;
  padding: 0;
}

body {
  display: flex;
}

#map {
  height: 100%;
  flex-grow: 4;
  flex-basis: auto;
}

index.html

<html>
  <head>
    <title>Austin recycling drop-off locations</title>
    <link rel="stylesheet" type="text/css" href="style.css" />
    <script src="app.js"></script>

    <script
      defer
    src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&v=weekly&libraries=places&callback=initialize&solution_channel=GMP_codelabs_fullstackstorelocator_v1_a"
    ></script>
  </head>

  <body>
    <div id="map"></div>
    <!-- Autocomplete div goes here -->
  </body>
</html>

โปรดให้ความสนใจเป็นพิเศษกับ src URL ในแท็กสคริปต์ขององค์ประกอบ head

  • แทนที่ข้อความตัวยึดตำแหน่ง "YOUR_API_KEY" ด้วยคีย์ API ที่คุณสร้างขึ้นในขั้นตอนการตั้งค่า คุณสามารถไปที่หน้า API และบริการ -> ข้อมูลเข้าสู่ระบบใน Cloud Console เพื่อดึงข้อมูลคีย์ API หรือสร้างคีย์ใหม่
  • โปรดทราบว่า URL มีพารามิเตอร์ callback=initialize. ตอนนี้เราจะสร้างไฟล์ JavaScript ที่มีฟังก์ชันเรียกกลับนั้น ในส่วนนี้ แอปจะโหลดตำแหน่งจากแบ็กเอนด์ ส่งไปยัง Maps API และใช้ผลลัพธ์เพื่อทำเครื่องหมายตำแหน่งที่กำหนดเองบนแผนที่ ซึ่งทั้งหมดนี้จะแสดงผลอย่างสวยงามในหน้าเว็บ
  • พารามิเตอร์ libraries=places จะโหลด Places Library ซึ่งจำเป็นสำหรับฟีเจอร์ต่างๆ เช่น การเติมข้อความอัตโนมัติของที่อยู่ที่จะเพิ่มในภายหลัง

app.js

let distanceMatrixService;
let map;
let originMarker;
let infowindow;
let circles = [];
let stores = [];
// The location of Austin, TX
const AUSTIN = { lat: 30.262129, lng: -97.7468 };

async function initialize() {
  initMap();

  // TODO: Initialize an infoWindow

  // Fetch and render stores as circles on map
  fetchAndRenderStores(AUSTIN);

  // TODO: Initialize the Autocomplete widget
}

const initMap = () => {
  // TODO: Start Distance Matrix service

  // The map, centered on Austin, TX
  map = new google.maps.Map(document.querySelector("#map"), {
    center: AUSTIN,
    zoom: 14,
    // mapId: 'YOUR_MAP_ID_HERE',
    clickableIcons: false,
    fullscreenControl: false,
    mapTypeControl: false,
    rotateControl: true,
    scaleControl: false,
    streetViewControl: true,
    zoomControl: true,
  });
};

const fetchAndRenderStores = async (center) => {
  // Fetch the stores from the data source
  stores = (await fetchStores(center)).features;

  // Create circular markers based on the stores
  circles = stores.map((store) => storeToCircle(store, map));
};

const fetchStores = async (center) => {
  const url = `/data/dropoffs`;
  const response = await fetch(url);
  return response.json();
};

const storeToCircle = (store, map) => {
  const [lng, lat] = store.geometry.coordinates;
  const circle = new google.maps.Circle({
    radius: 50,
    strokeColor: "#579d42",
    strokeOpacity: 0.8,
    strokeWeight: 5,
    center: { lat, lng },
    map,
  });

  return circle;
};

โค้ดนี้จะแสดงตำแหน่งร้านค้าบนแผนที่ หากต้องการทดสอบสิ่งที่เรามีจนถึงตอนนี้ ให้กลับไปที่ไดเรกทอรีหลักจากบรรทัดคำสั่งโดยใช้คำสั่งต่อไปนี้

cd ..

ตอนนี้ให้เรียกใช้แอปในโหมดการพัฒนาอีกครั้งโดยใช้คำสั่งต่อไปนี้

go run *.go

ดูตัวอย่างเหมือนที่เคยทำ คุณควรเห็นแผนที่ที่มีวงกลมสีเขียวขนาดเล็กดังนี้

58a6680e9c8e7396.png

คุณแสดงตำแหน่งในแผนที่ได้แล้ว และเราเพิ่งทำ Codelab ไปได้ครึ่งทางเท่านั้น เยี่ยมเลย ตอนนี้มาเพิ่มการโต้ตอบกัน

6. แสดงรายละเอียดตามต้องการ

ตอบสนองต่อเหตุการณ์การคลิกเครื่องหมายบนแผนที่

การแสดงเครื่องหมายจำนวนมากบนแผนที่เป็นจุดเริ่มต้นที่ดี แต่เราต้องการให้ผู้เข้าชมคลิกเครื่องหมายใดเครื่องหมายหนึ่งและดูข้อมูลเกี่ยวกับสถานที่นั้น (เช่น ชื่อธุรกิจ ที่อยู่ ฯลฯ) ได้ ชื่อของหน้าต่างข้อมูลเล็กๆ ที่มักจะปรากฏขึ้นเมื่อคุณคลิกเครื่องหมายใน Google Maps คือหน้าต่างข้อมูล

สร้างออบเจ็กต์ InfoWindow เพิ่มข้อความต่อไปนี้ลงในฟังก์ชัน initialize โดยแทนที่บรรทัดที่แสดงความคิดเห็นซึ่งอ่านว่า "// TODO: Initialize an info window"

app.js - initialize

  // Add an info window that pops up when user clicks on an individual
  // location. Content of info window is entirely up to us.
  infowindow = new google.maps.InfoWindow();

แทนที่คำจำกัดความฟังก์ชัน fetchAndRenderStores ด้วยเวอร์ชันที่แตกต่างกันเล็กน้อยนี้ ซึ่งจะเปลี่ยนบรรทัดสุดท้ายเป็นการเรียกใช้ storeToCircle ด้วยอาร์กิวเมนต์เพิ่มเติม infowindow

app.js - fetchAndRenderStores

const fetchAndRenderStores = async (center) => {
  // Fetch the stores from the data source
  stores = (await fetchStores(center)).features;

  // Create circular markers based on the stores
  circles = stores.map((store) => storeToCircle(store, map, infowindow));
};

แทนที่คำจำกัดความ storeToCircle ด้วยเวอร์ชันที่ยาวขึ้นเล็กน้อยนี้ ซึ่งตอนนี้ใช้หน้าต่างข้อมูลเป็นอาร์กิวเมนต์ที่สาม

app.js - storeToCircle

const storeToCircle = (store, map, infowindow) => {
  const [lng, lat] = store.geometry.coordinates;
  const circle = new google.maps.Circle({
    radius: 50,
    strokeColor: "#579d42",
    strokeOpacity: 0.8,
    strokeWeight: 5,
    center: { lat, lng },
    map,
  });
  circle.addListener("click", () => {
    infowindow.setContent(`${store.properties.business_name}<br />
      ${store.properties.address_address}<br />
      Austin, TX ${store.properties.zip_code}`);
    infowindow.setPosition({ lat, lng });
    infowindow.setOptions({ pixelOffset: new google.maps.Size(0, -30) });
    infowindow.open(map);
  });
  return circle;
};

โค้ดใหม่ด้านบนจะแสดง infoWindow พร้อมข้อมูลของร้านค้าที่เลือกทุกครั้งที่มีการคลิกเครื่องหมายร้านค้าบนแผนที่

หากเซิร์ฟเวอร์ยังทำงานอยู่ ให้หยุดและรีสตาร์ท รีเฟรชหน้าแผนที่แล้วลองคลิกเครื่องหมายแผนที่ หน้าต่างข้อมูลเล็กๆ ควรปรากฏขึ้นพร้อมชื่อและที่อยู่ของธุรกิจ โดยมีลักษณะดังนี้

1af0ab72ad0eadc5.png

7. รับตำแหน่งเริ่มต้นของผู้ใช้

โดยปกติแล้ว ผู้ใช้เครื่องมือระบุตำแหน่งร้านค้าต้องการทราบว่าร้านค้าใดอยู่ใกล้ตนมากที่สุด หรือที่อยู่ที่ผู้ใช้วางแผนจะเริ่มต้นการเดินทาง เพิ่มแถบค้นหาการเติมข้อความอัตโนมัติของสถานที่เพื่อให้ผู้ใช้ป้อนที่อยู่เริ่มต้นได้อย่างง่ายดาย การเติมข้อความอัตโนมัติของสถานที่ให้ฟังก์ชันการพิมพ์ล่วงหน้าคล้ายกับวิธีที่การเติมข้อความอัตโนมัติทำงานในแถบค้นหาอื่นๆ ของ Google ยกเว้นการคาดคะเนทั้งหมดเป็นสถานที่ใน Google Maps Platform

สร้างช่องป้อนข้อมูลของผู้ใช้

กลับไปแก้ไข style.css เพื่อเพิ่มการจัดรูปแบบสำหรับแถบค้นหาเติมข้อความอัตโนมัติและแผงด้านข้างของผลการค้นหาที่เกี่ยวข้อง ขณะที่เราอัปเดตสไตล์ CSS เราจะเพิ่มสไตล์สำหรับแถบด้านข้างในอนาคตซึ่งจะแสดงข้อมูลร้านค้าเป็นรายการควบคู่ไปกับแผนที่ด้วย

เพิ่มโค้ดนี้ที่ส่วนท้ายของไฟล์

style.css

#panel {
  height: 100%;
  flex-basis: 0;
  flex-grow: 0;
  overflow: auto;
  transition: all 0.2s ease-out;
}

#panel.open {
  flex-basis: auto;
}

#panel .place {
  font-family: "open sans", arial, sans-serif;
  font-size: 1.2em;
  font-weight: 500;
  margin-block-end: 0px;
  padding-left: 18px;
  padding-right: 18px;
}

#panel .distanceText {
  color: silver;
  font-family: "open sans", arial, sans-serif;
  font-size: 1em;
  font-weight: 400;
  margin-block-start: 0.25em;
  padding-left: 18px;
  padding-right: 18px;
}

/* Styling for Autocomplete search bar */
#pac-card {
  background-color: #fff;
  border-radius: 2px 0 0 2px;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
  box-sizing: border-box;
  font-family: Roboto;
  margin: 10px 10px 0 0;
  -moz-box-sizing: border-box;
  outline: none;
}

#pac-container {
  padding-top: 12px;
  padding-bottom: 12px;
  margin-right: 12px;
}

#pac-input {
  background-color: #fff;
  font-family: Roboto;
  font-size: 15px;
  font-weight: 300;
  margin-left: 12px;
  padding: 0 11px 0 13px;
  text-overflow: ellipsis;
  width: 400px;
}

#pac-input:focus {
  border-color: #4d90fe;
}

#pac-title {
  color: #fff;
  background-color: #acbcc9;
  font-size: 18px;
  font-weight: 400;
  padding: 6px 12px;
}

.hidden {
  display: none;
}

ทั้งแถบค้นหาการเติมข้อความอัตโนมัติและแผงแบบสไลด์จะซ่อนไว้ในตอนแรกจนกว่าจะจำเป็นต้องใช้

เตรียม div สำหรับวิดเจ็ตการเติมข้อความอัตโนมัติโดยแทนที่ความคิดเห็นใน index.html ที่อ่านว่า "<!-- Autocomplete div goes here -->" ด้วยโค้ดต่อไปนี้ ในขณะที่ทำการแก้ไขนี้ เราจะเพิ่ม div สำหรับแผงสไลด์ออกด้วย

index.html

     <div id="panel" class="closed"></div>
     <div class="hidden">
      <div id="pac-card">
        <div id="pac-title">Find the nearest location</div>
        <div id="pac-container">
          <input
            id="pac-input"
            type="text"
            placeholder="Enter an address"
            class="pac-target-input"
            autocomplete="off"
          />
        </div>
      </div>
    </div>

ตอนนี้ให้กำหนดฟังก์ชันเพื่อเพิ่มวิดเจ็ตการเติมข้อความอัตโนมัติลงในแผนที่โดยเพิ่มโค้ดต่อไปนี้ที่ส่วนท้ายของ app.js

app.js

const initAutocompleteWidget = () => {
  // Add search bar for auto-complete
  // Build and add the search bar
  const placesAutoCompleteCardElement = document.getElementById("pac-card");
  const placesAutoCompleteInputElement = placesAutoCompleteCardElement.querySelector(
    "input"
  );
  const options = {
    types: ["address"],
    componentRestrictions: { country: "us" },
    map,
  };
  map.controls[google.maps.ControlPosition.TOP_RIGHT].push(
    placesAutoCompleteCardElement
  );
  // Make the search bar into a Places Autocomplete search bar and select
  // which detail fields should be returned about the place that
  // the user selects from the suggestions.
  const autocomplete = new google.maps.places.Autocomplete(
    placesAutoCompleteInputElement,
    options
  );
  autocomplete.setFields(["address_components", "geometry", "name"]);
  map.addListener("bounds_changed", () => {
    autocomplete.setBounds(map.getBounds());
  });

  // TODO: Respond when a user selects an address
};

โค้ดจะจำกัดคำแนะนำในการเติมข้อความอัตโนมัติให้แสดงเฉพาะที่อยู่ (เนื่องจาก Place Autocomplete ยังสามารถจับคู่ชื่อสถานประกอบการและสถานที่ตั้งที่เป็นเขตการปกครองได้ด้วย) และจำกัดที่อยู่ที่แสดงให้แสดงเฉพาะที่อยู่ในสหรัฐอเมริกา การเพิ่มข้อกำหนดที่ไม่บังคับเหล่านี้จะช่วยลดจำนวนอักขระที่ผู้ใช้ต้องป้อนเพื่อจำกัดการคาดคะเนให้แสดงที่อยู่ที่ผู้ใช้กำลังมองหา

จากนั้นจะย้ายการเติมข้อความอัตโนมัติ div ที่คุณสร้างไว้ไปยังมุมขวาบนของแผนที่ และระบุช่องที่ควรแสดงเกี่ยวกับสถานที่แต่ละแห่งในการตอบกลับ

สุดท้าย ให้เรียกใช้ฟังก์ชัน initAutocompleteWidget ที่ส่วนท้ายของฟังก์ชัน initialize โดยแทนที่ความคิดเห็นที่ระบุว่า "// TODO: Initialize the Autocomplete widget"

app.js - initialize

 // Initialize the Places Autocomplete Widget
 initAutocompleteWidget();

รีสตาร์ทเซิร์ฟเวอร์โดยเรียกใช้คำสั่งต่อไปนี้ แล้วรีเฟรชตัวอย่าง

go run *.go

ตอนนี้คุณควรเห็นวิดเจ็ตการเติมข้อความอัตโนมัติที่มุมขวาบนของแผนที่ ซึ่งจะแสดงที่อยู่ของสหรัฐอเมริกาที่ตรงกับสิ่งที่คุณพิมพ์ โดยจะเน้นที่พื้นที่ที่มองเห็นได้ของแผนที่

58e9bbbcc4bf18d1.png

อัปเดตแผนที่เมื่อผู้ใช้เลือกที่อยู่เริ่มต้น

ตอนนี้คุณต้องจัดการเมื่อผู้ใช้เลือกการคาดคะเนจากวิดเจ็ตการเติมข้อความอัตโนมัติ และใช้ตำแหน่งนั้นเป็นพื้นฐานในการคำนวณระยะทางไปยังร้านค้า

เพิ่มโค้ดต่อไปนี้ที่ส่วนท้ายของ initAutocompleteWidget ใน app.js โดยแทนที่ความคิดเห็น "// TODO: Respond when a user selects an address"

app.js - initAutocompleteWidget

  // Respond when a user selects an address
  // Set the origin point when the user selects an address
  originMarker = new google.maps.Marker({ map: map });
  originMarker.setVisible(false);
  let originLocation = map.getCenter();
  autocomplete.addListener("place_changed", async () => {
    // circles.forEach((c) => c.setMap(null)); // clear existing stores
    originMarker.setVisible(false);
    originLocation = map.getCenter();
    const place = autocomplete.getPlace();

    if (!place.geometry) {
      // User entered the name of a Place that was not suggested and
      // pressed the Enter key, or the Place Details request failed.
      window.alert("No address available for input: '" + place.name + "'");
      return;
    }
    // Recenter the map to the selected address
    originLocation = place.geometry.location;
    map.setCenter(originLocation);
    map.setZoom(15);
    originMarker.setPosition(originLocation);
    originMarker.setVisible(true);

    // await fetchAndRenderStores(originLocation.toJSON());
    // TODO: Calculate the closest stores
  });

โค้ดจะเพิ่ม Listener เพื่อให้เมื่อผู้ใช้คลิกคำแนะนำรายการใดรายการหนึ่ง แผนที่จะจัดกึ่งกลางใหม่ที่ที่อยู่ที่เลือกและตั้งค่าต้นทางเป็นพื้นฐานสำหรับการคำนวณระยะทาง คุณจะใช้การคำนวณระยะทางในขั้นตอนถัดไป

หยุดและรีสตาร์ทเซิร์ฟเวอร์ แล้วรีเฟรชตัวอย่างเพื่อดูการตั้งค่าแผนที่ใหม่อีกครั้งหลังจากป้อนที่อยู่ในแถบค้นหาที่เติมข้อความอัตโนมัติ

8. ปรับขนาดด้วย Cloud SQL

ตอนนี้เรามีเครื่องมือระบุตำแหน่งร้านค้าที่ค่อนข้างดี โดยจะใช้ประโยชน์จากข้อเท็จจริงที่ว่ามีสถานที่ตั้งเพียงประมาณ 100 แห่งที่แอปจะใช้ โดยการโหลดสถานที่ตั้งเหล่านั้นลงในหน่วยความจำที่แบ็กเอนด์ (แทนที่จะอ่านจากไฟล์ซ้ำๆ) แต่หากอุปกรณ์ติดตามของคุณต้องทำงานในระดับที่แตกต่างออกไปล่ะ หากคุณมีสถานที่ตั้งหลายร้อยแห่งกระจายอยู่ทั่วพื้นที่ทางภูมิศาสตร์ขนาดใหญ่ (หรือหลายพันแห่งทั่วโลก) การเก็บสถานที่ตั้งทั้งหมดไว้ในหน่วยความจำไม่ใช่แนวคิดที่ดีอีกต่อไป และการแบ่งโซนออกเป็นไฟล์แต่ละไฟล์จะทำให้เกิดปัญหาของตัวเอง

ถึงเวลาโหลดสถานที่ตั้งจากฐานข้อมูลแล้ว สำหรับขั้นตอนนี้ เราจะย้ายข้อมูลสถานที่ทั้งหมดในไฟล์ GeoJSON ไปยังฐานข้อมูล Cloud SQL และอัปเดตแบ็กเอนด์ Go เพื่อดึงผลลัพธ์จากฐานข้อมูลนั้นแทนที่จะดึงจากแคชในเครื่องทุกครั้งที่มีคำขอเข้ามา

สร้างอินสแตนซ์ Cloud SQL ด้วยฐานข้อมูล PostgreSQL

คุณสร้างอินสแตนซ์ Cloud SQL ผ่าน Google Cloud Console ได้ แต่การใช้ยูทิลิตี gcloud เพื่อสร้างจากบรรทัดคำสั่งจะง่ายกว่า ใน Cloud Shell ให้สร้างอินสแตนซ์ Cloud SQL ด้วยคำสั่งต่อไปนี้

gcloud sql instances create locations \
--database-version=POSTGRES_12 \
--tier=db-custom-1-3840 --region=us-central1
  • อาร์กิวเมนต์ locations คือชื่อที่เราเลือกตั้งให้กับอินสแตนซ์ Cloud SQL นี้
  • tier แฟล็กเป็นวิธีเลือกจากเครื่องที่กำหนดไว้ล่วงหน้าบางเครื่องได้อย่างสะดวก
  • ค่า db-custom-1-3840 บ่งบอกว่าอินสแตนซ์ที่สร้างขึ้นควรมี vCPU 1 รายการและหน่วยความจำประมาณ 3.75 GB

ระบบจะสร้างและเริ่มต้นอินสแตนซ์ Cloud SQL ด้วยฐานข้อมูล PostgreSQL โดยมีผู้ใช้เริ่มต้น postgres รหัสผ่านของผู้ใช้รายนี้คืออะไร เป็นคำถามที่ดีมาก ไม่มี คุณต้องกำหนดค่าก่อนจึงจะเข้าสู่ระบบได้

ตั้งรหัสผ่านด้วยคำสั่งต่อไปนี้

gcloud sql users set-password postgres \
    --instance=locations --prompt-for-password

จากนั้นป้อนรหัสผ่านที่คุณเลือกเมื่อได้รับแจ้ง

เปิดใช้ส่วนขยาย PostGIS

PostGIS เป็นส่วนขยายสำหรับ PostGresSQL ซึ่งช่วยให้จัดเก็บข้อมูลเชิงพื้นที่ประเภทมาตรฐานได้ง่ายขึ้น ในสถานการณ์ปกติ เราจะต้องผ่านกระบวนการติดตั้งทั้งหมดเพื่อเพิ่ม PostGIS ลงในฐานข้อมูล โชคดีที่ Cloud SQL รองรับส่วนขยายนี้สำหรับ PostgreSQL

เชื่อมต่อกับอินสแตนซ์ฐานข้อมูลโดยเข้าสู่ระบบในฐานะผู้ใช้ postgres ด้วยคำสั่งต่อไปนี้ในเทอร์มินัล Cloud Shell

gcloud sql connect locations --user=postgres --quiet

ป้อนรหัสผ่านที่คุณเพิ่งสร้าง ตอนนี้ให้เพิ่มส่วนขยาย PostGIS ที่พรอมต์คำสั่ง postgres=>

CREATE EXTENSION postgis;

หากสำเร็จ เอาต์พุตควรเป็น CREATE EXTENSION ดังที่แสดงด้านล่าง

ตัวอย่างเอาต์พุตของคำสั่ง

CREATE EXTENSION

สุดท้าย ให้ยกเลิกการเชื่อมต่อฐานข้อมูลโดยป้อนคำสั่ง quit ที่พรอมต์คำสั่ง postgres=>

\q

นำเข้าข้อมูลทางภูมิศาสตร์ไปยังฐานข้อมูล

ตอนนี้เราต้องนำเข้าข้อมูลตำแหน่งทั้งหมดจากไฟล์ GeoJSON ไปยังฐานข้อมูลใหม่

โชคดีที่ปัญหานี้เป็นปัญหาที่พบได้บ่อย และมีเครื่องมือหลายอย่างบนอินเทอร์เน็ตที่ช่วยคุณทำให้กระบวนการนี้เป็นแบบอัตโนมัติ เราจะใช้เครื่องมือที่ชื่อ ogr2ogr ซึ่งแปลงรูปแบบที่ใช้กันทั่วไปหลายรูปแบบสำหรับการจัดเก็บข้อมูลเชิงพื้นที่ ซึ่งหนึ่งในตัวเลือกเหล่านั้นก็คือการแปลงจาก GeoJSON เป็นไฟล์ SQL Dump จากนั้นคุณจะใช้ไฟล์การดัมพ์ SQL เพื่อสร้างตารางและคอลัมน์สำหรับฐานข้อมูล และโหลดข้อมูลทั้งหมดที่มีอยู่ในไฟล์ GeoJSON ได้

สร้างไฟล์ SQL Dump

ก่อนอื่น ให้ติดตั้ง ogr2ogr

sudo apt-get install gdal-bin

จากนั้นใช้ ogr2ogr เพื่อสร้างไฟล์ SQL Dump ไฟล์นี้จะสร้างตารางชื่อ austinrecycling

ogr2ogr --config PG_USE_COPY YES -f PGDump datadump.sql \
data/recycling-locations.geojson -nln austinrecycling

คำสั่งด้านบนอิงตามการเรียกใช้จากโฟลเดอร์ austin-recycling หากต้องการเรียกใช้จากไดเรกทอรีอื่น ให้แทนที่ data ด้วยเส้นทางไปยังไดเรกทอรีที่จัดเก็บ recycling-locations.geojson

ป้อนข้อมูลสถานที่รีไซเคิลลงในฐานข้อมูล

เมื่อดำเนินการคำสั่งสุดท้ายเสร็จแล้ว คุณควรมีไฟล์ datadump.sql, ในไดเรกทอรีเดียวกับที่เรียกใช้คำสั่ง หากเปิดไฟล์นี้ คุณจะเห็น SQL ประมาณ 100 บรรทัดที่สร้างตาราง austinrecycling และป้อนข้อมูลสถานที่ตั้งลงในตาราง

ตอนนี้ ให้เปิดการเชื่อมต่อกับฐานข้อมูลและเรียกใช้สคริปต์นั้นด้วยคำสั่งต่อไปนี้

gcloud sql connect locations --user=postgres --quiet < datadump.sql

หากสคริปต์ทำงานสำเร็จ บรรทัดสุดท้ายของเอาต์พุตจะมีลักษณะดังนี้

ตัวอย่างเอาต์พุตของคำสั่ง

ALTER TABLE
ALTER TABLE
ATLER TABLE
ALTER TABLE
COPY 103
COMMIT
WARNING: there is no transaction in progress
COMMIT

อัปเดตแบ็กเอนด์ Go ให้ใช้ Cloud SQL

ตอนนี้เรามีข้อมูลทั้งหมดนี้ในฐานข้อมูลแล้ว จึงถึงเวลาอัปเดตโค้ด

อัปเดตส่วนหน้าเพื่อส่งข้อมูลตำแหน่ง

มาเริ่มกันที่การอัปเดตเล็กๆ ที่ส่วนหน้ากันก่อน เนื่องจากตอนนี้เรากำลังเขียนแอปนี้เพื่อรองรับการขยายขนาดในกรณีที่เราไม่ต้องการให้ทุกสถานที่แสดงที่ส่วนหน้าทุกครั้งที่มีการเรียกใช้คำค้นหา เราจึงต้องส่งข้อมูลพื้นฐานบางอย่างจากส่วนหน้าเกี่ยวกับสถานที่ที่ผู้ใช้สนใจ

เปิด app.js แล้วแทนที่คำจำกัดความฟังก์ชัน fetchStores ด้วยเวอร์ชันนี้เพื่อรวมละติจูดและลองจิจูดที่สนใจใน URL

app.js - fetchStores

const fetchStores = async (center) => {
  const url = `/data/dropoffs?centerLat=${center.lat}&centerLng=${center.lng}`;
  const response = await fetch(url);
  return response.json();
};

หลังจากทำตามขั้นตอนนี้ของ Codelab แล้ว คำตอบจะแสดงเฉพาะร้านค้าที่ใกล้กับพิกัดแผนที่ที่ระบุในพารามิเตอร์ center เท่านั้น สำหรับการดึงข้อมูลครั้งแรกในฟังก์ชัน initialize ตัวอย่างโค้ดที่ให้ไว้ในแล็บนี้ใช้พิกัดกลางของออสติน รัฐเท็กซัส

เนื่องจากตอนนี้ fetchStores จะแสดงเฉพาะตำแหน่งร้านค้าบางส่วนเท่านั้น เราจึงต้องดึงข้อมูลร้านค้าอีกครั้งทุกครั้งที่ผู้ใช้เปลี่ยนตำแหน่งเริ่มต้น

อัปเดตฟังก์ชัน initAutocompleteWidget เพื่อรีเฟรชตำแหน่งทุกครั้งที่มีการตั้งค่าต้นทางใหม่ โดยต้องแก้ไข 2 จุดดังนี้

  1. ใน initAutocompleteWidget ให้ค้นหาการเรียกกลับสำหรับเครื่องฟัง place_changed ยกเลิกการแสดงความคิดเห็นในบรรทัดที่ล้างวงกลมที่มีอยู่ เพื่อให้บรรทัดนั้นทำงานทุกครั้งที่ผู้ใช้เลือกที่อยู่จากแถบค้นหาการเติมข้อความอัตโนมัติของสถานที่

app.js - initAutocompleteWidget

  autocomplete.addListener("place_changed", async () => {
    circles.forEach((c) => c.setMap(null)); // clear existing stores
    // ...
  1. เมื่อใดก็ตามที่มีการเปลี่ยนแปลงต้นทางที่เลือก ระบบจะอัปเดตตัวแปร originLocation ที่ส่วนท้ายของแฮนเดิล "place_changed" ให้ยกเลิกการแสดงความคิดเห็นในบรรทัดเหนือบรรทัด "// TODO: Calculate the closest stores" เพื่อส่งต้นทางใหม่นี้ไปยังการเรียกฟังก์ชัน fetchAndRenderStores ใหม่

app.js - initAutocompleteWidget

    await fetchAndRenderStores(originLocation.toJSON());
    // TODO: Calculate the closest stores

อัปเดตแบ็กเอนด์ให้ใช้ CloudSQL แทนไฟล์ JSON แบบเรียบ

นำการอ่านและการแคช GeoJSON แบบแฟลตไฟล์ออก

ก่อนอื่นให้เปลี่ยน main.go เพื่อนำโค้ดที่โหลดและแคชไฟล์ GeoJSON แบบแบนออก นอกจากนี้ เรายังสามารถกำจัดฟังก์ชัน dropoffsHandler ได้ด้วย เนื่องจากเราจะเขียนฟังก์ชันที่ทำงานด้วย Cloud SQL ในไฟล์อื่น

main.go ใหม่จะสั้นลงมาก

main.go

package main

import (

        "log"
        "net/http"
        "os"
)

func main() {

        initConnectionPool()

        // Request for data should be handled by Go.  Everything else should be directed
        // to the folder of static files.
        http.HandleFunc("/data/dropoffs", dropoffsHandler)
        http.Handle("/", http.FileServer(http.Dir("./static/")))

        // Open up a port for the webserver.
        port := os.Getenv("PORT")
        if port == "" {
                port = "8080"
        }
        log.Printf("Listening on port %s", port)
        if err := http.ListenAndServe(":"+port, nil); err != nil {
                log.Fatal(err)
        }
}

สร้างตัวแฮนเดิลใหม่สำหรับคำขอตำแหน่ง

ตอนนี้มาสร้างไฟล์อีกไฟล์หนึ่งชื่อ locations.go ในไดเรกทอรี austin-recycling กัน เริ่มต้นด้วยการติดตั้งใช้งานตัวแฮนเดิลสำหรับคำขอตำแหน่งอีกครั้ง

locations.go

package main

import (
        "database/sql"
        "fmt"
        "log"
        "net/http"
        "os"

        _ "github.com/jackc/pgx/stdlib"
)

// queryBasic demonstrates issuing a query and reading results.
func dropoffsHandler(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-type", "application/json")
        centerLat := r.FormValue("centerLat")
        centerLng := r.FormValue("centerLng")
        geoJSON, err := getGeoJSONFromDatabase(centerLat, centerLng)
        if err != nil {
                str := fmt.Sprintf("Couldn't encode results: %s", err)
                http.Error(w, str, 500)
                return
        }
        fmt.Fprintf(w, geoJSON)
}

ตัวแฮนเดิลจะดำเนินการที่สำคัญต่อไปนี้

  • โดยจะดึงละติจูดและลองจิจูดจากออบเจ็กต์คำขอ (จำได้ไหมว่าเราเพิ่มข้อมูลเหล่านั้นลงใน URL อย่างไร) )
  • ซึ่งจะเรียกใช้ getGeoJsonFromDatabase ซึ่งจะแสดงสตริง GeoJSON (เราจะเขียนสตริงนี้ในภายหลัง)
  • โดยใช้ ResponseWriter เพื่อพิมพ์สตริง GeoJSON นั้นในการตอบกลับ

จากนั้นเราจะสร้าง Connection Pool เพื่อช่วยให้การใช้งานฐานข้อมูลปรับขนาดได้ดีเมื่อมีผู้ใช้พร้อมกัน

สร้าง Connection Pool

Connection Pool คือชุดการเชื่อมต่อฐานข้อมูลที่ใช้งานอยู่ซึ่งเซิร์ฟเวอร์สามารถนำกลับมาใช้ใหม่เพื่อให้บริการคำขอของผู้ใช้ ซึ่งช่วยลดค่าใช้จ่ายจำนวนมากเมื่อจำนวนผู้ใช้ที่ใช้งานอยู่เพิ่มขึ้น เนื่องจากเซิร์ฟเวอร์ไม่ต้องเสียเวลาสร้างและทำลายการเชื่อมต่อสำหรับผู้ใช้ที่ใช้งานอยู่ทุกคน คุณอาจสังเกตเห็นในส่วนก่อนหน้านี้ว่าเราได้นำเข้าไลบรารี github.com/jackc/pgx/stdlib. ซึ่งเป็นไลบรารียอดนิยมสำหรับการทำงานกับ Connection Pool ใน Go

ที่ส่วนท้ายของ locations.go ให้สร้างฟังก์ชัน initConnectionPool (เรียกจาก main.go) ที่เริ่มต้นพูลการเชื่อมต่อ เพื่อความชัดเจน เราจะใช้วิธีการช่วยเหลือ 2-3 วิธีในข้อมูลโค้ดนี้ configureConnectionPool เป็นที่ที่สะดวกในการปรับการตั้งค่าพูล เช่น จำนวนการเชื่อมต่อและอายุการใช้งานต่อการเชื่อมต่อ mustGetEnv จะเรียกใช้ฟังก์ชันเพื่อรับตัวแปรสภาพแวดล้อมที่จำเป็น ดังนั้นระบบจึงแสดงข้อความแสดงข้อผิดพลาดที่เป็นประโยชน์ได้หากอินสแตนซ์ไม่มีข้อมูลสำคัญ (เช่น IP หรือชื่อของฐานข้อมูลที่จะเชื่อมต่อ)

locations.go

// The connection pool
var db *sql.DB

// Each struct instance contains a single row from the query result.
type result struct {
        featureCollection string
}

func initConnectionPool() {
        // If the optional DB_TCP_HOST environment variable is set, it contains
        // the IP address and port number of a TCP connection pool to be created,
        // such as "127.0.0.1:5432". If DB_TCP_HOST is not set, a Unix socket
        // connection pool will be created instead.
        if os.Getenv("DB_TCP_HOST") != "" {
                var (
                        dbUser    = mustGetenv("DB_USER")
                        dbPwd     = mustGetenv("DB_PASS")
                        dbTCPHost = mustGetenv("DB_TCP_HOST")
                        dbPort    = mustGetenv("DB_PORT")
                        dbName    = mustGetenv("DB_NAME")
                )

                var dbURI string
                dbURI = fmt.Sprintf("host=%s user=%s password=%s port=%s database=%s", dbTCPHost, dbUser, dbPwd, dbPort, dbName)

                // dbPool is the pool of database connections.
                dbPool, err := sql.Open("pgx", dbURI)
                if err != nil {
                        dbPool = nil
                        log.Fatalf("sql.Open: %v", err)
                }

                configureConnectionPool(dbPool)

                if err != nil {

                        log.Fatalf("initConnectionPool: unable to connect: %s", err)
                }
                db = dbPool
        }
}

// configureConnectionPool sets database connection pool properties.
// For more information, see https://golang.org/pkg/database/sql
func configureConnectionPool(dbPool *sql.DB) {
        // Set maximum number of connections in idle connection pool.
        dbPool.SetMaxIdleConns(5)
        // Set maximum number of open connections to the database.
        dbPool.SetMaxOpenConns(7)
        // Set Maximum time (in seconds) that a connection can remain open.
        dbPool.SetConnMaxLifetime(1800)
}

// mustGetEnv is a helper function for getting environment variables.
// Displays a warning if the environment variable is not set.
func mustGetenv(k string) string {
        v := os.Getenv(k)
        if v == "" {
                log.Fatalf("Warning: %s environment variable not set.\n", k)
        }
        return v
}

ค้นหาฐานข้อมูลเพื่อหาตำแหน่งและรับ JSON เป็นผลลัพธ์

ตอนนี้เราจะเขียนการค้นหาฐานข้อมูลซึ่งใช้พิกัดแผนที่และแสดงผลสถานที่ 25 แห่งที่ใกล้ที่สุด ไม่เพียงเท่านั้น แต่ด้วยฟังก์ชันฐานข้อมูลที่ทันสมัยและซับซ้อน ระบบจะแสดงข้อมูลดังกล่าวเป็น GeoJSON ผลลัพธ์สุดท้ายของทั้งหมดนี้คือ ตราบใดที่โค้ดส่วนหน้าสามารถบอกได้ว่าไม่มีอะไรเปลี่ยนแปลง ก่อนที่จะส่งคำขอไปยัง URL และรับ GeoJSON จำนวนมาก ตอนนี้มันจะส่งคำขอไปยัง URL และ... รับ GeoJSON กลับมาจำนวนมาก

ฟังก์ชันที่ใช้ในการสร้างความมหัศจรรย์นี้มีดังนี้ เพิ่มฟังก์ชันต่อไปนี้หลังโค้ดตัวแฮนเดิลและการจัดกลุ่มการเชื่อมต่อที่คุณเพิ่งเขียนที่ด้านล่างของ locations.go

locations.go

func getGeoJSONFromDatabase(centerLat string, centerLng string) (string, error) {

        // Obviously you can one-line this, but for testing purposes let's make it easy to modify on the fly.
        const milesRadius = 10
        const milesToMeters = 1609
        const radiusInMeters = milesRadius * milesToMeters

        const tableName = "austinrecycling"

        var queryStr = fmt.Sprintf(
                `SELECT jsonb_build_object(
                        'type',
                        'FeatureCollection',
                        'features',
                        jsonb_agg(feature)
                )
        FROM (
                        SELECT jsonb_build_object(
                                        'type',
                                        'Feature',
                                        'id',
                                        ogc_fid,
                                        'geometry',
                                        ST_AsGeoJSON(wkb_geometry)::jsonb,
                                        'properties',
                                        to_jsonb(row) - 'ogc_fid' - 'wkb_geometry'
                                ) AS feature
                        FROM (
                                        SELECT *,
                                                ST_Distance(
                                                        ST_GEOGFromWKB(wkb_geometry),
                                                        -- Los Angeles (LAX)
                                                        ST_GEOGFromWKB(st_makepoint(%v, %v))
                                                ) as distance
                                        from %v
                                        order by distance
                                        limit 25
                                ) row
                        where distance < %v
                ) features
                `, centerLng, centerLat, tableName, radiusInMeters)

        log.Println(queryStr)

        rows, err := db.Query(queryStr)

        defer rows.Close()

        rows.Next()
        queryResult := result{}
        err = rows.Scan(&queryResult.featureCollection)
        return queryResult.featureCollection, err
}

ฟังก์ชันนี้ส่วนใหญ่เป็นเพียงการตั้งค่า การล้างข้อมูล และการจัดการข้อผิดพลาดสำหรับการส่งคำขอไปยังฐานข้อมูล มาดู SQL จริงกัน ซึ่งทำสิ่งต่างๆ ที่น่าสนใจมากมายในเลเยอร์ฐานข้อมูล คุณจึงไม่ต้องกังวลเรื่องการนำสิ่งเหล่านี้ไปใช้ในโค้ด

คำค้นหาดิบที่เรียกใช้เมื่อแยกวิเคราะห์สตริงและแทรกอักษรสตริงทั้งหมดในตำแหน่งที่เหมาะสมแล้วจะมีลักษณะดังนี้

parsed.sql

SELECT jsonb_build_object(
        'type',
        'FeatureCollection',
        'features',
        jsonb_agg(feature)
    )
FROM (
        SELECT jsonb_build_object(
                'type',
                'Feature',
                'id',
                ogc_fid,
                'geometry',
                ST_AsGeoJSON(wkb_geometry)::jsonb,
                'properties',
                to_jsonb(row) - 'ogc_fid' - 'wkb_geometry'
            ) AS feature
        FROM (
                SELECT *,
                    ST_Distance(
                        ST_GEOGFromWKB(wkb_geometry),
                        -- Los Angeles (LAX)
                        ST_GEOGFromWKB(st_makepoint(-97.7624043, 30.523725))
                    ) as distance
                from austinrecycling
                order by distance
                limit 25
            ) row
        where distance < 16090
    ) features

การค้นหานี้ถือเป็นการค้นหาหลัก 1 รายการและฟังก์ชันการตัด JSON บางรายการ

SELECT * ... LIMIT 25 จะเลือกช่องทั้งหมดสำหรับแต่ละสถานที่ จากนั้นจะใช้ฟังก์ชัน ST_DISTANCE (ส่วนหนึ่งของชุดฟังก์ชันการวัดทางภูมิศาสตร์ของ PostGIS) เพื่อกำหนดระยะห่างระหว่างแต่ละสถานที่ในฐานข้อมูลกับคู่ละติจูด/ลองจิจูดของสถานที่ที่ผู้ใช้ระบุในส่วนหน้า โปรดทราบว่าระยะทางเหล่านี้เป็นระยะทางเชิงพื้นที่ทางภูมิศาสตร์ ซึ่งแตกต่างจากเมตริกซ์ระยะทางที่ให้ระยะทางในการขับรถ เพื่อประสิทธิภาพ จากนั้นจะใช้ระยะทางดังกล่าวเพื่อจัดเรียงและแสดงสถานที่ 25 แห่งที่อยู่ใกล้กับตำแหน่งที่ผู้ใช้ระบุมากที่สุด

**SELECT json_build_object(‘type', ‘F**eature') จะครอบคลุมการค้นหาก่อนหน้า โดยนำผลลัพธ์มาใช้เพื่อสร้างออบเจ็กต์ฟีเจอร์ GeoJSON โดยไม่คาดคิด คิวรีนี้ยังเป็นที่ที่ใช้รัศมีสูงสุดด้วย "16090" คือจำนวนเมตรใน 10 ไมล์ ซึ่งเป็นขีดจำกัดที่กำหนดโดยแบ็กเอนด์ Go หากคุณสงสัยว่าเหตุใดจึงไม่ได้เพิ่มคําสั่ง WHERE นี้ลงในคําค้นหาภายใน (ซึ่งเป็นที่กําหนดระยะทางของแต่ละสถานที่) แทน ก็เป็นเพราะวิธีที่ SQL ดําเนินการเบื้องหลัง ฟิลด์นั้นอาจยังไม่ได้คํานวณเมื่อมีการตรวจสอบคําสั่ง WHERE ในความเป็นจริง หากคุณพยายามย้ายคําสั่ง WHERE นี้ไปยังคําค้นหาด้านใน ระบบจะแสดงข้อผิดพลาด

**SELECT json_build_object(‘type', ‘FeatureColl**ection') การค้นหานี้จะรวมแถวผลลัพธ์ทั้งหมดจากการค้นหาที่สร้าง JSON ไว้ในออบเจ็กต์ FeatureCollection ของ GeoJSON

เพิ่มไลบรารี PGX ลงในโปรเจ็กต์

เราต้องเพิ่มการอ้างอิง 1 รายการลงในโปรเจ็กต์ของคุณ นั่นคือ PostGres Driver & Toolkit ซึ่งช่วยให้ใช้การจัดกลุ่มการเชื่อมต่อได้ วิธีที่ง่ายที่สุดในการทำเช่นนี้คือการใช้ Go Modules เริ่มต้นโมดูลด้วยคำสั่งนี้ใน Cloud Shell

go mod init my_locator

จากนั้นเรียกใช้คำสั่งนี้เพื่อสแกนโค้ดหาทรัพยากร Dependency เพิ่มรายการทรัพยากร Dependency ลงในไฟล์ม็อด และดาวน์โหลดทรัพยากรเหล่านั้น

go mod tidy

สุดท้าย ให้เรียกใช้คำสั่งนี้เพื่อดึงทรัพยากร Dependency ลงในไดเรกทอรีโปรเจ็กต์โดยตรง เพื่อให้สร้างคอนเทนเนอร์สำหรับ App Engine Flex ได้ง่ายๆ

go mod vendor

โอเค คุณพร้อมที่จะทดสอบแล้ว

ทดสอบ

โอเค เราเพิ่งทำอะไรไปเยอะมาก มาดูวิธีการทำงานกัน

เพื่อให้เครื่องพัฒนา (รวมถึง Cloud Shell) เชื่อมต่อกับฐานข้อมูลได้ เราจะต้องใช้พร็อกซี Cloud SQL เพื่อจัดการการเชื่อมต่อฐานข้อมูล วิธีตั้งค่าพร็อกซี Cloud SQL

  1. ไปที่นี่เพื่อเปิดใช้ Cloud SQL Admin API
  2. หากคุณใช้เครื่องพัฒนาซอฟต์แวร์ในเครื่อง ให้ติดตั้งเครื่องมือพร็อกซี Cloud SQL หากใช้ Cloud Shell คุณสามารถข้ามขั้นตอนนี้ได้ เนื่องจากมีการติดตั้งไว้แล้ว โปรดทราบว่าวิธีการจะอ้างอิงถึงบัญชีบริการ เราได้สร้างบัญชีดังกล่าวให้คุณแล้ว และจะอธิบายการเพิ่มสิทธิ์ที่จำเป็นลงในบัญชีนั้นในส่วนถัดไป
  3. สร้างแท็บใหม่ (ใน Cloud Shell หรือเทอร์มินัลของคุณเอง) เพื่อเริ่มพร็อกซี

bcca42933bfbd497.png

  1. ไปที่ https://console.cloud.google.com/sql/instances/locations/overview แล้วเลื่อนลงเพื่อหาช่องชื่อการเชื่อมต่อ คัดลอกชื่อดังกล่าวเพื่อใช้ในคำสั่งถัดไป
  2. ในแท็บนั้น ให้เรียกใช้พร็อกซี Cloud SQL ด้วยคำสั่งนี้ โดยแทนที่ CONNECTION_NAME ด้วยชื่อการเชื่อมต่อที่แสดงในขั้นตอนก่อนหน้า
cloud_sql_proxy -instances=CONNECTION_NAME=tcp:5432

กลับไปที่แท็บแรกของ Cloud Shell และกำหนดตัวแปรสภาพแวดล้อมที่ Go จะต้องใช้เพื่อสื่อสารกับแบ็กเอนด์ของฐานข้อมูล จากนั้นเรียกใช้เซิร์ฟเวอร์ในลักษณะเดียวกับที่คุณเคยทำ

ไปที่รูทไดเรกทอรีของโปรเจ็กต์หากยังไม่ได้ไป

cd YOUR_PROJECT_ROOT

สร้างตัวแปรสภาพแวดล้อม 5 รายการต่อไปนี้ (แทนที่ YOUR_PASSWORD_HERE ด้วยรหัสผ่านที่คุณสร้างไว้ข้างต้น)

export DB_USER=postgres
export DB_PASS=YOUR_PASSWORD_HERE
export DB_TCP_HOST=127.0.0.1 # Proxy
export DB_PORT=5432 #Default for PostGres
export DB_NAME=postgres

เรียกใช้อินสแตนซ์ในเครื่อง

go run *.go

เปิดหน้าต่างแสดงตัวอย่าง แล้วหน้าต่างควรทำงานเหมือนไม่มีอะไรเปลี่ยนแปลง คุณสามารถป้อนที่อยู่เริ่มต้น ซูมไปรอบๆ แผนที่ และคลิกสถานที่รีไซเคิลได้ แต่ตอนนี้ระบบจะใช้ฐานข้อมูลและพร้อมสำหรับการปรับขนาดแล้ว

9. แสดงร้านค้าที่ใกล้ที่สุด

Directions API ทำงานคล้ายกับการขอเส้นทางในแอป Google Maps ซึ่งก็คือการป้อนต้นทางและปลายทางเดียวเพื่อรับเส้นทางระหว่าง 2 จุด Distance Matrix API จะนำแนวคิดนี้ไปใช้ต่อเพื่อระบุการจับคู่ที่เหมาะสมที่สุดระหว่างต้นทางที่เป็นไปได้หลายแห่งกับปลายทางที่เป็นไปได้หลายแห่งโดยอิงตามเวลาในการเดินทางและระยะทาง ในกรณีนี้ เพื่อช่วยให้ผู้ใช้ค้นหาร้านค้าที่ใกล้กับที่อยู่ที่เลือกมากที่สุด คุณจะต้องระบุต้นทาง 1 แห่งและอาร์เรย์ของตำแหน่งร้านค้าเป็นปลายทาง

เพิ่มระยะทางจากต้นทางไปยังร้านค้าแต่ละแห่ง

ที่จุดเริ่มต้นของinitMapคำจำกัดความฟังก์ชัน ให้แทนที่ความคิดเห็น "// TODO: Start Distance Matrix service" ด้วยโค้ดต่อไปนี้

app.js - initMap

distanceMatrixService = new google.maps.DistanceMatrixService();

เพิ่มฟังก์ชันใหม่ที่ส่วนท้ายของ app.js ชื่อ calculateDistances

app.js

async function calculateDistances(origin, stores) {
  // Retrieve the distances of each store from the origin
  // The returned list will be in the same order as the destinations list
  const response = await getDistanceMatrix({
    origins: [origin],
    destinations: stores.map((store) => {
      const [lng, lat] = store.geometry.coordinates;
      return { lat, lng };
    }),
    travelMode: google.maps.TravelMode.DRIVING,
    unitSystem: google.maps.UnitSystem.METRIC,
  });
  response.rows[0].elements.forEach((element, index) => {
    stores[index].properties.distanceText = element.distance.text;
    stores[index].properties.distanceValue = element.distance.value;
  });
}

const getDistanceMatrix = (request) => {
  return new Promise((resolve, reject) => {
    const callback = (response, status) => {
      if (status === google.maps.DistanceMatrixStatus.OK) {
        resolve(response);
      } else {
        reject(response);
      }
    };
    distanceMatrixService.getDistanceMatrix(request, callback);
  });
};

ฟังก์ชันจะเรียกใช้ Distance Matrix API โดยใช้ต้นทางที่ส่งไปยังฟังก์ชันเป็นต้นทางเดียว และใช้สถานที่ตั้งของร้านค้าเป็นอาร์เรย์ของปลายทาง จากนั้นจะสร้างอาร์เรย์ของออบเจ็กต์ที่จัดเก็บรหัสของร้านค้า ระยะทางที่แสดงเป็นสตริงที่มนุษย์อ่านได้ ระยะทางเป็นเมตรในรูปแบบค่าตัวเลข และจัดเรียงอาร์เรย์

อัปเดตฟังก์ชัน initAutocompleteWidget เพื่อคำนวณระยะทางของร้านค้าทุกครั้งที่มีการเลือกต้นทางใหม่จากแถบค้นหาการเติมข้อความอัตโนมัติของ Places ที่ด้านล่างของฟังก์ชัน initAutocompleteWidget ให้แทนที่ความคิดเห็น "// TODO: Calculate the closest stores" ด้วยโค้ดต่อไปนี้

app.js - initAutocompleteWidget

    // Use the selected address as the origin to calculate distances
    // to each of the store locations
    await calculateDistances(originLocation, stores);
    renderStoresPanel();

แสดงมุมมองรายการของร้านค้าที่เรียงตามระยะทาง

ผู้ใช้คาดหวังว่าจะเห็นรายชื่อร้านค้าที่เรียงตามลำดับจากใกล้ที่สุดไปไกลที่สุด ป้อนข้อมูลในรายการแผงด้านข้างสำหรับร้านค้าแต่ละแห่งโดยใช้รายการที่ฟังก์ชัน calculateDistances แก้ไขเพื่อแจ้งลำดับการแสดงร้านค้า

เพิ่มฟังก์ชันใหม่ 2 ฟังก์ชันที่ส่วนท้ายของ app.js ซึ่งมีชื่อว่า renderStoresPanel() และ storeToPanelRow()

app.js

function renderStoresPanel() {
  const panel = document.getElementById("panel");

  if (stores.length == 0) {
    panel.classList.remove("open");
    return;
  }

  // Clear the previous panel rows
  while (panel.lastChild) {
    panel.removeChild(panel.lastChild);
  }
  stores
    .sort((a, b) => a.properties.distanceValue - b.properties.distanceValue)
    .forEach((store) => {
      panel.appendChild(storeToPanelRow(store));
    });
  // Open the panel
  panel.classList.add("open");
  return;
}

const storeToPanelRow = (store) => {
  // Add store details with text formatting
  const rowElement = document.createElement("div");
  const nameElement = document.createElement("p");
  nameElement.classList.add("place");
  nameElement.textContent = store.properties.business_name;
  rowElement.appendChild(nameElement);
  const distanceTextElement = document.createElement("p");
  distanceTextElement.classList.add("distanceText");
  distanceTextElement.textContent = store.properties.distanceText;
  rowElement.appendChild(distanceTextElement);
  return rowElement;
};

รีสตาร์ทเซิร์ฟเวอร์และรีเฟรชตัวอย่างโดยเรียกใช้คำสั่งต่อไปนี้

go run *.go

สุดท้าย ให้ป้อนที่อยู่ในออสติน รัฐเท็กซัส ลงในแถบค้นหาที่เติมข้อความอัตโนมัติ แล้วคลิกคำแนะนำรายการใดรายการหนึ่ง

แผนที่ควรมีที่อยู่นั้นเป็นจุดศูนย์กลาง และแถบด้านข้างควรปรากฏขึ้นเพื่อแสดงรายการสถานที่ตั้งของร้านค้าตามลำดับระยะทางจากที่อยู่ที่เลือก ตัวอย่างหนึ่งแสดงดังนี้

96e35794dd0e88c9.png

10. จัดรูปแบบแผนที่

วิธีที่มีประสิทธิภาพสูงในการทำให้แผนที่ของคุณโดดเด่นในด้านภาพคือการเพิ่มสไตล์ การจัดรูปแบบแผนที่ในระบบคลาวด์ช่วยให้คุณควบคุมการปรับแต่งแผนที่จาก Cloud Console ได้โดยใช้การจัดรูปแบบแผนที่ในระบบคลาวด์ (เบต้า) หากต้องการจัดรูปแบบแผนที่ด้วยฟีเจอร์ที่ไม่ใช่เวอร์ชันเบต้า คุณสามารถใช้เอกสารประกอบการจัดรูปแบบแผนที่เพื่อช่วยสร้าง JSON สำหรับการจัดรูปแบบแผนที่โดยใช้โปรแกรม วิธีการด้านล่างจะแนะนําคุณตลอดการจัดรูปแบบแผนที่ในระบบคลาวด์ (เบต้า)

สร้างรหัสแผนที่

ก่อนอื่น ให้เปิด Cloud Console แล้วพิมพ์ "การจัดการแผนที่" ในช่องค้นหา คลิกผลการค้นหาที่ระบุว่า "การจัดการแผนที่ (Google Maps)" 64036dd0ed200200.png

คุณจะเห็นปุ่มที่ด้านบน (ใต้ช่องค้นหา) ซึ่งระบุว่าสร้างรหัสแผนที่ใหม่ คลิกที่ชื่อนั้น แล้วป้อนชื่อที่ต้องการ สำหรับประเภทแผนที่ ให้เลือก JavaScript และเมื่อตัวเลือกเพิ่มเติมปรากฏขึ้น ให้เลือกเวกเตอร์จากรายการ ผลลัพธ์สุดท้ายควรมีลักษณะคล้ายกับรูปภาพด้านล่าง

70f55a759b4c4212.png

คลิก "ถัดไป" แล้วคุณจะได้รับรหัสแผนที่ใหม่ คุณสามารถคัดลอกรหัสนี้ได้เลยหากต้องการ แต่ไม่ต้องกังวลเพราะคุณค้นหารหัสนี้ได้ง่ายๆ ในภายหลัง

จากนั้นเราจะสร้างสไตล์เพื่อใช้กับแผนที่นั้น

สร้างรูปแบบแผนที่

หากยังอยู่ในส่วน Maps ของ Cloud Console ให้คลิก "รูปแบบแผนที่ " ที่ด้านล่างของเมนูการนำทางทางด้านซ้าย ไม่เช่นนั้น คุณจะค้นหาหน้าเว็บที่ถูกต้องได้โดยพิมพ์ "รูปแบบแผนที่" ในช่องค้นหา แล้วเลือก "รูปแบบแผนที่ (Google Maps)" จากผลการค้นหา เช่น ในรูปภาพด้านล่าง

9284cd200f1a9223.png

จากนั้นคลิกปุ่มที่อยู่ใกล้ด้านบนซึ่งมีข้อความว่า "+ สร้างรูปแบบแผนที่ใหม่"

  1. หากต้องการให้รูปแบบตรงกับแผนที่ที่แสดงในแล็บนี้ ให้คลิกแท็บ "นำเข้า JSON" แล้ววาง Blob JSON ด้านล่าง หรือหากต้องการสร้างรูปแบบของคุณเอง ให้เลือกรูปแบบแผนที่ที่ต้องการใช้เป็นจุดเริ่มต้น แล้วคลิกถัดไป
  2. เลือกรหัสแมปที่เพิ่งสร้างเพื่อเชื่อมโยงรหัสแมปนั้นกับรูปแบบนี้ แล้วคลิกถัดไปอีกครั้ง
  3. ในขั้นตอนนี้ คุณจะมีตัวเลือกในการปรับแต่งรูปแบบของแผนที่เพิ่มเติม หากต้องการลองใช้ฟีเจอร์นี้ ให้คลิกปรับแต่งในเครื่องมือแก้ไขสไตล์ แล้วลองใช้สีและตัวเลือกต่างๆ จนกว่าจะได้สไตล์แผนที่ที่ต้องการ หรือคลิกข้าม
  4. ในขั้นตอนถัดไป ให้ป้อนชื่อและคำอธิบายของสไตล์ แล้วคลิกบันทึกและเผยแพร่

ต่อไปนี้คือ Blob JSON ที่ไม่บังคับเพื่อนําเข้าในขั้นตอนแรก

[
  {
    "elementType": "geometry",
    "stylers": [
      {
        "color": "#d6d2c4"
      }
    ]
  },
  {
    "elementType": "labels.icon",
    "stylers": [
      {
        "visibility": "off"
      }
    ]
  },
  {
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#616161"
      }
    ]
  },
  {
    "elementType": "labels.text.stroke",
    "stylers": [
      {
        "color": "#f5f5f5"
      }
    ]
  },
  {
    "featureType": "administrative.land_parcel",
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#bdbdbd"
      }
    ]
  },
  {
    "featureType": "landscape.man_made",
    "elementType": "geometry.fill",
    "stylers": [
      {
        "color": "#c0baa5"
      },
      {
        "visibility": "on"
      }
    ]
  },
  {
    "featureType": "landscape.man_made",
    "elementType": "geometry.stroke",
    "stylers": [
      {
        "color": "#9cadb7"
      },
      {
        "visibility": "on"
      }
    ]
  },
  {
    "featureType": "poi",
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#757575"
      }
    ]
  },
  {
    "featureType": "poi.park",
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#9e9e9e"
      }
    ]
  },
  {
    "featureType": "road",
    "elementType": "geometry",
    "stylers": [
      {
        "color": "#ffffff"
      }
    ]
  },
  {
    "featureType": "road.arterial",
    "elementType": "geometry",
    "stylers": [
      {
        "weight": 1
      }
    ]
  },
  {
    "featureType": "road.arterial",
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#757575"
      }
    ]
  },
  {
    "featureType": "road.highway",
    "elementType": "geometry",
    "stylers": [
      {
        "color": "#bf5700"
      }
    ]
  },
  {
    "featureType": "road.highway",
    "elementType": "geometry.stroke",
    "stylers": [
      {
        "visibility": "off"
      }
    ]
  },
  {
    "featureType": "road.highway",
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#616161"
      }
    ]
  },
  {
    "featureType": "road.local",
    "elementType": "geometry",
    "stylers": [
      {
        "weight": 0.5
      }
    ]
  },
  {
    "featureType": "road.local",
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#9e9e9e"
      }
    ]
  },
  {
    "featureType": "transit.line",
    "elementType": "geometry",
    "stylers": [
      {
        "color": "#e5e5e5"
      }
    ]
  },
  {
    "featureType": "transit.station",
    "elementType": "geometry",
    "stylers": [
      {
        "color": "#eeeeee"
      }
    ]
  },
  {
    "featureType": "water",
    "elementType": "geometry",
    "stylers": [
      {
        "color": "#333f48"
      }
    ]
  },
  {
    "featureType": "water",
    "elementType": "labels.text.fill",
    "stylers": [
      {
        "color": "#9e9e9e"
      }
    ]
  }
]

เพิ่มรหัสแมปลงในโค้ด

ตอนนี้คุณได้สร้างสไตล์แผนที่นี้แล้ว คุณจะใช้สไตล์แผนที่นี้ในแผนที่ของคุณเองได้อย่างไร คุณต้องทำการเปลี่ยนแปลงเล็กๆ 2 อย่างดังนี้

  1. เพิ่มรหัสแผนที่เป็นพารามิเตอร์ของ URL ลงในแท็กสคริปต์ใน index.html
  2. Add รหัสแผนที่เป็นอาร์กิวเมนต์ของตัวสร้างเมื่อสร้างแผนที่ในเมธอด initMap()

แทนที่แท็กสคริปต์ที่โหลด Maps JavaScript API ในไฟล์ HTML ด้วย URL ของโปรแกรมโหลดด้านล่าง โดยแทนที่ตัวยึดตำแหน่งสำหรับ "YOUR_API_KEY" และ "YOUR_MAP_ID"

index.html

...
<script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&v=weekly&libraries=places&callback=initialize&map_ids=YOUR_MAP_ID&solution_channel=GMP_codelabs_fullstackstorelocator_v1_a">
  </script>
...

ในinitMapเมธอดของ app.js ที่กำหนดค่าคงที่ map ให้ยกเลิกการแสดงความคิดเห็นของบรรทัดสำหรับพร็อพเพอร์ตี้ mapId และแทนที่ "YOUR_MAP_ID_HERE" ด้วยรหัสแผนที่ที่คุณเพิ่งสร้าง

app.js - initMap

...

// The map, centered on Austin, TX
 const map = new google.maps.Map(document.querySelector('#map'), {
   center: austin,
   zoom: 14,
   mapId: 'YOUR_MAP_ID_HERE',
// ...
});
...

รีสตาร์ทเซิร์ฟเวอร์

go run *.go

เมื่อรีเฟรชตัวอย่างแล้ว แผนที่ควรมีลักษณะตามสไตล์ที่คุณต้องการ ตัวอย่างการใช้การจัดรูปแบบ JSON ด้านบนมีดังนี้

2ece59c64c06e9da.png

11. ติดตั้งใช้งานในเวอร์ชันที่ใช้งานจริง

หากต้องการดูแอปที่ทำงานจาก App Engine Flex (และไม่ใช่แค่เว็บเซิร์ฟเวอร์ในเครื่องบนเครื่องมือพัฒนา / Cloud Shell ซึ่งเป็นสิ่งที่คุณทำอยู่) ก็ทำได้ง่ายๆ เราเพียงแค่ต้องเพิ่ม 2-3 อย่างเพื่อให้การเข้าถึงฐานข้อมูลทํางานในสภาพแวดล้อมเวอร์ชันที่ใช้งานจริง ซึ่งอธิบายไว้ทั้งหมดในหน้าเอกสารประกอบเกี่ยวกับการเชื่อมต่อจาก App Engine Flex ไปยัง Cloud SQL

เพิ่มตัวแปรสภาพแวดล้อมลงใน App.yaml

ก่อนอื่น คุณต้องเพิ่มตัวแปรสภาพแวดล้อมทั้งหมดที่ใช้ทดสอบในเครื่องไว้ที่ด้านล่างของไฟล์ app.yaml ของแอปพลิเคชัน

  1. ไปที่ https://console.cloud.google.com/sql/instances/locations/overview เพื่อค้นหาชื่อการเชื่อมต่ออินสแตนซ์
  2. วางโค้ดต่อไปนี้ที่ส่วนท้ายของ app.yaml
  3. แทนที่ YOUR_DB_PASSWORD_HERE ด้วยรหัสผ่านที่คุณสร้างสำหรับชื่อผู้ใช้ postgres ก่อนหน้านี้
  4. แทนที่ YOUR_CONNECTION_NAME_HERE ด้วยค่าจากขั้นตอนที่ 1

app.yaml

# ...
# Set environment variables
env_variables:
    DB_USER: postgres
    DB_PASS: YOUR_DB_PASSWORD_HERE
    DB_NAME: postgres
    DB_TCP_HOST: 172.17.0.1
    DB_PORT: 5432

#Enable TCP Port
# You can look up your instance connection name by going to the page for
# your instance in the Cloud Console here : https://console.cloud.google.com/sql/instances/
beta_settings:
  cloud_sql_instances: YOUR_CONNECTION_NAME_HERE=tcp:5432

โปรดทราบว่า DB_TCP_HOST ควรมีค่าเป็น 172.17.0.1 เนื่องจากแอปนี้เชื่อมต่อผ่าน AppEngine Flex** เนื่องจากจะสื่อสารกับ Cloud SQL ผ่านพร็อกซีในลักษณะเดียวกับที่คุณเคยทำ

เพิ่มสิทธิ์ไคลเอ็นต์ SQL ให้บัญชีบริการ App Engine Flex

ไปที่หน้าผู้ดูแลระบบ IAM ใน Cloud Console แล้วมองหาบัญชีบริการที่มีชื่อตรงกับรูปแบบ service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com นี่คือบัญชีบริการที่ App Engine Flex จะใช้เพื่อเชื่อมต่อกับฐานข้อมูล คลิกปุ่มแก้ไขที่ท้ายแถว แล้วเพิ่มบทบาท "ไคลเอ็นต์ Cloud SQL"

b04ccc0b4022b905.png

คัดลอกโค้ดโปรเจ็กต์ไปยังเส้นทาง Go

AppEngine ต้องค้นหาไฟล์ที่เกี่ยวข้องในเส้นทาง Go ได้จึงจะเรียกใช้โค้ดได้ ตรวจสอบว่าคุณอยู่ในไดเรกทอรีรูทของโปรเจ็กต์

cd YOUR_PROJECT_ROOT

คัดลอกไดเรกทอรีไปยังเส้นทาง Go

mkdir -p ~/gopath/src/austin-recycling
cp -r ./ ~/gopath/src/austin-recycling

เปลี่ยนเป็นไดเรกทอรีนั้น

cd ~/gopath/src/austin-recycling

ทําให้แอปใช้งานได้

ใช้ gcloud CLI เพื่อทำให้แอปใช้งานได้ โดยระบบจะใช้เวลาสักครู่ในการทำให้ใช้งานได้

gcloud app deploy

ใช้คำสั่ง browse เพื่อรับลิงก์ที่คุณคลิกเพื่อดูเครื่องมือระบุตำแหน่งร้านค้าที่สวยงามระดับองค์กรซึ่งติดตั้งใช้งานอย่างเต็มรูปแบบ

gcloud app browse

หากคุณเรียกใช้ gcloud นอก Cloud Shell การเรียกใช้ gcloud app browse จะเปิดแท็บเบราว์เซอร์ใหม่

12. (แนะนำ) ล้างข้อมูล

การทำ Codelab นี้จะอยู่ภายในขีดจำกัดของระดับฟรีสำหรับการประมวลผล BigQuery และการเรียกใช้ Maps Platform API แต่หากคุณทำเพื่อเป็นแบบฝึกหัดเพื่อการศึกษาเท่านั้นและต้องการหลีกเลี่ยงค่าใช้จ่ายในอนาคต วิธีที่ง่ายที่สุดในการลบทรัพยากรที่เชื่อมโยงกับโปรเจ็กต์นี้คือการลบโปรเจ็กต์เอง

ลบโปรเจ็กต์

ในคอนโซล GCP ให้ไปที่หน้า Cloud Resource Manager

ในรายการโปรเจ็กต์ ให้เลือกโปรเจ็กต์ที่เราใช้ทำงานอยู่ แล้วคลิกลบ ระบบจะแจ้งให้คุณพิมพ์รหัสโปรเจ็กต์ ป้อนรหัสแล้วคลิกปิด

หรือคุณจะลบทั้งโปรเจ็กต์ออกจาก Cloud Shell โดยตรงด้วย gcloud โดยเรียกใช้คำสั่งต่อไปนี้และแทนที่ตัวยึดตำแหน่ง GOOGLE_CLOUD_PROJECT ด้วยรหัสโปรเจ็กต์ของคุณก็ได้

gcloud projects delete GOOGLE_CLOUD_PROJECT

13. ขอแสดงความยินดี

ยินดีด้วย คุณทำ Codelab เสร็จสมบูรณ์แล้ว

หรือคุณข้ามไปที่หน้าสุดท้าย ยินดีด้วย คุณเลื่อนไปที่หน้าสุดท้ายแล้ว

ใน Codelab นี้ คุณได้ทำงานกับเทคโนโลยีต่อไปนี้

อ่านเพิ่มเติม

เรายังต้องเรียนรู้เกี่ยวกับเทคโนโลยีเหล่านี้อีกมาก ด้านล่างนี้คือลิงก์ที่เป็นประโยชน์สำหรับหัวข้อที่เราไม่มีเวลาพูดถึงในโค้ดแล็บนี้ แต่ก็อาจเป็นประโยชน์สำหรับคุณในการสร้างโซลูชันเครื่องมือระบุตำแหน่งร้านค้าที่เหมาะกับความต้องการเฉพาะของคุณ