สร้างตัวระบุตําแหน่งร้านค้าแบบครบวงจรด้วย Google Maps Platform และ Google Cloud

1. บทนำ

บทคัดย่อ

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

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

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

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

ไฟล์ 2ece59c64c06e9da.png

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

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

  • Maps JavaScript API: แสดงตําแหน่งจํานวนมากบนแผนที่เว็บที่กําหนดเอง
  • GeoJSON: รูปแบบที่จัดเก็บข้อมูลเมตาเกี่ยวกับสถานที่ตั้ง
  • เติมข้อความอัตโนมัติในสถานที่: ช่วยให้ผู้ใช้เริ่มต้นสถานที่ได้อย่างรวดเร็วและแม่นยํายิ่งขึ้น
  • 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

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

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

5f504766b9b3be17.png

ซึ่งจะเป็นการเปิด Shell ใหม่ในส่วนล่างของเบราว์เซอร์หลังจากอาจแสดงคั่นระหว่างหน้าคั่นระหว่างหน้า

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>

เปิดใช้ AppEngine Flex API

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

3. สวัสดีทุกคน

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

ในอินสแตนซ์ Cloud Shell คุณจะเริ่มจากการสร้างแอป Go App Engine Flex ที่จะใช้เป็นฐานของ Codelab ส่วนที่เหลือ

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

b63f7baad67b6601.png

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

3f6625ff8461c551.png

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

mkdir -p austin-recycling && cd $_

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

นอกจากนี้ ไดเรกทอรี austin-recycling ควรปรากฏในรายการโฟลเดอร์ของ Editor&#39 ทางด้านซ้าย สร้างไฟล์ชื่อ app.yaml ในไดเรกทอรี austin-recycling ใส่เนื้อหาต่อไปนี้ในไฟล์ 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 ที่ฟังบนพอร์ต localhost 8080 คุณสามารถเปิดแท็บเว็บเบราว์เซอร์ในแอปนี้ได้โดยกดปุ่มแสดงตัวอย่างเว็บ และเลือกรายการในเมนูแสดงตัวอย่างบนพอร์ต 8080 ในแถบเครื่องมือ Cloud Shell

4155fc1dc717ac67.png

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

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

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

GeoJSON, ภาษาสื่อกลางของ GIS ของโลก

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

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

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

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

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

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

ดึงข้อมูล

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

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

เริ่มต้นด้วยการดาวน์โหลดข้อมูล GeoJSON จากเว็บไซต์ City of Austin ลงในแอปของคุณ

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

ในตอนนี้ ให้ใช้ URL เพื่อเรียกข้อมูลตําแหน่งการรีไซเคิล

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

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

cd ..

5. จับคู่สถานที่

ก่อนอื่นให้อัปเดตไฟล์ app.yaml ให้สอดคล้องกับแอป Hello World ที่มีประสิทธิภาพมากขึ้น ไม่ใช่เพียงแอป 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 เนื่องจาก GeoJSON จะได้รับการประมวลผลและส่งโดยแบ็กเอนด์ของ Go เพื่อให้เราสร้างฟีเจอร์สุดหรูบางอย่างในขั้นตอนต่อๆ ไปได้ เปลี่ยนไฟล์ main.go เป็นดังนี้

main.go

package main

import (
        "fmt"
        "io/ioutil"
        "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 := ioutil.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>

ใส่ใจเป็นพิเศษกับ URL src ในแท็กสคริปต์ขององค์ประกอบ head

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

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 - เริ่มต้น

  // 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 ด้วยเวอร์ชันที่นานขึ้นเล็กน้อยนี้ ซึ่งตอนนี้จะนําหน้าต่างข้อมูลเป็นอาร์กิวเมนต์ที่ 3

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 ที่มีข้อมูลของร้านค้าที่เลือกเมื่อมีการคลิกเครื่องหมายร้านค้าบนแผนที่

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

.af0ab72ad0eadc5.png

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

ผู้ใช้เครื่องระบุตําแหน่งร้านมักจะต้องการทราบว่าร้านใดอยู่ใกล้มากที่สุดหรือที่อยู่ที่วางแผนจะเริ่มออกเดินทาง เพิ่มแถบค้นหาการเติมข้อความสถานที่อัตโนมัติเพื่อให้ผู้ใช้ป้อนที่อยู่เริ่มต้นได้อย่างง่ายดาย การเติมข้อความอัตโนมัติภายในสถานที่ช่วยให้ฟังก์ชัน typeahead คล้ายกับการทํางานการเติมข้อความอัตโนมัติในแถบค้นหาอื่นๆ ของ Google ยกเว้นการคาดคะเนคือ Places ทั้งหมดใน 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
};

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

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

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

app.js - เริ่มต้น

 // 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 และอัปเดตแบ็กเอนด์ "ไป" เพื่อดึงผลลัพธ์จากฐานข้อมูลนั้นแทนจากแคชในเครื่องเมื่อมีคําขอเข้ามา

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

คุณสร้างอินสแตนซ์ 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 บ่งบอกว่าอินสแตนซ์ที่กําลังสร้างควรมี WHOIS จํานวน 1 รายการ และมีหน่วยความจําประมาณ 3.75 GB

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

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

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

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

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

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

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

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

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

CREATE EXTENSION postgis;

หากสําเร็จ เอาต์พุตควรจะอ่าน "สร้างส่วนขยาย" ดังที่แสดงด้านล่าง

ตัวอย่างเอาต์พุตจากคําสั่ง

CREATE EXTENSION

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

\q

นําเข้าข้อมูลทางภูมิศาสตร์ลงในฐานข้อมูล

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

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

สร้างไฟล์ Dump ของ SQL

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

sudo apt-get install gdal-bin

จากนั้นใช้ ogr2ogr เพื่อสร้างไฟล์ Dump ของ SQL ไฟล์จะสร้างตารางที่ชื่อว่า 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

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

ตัวอย่างเอาต์พุตจากคําสั่ง

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

อัปเดตการย้อนกลับเพื่อใช้ Cloud SQL

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

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

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

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

app.js -fetchStore

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 ให้ค้นหาโค้ดเรียกกลับสําหรับ Listener place_changed ยกเลิกการแสดงความคิดเห็นที่ล้างวงกลมที่มีอยู่ออก เพื่อให้เส้นนี้ทํางานทุกครั้งที่ผู้ใช้เลือกที่อยู่จากการเติมข้อความอัตโนมัติของ Search Place

app.js - initAutocompleteWidget

  autocomplete.addListener("place_changed", async () => {
    circles.forEach((c) => c.setMap(null)); // clear existing stores
    // ...
  1. เมื่อใดก็ตามที่มีการเปลี่ยนแปลงต้นทางที่เลือกไว้ ระบบจะอัปเดต originorigin ของตัวแปร ที่ส่วนท้ายของ "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 เพื่อนําโค้ดที่โหลดและแคชไฟล์ JSON ออก และเรายังกําจัดฟังก์ชัน 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 ในไดเรกทอรีการรีไซเคิลออสตินด้วย เริ่มต้นด้วยการใช้เครื่องจัดการอีกครั้งสําหรับคําขอตําแหน่ง

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 เหล่านั้นลงใน URL ได้ไหม )
  • โดยจะเริ่มการทํางานของการเรียก getGeoJsonFromDatabase ซึ่งจะแสดงผลสตริง GeoJSON (เราจะเขียนในภายหลัง)
  • โดยจะใช้ ResponseWriter เพื่อพิมพ์สตริง GeoJSON ดังกล่าวไปยังการตอบกลับ

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

สร้างพูลการเชื่อมต่อ

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

ในตอนท้ายของ locations.go ให้สร้างฟังก์ชัน initConnectionPool (เรียกใช้จาก main.go) ซึ่งจะเริ่มต้นกลุ่มการเชื่อมต่อ เพื่อความชัดเจน ระบบจะใช้วิธีการช่วยเหลือบางส่วนในข้อมูลโค้ดนี้ 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 จริง ซึ่งทําหน้าที่ที่น่าสนใจหลายอย่างในฐานข้อมูล คุณจึงไม่ต้องกังวลเรื่องการติดตั้งใช้งานโค้ดเหล่านี้เลย

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

แยกวิเคราะห์ 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 ในออบเจ็กต์ GeoJSON FeatureCollection

เพิ่มไลบรารี PGX ในโครงการ

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

go mod init my_locator

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

go mod tidy

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

go mod vendor

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

ทดสอบเลย

โอเค เราทําเสร็จหมดแล้ว มาดูกัน

เราจะใช้พร็อกซี Cloud SQL เพื่อจัดการการเชื่อมต่อฐานข้อมูลเพื่อให้เครื่องสําหรับการพัฒนา (ใช่ แม้กระทั่ง Cloud Shell) เชื่อมต่อกับฐานข้อมูลได้ วิธีตั้งค่าพร็อกซี 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 โดยป้อนต้นทาง 1 แห่งและจุดหมายเดียวเพื่อรับเส้นทางระหว่างทั้ง 2 แห่ง DISTANCE Matrix API นี้ใช้แนวคิดนี้ในการระบุการจับคู่ข้อมูลที่เหมาะสมที่สุดระหว่างต้นทางที่เป็นไปได้หลายแห่งและปลายทางหลายแห่งที่เป็นไปได้โดยอิงตามเวลาเดินทางและระยะทาง ในกรณีนี้ เพื่อช่วยให้ผู้ใช้ค้นหาร้านค้าที่ใกล้ที่สุดโดยใช้ที่อยู่ที่เลือกไว้ คุณต้องระบุต้นทาง 1 แห่งและอาร์เรย์ของตําแหน่งร้านค้าเป็นปลายทาง

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

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

app.js - initMaps

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 เพื่อคํานวณระยะทางจากร้านค้าเมื่อใดก็ตามที่เลือกต้นทางใหม่จากแถบค้นหาการเติมข้อความอัตโนมัติของสถานที่ ที่ด้านล่างของฟังก์ชัน 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 ในช่องค้นหา แล้วพิมพ์ " Map Management" คลิกผลการค้นหา "Maps Management (Google Maps)"64036dd0ed200200.png

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

70f55a759b4c4212.png

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

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

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

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

9284cd200f1a9223.png

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

  1. หากคุณต้องการจับคู่การจัดรูปแบบในแผนที่ที่แสดงในห้องทดลองนี้ ให้คลิกแท็บ "IMPORT JSON" แล้ววาง BLOB ของ JSON ด้านล่าง หรือหากต้องการสร้างรูปแบบของตนเอง ให้เลือกรูปแบบแผนที่ที่ต้องการเริ่มต้นด้วย แล้วคลิกถัดไป
  2. เลือกรหัสแผนที่ที่คุณเพิ่งสร้างเพื่อเชื่อมโยงรหัสแผนที่นั้นกับสไตล์นี้ แล้วคลิกถัดไปอีกครั้ง
  3. ณ จุดนี้ คุณจะมีตัวเลือกในการปรับแต่งการจัดรูปแบบแผนที่ของคุณเพิ่มเติม หากต้องการสํารวจประเทศเหล่านี้ ให้คลิกปรับแต่งในตัวแก้ไขรูปแบบ แล้วลองใช้ตัวเลือกสีและ AMP จนกว่าจะมีรูปแบบแผนที่ที่ชอบ หรือคลิกข้าม
  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 - initMaps

...

// 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. ทําให้ใช้งานได้กับเวอร์ชันที่ใช้งานจริง

หากคุณต้องการให้แอปทํางานจาก AppEngine 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 ให้กับบัญชีบริการ AppEngine Flex

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

b04ccc0b4022b905

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

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

cd YOUR_PROJECT_ROOT

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

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 นี้ คุณได้ร่วมงานกับเทคโนโลยีต่อไปนี้

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

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