Membuat pencari toko full stack dengan Google Maps Platform dan Google Cloud

1. Pengantar

Abstrak

Bayangkan Anda memiliki banyak tempat untuk diletakkan di peta dan Anda ingin pengguna dapat melihat lokasi tempat tersebut dan mengidentifikasi tempat yang ingin mereka kunjungi. Contoh umumnya mencakup:

  • pencari toko di situs retailer
  • peta lokasi pemungutan suara untuk pemilu mendatang
  • direktori lokasi khusus seperti tempat daur ulang baterai

Yang akan Anda buat

Di codelab ini, Anda akan membuat pencari lokasi (locator) yang didapat dari feed data langsung yang berisi lokasi khusus dan membantu pengguna menemukan lokasi paling dekat dengan titik awal mereka. Pencari lokasi full-stack dapat menangani jumlah tempat yang lebih besar dibandingkan pencari toko sederhana, yang dibatasi hingga maksimal 25 lokasi toko.

2ece59c64c06e9da.png

Yang akan Anda pelajari

Codelab ini menggunakan set data terbuka untuk menyimulasikan metadata tentang lokasi toko dalam jumlah besar yang diisi otomatis, sehingga Anda dapat fokus mempelajari konsep teknis utama.

  • Maps JavaScript API: menampilkan lokasi dalam jumlah besar pada peta web yang disesuaikan
  • GeoJSON: format yang menyimpan metadata tentang lokasi
  • Place Autocomplete: membantu pengguna memberikan lokasi awal dengan lebih cepat dan akurat
  • Go: Bahasa pemrograman yang digunakan untuk mengembangkan back-end aplikasi. Backend akan berinteraksi dengan database dan mengirimkan hasil kueri kembali ke front-end dalam JSON yang diformat.
  • App Engine: untuk menghosting aplikasi web

Prasyarat

  • Pengetahuan dasar tentang HTML dan JavaScript
  • Akun Google

2. Memulai persiapan

Pada Langkah 3 di bagian berikut, aktifkan Maps JavaScript API, Places API, dan Distance Matrix API untuk codelab ini.

Memulai Google Maps Platform

Jika Anda belum pernah menggunakan Google Maps Platform, ikuti panduan Memulai Google Maps Platform atau tonton playlist Memulai Google Maps Platform untuk menyelesaikan langkah-langkah berikut:

  1. Membuat akun penagihan.
  2. Membuat project.
  3. Mengaktifkan API dan SDK Google Maps Platform (tercantum di bagian sebelumnya).
  4. Membuat kunci API.

Mengaktifkan Cloud Shell

Di codelab ini, Anda menggunakan Cloud Shell, lingkungan command line yang berjalan di Google Cloud yang menyediakan akses ke produk dan resource yang berjalan di Google Cloud, sehingga Anda dapat menghosting dan menjalankan project sepenuhnya dari browser web.

Untuk mengaktifkan Cloud Shell dari Cloud Console, klik Aktifkan Cloud Shell 89665d8d348105cd.png (hanya perlu waktu beberapa saat untuk penyediaan dan koneksi ke lingkungan).

5f504766b9b3be17.png

Tindakan ini akan membuka shell baru di bagian bawah browser setelah kemungkinan menampilkan interstisial pengantar.

d3bb67d514893d1f.png

Mengonfirmasi project Anda

Setelah terhubung ke Cloud Shell, Anda akan melihat bahwa Anda telah diautentikasi dan project tersebut telah ditetapkan ke project ID yang Anda pilih selama penyiapan.

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

Jika project belum ditetapkan karena beberapa alasan, jalankan perintah berikut:

gcloud config set project <YOUR_PROJECT_ID>

Mengaktifkan App Engine Flex API

App Engine Flex API harus diaktifkan secara manual dari Cloud Console. Tindakan ini tidak hanya akan mengaktifkan API, namun juga membuat Akun Layanan Lingkungan Fleksibel App Engine, akun yang diautentikasi yang akan berinteraksi dengan layanan Google (seperti database SQL) atas nama pengguna.

3. Halo, Dunia

Backend: Halo Dunia dalam Go

Pada instance Cloud Shell, Anda akan memulai dengan membuat aplikasi Fleksibel App Engine Go yang akan berfungsi sebagai dasar untuk codelab lainnya.

Di toolbar Cloud Shell, klik tombol Open editor untuk membuka editor kode di tab baru. Editor kode berbasis web ini memungkinkan Anda mengedit file dengan mudah di instance Cloud Shell.

b63f7baad67b6601.png

Selanjutnya, klik ikon Buka di jendela baru untuk memindahkan editor dan terminal ke tab baru.

3f6625ff8461c551.png

Pada terminal di bagian bawah tab baru, buat direktori austin-recycling baru.

mkdir -p austin-recycling && cd $_

Selanjutnya, Anda akan membuat aplikasi Go App Engine kecil untuk memastikan semuanya berfungsi. Halo Dunia!

Direktori austin-recycling juga akan muncul dalam daftar folder Editor di sebelah kiri. Di direktori austin-recycling, buat file dengan nama app.yaml. Masukkan konten berikut dalam file app.yaml:

app.yaml

runtime: go
env: flex

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

File konfigurasi ini mengonfigurasi aplikasi App Engine Anda untuk menggunakan runtime Fleksibel Go. Untuk informasi latar belakang tentang arti item konfigurasi dalam file ini, lihat Dokumentasi Lingkungan Standar Google App Engine Go.

Selanjutnya, buat file main.go di samping file 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!")
}

Sebaiknya, kita berhenti sebentar di sini untuk memahami fungsi kode ini, setidaknya di tingkat tinggi. Anda telah menetapkan paket main yang memulai server http yang memproses port 8080, dan mendaftarkan fungsi pengendali untuk permintaan HTTP yang cocok dengan jalur "/".

Fungsi pengendali, yang disebut handler, menulis string teks "Hello, world!". Teks ini akan direlai kembali ke browser Anda, tempat Anda dapat membacanya. Di langkah selanjutnya, Anda akan membuat pengendali yang merespons dengan data GeoJSON, dan bukan string hard-code sederhana.

Setelah melakukan langkah-langkah ini, sekarang Anda akan memiliki editor yang terlihat seperti ini:

2084fdd5ef594ece.png

Melakukan pengujian

Untuk menguji aplikasi ini, Anda dapat menjalankan server pengembangan App Engine di dalam instance Cloud Shell. Kembali ke command line Cloud Shell, lalu ketik:

go run *.go

Anda akan melihat beberapa baris log output yang menunjukkan bahwa Anda benar-benar menjalankan server pengembangan pada instance Cloud Shell, dengan aplikasi web halo dunia yang memproses port localhost 8080. Anda dapat membuka tab browser web di aplikasi ini dengan menekan tombol Web Preview dan memilih item menu Preview on port 8080 di toolbar Cloud Shell.

4155fc1dc717ac67.png

Mengklik item menu ini akan membuka tab baru di browser web Anda dengan kata "Hello, world!" yang ditayangkan dari server pengembangan App Engine.

Pada langkah berikutnya, Anda akan menambahkan data daur ulang Kota Austin ke aplikasi ini, dan mulai memvisualisasikannya.

4. Mendapatkan data saat ini

GeoJSON, lingua franca di dunia GIS

Di langkah sebelumnya disebutkan bahwa Anda akan membuat pengendali di kode Go yang merender data GeoJSON ke browser web. Namun, apa yang dimaksud dengan GeoJSON?

Dalam dunia Sistem Informasi Geografis (GIS), kita harus dapat menyampaikan pengetahuan tentang entitas geografis di antara sistem komputer. Peta sangat mudah dibaca oleh manusia, namun komputer biasanya lebih memilih datanya dalam format yang lebih mudah dicerna.

GeoJSON adalah format untuk mengenkode struktur data geografis, seperti koordinat lokasi pembuangan sampah daur ulang di Austin, Texas. GeoJSON telah distandardisasi dalam standar Internet Engineering Task Force yang disebut RFC7946. GeoJSON didefinisikan dalam konteks JSON, JavaScript Object Notation, yang telah distandardisasi dalam ECMA-404, oleh organisasi yang sama yang menetapkan standar JavaScript, yaitu Ecma International.

Yang penting adalah GeoJSON merupakan format kawat yang didukung secara luas untuk menyampaikan pengetahuan geografis. Codelab ini menggunakan GeoJSON dalam cara berikut:

  • Menggunakan paket Go untuk mengurai data Austin ke dalam struktur data khusus GIS internal yang akan Anda gunakan untuk memfilter data yang diminta.
  • Melakukan serialisasi data yang diminta untuk transit antara server web dan browser web.
  • Menggunakan library JavaScript untuk mengonversi respons menjadi penanda pada peta.

Ini akan mengurangi secara signifikan jumlah pengetikan kode, karena Anda tidak perlu menulis parser dan generator untuk mengonversi aliran data pada kawat menjadi representasi dalam memori.

Mengambil data

Portal Data Terbuka Kota Austin, Texas menyediakan informasi geospasial tentang resource publik yang tersedia untuk digunakan oleh masyarakat. Di codelab ini, Anda akan memvisualisasikan set data lokasi pembuangan sampah daur ulang.

Anda akan memvisualisasikan data dengan penanda pada peta, yang dirender menggunakan Lapisan data Maps JavaScript API.

Mulailah dengan mendownload data GeoJSON dari situs Kota Austin di aplikasi Anda.

  1. Di jendela command line instance Cloud Shell, matikan server dengan mengetik [CTRL] + [C].
  2. Buat direktori data di dalam direktori austin-recycling, lalu ubah ke direktori tersebut:
mkdir -p data && cd data

Sekarang gunakan curl untuk mengambil lokasi daur ulang:

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

Terakhir, ubah kembali ke direktori induk.

cd ..

5. Memetakan lokasi

Pertama, update file app.yaml untuk mencerminkan aplikasi lebih kokoh yang akan Anda buat, dan "bukan sekadar aplikasi halo dunia yang biasa".

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

Konfigurasi app.yaml ini mengarahkan permintaan untuk /, /*.js, /*.css, dan /*.html ke kumpulan file statis. Ini berarti bahwa komponen HTML statis aplikasi Anda akan ditayangkan langsung oleh infrastruktur penayangan file App Engine, dan bukan aplikasi Go. Ini akan mengurangi beban server dan meningkatkan kecepatan penayangan.

Sekarang saatnya membuat backend aplikasi Anda di Go.

Membuat backend

Anda mungkin telah memperhatikan bahwa satu hal menarik yang tidak dilakukan file app.yaml adalah mengekspos file GeoJSON. Hal itu karena GeoJSON akan diproses dan dikirim oleh backend Go, sehingga kita dapat membuat beberapa fitur menarik pada langkah-langkah berikutnya. Ubah file main.go Anda agar terbaca sebagai berikut:

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"])
}

Backend Go sudah memberi kita fitur yang berharga: Instance AppEngine menyimpan di cache semua lokasi tersebut segera setelah dimulai. Ini akan menghemat waktu karena backend tidak perlu membaca file dari disk setiap kali pengguna melakukan refresh.

Membuat frontend

Hal pertama yang perlu dilakukan adalah membuat folder untuk menyimpan semua aset statis kita. Dari folder induk project Anda, buat folder static.

mkdir -p static && cd static

Kita akan membuat 3 file dalam folder ini.

  • index.html akan berisi semua HTML untuk aplikasi pencari toko satu halaman Anda.
  • style.css akan berisi gaya visual, seperti yang Anda harapkan
  • app.js akan bertanggung jawab untuk mengambil GeoJSON, melakukan panggilan ke Maps API, dan menempatkan penanda di peta kustom Anda.

Buat 3 file ini, dan pastikan untuk meletakkan semuanya di 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>

Berikan perhatian khusus pada URL src dalam tag skrip di elemen head.

  • Ganti teks placeholder "YOUR_API_KEY" dengan kunci API yang Anda buat selama langkah penyiapan. Anda dapat membuka halaman APIs & Services -> Credentials di Cloud Console untuk mengambil kunci API atau membuat kunci API yang baru.
  • Perhatikan bahwa URL berisi parameter callback=initialize. Sekarang kita akan membuat file JavaScript yang berisi fungsi callback tersebut. Ini adalah tempat aplikasi Anda akan memuat lokasi dari backend, mengirimkannya ke Maps API, dan menggunakan hasilnya untuk menandai lokasi kustom di peta, semua dirender dengan sempurna ke halaman web Anda.
  • Parameter libraries=places memuat Places Library, yang diperlukan untuk fitur seperti pelengkapan otomatis alamat yang akan ditambahkan nanti.

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;
};

Kode ini merender lokasi toko pada peta. Untuk menguji apa yang sudah kita miliki sejauh ini, dari command line, kembali ke direktori induk:

cd ..

Sekarang, jalankan aplikasi Anda dalam mode pengembangan lagi menggunakan:

go run *.go

Lihat pratinjaunya seperti yang Anda lakukan sebelumnya. Anda akan melihat peta dengan lingkaran hijau kecil seperti ini.

58a6680e9c8e7396.png

Anda sudah merender lokasi peta, dan kita baru setengah jalan menyelesaikan codelab. Luar biasa. Sekarang, mari kita tambahkan beberapa interaktivitas.

6. Menampilkan detail sesuai permintaan

Merespons peristiwa klik pada penanda peta

Menampilkan sekumpulan penanda pada peta adalah awal yang bagus, namun kita benar-benar menginginkan pengunjung dapat mengklik salah satu penanda dan melihat informasi tentang lokasi tersebut (seperti nama bisnis, alamat, dsb). Nama jendela informasi kecil yang biasanya pop-up saat Anda mengklik penanda Google Maps adalah Jendela Info.

Buat objek infoWindow. Tambahkan hal berikut ke fungsi initialize, menggantikan baris komentar yang bertuliskan "// 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();

Ganti definisi fungsi fetchAndRenderStores dengan versi yang sedikit berbeda ini, yang mengubah baris terakhir untuk memanggil storeToCircle dengan argumen tambahan, 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));
};

Ganti definisi storeToCircle dengan versi yang sedikit lebih panjang ini, yang sekarang menggunakan Jendela Info sebagai argumen ketiga:

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;
};

Kode baru di atas menampilkan infoWindow dengan informasi toko yang dipilih setiap kali penanda toko pada peta diklik.

Jika server Anda masih berjalan, hentikan dan mulai ulang. Muat ulang halaman peta Anda dan coba klik penanda peta. Jendela info kecil akan pop-up dengan nama dan alamat bisnis, yang terlihat seperti ini:

1af0ab72ad0eadc5.png

7. Mendapatkan lokasi awal pengguna

Pengguna pencari toko biasanya ingin mengetahui toko mana yang paling dekat dengan posisi mereka atau alamat tempat mereka berencana memulai perjalanan. Tambahkan kotak penelusuran Place Autocomplete agar pengguna dapat memasukkan alamat titik awal dengan mudah. Place Autocomplete menyediakan fungsi prediksi sebelum mengetik yang mirip dengan cara kerja Autocomplete di kotak penelusuran Google lainnya, dengan pengecualian prediksinya adalah semua Tempat di Google Maps Platform.

Membuat kolom input pengguna

Kembali untuk mengedit style.css guna menambahkan gaya visual untuk kotak penelusuran Autocomplete dan panel samping hasil yang terkait. Saat memperbarui gaya CSS, kita juga akan menambahkan gaya untuk sidebar berikutnya yang menampilkan informasi toko sebagai daftar untuk melengkapi peta.

Tambahkan kode ini ke bagian akhir file.

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;
}

Kotak penelusuran Autocomplete dan panel slideout awalnya tersembunyi hingga diperlukan.

Siapkan div untuk widget Autocomplete dengan mengganti komentar di index.html yang bertuliskan "<!-- Autocomplete div goes here -->" dengan kode berikut. Saat melakukan pengeditan ini, kita juga akan menambahkan div untuk panel slideout.

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>

Sekarang, tentukan fungsi untuk menambahkan widget Autocomplete ke peta dengan menambahkan kode berikut ke akhir 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
};

Kode ini membatasi saran Autocomplete untuk hanya menampilkan alamat (karena Place Autocomplete dapat mencocokkan nama bangunan dan lokasi administratif) dan membatasi alamat yang ditampilkan hanya kepada orang yang berada di AS. Menambahkan spesifikasi opsional ini akan mengurangi jumlah karakter yang harus dimasukkan pengguna untuk mempersempit prediksi agar dapat menampilkan alamat yang mereka cari.

Kemudian, kode ini memindahkan div Autocomplete yang telah Anda buat ke sudut kanan atas peta dan menentukan kolom mana yang harus ditampilkan tentang setiap Tempat dalam respons.

Terakhir, panggil fungsi initAutocompleteWidget di akhir fungsi initialize, menggantikan komentar yang bertuliskan "// TODO: Initialize the Autocomplete widget".

app.js - initialize

 // Initialize the Places Autocomplete Widget
 initAutocompleteWidget();

Mulai ulang server Anda dengan menjalankan perintah berikut, lalu muat ulang pratinjau.

go run *.go

Anda akan melihat widget Autocomplete di sudut kanan atas peta, yang menunjukkan alamat di AS yang cocok dengan teks yang Anda ketik, yang dicondongkan ke area yang terlihat di peta.

58e9bbbcc4bf18d1.png

Mengupdate Peta Saat Pengguna Memilih Alamat Awal

Sekarang, Anda perlu mengatur kapan pengguna memilih prediksi dari widget Autocomplete dan menggunakan lokasi tersebut sebagai dasar untuk menghitung jarak ke toko Anda.

Tambahkan kode berikut ke akhir initAutocompleteWidget di app.js, menggantikan komentar "// 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
  });

Kode ini menambahkan pemroses sehingga saat pengguna mengklik salah satu saran, peta akan kembali ke pusat di alamat yang dipilih dan menetapkan tempat asal sebagai dasar penghitungan jarak Anda. Anda menerapkan penghitungan jarak pada langkah berikutnya.

Hentikan dan mulai ulang server Anda, lalu muat ulang pratinjau untuk mengamati pemusatan kembali peta setelah Anda memasukkan alamat ke dalam kotak penelusuran pelengkapan otomatis.

8. Menskalakan dengan Cloud SQL

Sejauh ini, kita memiliki pencari toko yang cukup andal. Aplikasi ini memanfaatkan fakta bahwa hanya ada sekitar seratus lokasi yang akan digunakan aplikasi, dengan memuatnya ke memori di backend (bukannya membaca dari file berulang kali.). Namun, bagaimana jika pencari lokasi harus beroperasi pada skala yang berbeda? Jika Anda memiliki ratusan lokasi yang tersebar di sekeliling area geografis yang luas (atau ribuan lokasi di seluruh dunia), menyimpan semua lokasi tersebut dalam memori bukan lagi ide terbaik, dan membagi zona menjadi file individual akan menimbulkan masalah tersendiri.

Kini saatnya untuk memuat lokasi Anda dari database. Untuk langkah ini, kita akan memigrasikan semua lokasi di file GeoJSON ke dalam database Cloud SQL, dan mengupdate backend Go untuk mengambil hasil dari database tersebut, bukan dari cache lokalnya setiap kali permintaan masuk.

Membuat Instance Cloud SQL dengan Database PostGres

Anda dapat membuat instance Cloud SQL melalui Google Cloud Console, tetapi akan lebih mudah menggunakan utilitas gcloud untuk membuat instance dari command line. Di Cloud Shell, buat instance Cloud SQL dengan perintah berikut:

gcloud sql instances create locations \
--database-version=POSTGRES_12 \
--tier=db-custom-1-3840 --region=us-central1
  • Argumen locations adalah nama yang kami pilih untuk diberikan pada instance Cloud SQL ini.
  • Tanda tier adalah cara untuk memilih dari beberapa komputer yang telah ditentukan dengan mudah.
  • Nilai db-custom-1-3840 menunjukkan instance yang dibuat harus memiliki satu vCPU dan memori sekitar 3,75 GB.

Instance Cloud SQL akan dibuat dan diinisialisasi dengan database PostGresSQL, dengan pengguna default postgres. Apa sandi pengguna ini? Pertanyaan bagus. Pengguna belum memiliki sandi. Anda perlu mengonfigurasikannya sebelum login.

Tetapkan sandi dengan perintah berikut:

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

Lalu masukkan sandi yang Anda pilih saat diminta untuk melakukannya.

Mengaktifkan Ekstensi PostGIS

PostGIS adalah ekstensi untuk PostGresSQL yang memudahkan penyimpanan jenis data geospasial standar. Dalam keadaan normal, kita harus melalui seluruh proses penginstalan untuk menambahkan PostGIS ke database. Untungnya, ini adalah salah satu ekstensi yang didukung Cloud SQL untuk PostGresSQL.

Hubungkan ke instance database dengan login sebagai pengguna postgres lewat perintah berikut di terminal Cloud Shell.

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

Masukkan sandi yang baru saja Anda buat. Sekarang, tambahkan ekstensi PostGIS pada command prompt postgres=>.

CREATE EXTENSION postgis;

Jika berhasil, output akan menampilkan CREATE EXTENSION, seperti yang ditunjukkan di bawah.

Contoh output perintah

CREATE EXTENSION

Terakhir, keluar dari koneksi database dengan memasukkan perintah keluar pada command prompt postgres=>.

\q

Mengimpor Data Geografi ke dalam Database

Sekarang, kita perlu mengimpor semua data lokasi tersebut dari file GeoJSON ke dalam database baru.

Untungnya, ini adalah masalah yang umum dan ada beberapa alat yang dapat ditemukan di internet untuk mengotomatiskannya untuk Anda. Kita akan menggunakan alat yang disebut ogr2ogr yang melakukan konversi di antara beberapa format umum untuk menyimpan data geospasial. Di antara opsi tersebut adalah mengonversi dari GeoJSON ke file dump SQL, persis seperti dugaan Anda. Selanjutnya, file dump SQL dapat digunakan untuk membuat tabel & kolom untuk database tersebut, dan memuatnya dengan semua data yang ada di file GeoJSON Anda.

Membuat file dump SQL

Pertama, instal ogr2ogr.

sudo apt-get install gdal-bin

Lalu, gunakan ogr2ogr untuk membuat file dump SQL. File ini akan membuat tabel yang disebut austinrecycling.

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

Perintah di atas didasarkan pada pengoperasian dari folder austin-recycling. Jika Anda perlu menjalankannya dari direktori lain, ganti data dengan jalur ke direktori tempat recycling-locations.geojson disimpan.

Mengisi database Anda dengan lokasi daur ulang

Setelah menyelesaikan perintah terakhir tersebut, sekarang Anda memiliki sebuah file, datadump.sql, di direktori yang sama tempat Anda menjalankan perintah. Jika dibuka, Anda akan melihat seratus lebih baris SQL, yang membuat tabel austinrecycling dan mengisinya dengan lokasi.

Sekarang, buka koneksi ke database dan jalankan skrip tersebut dengan perintah berikut.

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

Jika skrip berhasil dijalankan, berikut adalah bentuk tampilan beberapa baris terakhir output:

Contoh output perintah

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

Mengupdate backend Go untuk menggunakan Cloud SQL

Setelah kita memiliki semua data ini di database, kini saatnya mengupdate kode.

Mengupdate frontend untuk mengirim informasi lokasi

Mari kita mulai dengan satu update kecil pada front-end: Karena sekarang kita menulis aplikasi ini untuk skala terbatas agar setiap lokasi tidak dikirim ke front-end setiap kali kueri dijalankan, maka kita perlu meneruskan beberapa informasi dasar dari front-end tentang lokasi yang diprioritaskan oleh pengguna.

Buka app.js dan ganti definisi fungsi fetchStores dengan versi ini untuk menyertakan lintang dan bujur yang diinginkan dalam 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();
};

Setelah menyelesaikan langkah ini dalam codelab, respons hanya akan menampilkan toko yang terdekat dengan koordinat peta yang diberikan dalam parameter center. Untuk pengambilan awal dalam fungsi initialize, kode contoh yang diberikan di lab ini menggunakan koordinat pusat untuk Austin, Texas.

Karena sekarang fetchStores hanya akan menampilkan subkumpulan lokasi toko, kita perlu mengambil ulang toko setiap kali pengguna mengubah lokasi awalnya.

Update fungsi initAutocompleteWidget untuk memuat ulang lokasi setiap kali titik asal baru ditetapkan. Ini akan membutuhkan dua hasil edit:

  1. Dalam initAutocompleteWidget, temukan callback untuk pemroses place_changed. Batalkan komentar pada baris yang menghapus lingkaran yang ada, sehingga baris tersebut akan berjalan setiap kali pengguna memilih alamat dari kotak penelusuran Place Autocomplete.

app.js - initAutocompleteWidget

  autocomplete.addListener("place_changed", async () => {
    circles.forEach((c) => c.setMap(null)); // clear existing stores
    // ...
  1. Setiap kali titik awal yang dipilih berubah, variabel originLocation akan diupdate. Di akhir callback "place_changed", hapus komentar pada baris di atas baris "// TODO: Calculate the closest stores" untuk meneruskan titik asal baru ini untuk panggilan baru ke fungsi fetchAndRenderStores.

app.js - initAutocompleteWidget

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

Mengupdate backend untuk menggunakan CloudSQL, bukan file JSON datar

Menghapus pembacaan dan penyimpanan file GeoJSON datar dalam cache

Pertama, ubah main.go untuk menghapus kode yang memuat dan menyimpan file GeoJSON datar dalam cache. Kita juga dapat menghapus fungsi dropoffsHandler, karena kita akan menulis fungsi yang didukung Cloud SQL dalam file yang berbeda.

main.go baru Anda akan menjadi jauh lebih singkat.

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)
        }
}

Membuat pengendali baru untuk Permintaan Lokasi

Sekarang, mari kita buat file lain, locations.go, juga di direktori austin-recycling. Mulai dengan mengimplementasikan kembali pengendali untuk permintaan lokasi.

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)
}

Pengendali akan melakukan tugas penting berikut:

  • Mengambil lintang dan bujur dari objek permintaan (Ingat cara kita menambahkannya ke URL? )
  • Mengaktifkan panggilan getGeoJsonFromDatabase, yang menampilkan string GeoJSON (Kita akan menulisnya nanti.)
  • Menggunakan ResponseWriter untuk mencetak string GeoJSON tersebut ke respons.

Selanjutnya, kita akan membuat kumpulan koneksi untuk membantu penskalaan penggunaan database secara baik dengan pengguna simultan.

Membuat Kumpulan Koneksi

Kumpulan koneksi adalah kumpulan berisi koneksi database aktif yang dapat digunakan kembali oleh server untuk menangani permintaan pengguna. Ini akan menghapus banyak pengeluaran seiring dengan penyesuaian jumlah pengguna Anda, karena server tidak perlu menghabiskan waktu untuk membuat dan menghancurkan koneksi untuk setiap pengguna aktif. Anda mungkin telah melihat di bagian sebelumnya, kami mengimpor library github.com/jackc/pgx/stdlib. Ini adalah library populer untuk digunakan bersama kumpulan koneksi di Go.

Di akhir locations.go, buat fungsi initConnectionPool (dipanggil dari main.go) yang menginisialisasi kumpulan koneksi. Untuk lebih jelasnya, beberapa metode bantuan digunakan dalam cuplikan ini. configureConnectionPool menyediakan tempat praktis untuk menyesuaikan setelan kumpulan seperti jumlah koneksi dan masa pakai per koneksi. mustGetEnv menggabungkan panggilan untuk mendapatkan variabel lingkungan yang diperlukan, sehingga pesan error yang berguna dapat diberikan jika instance tidak memiliki informasi penting (seperti IP atau nama database yang akan dihubungkan).

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
}

Mengirimkan kueri database untuk lokasi, untuk mendapatkan JSON.

Sekarang, kita akan menulis kueri database yang mengambil koordinat peta dan menampilkan 25 lokasi terdekat. Tidak hanya itu, namun berkat beberapa fungsi database modern yang spesial, data tersebut akan ditampilkan sebagai GeoJSON. Hasil akhir dari semua ini adalah bahwa sejauh yang diketahui kode front-end, tidak ada yang berubah. Sebelumnya, fungsi mengaktifkan permintaan ke URL dan mendapatkan sekumpulan GeoJSON. Sekarang, fungsi mengaktifkan permintaan ke URL dan... mendapatkan sekumpulan GeoJSON.

Ini adalah fungsi yang luar biasa. Tambahkan fungsi berikut setelah pengendali dan kode penggabungan koneksi yang baru saja Anda tulis di bagian bawah 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
}

Fungsi ini umumnya merupakan penyiapan, pemutusan koneksi, dan penanganan error untuk mengaktifkan permintaan ke database. Mari kita lihat SQL yang sebenarnya, yang melakukan banyak hal yang sangat menarik di lapisan database, sehingga Anda tidak perlu lagi mengkhawatirkan penerapan salah satunya dalam kode.

Kueri mentah yang diaktifkan, setelah string diuraikan dan semua literal string yang disisipkan di tempat yang benar, terlihat seperti ini:

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

Kueri ini dapat dilihat sebagai satu kueri utama dan beberapa fungsi penggabungan JSON.

SELECT * ... LIMIT 25 memilih semua kolom untuk setiap lokasi. Kueri ini kemudian menggunakan fungsi ST_DISTANCE (bagian dari rangkaian PostGIS pada fungsi pengukuran geografi) untuk menentukan jarak antara setiap lokasi dalam database dan pasangan lintang/bujur lokasi yang diberikan oleh pengguna di front-end. Ingat bahwa tidak seperti Distance Matrix, yang bisa memberikan jarak tempuh, ini adalah jarak GeoSpatial. Agar efisien, kueri kemudian menggunakan jarak tersebut untuk mengurutkan dan menampilkan 25 lokasi terdekat dengan lokasi pengguna yang ditentukan.

**SELECT json_build_object(‘type', ‘F**eature') menggabungkan kueri sebelumnya, mengambil hasilnya, dan menggunakannya untuk membuat objek Feature GeoJSON. Tanpa diduga, kueri ini juga merupakan tempat radius maksimum diterapkan. Angka "16090" adalah jumlah meter yang sama dengan 10 mil, yaitu batas mutlak yang ditentukan oleh backend Go. Jika Anda ingin tahu mengapa klausa WHERE ini tidak ditambahkan ke kueri dalam (tempat jarak setiap lokasi akan ditentukan), hal itu karena cara SQL mengeksekusi di balik layar, kolom tersebut mungkin belum dihitung saat klausa WHERE diperiksa. Faktanya, jika Anda mencoba memindahkan klausa WHERE ini ke kueri dalam, error akan muncul.

**SELECT json_build_object(‘type', ‘FeatureColl**ection') Kueri ini menggabungkan semua baris yang dihasilkan dari kueri yang menghasilkan JSON dalam objek FeatureCollection GeoJSON.

Menambahkan library PGX ke project Anda

Kita perlu menambahkan satu dependensi ke project Anda: Driver & Toolkit PostGres, yang mengaktifkan penggabungan koneksi. Cara termudah untuk melakukannya adalah dengan Modul Go. Lakukan inisialisasi modul dengan perintah ini dalam Cloud Shell:

go mod init my_locator

Selanjutnya, jalankan perintah ini untuk memindai dependensi di kode, tambahkan daftar dependensi ke file mod, lalu download.

go mod tidy

Terakhir, jalankan perintah ini untuk menarik dependensi secara langsung ke direktori project Anda sehingga container dapat dibuat dengan mudah untuk AppEngine Flex.

go mod vendor

Oke, Anda siap mengujinya sekarang.

Melakukan pengujian

Oke, kita baru saja menyelesaikan BANYAK hal. Mari kita lihat cara kerjanya.

Agar mesin pengembangan Anda (ya, bahkan Cloud Shell) dapat terhubung ke database, kita harus menggunakan Proxy Cloud SQL untuk mengelola koneksi database. Untuk menyiapkan Proxy Cloud SQL:

  1. Buka di sini untuk mengaktifkan Cloud SQL Admin API
  2. Jika Anda menggunakan mesin pengembangan lokal, instal alat proxy Cloud SQL. Jika menggunakan Cloud Shell, Anda dapat melewati langkah ini karena sudah terinstal. Perhatikan bahwa petunjuk akan merujuk ke akun layanan. Satu akun telah dibuat untuk Anda, dan kita akan membahas cara menambahkan izin yang diperlukan ke akun tersebut di bagian berikutnya.
  3. Buat tab baru (di Cloud Shell atau terminal Anda sendiri) untuk memulai proxy.

bcca42933bfbd497.png

  1. Buka https://console.cloud.google.com/sql/instances/locations/overview dan scroll ke bawah untuk menemukan kolom Connection name. Salin nama tersebut untuk digunakan di perintah berikutnya.
  2. Di tab tersebut, jalankan proxy Cloud SQL dengan perintah ini, dengan mengganti CONNECTION_NAME dengan nama koneksi yang ditampilkan di langkah sebelumnya.
cloud_sql_proxy -instances=CONNECTION_NAME=tcp:5432

Kembali ke tab pertama di Cloud Shell dan tentukan variabel lingkungan yang akan diperlukan Go untuk berkomunikasi dengan backend database, lalu jalankan server dengan cara yang sama seperti sebelumnya:

Buka direktori utama project jika Anda belum membukanya.

cd YOUR_PROJECT_ROOT

Buat lima variabel lingkungan berikut (ganti YOUR_PASSWORD_HERE dengan sandi yang Anda buat di atas).

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

Jalankan instance lokal Anda.

go run *.go

Buka jendela pratinjau, dan instance seharusnya beroperasi seperti tidak ada yang berubah: Anda dapat memasukkan alamat awal, melakukan zoom di sekitar peta, dan mengklik lokasi daur ulang. Namun sekarang, instance didukung oleh database, dan siap untuk penskalaan.

9. Membuat daftar toko terdekat

Cara kerja Directions API mirip seperti pengalaman meminta rute di aplikasi Google Maps—memasukkan satu tempat asal dan satu tujuan untuk mendapatkan rute di antara kedua lokasi tersebut. Distance Matrix API menggunakan konsep ini lebih lanjut untuk mengidentifikasi pasangan optimal antara beberapa kemungkinan tempat asal dan beberapa kemungkinan tujuan berdasarkan waktu dan jarak perjalanan. Dalam hal ini, untuk membantu pengguna menemukan toko terdekat dengan alamat yang dipilih, Anda memberikan satu tempat asal dan array lokasi toko sebagai tujuan.

Menambahkan jarak dari tempat asal ke setiap toko

Di awal definisi fungsi initMap, ganti komentar "// TODO: Start Distance Matrix service" dengan kode berikut:

app.js - initMap

distanceMatrixService = new google.maps.DistanceMatrixService();

Tambahkan fungsi baru ke akhir app.js yang disebut 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);
  });
};

Fungsi ini memanggil Distance Matrix API menggunakan tempat asal yang dikirimkan sebagai satu tempat asal dan lokasi toko sebagai array tujuan. Kemudian, fungsi ini membuat array objek yang menyimpan ID toko, jarak yang dinyatakan dalam string yang dapat dibaca manusia, jarak dalam meter sebagai nilai numerik, dan mengurutkan array.

Update fungsi initAutocompleteWidget untuk menghitung jarak toko setiap kali titik asal baru dipilih dari kotak penelusuran Place Autocomplete. Di bagian bawah fungsi initAutocompleteWidget, ganti komentar "// TODO: Calculate the closest stores" dengan kode berikut:

app.js - initAutocompleteWidget

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

Menampilkan tampilan daftar toko yang diurutkan menurut jarak

Pengguna mengharapkan untuk melihat daftar toko yang diurutkan dari posisi yang terdekat hingga yang terjauh. Isi listingan panel samping untuk setiap toko menggunakan daftar yang diubah dari fungsi calculateDistances untuk menginformasikan urutan tampilan toko.

Tambahkan dua fungsi baru ke akhir app.js yang disebut renderStoresPanel() dan 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;
};

Mulai ulang server dan muat ulang pratinjau Anda dengan menjalankan perintah berikut.

go run *.go

Terakhir, masukkan alamat Austin, TX ke kotak penelusuran Autocomplete lalu klik salah satu saran.

Peta akan menempatkan alamat tersebut di tengah dan sidebar akan muncul yang mencantumkan lokasi toko dalam urutan jarak dari alamat yang dipilih. Salah satu contohnya digambarkan sebagai berikut:

96e35794dd0e88c9.png

10. Menentukan gaya peta

Salah satu cara yang memberikan dampak besar dalam membedakan peta Anda secara visual adalah dengan menambahkan gaya visual ke peta. Dengan gaya visual peta berbasis cloud, penyesuaian peta Anda dikontrol dari Cloud Console menggunakan Gaya Visual Peta Berbasis Cloud (beta). Jika Anda lebih suka menata gaya peta dengan fitur non-beta, Anda dapat menggunakan dokumentasi penataan gaya visual peta untuk membantu Anda membuat JSON untuk menata gaya visual peta secara terprogram. Petunjuk di bawah memandu Anda dalam Penataan Gaya Visual Peta Berbasis Cloud (beta).

Membuat ID Peta

Pertama, buka Cloud Console dan di kotak penelusuran, lalu ketik "Map Management" . Klik hasil yang bertuliskan "Map Management (Google Maps)". 64036dd0ed200200.png

Anda akan melihat tombol di dekat bagian atas (sisi kanan di bawah kotak Penelusuran) yang bertuliskan Create New Map ID. Klik, dan isi nama apa pun yang Anda inginkan. Untuk Map Type, pastikan memilih JavaScript, lalu saat opsi lebih lanjut muncul, pilih Vector dari daftar. Hasil akhirnya akan terlihat seperti gambar di bawah.

70f55a759b4c4212.png

Klik "Next" dan Anda akan diberikan ID Peta baru. Anda dapat menyalinnya sekarang jika mau, namun jangan khawatir, Anda dapat mencarinya dengan mudah nanti.

Selanjutnya, kita akan membuat gaya untuk diterapkan pada peta tersebut.

Membuat Gaya Peta

Jika Anda masih berada di bagian Maps pada Cloud Console, klik "Map Styles" di bagian bawah menu navigasi di sebelah kiri. Atau, sama seperti membuat ID Peta, Anda dapat menemukan halaman yang tepat dengan mengetikkan "Map Styles" di kotak penelusuran dan memilih "Map Styles (Google Maps)" dari hasilnya, seperti pada gambar di bawah.

9284cd200f1a9223.png

Selanjutnya, klik tombol di dekat bagian atas yang bertuliskan "+ Create New Map Style"

  1. Jika Anda ingin mencocokkan gaya visual pada peta yang ditampilkan di lab ini, klik tab "IMPORT JSON" dan tempelkan blob JSON di bawah. Atau, jika Anda ingin membuatnya sendiri, pilih Map Style yang Anda inginkan. Kemudian, klik Next.
  2. Pilih ID Peta yang baru saja Anda buat untuk mengaitkan ID Peta tersebut dengan gaya ini, lalu klik Next lagi.
  3. Pada titik ini, Anda diberikan opsi untuk menyesuaikan gaya visual peta lebih lanjut. Jika ini adalah sesuatu yang ingin Anda pelajari, klik Customize in Style Editor dan cobalah berbagai warna & opsi sampai Anda memiliki gaya peta yang Anda sukai. Jika tidak, klik Skip.
  4. Pada langkah berikutnya, masukkan nama dan deskripsi gaya Anda, lalu klik Save And Publish.

Berikut adalah blob JSON opsional yang akan diimpor pada langkah pertama.

[
  {
    "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"
      }
    ]
  }
]

Menambahkan ID Peta ke kode Anda

Setelah bersusah payah membuat gaya peta ini, bagaimana sebenarnya cara MENGGUNAKAN gaya peta ini di peta Anda sendiri? Anda harus membuat dua perubahan kecil:

  1. Tambahkan ID Peta sebagai parameter URL ke tag skrip di index.html
  2. Add ID Peta sebagai argumen konstruktor saat Anda membuat peta dalam metode initMap().

Ganti tag skrip yang memuat Maps JavaScript API dalam file HTML dengan URL loader di bawah ini, menggantikan placeholder untuk "YOUR_API_KEY" dan "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>
...

Pada metode initMap di app.js tempat konstanta map ditetapkan, batalkan komentar pada baris untuk properti mapId dan ganti "YOUR_MAP_ID_HERE" dengan ID Peta yang baru Anda buat:

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',
// ...
});
...

Mulai ulang server Anda.

go run *.go

Setelah memuat ulang pratinjau, peta akan terlihat sesuai dengan preferensi gaya Anda. Berikut ini contoh penggunaan gaya visual JSON di atas.

2ece59c64c06e9da.png

11. Men-deploy ke produksi

Jika ingin melihat aplikasi Anda berjalan dari App Engine Flex (dan bukan hanya server web lokal di mesin pengembangan/Cloud Shell, yang selama ini sudah Anda lakukan), caranya sangat mudah. Kita hanya perlu menambahkan beberapa hal agar database dapat diakses di lingkungan produksi. Ini semua diuraikan di halaman dokumentasi tentang Menghubungkan dari App Engine Flex ke Cloud SQL.

Menambahkan Variabel Lingkungan ke App.yaml

Pertama, semua variabel lingkungan yang Anda gunakan untuk menguji secara lokal harus ditambahkan ke bagian bawah file app.yaml aplikasi Anda.

  1. Kunjungi https://console.cloud.google.com/sql/in State/locations/overview untuk mencari nama koneksi instance.
  2. Tempel kode berikut di akhir app.yaml.
  3. Ganti YOUR_DB_PASSWORD_HERE dengan sandi yang Anda buat untuk nama pengguna postgres sebelumnya.
  4. Ganti YOUR_CONNECTION_NAME_HERE dengan nilai dari langkah 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

Perhatikan bahwa DB_TCP_HOST harus memiliki nilai 172.17.0.1 karena aplikasi ini terhubung melalui App Engine Flex**.** Hal ini karena aplikasi akan berkomunikasi dengan Cloud SQL melalui proxy, mirip dengan cara Anda sebelumnya.

Menambahkan izin SQL Client ke akun layanan App Engine Flex

Buka halaman IAM-Admin di Cloud Console dan cari akun layanan yang namanya cocok dengan format service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com. Ini adalah akun layanan yang akan digunakan App Engine Flex untuk terhubung ke database. Klik tombol Edit di akhir baris dan tambahkan peran "Cloud SQL Client".

b04ccc0b4022b905.png

Menyalin kode project Anda ke jalur Go

Agar App Engine dapat menjalankan kode Anda, App Engine harus dapat menemukan file yang relevan di jalur Go. Pastikan Anda berada di direktori utama project.

cd YOUR_PROJECT_ROOT

Salin direktori ke jalur Go.

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

Ubah ke direktori tersebut.

cd ~/gopath/src/austin-recycling

Men-deploy Aplikasi Anda

Gunakan alat gcloud untuk men-deploy aplikasi Anda. Perlu waktu beberapa saat untuk men-deploy-nya.

gcloud app deploy

Gunakan perintah browse untuk mendapatkan link yang dapat diklik untuk melihat cara kerja pencari toko tingkat perusahaan yang secara estetika terlihat bagus dan sepenuhnya di-deploy.

gcloud app browse

Jika Anda menjalankan gcloud di luar Cloud Shell, maka menjalankan gcloud app browse akan membuka tab browser yang baru.

12. (Direkomendasikan) Pembersihan

Menjalankan codelab ini tidak akan melampaui batas paket gratis untuk pemrosesan BigQuery dan panggilan Maps Platform API, tetapi jika Anda melakukan ini hanya sebagai latihan edukasi dan ingin menghindari tagihan di masa mendatang, cara termudah untuk menghapus resource yang terkait dengan project ini adalah dengan menghapus project itu sendiri.

Menghapus Project

Di GCP Console, buka halaman Cloud Resource Manager:

Dalam daftar project, pilih project yang sedang kita kerjakan lalu klik Delete. Anda akan diminta untuk mengetikkan ID project. Masukkan dan klik Shut Down.

Atau, Anda dapat menghapus seluruh project langsung dari Cloud Shell menggunakan gcloud dengan menjalankan perintah berikut dan mengganti placeholder GOOGLE_CLOUD_PROJECT dengan ID project Anda:

gcloud projects delete GOOGLE_CLOUD_PROJECT

13. Selamat

Selamat. Anda berhasil menyelesaikan codelab.

Atau Anda membaca sekilas hingga halaman terakhir. Selamat. Anda sudah membaca sekilas hingga halaman terakhir.

Selama codelab ini, Anda telah menggunakan teknologi berikut:

Bacaan Lebih Lanjut

Masih banyak yang harus dipelajari tentang semua teknologi ini. Di bawah ini adalah beberapa link berguna untuk topik yang tidak kita bahas dalam codelab ini, tetapi tentu saja dapat berguna bagi Anda dalam membangun solusi pencari toko yang sesuai dengan kebutuhan spesifik Anda.