Google Haritalar Platformu ve Google Cloud ile tam yığın mağaza bulma aracı oluşturma

1. Giriş

Soyut

Haritaya yerleştireceğiniz birçok yer olduğunu ve kullanıcıların bu yerlerin nerede olduğunu görmesini ve hangi yeri ziyaret etmek istediklerini belirleyebilmesini istediğinizi düşünün. Buna yaygın örnekler şunlardır:

  • Perakendecinin web sitesindeki mağaza bulma aracı
  • Yaklaşan bir seçim için oy kullanma yerlerinin haritası
  • pil geri dönüşüm kutuları gibi özel yerlerin dizini

Ne oluşturacaksınız?

Bu codelab'de, özel konumların canlı veri feed'inden yararlanan ve kullanıcının başlangıç noktasına en yakın konumu bulmasına yardımcı olan bir bulucu oluşturacaksınız. Bu tam yığınlı konum bulucu, 25 veya daha az mağaza konumuyla sınırlı olan basit mağaza konum bulucudan çok daha fazla sayıda yeri işleyebilir.

2ece59c64c06e9da.png

Neler öğreneceksiniz?

Bu codelab'de, temel teknik kavramları öğrenmeye odaklanabilmeniz için çok sayıda mağaza konumuyla ilgili önceden doldurulmuş meta verileri simüle etmek üzere açık bir veri kümesi kullanılır.

  • Maps JavaScript API: Özelleştirilmiş bir web haritasında çok sayıda konum görüntüleme
  • GeoJSON: Konumlarla ilgili meta verileri depolayan bir biçim
  • Yer otomatik tamamlama: Kullanıcıların başlangıç konumlarını daha hızlı ve doğru bir şekilde sağlamasına yardımcı olun
  • Go: Uygulama arka ucunu geliştirmek için kullanılan programlama dili. Arka uç, veritabanıyla etkileşime girer ve sorgu sonuçlarını biçimlendirilmiş JSON olarak ön uca geri gönderir.
  • App Engine: Web uygulamasını barındırmak için

Ön koşullar

  • HTML ve JavaScript hakkında temel bilgi
  • Google Hesabı

2. Hazırlanın

Aşağıdaki bölümün 3. adımında bu codelab için Maps JavaScript API, Places API ve Distance Matrix API'yi etkinleştirin.

Google Haritalar Platformu'nu kullanmaya başlayın

Google Haritalar Platformu'nu daha önce kullanmadıysanız aşağıdaki adımları tamamlamak için Google Haritalar Platformu'nu Kullanmaya Başlama Kılavuzu'nu inceleyin veya Google Haritalar Platformu'nu Kullanmaya Başlama oynatma listesini izleyin:

  1. Faturalandırma hesabı oluşturun.
  2. Proje oluşturun.
  3. Google Haritalar Platformu API'lerini ve SDK'larını (önceki bölümde listelenmiştir) etkinleştirin.
  4. API anahtarı oluşturun.

Cloud Shell'i etkinleştirme

Bu codelab'de, projenizi tamamen web tarayıcınızdan barındırıp çalıştırabilmeniz için Google Cloud'da çalışan ve Google Cloud'da çalışan ürünlere ve kaynaklara erişim sağlayan bir komut satırı ortamı olan Cloud Shell'i kullanacaksınız.

Cloud Shell'i Cloud Console'dan etkinleştirmek için Cloud Shell'i etkinleştir 'i 89665d8d348105cd.png tıklayın (ortamın sağlanması ve bağlantının kurulması yalnızca birkaç saniye sürer).

5f504766b9b3be17.png

Bu işlem, muhtemelen bir tanıtım geçiş reklamı gösterdikten sonra tarayıcınızın alt kısmında yeni bir kabuk açar.

d3bb67d514893d1f.png

Projenizi onaylama

Cloud Shell'e bağlandıktan sonra kimliğinizin doğrulandığını ve projenin, kurulum sırasında seçtiğiniz proje kimliğine ayarlandığını görürsünüz.

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

Herhangi bir nedenle proje ayarlanmamışsa aşağıdaki komutu çalıştırın:

gcloud config set project <YOUR_PROJECT_ID>

App Engine Flex API'yi etkinleştirme

App Engine Flex API'nin Cloud Console'dan manuel olarak etkinleştirilmesi gerekir. Bu işlem yalnızca API'yi etkinleştirmekle kalmaz, aynı zamanda kullanıcının adına Google hizmetleriyle (ör. SQL veritabanları) etkileşimde bulunacak kimliği doğrulanmış hesap olan App Engine Esnek Ortam Hizmet Hesabı'nı da oluşturur.

3. Hello, World

Arka uç: Go'da Hello World

Cloud Shell örneğinizde, bu codelab'in geri kalanında temel olarak kullanılacak bir Go App Engine Flex uygulaması oluşturarak başlayacaksınız.

Yeni bir sekmede kod düzenleyiciyi açmak için Cloud Shell'in araç çubuğunda Open editor (Düzenleyiciyi aç) düğmesini tıklayın. Bu web tabanlı kod düzenleyici, Cloud Shell örneğindeki dosyaları kolayca düzenlemenize olanak tanır.

b63f7baad67b6601.png

Ardından, düzenleyiciyi ve terminali yeni bir sekmeye taşımak için Yeni pencerede aç simgesini tıklayın.

3f6625ff8461c551.png

Yeni sekmenin alt kısmındaki terminalde yeni bir austin-recycling dizini oluşturun.

mkdir -p austin-recycling && cd $_

Ardından, her şeyin çalıştığından emin olmak için küçük bir Go App Engine uygulaması oluşturacaksınız. Merhaba Dünya!

austin-recycling dizini, soldaki düzenleyicinin klasör listesinde de görünmelidir. austin-recycling dizininde app.yaml adlı bir dosya oluşturun. app.yaml dosyasına aşağıdaki içeriği ekleyin:

app.yaml

runtime: go
env: flex

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

Bu yapılandırma dosyası, App Engine uygulamanızı Go Flex çalışma zamanını kullanacak şekilde yapılandırır. Bu dosyadaki yapılandırma öğelerinin anlamı hakkında arka plan bilgisi için Google App Engine Go Standart Ortam Belgeleri'ne bakın.

Ardından, app.yaml dosyasının yanında bir main.go dosyası oluşturun:

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

Bu kodun ne yaptığını anlamak için burada bir an duraklamak faydalı olacaktır (en azından genel hatlarıyla). 8080 numaralı bağlantı noktasında dinleme yapan bir HTTP sunucusu başlatan ve "/" yoluyla eşleşen HTTP istekleri için bir işleyici işlevi kaydeden bir paket main tanımladınız.

handler olarak adlandırılan işleyici işlevi, "Hello, world!" metin dizesini yazar. Bu metin, tarayıcınıza geri iletilir ve burada okuyabilirsiniz. İlerleyen adımlarda, basitçe kodlanmış dizeler yerine GeoJSON verileriyle yanıt veren işleyiciler oluşturacaksınız.

Bu adımları uyguladıktan sonra artık aşağıdaki gibi bir düzenleyiciniz olmalıdır:

2084fdd5ef594ece.png

Deneyin

Bu uygulamayı test etmek için Cloud Shell örneğinde App Engine geliştirme sunucusunu çalıştırabilirsiniz. Cloud Shell komut satırına dönüp aşağıdakileri yazın:

go run *.go

Geliştirme sunucusunu Cloud Shell örneğinde gerçekten çalıştırdığınızı gösteren bazı günlük çıkışı satırları görürsünüz. Bu satırlarda, localhost bağlantı noktası 8080'de dinleme yapan "Merhaba Dünya" web uygulaması yer alır. Web Önizlemesi düğmesine basıp Cloud Shell araç çubuğunda 8080 bağlantı noktasında önizle menü öğesini seçerek bu uygulamada bir web tarayıcısı sekmesi açabilirsiniz.

4155fc1dc717ac67.png

Bu menü öğesini tıkladığınızda, web tarayıcınızda App Engine geliştirme sunucusundan sunulan "Hello, world!" (Merhaba dünya!) kelimelerini içeren yeni bir sekme açılır.

Bir sonraki adımda, Austin şehrinin geri dönüşüm verilerini bu uygulamaya ekleyip görselleştirmeye başlayacaksınız.

4. Güncel verileri alma

Coğrafi Bilgi Sistemi dünyasının ortak dili olan GeoJSON

Önceki adımda, Go kodunuzda GeoJSON verilerini web tarayıcısında oluşturacak işleyiciler oluşturacağınız belirtilmişti. Peki GeoJSON nedir?

Coğrafi Bilgi Sistemi (GIS) dünyasında, coğrafi varlıklarla ilgili bilgileri bilgisayar sistemleri arasında iletebilmemiz gerekir. Haritalar insanlar için kolay okunur olsa da bilgisayarlar genellikle verilerini daha kolay işlenebilir biçimlerde tercih eder.

GeoJSON, Teksas'taki Austin şehrinde bulunan geri dönüşüm merkezlerinin koordinatları gibi coğrafi veri yapılarını kodlamak için kullanılan bir biçimdir. GeoJSON, Internet Engineering Task Force standardı olan RFC7946 ile standartlaştırılmıştır. GeoJSON, JavaScript'i standartlaştıran kuruluş olan Ecma International tarafından ECMA-404'te standartlaştırılan JavaScript Object Notation (JavaScript Nesne Gösterimi) JSON açısından tanımlanır.

Önemli olan, GeoJSON'un coğrafi bilgileri iletmek için yaygın olarak desteklenen bir tel biçimi olmasıdır. Bu codelab'de GeoJSON aşağıdaki şekillerde kullanılır:

  • İstenen verileri filtrelemek için kullanacağınız Austin verilerini, dahili bir GIS'e özgü veri yapısına ayrıştırmak üzere Go paketlerini kullanın.
  • Web sunucusu ile web tarayıcısı arasında aktarım için istenen verileri serileştirin.
  • Yanıtı haritada işaretçilere dönüştürmek için bir JavaScript kitaplığı kullanın.

Bu sayede, kodda önemli ölçüde yazma işlemi yapmanız gerekmez. Bunun nedeni, ağ üzerinden gelen veri akışını bellekteki gösterimlere dönüştürmek için ayrıştırıcılar ve oluşturucular yazmanız gerekmemesidir.

Verileri alma

City of Austin, Texas Open Data Portal, kamu kaynaklarıyla ilgili coğrafi bilgileri kamu kullanımına sunar. Bu codelab'de geri dönüşüm için bırakma yerleri veri kümesini görselleştireceksiniz.

Verileri, Maps JavaScript API'nin veri katmanı kullanılarak oluşturulan haritadaki işaretçilerle görselleştirirsiniz.

Austin Şehri web sitesinden GeoJSON verilerini uygulamanıza indirerek başlayın.

  1. Cloud Shell örneğinizin komut satırı penceresinde [CTRL] + [C] yazarak sunucuyu kapatın.
  2. austin-recycling dizininin içinde bir data dizini oluşturun ve bu dizine geçin:
mkdir -p data && cd data

Geri dönüşüm yerlerini almak için curl komutunu kullanın:

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

Son olarak, üst dizine geri dönün.

cd ..

5. Konumları haritalama

Öncelikle, oluşturmak üzere olduğunuz daha sağlam, "artık sadece bir "Merhaba dünya" uygulaması değil" uygulamasını yansıtacak şekilde app.yaml dosyasını güncelleyin.

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

Bu app.yaml yapılandırması, /, /*.js, /*.css ve /*.html isteklerini bir dizi statik dosyaya yönlendirir. Bu, uygulamanızın statik HTML bileşeninin doğrudan App Engine dosya sunma altyapısı tarafından sunulacağı ve Go uygulamanız tarafından sunulmayacağı anlamına gelir. Bu, sunucu yükünü azaltır ve sunma hızını artırır.

Şimdi uygulamanızın arka ucunu Go ile oluşturma zamanı.

Arka ucu oluşturma

app.yaml dosyanızın yapmadığı ilginç bir şeyin GeoJSON dosyasını kullanıma sunmak olduğunu fark etmiş olabilirsiniz. Bunun nedeni, GeoJSON'un Go arka uçumuz tarafından işlenip gönderilecek olmasıdır. Bu sayede, sonraki adımlarda bazı gelişmiş özellikler ekleyebiliriz. main.go dosyanızı aşağıdaki gibi okuyacak şekilde değiştirin:

main.go

package main

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

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

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

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

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


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

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

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

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

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

Go arka ucu bize değerli bir özellik sunuyor: App Engine örneği, başlatıldığı anda tüm bu konumları önbelleğe alıyor. Arka uç, her kullanıcının her yenilemesinde dosyayı diskten okumak zorunda kalmayacağından bu işlem zaman kazandırır.

Ön ucu oluşturma

İlk olarak, tüm statik öğelerimizi barındıracak bir klasör oluşturmamız gerekiyor. Projenizin üst klasöründe bir static klasörü oluşturun.

mkdir -p static && cd static

Bu klasörde 3 dosya oluşturacağız.

  • index.html, tek sayfalık mağaza bulucu uygulamanızın tüm HTML'sini içerir.
  • style.css, beklediğiniz gibi stili içerecek
  • app.js, GeoJSON'u alma, Haritalar API'sine çağrı yapma ve özel haritanıza işaretçi yerleştirme işlemlerinden sorumludur.

Bu 3 dosyayı oluşturun ve static/ içine yerleştirin .

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>

head öğesinin komut etiketi içindeki src URL'sine özellikle dikkat edin.

  • "YOUR_API_KEY" yer tutucu metnini, kurulum adımında oluşturduğunuz API anahtarıyla değiştirin. API anahtarınızı almak veya yeni bir anahtar oluşturmak için Cloud Console'da API'ler ve Hizmetler -> Kimlik Bilgileri sayfasına gidebilirsiniz.
  • URL'nin callback=initialize. parametresini içerdiğini unutmayın. Şimdi bu geri çağırma işlevini içeren JavaScript dosyasını oluşturacağız. Uygulamanız, arka uçtaki konumları yükleyip Haritalar API'ye gönderir ve sonucu kullanarak haritada özel konumları işaretler. Tüm bu işlemler, web sayfanızda güzel bir şekilde oluşturulur.
  • libraries=places parametresi, daha sonra eklenecek adres otomatik tamamlama gibi özellikler için gerekli olan Yerler Kitaplığı'nı yükler.

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

Bu kod, mağaza konumlarını haritada gösterir. Şimdiye kadar yaptıklarımızı test etmek için komut satırından üst dizine dönün:

cd ..

Şimdi uygulamanızı geliştirme modunda tekrar çalıştırın:

go run *.go

Daha önce yaptığınız gibi önizleyin. Buna benzer küçük yeşil dairelerin olduğu bir harita görürsünüz.

58a6680e9c8e7396.png

Harita konumlarını zaten oluşturuyorsunuz ve codelab'in sadece yarısını tamamladık. Harika. Şimdi biraz etkileşim ekleyelim.

6. İsteğe bağlı olarak ayrıntıları göster

Harita işaretçilerindeki tıklama etkinliklerine yanıt verme

Haritada bir dizi işaretçi göstermek iyi bir başlangıçtır ancak ziyaretçilerin bu işaretçilerden birini tıklayıp konumla ilgili bilgileri (ör. işletmenin adı, adresi vb.) görmesi gerekir. Genellikle bir Google Haritalar işaretçisini tıkladığınızda açılan küçük bilgi penceresinin adı Bilgi Penceresi'dir.

Bir infoWindow nesnesi oluşturun. initialize işlevine aşağıdakileri ekleyin. "// TODO: Initialize an info window" yazan yorum satırını değiştirin.

app.js - initialize

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

fetchAndRenderStores işlev tanımını, son satırı storeToCircle işlevini ek bir bağımsız değişken olan infowindow ile çağıracak şekilde değiştiren bu biraz farklı sürümle değiştirin:

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 tanımını, artık üçüncü bağımsız değişken olarak bilgi penceresi alan bu biraz daha uzun sürümle değiştirin:

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

Yukarıdaki yeni kod, haritadaki bir mağaza işaretçisi her tıklandığında seçilen mağazanın bilgilerini içeren bir infoWindow gösterir.

Sunucunuz hâlâ çalışıyorsa durdurup yeniden başlatın. Harita sayfanızı yenileyip bir harita işaretçisini tıklamayı deneyin. İşletmenin adı ve adresinin yer aldığı küçük bir bilgi penceresi açılır. Bu pencere aşağıdaki gibi görünür:

1af0ab72ad0eadc5.png

7. Kullanıcının başlangıç konumunu alma

Mağaza bulucuları kullananlar genellikle kendilerine en yakın mağazayı veya yolculuklarına başlamayı planladıkları adresi öğrenmek ister. Kullanıcının başlangıç adresini kolayca girmesine olanak tanımak için otomatik yer tamamlama özelliğine sahip bir arama çubuğu ekleyin. Otomatik Yer Tamamlama, diğer Google arama çubuklarındaki otomatik tamamlama işlevine benzer bir önden yazma işlevi sunar. Ancak tahminler, Google Haritalar Platformu'ndaki tüm yerlerdir.

Kullanıcı girişi alanı oluşturma

Otomatik tamamlama arama çubuğu ve ilişkili sonuç yan paneli için stil eklemek üzere style.css düzenleme işlemine geri dönün. CSS stillerini güncellerken, mağaza bilgilerini haritayla birlikte liste olarak gösteren gelecekteki bir kenar çubuğu için de stiller ekleyeceğiz.

Bu kodu dosyanın sonuna ekleyin.

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

Hem otomatik tamamlama arama çubuğu hem de açılır panel, ihtiyaç duyulana kadar gizlidir.

index.html dosyasındaki "<!-- Autocomplete div goes here --> yorumunu aşağıdaki kodla değiştirerek otomatik tamamlama widget'ı için bir div hazırlayın. Bu düzenlemeyi yaparken, kayarak açılan panel için div'i de ekleyeceğiz.

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>

Şimdi, aşağıdaki kodu app.js öğesinin sonuna ekleyerek otomatik tamamlama widget'ını haritaya ekleyecek bir işlev tanımlayın.

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

Kod, otomatik tamamlama önerilerini yalnızca adres döndürecek şekilde kısıtlar (çünkü yer otomatik tamamlama, kuruluş adları ve idari konumlarla da eşleşebilir) ve döndürülen adresleri yalnızca ABD'dekilerle sınırlar. Bu isteğe bağlı özellikleri eklemek, kullanıcının aradığı adresi göstermek için tahminleri daraltmak üzere girmesi gereken karakter sayısını azaltır.

Ardından, oluşturduğunuz otomatik tamamlama div öğesini haritanın sağ üst köşesine taşır ve yanıtta her yerle ilgili hangi alanların döndürülmesi gerektiğini belirtir.

Son olarak, initAutocompleteWidget işlevini initialize işlevinin sonunda çağırın ve "// TODO: Initialize the Autocomplete widget" yorumunu değiştirin.

app.js - initialize

 // Initialize the Places Autocomplete Widget
 initAutocompleteWidget();

Aşağıdaki komutu çalıştırarak sunucunuzu yeniden başlatın ve ardından önizlemenizi yenileyin.

go run *.go

Artık haritanızın sağ üst köşesinde, yazdıklarınızla eşleşen ABD adreslerini gösteren bir otomatik tamamlama widget'ı görmelisiniz. Bu widget, haritanın görünür alanına göre önyargılıdır.

58e9bbbcc4bf18d1.png

Kullanıcı bir başlangıç adresi seçtiğinde haritayı güncelleme

Şimdi, kullanıcının otomatik tamamlama widget'ından bir tahmin seçtiği durumu ele almanız ve bu konumu mağazalarınıza olan mesafeleri hesaplamak için temel olarak kullanmanız gerekir.

app.js içindeki initAutocompleteWidget öğesinin sonuna aşağıdaki kodu ekleyin ve "// TODO: Respond when a user selects an address" yorumunu değiştirin.

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

Kod, kullanıcı önerilerden birini tıkladığında haritanın seçilen adrese göre yeniden ortalanması ve mesafelerin hesaplanmasında başlangıç noktası olarak belirlenmesi için bir dinleyici ekler. Mesafe hesaplamalarını sonraki bir adımda uygularsınız.

Sunucunuzu durdurup yeniden başlatın ve otomatik tamamlama arama çubuğuna bir adres girdikten sonra haritanın yeniden ortalandığını görmek için önizlemenizi yenileyin.

8. Cloud SQL ile ölçeklendirme

Şu ana kadar oldukça iyi bir mağaza bulucu oluşturduk. Uygulamanın yalnızca yaklaşık yüz konum kullanacağı gerçeğinden yararlanarak bu konumları arka uçta belleğe yükler (dosyadan tekrar tekrar okumak yerine). Ancak konum bulucunuzun farklı bir ölçekte çalışması gerekiyorsa ne olur? Geniş bir coğrafi alana yayılmış yüzlerce konumunuz (veya tüm dünyada binlerce konumunuz) varsa bu konumların hepsini bellekte tutmak artık en iyi seçenek değildir ve bölgeleri ayrı dosyalara bölmek kendi sorunlarını ortaya çıkarır.

Konumlarınızı bir veritabanından yüklemenin zamanı geldi. Bu adımda, GeoJSON dosyanızdaki tüm konumları bir Cloud SQL veritabanına taşıyacağız ve bir istek geldiğinde sonuçları yerel önbelleğinden değil, bu veritabanından çekecek şekilde Go arka ucunu güncelleyeceğiz.

PostgreSQL veritabanı ile Cloud SQL örneği oluşturma

Google Cloud Console'u kullanarak Cloud SQL örneği oluşturabilirsiniz ancak komut satırından oluşturmak için gcloud yardımcı programını kullanmak daha kolaydır. Cloud Shell'de aşağıdaki komutla bir Cloud SQL örneği oluşturun:

gcloud sql instances create locations \
--database-version=POSTGRES_12 \
--tier=db-custom-1-3840 --region=us-central1
  • locations bağımsız değişkeni, bu Cloud SQL örneğine vermek için seçtiğimiz addır.
  • tier işareti, bazı uygun önceden tanımlanmış makineler arasından seçim yapmanın bir yoludur.
  • db-custom-1-3840 değeri, oluşturulan örneğin bir vCPU'ya ve yaklaşık 3,75 GB belleğe sahip olması gerektiğini gösterir.

Cloud SQL örneği, varsayılan kullanıcı postgres ile bir PostgreSQL veritabanı kullanılarak oluşturulur ve başlatılır. Bu kullanıcının şifresi nedir? Bu sorunuzla önemli bir noktaya değindiniz. Bu kullanıcıların profili yok. Oturum açabilmek için önce bir tane yapılandırmanız gerekir.

Aşağıdaki komutla şifreyi ayarlayın:

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

Ardından, istendiğinde seçtiğiniz şifreyi girin.

PostGIS uzantısını etkinleştirme

PostGIS, PostGresSQL için bir uzantıdır ve standartlaştırılmış coğrafi veri türlerini depolamayı kolaylaştırır. Normal şartlar altında, PostGIS'i veritabanımıza eklemek için tam bir yükleme sürecinden geçmemiz gerekirdi. Neyse ki bu uzantı, PostgreSQL için Cloud SQL'in desteklediği uzantılardan biridir.

Cloud Shell terminalinde aşağıdaki komutu kullanarak postgres kullanıcısı olarak giriş yapıp veritabanı örneğine bağlanın.

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

Yeni oluşturduğunuz şifreyi girin. Şimdi postgres=> komut istemine PostGIS uzantısını ekleyin.

CREATE EXTENSION postgis;

İşlem başarılı olursa çıkışta aşağıda gösterildiği gibi CREATE EXTENSION yazmalıdır.

Örnek komut çıkışı

CREATE EXTENSION

Son olarak, postgres=> komut istemine quit komutunu girerek veritabanı bağlantısını sonlandırın.

\q

Coğrafi verileri veritabanına aktarma

Şimdi, GeoJSON dosyalarındaki tüm konum verilerini yeni veritabanımıza aktarmamız gerekiyor.

Neyse ki bu sorun yaygın olarak yaşanmaktadır ve internette bu işlemi sizin için otomatikleştirecek çeşitli araçlar bulabilirsiniz. Jeo-uzamsal verileri depolamak için kullanılan yaygın biçimler arasında dönüşüm yapan ogr2ogr adlı bir araç kullanacağız. Bu seçeneklerden biri de GeoJSON'dan SQL döküm dosyasına dönüştürmedir. SQL döküm dosyası daha sonra veritabanı için tablolarınızı ve sütunlarınızı oluşturmak ve GeoJSON dosyalarınızda bulunan tüm verilerle yüklemek için kullanılabilir.

SQL döküm dosyası oluşturma

Öncelikle ogr2ogr'yi yükleyin.

sudo apt-get install gdal-bin

Ardından, SQL döküm dosyasını oluşturmak için ogr2ogr'yi kullanın. Bu dosya, austinrecycling adlı bir tablo oluşturur.

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

Yukarıdaki komut, austin-recycling klasöründen çalıştırılmaya dayanmaktadır. Komutu başka bir dizinden çalıştırmanız gerekiyorsa data yerine recycling-locations.geojson öğesinin depolandığı dizinin yolunu girin.

Veritabanınızı geri dönüşüm yerleriyle doldurma

Bu son komut tamamlandığında, komutu çalıştırdığınız dizinde datadump.sql, adlı bir dosyanız olmalıdır. Bu dosyayı açtığınızda, austinrecycling tablosunu oluşturan ve konumlarla dolduran yüzü aşkın SQL satırı görürsünüz.

Şimdi veritabanına bağlantı açın ve aşağıdaki komutla bu komut dosyasını çalıştırın.

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

Komut dosyası başarıyla çalışırsa çıkışın son birkaç satırı şu şekilde görünür:

Örnek komut çıkışı

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

Go arka ucunu Cloud SQL kullanacak şekilde güncelleme

Tüm bu veriler artık veritabanımızda olduğuna göre kodumuzu güncelleme zamanı geldi.

Konum bilgilerini göndermek için ön ucu güncelleme

Ön uçta çok küçük bir güncellemeyle başlayalım: Bu uygulamayı artık sorgu her çalıştırıldığında her konumun ön uca iletilmesini istemediğimiz bir ölçekte yazdığımız için, kullanıcının ilgilendiği konumla ilgili bazı temel bilgileri ön uçtan geçirmemiz gerekiyor.

app.js dosyasını açın ve URL'ye ilgilenilen enlem ve boylamı eklemek için fetchStores işlev tanımını bu sürümle değiştirin.

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

Bu codelab adımını tamamladıktan sonra yanıt yalnızca center parametresinde sağlanan harita koordinatlarına en yakın mağazaları döndürür. initialize işlevindeki ilk getirme işlemi için bu laboratuvarda sağlanan örnek kod, Austin, Teksas'ın merkezi koordinatlarını kullanır.

fetchStores artık yalnızca mağaza konumlarının bir alt kümesini döndüreceğinden, kullanıcı başlangıç konumunu her değiştirdiğinde mağazaları yeniden getirmemiz gerekecek.

Yeni bir kaynak ayarlandığında konumları yenilemek için initAutocompleteWidget işlevini güncelleyin. Bu işlem için iki düzenleme yapılması gerekir:

  1. initAutocompleteWidget içinde place_changed dinleyicisinin geri çağırma işlevini bulun. Mevcut daireleri temizleyen satırın yorumunu kaldırın. Böylece kullanıcı, Yer Otomatik Tamamlama arama çubuğundan bir adres seçtiğinde bu satır çalışır.

app.js - initAutocompleteWidget

  autocomplete.addListener("place_changed", async () => {
    circles.forEach((c) => c.setMap(null)); // clear existing stores
    // ...
  1. Seçilen kaynak her değiştirildiğinde originLocation değişkeni güncellenir. "place_changed" geri çağırmasının sonunda, bu yeni kaynağı fetchAndRenderStores işlevine yapılan yeni bir çağrıya iletmek için "// TODO: Calculate the closest stores" satırının üzerindeki satırın yorumunu kaldırın.

app.js - initAutocompleteWidget

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

Arka ucu düz JSON dosyası yerine CloudSQL kullanacak şekilde güncelleyin.

Düz dosya GeoJSON okuma ve önbelleğe alma özelliğini kaldırma

Öncelikle, düz GeoJSON dosyasını yükleyen ve önbelleğe alan kodu kaldırmak için main.go değerini değiştirin. Ayrıca, farklı bir dosyada Cloud SQL destekli bir işlev yazacağımız için dropoffsHandler işlevini de kaldırabiliriz.

Yeni main.go çok daha kısa olacak.

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

Konum istekleri için yeni bir işleyici oluşturma

Şimdi de austin-recycling dizininde locations.go adlı başka bir dosya oluşturalım. Konum istekleri için işleyiciyi yeniden uygulayarak başlayın.

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

İşleyici aşağıdaki önemli görevleri yerine getirir:

  • Enlem ve boylamı istek nesnesinden alır (Bunları URL'ye nasıl eklediğimizi hatırlıyor musunuz? )
  • getGeoJsonFromDatabase çağrısını tetikler. Bu çağrı, GeoJSON dizesini döndürür (Bunu daha sonra yazacağız).
  • Bu dizeyi yanıta yazdırmak için ResponseWriter kullanılır.

Ardından, veritabanı kullanımının eşzamanlı kullanıcılarla iyi ölçeklenmesine yardımcı olmak için bir bağlantı havuzu oluşturacağız.

Bağlantı havuzu oluşturma

Bağlantı havuzu, sunucunun kullanıcı isteklerine hizmet vermek için yeniden kullanabileceği etkin veritabanı bağlantılarının bir koleksiyonudur. Sunucunun her etkin kullanıcı için bağlantı oluşturup yok etmesi gerekmediğinden, etkin kullanıcı sayınız arttıkça çok fazla ek yük ortadan kalkar. Önceki bölümde github.com/jackc/pgx/stdlib. kitaplığını içe aktardığımızı fark etmiş olabilirsiniz. Bu, Go'da bağlantı havuzlarıyla çalışmak için kullanılan popüler bir kitaplıktır.

locations.go dosyasının sonunda, bağlantı havuzunu başlatan initConnectionPool işlevini (main.go dosyasından çağrılır) oluşturun. Anlaşılırlık için bu snippet'te birkaç yardımcı yöntem kullanılmıştır. configureConnectionPool, bağlantı sayısı ve bağlantı başına ömür gibi havuz ayarlarını düzenlemek için kullanışlı bir yer sağlar. mustGetEnv, gerekli ortam değişkenlerini almak için yapılan çağrıları sarmalar. Böylece, örnekte kritik bilgiler (ör. bağlanılacak veritabanının IP'si veya adı) eksikse yararlı hata mesajları verilebilir.

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
}

Konumlar için veritabanına sorgu gönderin ve karşılığında JSON alın.

Şimdi harita koordinatlarını alan ve en yakın 25 konumu döndüren bir veritabanı sorgusu yazacağız. Bununla da kalmayıp bazı gelişmiş modern veritabanı işlevleri sayesinde bu verileri GeoJSON olarak döndürür. Tüm bunların sonucunda, ön uç kodu açısından hiçbir şey değişmez. URL'ye istek gönderip bir sürü GeoJSON almadan önce. Şimdi bir URL'ye istek gönderiyor ve bir sürü GeoJSON alıyor.

İşte bu sihirli işlevi gerçekleştirecek fonksiyon. locations.go dosyasının en altına, az önce yazdığınız işleyici ve bağlantı havuzu oluşturma kodunun ardından aşağıdaki işlevi ekleyin.

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
}

Bu işlev, çoğunlukla veritabanına istek göndermek için kurulum, yıkım ve hata işleme işlemlerini kapsar. Veritabanı katmanında gerçekten ilginç birçok şey yapan gerçek SQL'e bakalım. Böylece, bunların hiçbirini kodda uygulamanız gerekmez.

Dize ayrıştırılıp tüm dize değişmezleri uygun yerlere eklendikten sonra gönderilen ham sorgu şu şekilde görünür:

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

Bu sorgu, birincil sorgu ve bazı JSON sarmalama işlevleri olarak görüntülenebilir.

SELECT * ... LIMIT 25, her konum için tüm alanları seçer. Ardından, veritabanındaki her konum ile kullanıcının ön uçta sağladığı konumun enlem/boylam çifti arasındaki mesafeyi belirlemek için ST_DISTANCE işlevini (PostGIS'in coğrafya ölçümü işlevleri paketinin bir parçası) kullanır. Sürüş mesafesini verebilen Mesafe Matrisi'nin aksine, bu mesafelerin coğrafi mesafeler olduğunu unutmayın. Ardından, verimlilik için bu mesafeyi kullanarak sıralama yapar ve kullanıcının belirttiği konuma en yakın 25 konumu döndürür.

**SELECT json_build_object(‘type', ‘F**eature') önceki sorguyu sarmalar, sonuçları alır ve bunları GeoJSON Feature nesnesi oluşturmak için kullanır. Beklenmedik bir şekilde, bu sorguda maksimum yarıçap da uygulanır. "16090", 10 milin metre cinsinden değeridir ve Go arka ucu tarafından belirtilen kesin sınırdır. Bu WHERE ifadesinin neden her konumun mesafesinin belirlendiği iç sorguya eklenmediğini merak ediyorsanız bunun nedeni, SQL'in arka planda yürütülme şekli nedeniyle WHERE ifadesi incelenirken bu alanın hesaplanmamış olabilmesidir. Hatta bu WHERE ifadesini iç sorguya taşımaya çalışırsanız hata alırsınız.

**SELECT json_build_object(‘type', ‘FeatureColl**ection') Bu sorgu, JSON oluşturan sorgudan elde edilen tüm sonuç satırlarını GeoJSON FeatureCollection nesnesi olarak sarmalar.

PGX kitaplığını projenize ekleme

Projenize bir bağımlılık eklememiz gerekiyor: Bağlantı havuzunu etkinleştiren PostGres Driver & Toolkit. Bunu yapmanın en kolay yolu Go Modules'ü kullanmaktır. Cloud Shell'de aşağıdaki komutla bir modülü başlatın:

go mod init my_locator

Ardından, kodu bağımlılıklar için taramak, bağımlılıkların listesini mod dosyasına eklemek ve bunları indirmek için bu komutu çalıştırın.

go mod tidy

Son olarak, bağımlılıkları doğrudan proje dizininize çekmek için bu komutu çalıştırın. Böylece, App Engine Flex için kapsayıcı kolayca oluşturulabilir.

go mod vendor

Artık özelliği test etmeye hazırsınız.

Deneyin

Evet, bu bölümde çok şey öğrendik. Nasıl çalıştığını izleyelim.

Geliştirme makinenizin (evet, Cloud Shell bile) veritabanına bağlanabilmesi için veritabanı bağlantısını yönetmek üzere Cloud SQL Proxy'yi kullanmamız gerekecek. Cloud SQL Proxy'yi ayarlamak için:

  1. Cloud SQL Admin API'yi etkinleştirmek için buraya gidin.
  2. Yerel bir geliştirme makinesindeyseniz Cloud SQL proxy aracını yükleyin. Cloud Shell kullanıyorsanız bu adımı atlayabilirsiniz. Cloud Shell'de zaten yüklüdür. Talimatlarda hizmet hesabından bahsedileceğini unutmayın. Sizin için zaten bir hesap oluşturuldu. Gerekli izinleri bu hesaba ekleme konusunu bir sonraki bölümde ele alacağız.
  3. Proxy'yi başlatmak için yeni bir sekme oluşturun (Cloud Shell'de veya kendi terminalinizde).

bcca42933bfbd497.png

  1. https://console.cloud.google.com/sql/instances/locations/overview adresine gidin ve Bağlantı adı alanını bulmak için aşağı kaydırın. Sonraki komutta kullanmak üzere bu adı kopyalayın.
  2. Bu sekmede, CONNECTION_NAME yerine önceki adımda gösterilen bağlantı adını yazarak Cloud SQL proxy'sini şu komutla çalıştırın.
cloud_sql_proxy -instances=CONNECTION_NAME=tcp:5432

Cloud Shell'inizin ilk sekmesine dönün ve Go'nun veritabanı arka ucuyla iletişim kurmak için ihtiyaç duyacağı ortam değişkenlerini tanımlayın. Ardından, sunucuyu daha önce yaptığınız gibi çalıştırın:

Henüz orada değilseniz projenin kök dizinine gidin.

cd YOUR_PROJECT_ROOT

Aşağıdaki beş ortam değişkenini oluşturun (YOUR_PASSWORD_HERE yerine yukarıda oluşturduğunuz şifreyi girin).

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

Yerel örneğinizi çalıştırın.

go run *.go

Önizleme penceresini açtığınızda hiçbir şey değişmemiş gibi çalışır: Başlangıç adresi girebilir, haritada yakınlaştırıp uzaklaştırabilir ve geri dönüşüm yerlerini tıklayabilirsiniz. Ancak artık bir veritabanı tarafından destekleniyor ve ölçeklendirmeye hazır.

9. En yakın mağazaları listele

Directions API, Google Haritalar uygulamasında yol tarifi isteme deneyimine benzer şekilde çalışır. İki nokta arasındaki rotayı almak için tek bir başlangıç ve tek bir hedef girilir. Distance Matrix API, seyahat sürelerine ve mesafelere göre birden fazla olası başlangıç noktası ile birden fazla olası varış noktası arasındaki en uygun eşleşmeleri belirlemek için bu kavramı daha da ileriye taşır. Bu durumda, kullanıcının seçilen adrese en yakın mağazayı bulmasına yardımcı olmak için bir başlangıç noktası ve hedef olarak bir dizi mağaza konumu sağlarsınız.

Her mağazaya başlangıç noktasına olan mesafeyi ekleyin.

initMap işlev tanımının başında, "// TODO: Start Distance Matrix service" yorumunu aşağıdaki kodla değiştirin:

app.js - initMap

distanceMatrixService = new google.maps.DistanceMatrixService();

app.js adlı dizenin sonuna calculateDistances adlı yeni bir işlev ekleyin.

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

İşlev, kendisine tek bir başlangıç noktası olarak iletilen başlangıç noktasını ve mağaza konumlarını hedef dizisi olarak kullanarak Distance Matrix API'yi çağırır. Ardından, mağazanın kimliğini, kullanıcıların okuyabileceği bir dize olarak ifade edilen mesafeyi, sayısal değer olarak metre cinsinden mesafeyi depolayan bir nesne dizisi oluşturur ve diziyi sıralar.

initAutocompleteWidget işlevini, Yer Otomatik Tamamlama arama çubuğundan yeni bir başlangıç noktası seçildiğinde mağaza mesafelerini hesaplayacak şekilde güncelleyin. initAutocompleteWidget işlevinin en altında, "// TODO: Calculate the closest stores" yorumunu aşağıdaki kodla değiştirin:

app.js - initAutocompleteWidget

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

Mesafeye göre sıralanmış mağazaların liste görünümünü gösterme

Kullanıcı, mağazaların en yakından en uzağa doğru sıralandığı bir liste görmeyi bekler. Mağazaların gösterilme sırasını belirlemek için calculateDistances işleviyle değiştirilen listeyi kullanarak her mağaza için bir yan panel girişi oluşturun.

app.js işlevinin sonuna renderStoresPanel() ve storeToPanelRow() adlı iki yeni işlev ekleyin.

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

Aşağıdaki komutu çalıştırarak sunucunuzu yeniden başlatın ve önizlemenizi yenileyin.

go run *.go

Son olarak, otomatik tamamlama arama çubuğuna Austin, TX adresini girin ve önerilerden birini tıklayın.

Harita, bu adresin merkezinde olmalı ve seçilen adrese olan uzaklığa göre mağaza konumlarının listelendiği bir kenar çubuğu görünmelidir. Bir örnek aşağıda gösterilmiştir:

96e35794dd0e88c9.png

10. Haritayı stilize etme

Haritanızı görsel olarak öne çıkarmanın etkili bir yolu, haritaya stil eklemektir. Bulut tabanlı harita stiliyle, haritalarınızın özelleştirilmesi Cloud Console'dan Cloud-based Map Styling (beta) kullanılarak kontrol edilir. Haritanızı beta olmayan bir özellik ile stilize etmeyi tercih ederseniz haritayı programatik olarak stilize etmek için JSON oluşturmanıza yardımcı olacak harita stilize etme dokümanlarını kullanabilirsiniz. Aşağıdaki talimatlar, Cloud-based Map Styling (beta) konusunda size yol gösterir.

Harita kimliği oluşturma

Öncelikle Cloud Console'u açın ve arama kutusuna "Map Management" (Harita Yönetimi) yazın. "Harita Yönetimi (Google Haritalar)" yazan sonucu tıklayın. 64036dd0ed200200.png

Üst kısma yakın bir yerde (Arama kutusunun hemen altında) Yeni Harita Kimliği Oluştur yazan bir düğme görürsünüz. Bunu tıklayın ve istediğiniz adı girin. Harita Türü için JavaScript'i seçtiğinizden emin olun. Diğer seçenekler gösterildiğinde listeden Vektör'ü seçin. Sonuç aşağıdaki resme benzer şekilde görünmelidir.

70f55a759b4c4212.png

"İleri"yi tıkladığınızda yepyeni bir harita kimliği oluşturulur. İsterseniz şimdi kopyalayabilirsiniz ancak daha sonra kolayca bulabileceğiniz için endişelenmenize gerek yok.

Ardından, bu haritaya uygulanacak bir stil oluşturacağız.

Harita stili oluşturma

Hâlâ Cloud Console'un Haritalar bölümündeyseniz soldaki gezinme menüsünün alt kısmında "Harita Stilleri"ni tıklayın. Aksi takdirde, harita kimliği oluşturmaya benzer şekilde, arama kutusuna "Harita Stilleri" yazıp sonuçlardan "Harita Stilleri (Google Haritalar)"ı seçerek doğru sayfayı bulabilirsiniz (aşağıdaki resimde gösterildiği gibi).

9284cd200f1a9223.png

Ardından, üst kısımlarda yer alan "+ Yeni Harita Stili Oluştur" düğmesini tıklayın.

  1. Bu laboratuvarda gösterilen haritadaki stili kullanmak istiyorsanız "JSON'U İÇE AKTAR" sekmesini tıklayın ve aşağıdaki JSON blob'unu yapıştırın. Kendi harita stilinizi oluşturmak istiyorsanız başlamak istediğiniz harita stilini seçin. Ardından, İleri'yi tıklayın.
  2. Bu harita kimliğini bu stille ilişkilendirmek için az önce oluşturduğunuz harita kimliğini seçin ve tekrar Sonraki'yi tıklayın.
  3. Bu noktada, haritanızın stilini daha fazla özelleştirme seçeneği sunulur. Bu özelliği incelemek istiyorsanız Stil düzenleyicide özelleştir'i tıklayın ve beğendiğiniz bir harita stili elde edene kadar renkler ve seçeneklerle oynayın. Aksi takdirde Atla'yı tıklayın.
  4. Sonraki adımda, stilinizin adını ve açıklamasını girip Kaydet ve Yayınla'yı tıklayın.

İlk adımda içe aktarılacak isteğe bağlı bir JSON blob'u aşağıda verilmiştir.

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

Kodunuza harita kimliği ekleme

Bu harita stilini oluşturmak için uğraştığınıza göre, bu harita stilini kendi haritanızda nasıl KULLANABİLİRSİNİZ? İki küçük değişiklik yapmanız gerekir:

  1. Harita kimliğini index.html içindeki komut dosyası etiketine URL parametresi olarak ekleyin.
  2. Add yönteminizde haritayı oluştururken oluşturucu bağımsız değişkeni olarak harita kimliği.initMap()

HTML dosyasında Maps JavaScript API'yi yükleyen komut dosyası etiketini aşağıdaki yükleyici URL'siyle değiştirin. "YOUR_API_KEY" ve "YOUR_MAP_ID" yer tutucularını değiştirin:

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>
...

Sabit map değerinin tanımlandığı initMap app.js yönteminde, mapId özelliği için yorum işaretini kaldırın ve "YOUR_MAP_ID_HERE" değerini yeni oluşturduğunuz harita kimliğiyle değiştirin:

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

Sunucunuzu yeniden başlatın.

go run *.go

Önizlemenizi yenilediğinizde harita, tercihleriniz doğrultusunda şekillendirilmiş olarak görünmelidir. Yukarıdaki JSON stilini kullanan bir örneği burada bulabilirsiniz.

2ece59c64c06e9da.png

11. Üretime dağıtma

Uygulamanızın App Engine Flex'te (geliştirme makinenizdeki / Cloud Shell'deki yerel bir web sunucusunda değil) çalıştığını görmek istiyorsanız bu işlem çok kolaydır. Veritabanı erişiminin üretim ortamında çalışması için birkaç şey eklememiz gerekiyor. Bu işlemlerin tümü, App Engine Flex'ten Cloud SQL'e bağlanma başlıklı doküman sayfasında açıklanmıştır.

App.yaml dosyasına ortam değişkenleri ekleme

Öncelikle, yerel olarak test etmek için kullandığınız tüm ortam değişkenlerinin uygulamanızın app.yaml dosyasının en altına eklenmesi gerekir.

  1. Örnek bağlantı adını bulmak için https://console.cloud.google.com/sql/instances/locations/overview adresini ziyaret edin.
  2. Aşağıdaki kodu app.yaml öğesinin sonuna yapıştırın.
  3. YOUR_DB_PASSWORD_HERE yerine, daha önce postgres kullanıcı adı için oluşturduğunuz şifreyi girin.
  4. YOUR_CONNECTION_NAME_HERE yerine 1. adımdaki değeri girin.

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

Bu uygulama App Engine Flex üzerinden bağlandığından DB_TCP_HOST değerinin 172.17.0.1 olması gerektiğini unutmayın.** Bunun nedeni, sizin yaptığınız gibi Cloud SQL ile bir proxy üzerinden iletişim kuracak olmasıdır.

App Engine Flex hizmet hesabına SQL İstemcisi izinleri ekleme

Cloud Console'da IAM-Admin sayfasına gidin ve adı service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com biçimine uyan bir hizmet hesabı arayın. Bu, App Engine Flex'in veritabanına bağlanmak için kullanacağı hizmet hesabıdır. Satırın sonundaki Düzenle düğmesini tıklayın ve "Cloud SQL İstemcisi" rolünü ekleyin.

b04ccc0b4022b905.png

Proje kodunuzu Go yoluna kopyalama

App Engine'in kodunuzu çalıştırması için Go yolundaki ilgili dosyaları bulabilmesi gerekir. Projenizin kök dizininde olduğunuzdan emin olun.

cd YOUR_PROJECT_ROOT

Dizini go yoluna kopyalayın.

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

Bu dizine geçin.

cd ~/gopath/src/austin-recycling

Uygulamanızı Dağıtma

Uygulamanızı dağıtmak için gcloud CLI'yı kullanın. Dağıtım işlemi biraz zaman alır.

gcloud app deploy

Tam olarak dağıtılmış, kurumsal düzeyde ve estetik açıdan çarpıcı mağaza bulucunuzu çalışırken görmek için tıklayabileceğiniz bir bağlantı almak üzere browse komutunu kullanın.

gcloud app browse

gcloud komutunu Cloud Shell dışında çalıştırıyorsanız gcloud app browse komutunu çalıştırdığınızda yeni bir tarayıcı sekmesi açılır.

12. (Önerilen) Temizleme

Bu codelab'i yaptığınızda BigQuery işleme ve Haritalar Platformu API çağrıları için ücretsiz katman sınırları aşılmaz. Ancak bu codelab'i yalnızca eğitici bir alıştırma olarak yaptıysanız ve gelecekte herhangi bir ücret ödemek istemiyorsanız bu projeyle ilişkili kaynakları silmenin en kolay yolu projenin kendisini silmektir.

Projeyi silme

GCP Console'da Cloud Resource Manager sayfasına gidin:

Proje listesinde, üzerinde çalıştığımız projeyi seçin ve Sil'i tıklayın. Proje kimliğini yazmanız istenir. Girip Kapat'ı tıklayın.

Alternatif olarak, aşağıdaki komutu çalıştırıp yer tutucu GOOGLE_CLOUD_PROJECT yerine proje kimliğinizi yazarak gcloud ile doğrudan Cloud Shell'den projenin tamamını silebilirsiniz:

gcloud projects delete GOOGLE_CLOUD_PROJECT

13. Tebrikler

Tebrikler! Codelab'i başarıyla tamamladınız.

Veya son sayfaya göz attınız. Tebrikler! Son sayfaya kadar göz attınız.

Bu codelab boyunca aşağıdaki teknolojilerle çalıştınız:

Daha fazla bilgi

Bu teknolojilerin tümü hakkında öğrenilecek çok şey var. Bu codelab'de ele almaya zamanımızın yetmediği ancak özel ihtiyaçlarınıza uygun bir mağaza bulucu çözümü oluştururken kesinlikle faydalı olabilecek konularla ilgili bazı yararlı bağlantıları aşağıda bulabilirsiniz.