با پلتفرم Google Maps و Google Cloud یک مکان یاب فروشگاه کامل بسازید

1. مقدمه

خلاصه

تصور کنید مکان‌های زیادی برای قرار دادن روی نقشه دارید و می‌خواهید کاربران بتوانند ببینند این مکان‌ها کجا هستند و مکان‌هایی را که می‌خواهند از آن بازدید کنند، شناسایی کنند. نمونه های رایج این مورد عبارتند از:

  • مکان یاب فروشگاه در وب سایت یک خرده فروش
  • نقشه محل های رای گیری برای انتخابات آینده
  • فهرستی از مکان های تخصصی مانند ظروف بازیافت باتری

چیزی که خواهی ساخت

در این نرم افزار کد، شما یک مکان یاب ایجاد می کنید که از فید داده های زنده مکان های تخصصی ترسیم می کند و به کاربر کمک می کند نزدیک ترین مکان را به نقطه شروع خود پیدا کند. این مکان یاب تمام پشته می تواند تعداد مکان های بسیار بیشتری را نسبت به مکان یاب فروشگاهی ساده که به 25 یا کمتر مکان فروشگاه محدود می شود، اداره کند.

2ece59c64c06e9da.png

چیزی که یاد خواهید گرفت

این نرم افزار کد از یک مجموعه داده باز برای شبیه سازی ابرداده های از پیش جمع شده در مورد تعداد زیادی مکان فروشگاه استفاده می کند تا بتوانید بر یادگیری مفاهیم فنی کلیدی تمرکز کنید.

  • Maps JavaScript API: تعداد زیادی مکان را روی یک نقشه وب سفارشی نمایش می دهد
  • GeoJSON: قالبی که ابرداده‌های مکان‌ها را ذخیره می‌کند
  • تکمیل خودکار مکان: به کاربران کمک می کند مکان های شروع را سریعتر و دقیق تر ارائه دهند
  • Go: زبان برنامه نویسی مورد استفاده برای توسعه برنامه back-end. Backend با پایگاه داده تعامل خواهد داشت و نتایج پرس و جو را با فرمت JSON به قسمت جلویی ارسال می کند.
  • App Engine: برای میزبانی برنامه وب

پیش نیازها

  • دانش اولیه HTML و جاوا اسکریپت
  • یک حساب کاربری گوگل

2. راه اندازی شوید

در مرحله 3 از بخش زیر، Maps JavaScript API ، Places API و Distance Matrix API را برای این Codelab فعال کنید.

با پلتفرم Google Maps شروع کنید

اگر قبلاً از Google Maps Platform استفاده نکرده‌اید، راهنمای Get Started with Google Maps Platform را دنبال کنید یا لیست پخش Started with Google Maps Platform را برای تکمیل مراحل زیر تماشا کنید:

  1. یک حساب صورتحساب ایجاد کنید.
  2. یک پروژه ایجاد کنید.
  3. APIها و SDKهای پلتفرم Google Maps را فعال کنید (در قسمت قبل فهرست شده است).
  4. یک کلید API ایجاد کنید.

Cloud Shell را فعال کنید

در این کد لبه از Cloud Shell استفاده می‌کنید، یک محیط خط فرمان که در Google Cloud اجرا می‌شود و دسترسی به محصولات و منابع در حال اجرا در Google Cloud را فراهم می‌کند تا بتوانید پروژه خود را به طور کامل از مرورگر وب خود میزبانی و اجرا کنید.

برای فعال کردن Cloud Shell از Cloud Console، روی Activate Cloud Shell کلیک کنید 89665d8d348105cd.png (تهیه و اتصال به محیط فقط چند لحظه طول می کشد).

5f504766b9b3be17.png

این یک پوسته جدید را در قسمت پایین مرورگر شما پس از احتمالاً نشان دادن یک بینابینی مقدماتی باز می کند.

d3bb67d514893d1f.png

پروژه خود را تایید کنید

پس از اتصال به Cloud Shell، باید ببینید که قبلاً احراز هویت شده اید و پروژه قبلاً روی ID پروژه ای که در هنگام راه اندازی انتخاب کرده اید تنظیم شده است.

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

اگر به دلایلی پروژه تنظیم نشد، دستور زیر را اجرا کنید:

gcloud config set project <YOUR_PROJECT_ID>

AppEngine Flex API را فعال کنید

AppEngine Flex API باید به صورت دستی از کنسول Cloud فعال شود. انجام این کار نه تنها API را فعال می‌کند، بلکه حساب AppEngine Flexible Environment Service را نیز ایجاد می‌کند، حساب تأیید شده‌ای که از طرف کاربر با سرویس‌های Google (مانند پایگاه‌های داده SQL) در تعامل است.

3. سلام، جهان

Backend: Hello World in Go

در نمونه Cloud Shell خود، با ایجاد یک برنامه Go App Engine Flex شروع می‌کنید که به‌عنوان پایه‌ای برای بقیه بخش کدها عمل می‌کند.

در نوار ابزار Cloud Shell، روی دکمه Open editor کلیک کنید تا ویرایشگر کد در یک تب جدید باز شود. این ویرایشگر کد مبتنی بر وب به شما این امکان را می دهد که به راحتی فایل ها را در نمونه Cloud Shell ویرایش کنید.

b63f7baad67b6601.png

سپس روی نماد Open in new window کلیک کنید تا ویرایشگر و ترمینال به تب جدید منتقل شود.

3f6625ff8461c551.png

در ترمینال در پایین برگه جدید، یک فهرست راهنمای austin-recycling جدید ایجاد کنید.

mkdir -p austin-recycling && cd $_

سپس یک برنامه کوچک Go App Engine ایجاد خواهید کرد تا مطمئن شوید همه چیز کار می کند. سلام دنیا!

فهرست راهنمای austin-recycling نیز باید در لیست پوشه ویرایشگر در سمت چپ ظاهر شود. در فهرست راهنمای austin-recycling ، یک فایل با نام app.yaml ایجاد کنید. محتوای زیر را در فایل app.yaml قرار دهید:

app.yaml

runtime: go
env: flex

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

این فایل پیکربندی برنامه App Engine شما را برای استفاده از زمان اجرا Go Flex پیکربندی می کند. برای اطلاعات پس‌زمینه درباره معنای موارد پیکربندی در این فایل، به مستندات محیط استاندارد Google App Engine Go مراجعه کنید.

سپس، یک فایل main.go در کنار فایل app.yaml ایجاد کنید:

main.go

package main

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

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

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

ارزش آن را دارد که در اینجا لحظه ای مکث کنیم تا بفهمیم این کد حداقل در سطح بالایی چه می کند. شما یک بسته main تعریف کرده‌اید که یک سرور http راه‌اندازی می‌کند که در پورت 8080 گوش می‌دهد و یک تابع کنترل‌کننده برای درخواست‌های HTTP مطابق با مسیر "/" ثبت می‌کند.

تابع handler، که به راحتی handler نامیده می شود، رشته متن "Hello, world!" . این متن به مرورگر شما بازگردانده می شود، جایی که می توانید آن را بخوانید. در مراحل بعدی، کنترل‌کننده‌هایی خواهید ساخت که به‌جای رشته‌های کدگذاری سخت ساده، با داده‌های GeoJSON پاسخ می‌دهند.

پس از انجام این مراحل، اکنون باید یک ویرایشگر به شکل زیر داشته باشید:

2084fdd5ef594ece.png

تستش کن

برای آزمایش این برنامه، می توانید سرور توسعه App Engine را در داخل نمونه Cloud Shell اجرا کنید. به خط فرمان Cloud Shell برگردید و عبارت زیر را تایپ کنید:

go run *.go

برخی از خطوط خروجی گزارش را مشاهده خواهید کرد که به شما نشان می دهد که واقعاً سرور توسعه را روی نمونه Cloud Shell اجرا می کنید، با برنامه وب hello world در حال گوش دادن به پورت localhost 8080. می توانید با فشار دادن وب ، یک برگه مرورگر وب را در این برنامه باز کنید. دکمه Preview و انتخاب آیتم منوی Preview on port 8080 در نوار ابزار Cloud Shell.

4155fc1dc717ac67.png

با کلیک بر روی این آیتم منو، تب جدیدی در مرورگر وب شما باز می شود که عبارت "سلام، دنیا!" از سرور توسعه App Engine ارائه شده است.

در مرحله بعدی داده‌های بازیافت شهر آستین را به این برنامه اضافه می‌کنید و شروع به تجسم آن می‌کنید.

4. داده های فعلی را دریافت کنید

GeoJSON، زبان فرانک دنیای GIS

در مرحله قبل ذکر شد که شما در کد Go خود کنترل کننده هایی ایجاد می کنید که داده های GeoJSON را به مرورگر وب ارائه می کنند. اما GeoJSON چیست؟

در دنیای سیستم اطلاعات جغرافیایی (GIS)، ما باید بتوانیم دانش مربوط به موجودات جغرافیایی را بین سیستم های کامپیوتری به اشتراک بگذاریم. نقشه‌ها برای خواندن انسان‌ها عالی هستند، اما رایانه‌ها معمولاً داده‌هایشان را در قالب‌های هضم‌تر ترجیح می‌دهند.

GeoJSON قالبی برای رمزگذاری ساختارهای داده های جغرافیایی است، مانند مختصات مکان های بازیافتی در آستین، تگزاس. GeoJSON در یک استاندارد نیروی کار مهندسی اینترنت به نام RFC7946 استاندارد شده است. GeoJSON بر اساس JSON تعریف شده است، جاوا اسکریپت Object Notation، که خود در ECMA-404 توسط همان سازمانی که جاوا اسکریپت را استاندارد کرده است، Ecma International ، استاندارد شده است.

نکته مهم این است که GeoJSON یک قالب سیمی است که به طور گسترده برای انتقال دانش جغرافیایی پشتیبانی می شود. این کد لبه از GeoJSON به روش های زیر استفاده می کند:

  • از بسته های Go برای تجزیه داده های آستین در یک ساختار داده خاص GIS داخلی استفاده کنید که از آن برای فیلتر کردن داده های درخواستی استفاده می کنید.
  • داده های درخواستی را برای انتقال بین وب سرور و مرورگر وب سریال کنید.
  • از کتابخانه جاوا اسکریپت برای تبدیل پاسخ به نشانگر روی نقشه استفاده کنید.

با این کار مقدار قابل توجهی در تایپ کد صرفه جویی خواهید کرد، زیرا برای تبدیل جریان داده روی سیم به نمایش های درون حافظه، نیازی به نوشتن تجزیه کننده ها و ژنراتورها ندارید.

داده ها را بازیابی کنید

پورتال داده باز شهر آستین، تگزاس، اطلاعات مکانی در مورد منابع عمومی را برای استفاده عمومی در دسترس قرار می دهد. در این آزمایشگاه کد، مجموعه داده‌های مکان‌های تخلیه بازیافت را تجسم خواهید کرد.

داده ها را با نشانگرهایی روی نقشه که با استفاده از لایه داده در Maps JavaScript API ارائه شده اند، تجسم خواهید کرد.

با دانلود داده های GeoJSON از وب سایت شهر آستین در برنامه خود شروع کنید.

  1. در پنجره خط فرمان نمونه Cloud Shell، سرور را با تایپ کردن [CTRL] + [C] خاموش کنید.
  2. یک دایرکتوری data در داخل پوشه austin-recycling ایجاد کنید و به آن دایرکتوری تغییر دهید:
mkdir -p data && cd data

اکنون از curl برای بازیابی مکان های بازیافت استفاده کنید:

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

در نهایت به دایرکتوری والد بک آپ تغییر دهید.

cd ..

5. نقشه مکان ها

ابتدا، فایل app.yaml را به‌روزرسانی کنید تا برنامه قوی‌تری را که می‌خواهید بسازید، «نه فقط یک برنامه hello world» منعکس کند.

app.yaml

runtime: go
env: flex

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

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

این پیکربندی app.yaml درخواست‌های / ، /*.js ، /*.css و /*.html را به مجموعه‌ای از فایل‌های ثابت هدایت می‌کند. این بدان معنی است که مؤلفه HTML ایستا برنامه شما مستقیماً توسط زیرساخت ارائه فایل App Engine ارائه می شود و نه برنامه Go شما. این باعث کاهش بار سرور و افزایش سرعت سرویس دهی می شود.

اکنون زمان آن است که باطن برنامه خود را در Go بسازید!

قسمت پشتی را بسازید

شاید متوجه شده باشید، یک چیز جالب که فایل app.yaml شما انجام نمی دهد این است که فایل GeoJSON را در معرض دید قرار می دهد. دلیلش این است که GeoJSON توسط Go ما پردازش و ارسال می‌شود و به ما امکان می‌دهد در مراحل بعدی برخی ویژگی‌های فانتزی را ایجاد کنیم. فایل main.go خود را به صورت زیر تغییر دهید:

main.go

package main

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

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

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

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

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


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

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

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

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

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

در حال حاضر باطن Go یک ویژگی ارزشمند به ما می دهد: نمونه AppEngine همه آن مکان ها را به محض راه اندازی در حافظه پنهان ذخیره می کند. این باعث صرفه جویی در زمان می شود زیرا باطن نیازی به خواندن فایل از روی دیسک در هر بازخوانی هر کاربر ندارد!

قسمت جلویی را بسازید

اولین کاری که باید انجام دهیم این است که یک پوشه برای نگه داشتن تمام دارایی های ثابت ما ایجاد کنیم. از پوشه والد پروژه خود، یک پوشه static ایجاد کنید.

mkdir -p static && cd static

ما در این پوشه 3 فایل ایجاد می کنیم.

  • index.html شامل تمام HTML برنامه مکان یاب فروشگاه یک صفحه ای شما خواهد بود.
  • style.css ، همانطور که انتظار دارید، شامل یک ظاهر طراحی خواهد شد
  • app.js مسئول بازیابی GeoJSON، برقراری تماس با Maps API و قرار دادن نشانگرها بر روی نقشه سفارشی شما خواهد بود.

این 3 فایل را ایجاد کنید، مطمئن شوید که آنها را در static/ قرار دهید.

style.css

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

body {
  display: flex;
}

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

index.html

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

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

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

توجه ویژه ای به URL src در تگ اسکریپت عنصر head داشته باشید.

  • متن مکان‌نمای « YOUR_API_KEY » را با کلید API که در مرحله راه‌اندازی ایجاد کردید، جایگزین کنید. برای بازیابی کلید API یا ایجاد یک کلید جدید، می‌توانید از صفحه APIs & Services -> Credentials در Cloud Console دیدن کنید.
  • توجه داشته باشید که URL حاوی پارامتر callback=initialize. اکنون می‌خواهیم فایل جاوا اسکریپت حاوی تابع callback را ایجاد کنیم. اینجاست که برنامه شما مکان‌ها را از پشتیبان بارگیری می‌کند، آن‌ها را به Maps API می‌فرستد و از نتیجه برای علامت‌گذاری مکان‌های سفارشی روی نقشه استفاده می‌کند که همه به زیبایی در صفحه وب شما ارائه می‌شوند.
  • پارامتر libraries=places را بارگیری می‌کند، که برای ویژگی‌هایی مانند تکمیل خودکار آدرس که بعدا اضافه خواهد شد، ضروری است.

app.js

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

async function initialize() {
  initMap();

  // TODO: Initialize an infoWindow

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

  // TODO: Initialize the Autocomplete widget
}

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

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

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

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

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

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

  return circle;
};

این کد مکان های فروشگاه را بر روی نقشه نمایش می دهد. برای آزمایش آنچه تاکنون داشته ایم، از خط فرمان به دایرکتوری والد برگردیم:

cd ..

اکنون، دوباره برنامه خود را در حالت توسعه با استفاده از:

go run *.go

پیش نمایش آن را همانطور که قبلا انجام دادید. شما باید نقشه ای با دایره های سبز کوچک مانند این ببینید.

58a6680e9c8e7396.png

شما در حال رندر کردن مکان‌های نقشه هستید، و ما فقط در نیمه راه از آزمایشگاه کد عبور کرده‌ایم! شگفت انگيز. حالا بیایید کمی تعامل اضافه کنیم.

6. نمایش جزئیات در صورت تقاضا

به رویدادهای کلیک روی نشانگرهای نقشه پاسخ دهید

نمایش دسته ای از نشانگرها بر روی نقشه یک شروع عالی است، اما ما واقعاً به یک بازدیدکننده نیاز داریم که بتواند روی یکی از آن نشانگرها کلیک کند و اطلاعات مربوط به آن مکان (مانند نام کسب و کار، آدرس و غیره) را ببیند. نام پنجره اطلاعات کوچکی که معمولاً با کلیک بر روی نشانگر Google Maps ظاهر می‌شود، یک پنجره اطلاعات است.

یک شی infoWindow ایجاد کنید. موارد زیر را به تابع initialize اضافه کنید، به جای خط نظری که به عنوان خوانده شده " // TODO: Initialize an info window " را جایگزین کنید.

app.js - مقداردهی اولیه

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

تعریف تابع fetchAndRenderStores را با این نسخه کمی متفاوت جایگزین کنید، که خط نهایی را برای فراخوانی storeToCircle با یک آرگومان اضافی، infowindow تغییر می‌دهد:

app.js - fetchAndRenderStores

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

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

تعریف storeToCircle را با این نسخه کمی طولانی تر جایگزین کنید، که اکنون یک پنجره اطلاعات را به عنوان آرگومان سوم می گیرد:

app.js - storeToCircle

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

هر زمان که روی نشانگر فروشگاه روی نقشه کلیک شود، کد جدید بالا یک infoWindow را با اطلاعات فروشگاه انتخاب شده نمایش می دهد.

اگر سرور شما همچنان در حال اجراست، آن را متوقف کرده و مجددا راه اندازی کنید. صفحه نقشه خود را بازخوانی کنید و روی نشانگر نقشه کلیک کنید. یک پنجره اطلاعات کوچک باید با نام و آدرس کسب و کار ظاهر شود، چیزی شبیه به این:

1af0ab72ad0eadc5.png

7. مکان شروع کاربر را دریافت کنید

کاربران مکان یاب فروشگاه معمولاً می خواهند بدانند کدام فروشگاه به آنها نزدیکتر است یا آدرسی که قصد دارند سفر خود را از آنجا شروع کنند. یک نوار جستجوی تکمیل خودکار مکان اضافه کنید تا کاربر بتواند به راحتی آدرس شروع را وارد کند. «تکمیل خودکار مکان» عملکردی شبیه به روش تکمیل خودکار در سایر نوارهای جستجوی Google ارائه می‌کند، به جز اینکه پیش‌بینی‌ها همه مکان‌ها در پلتفرم نقشه‌های Google هستند.

یک فیلد ورودی کاربر ایجاد کنید

برای افزودن یک ظاهر به نوار جستجوی تکمیل خودکار و پانل جانبی مرتبط نتایج، به edit style.css برگردید. در حالی که ما در حال به روز رسانی سبک های CSS هستیم، همچنین سبک هایی را برای نوار کناری آینده اضافه می کنیم که اطلاعات فروشگاه را به عنوان لیستی به همراه نقشه نمایش می دهد.

این کد را به انتهای فایل اضافه کنید.

style.css

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

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

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

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

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

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

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

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

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

.hidden {
  display: none;
}

هم نوار جستجوی تکمیل خودکار و هم پانل کشویی در ابتدا تا زمانی که مورد نیاز نباشد پنهان می شوند.

یک div برای ویجت تکمیل خودکار با جایگزین کردن کامنت در index.html که به عنوان "<!-- Autocomplete div goes here --> " با کد زیر آماده کنید. در حین انجام این ویرایش، div را نیز برای پانل کشویی اضافه می کنیم.

index.html

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

اکنون، تابعی را برای افزودن ویجت تکمیل خودکار به نقشه با افزودن کد زیر به انتهای app.js کنید.

app.js

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

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

کد پیشنهادات تکمیل خودکار را فقط به آدرس‌های برگشتی محدود می‌کند (زیرا تکمیل خودکار مکان می‌تواند با نام مؤسسات و مکان‌های اداری نیز مطابقت داشته باشد) و آدرس‌های بازگردانده شده را فقط به آدرس‌هایی در ایالات متحده محدود می‌کند. افزودن این مشخصات اختیاری تعداد نویسه‌هایی را که کاربر باید وارد کند کاهش می‌دهد تا پیش‌بینی‌ها را برای نشان دادن آدرس مورد نظرش محدود کند.

سپس، div تکمیل خودکار را که ایجاد کرده‌اید به گوشه سمت راست بالای نقشه منتقل می‌کند و مشخص می‌کند که کدام قسمت‌ها باید در مورد هر مکان در پاسخ بازگردانده شوند.

در نهایت، تابع initAutocompleteWidget را در انتهای تابع initialize فراخوانی کنید، و به جای کامنتی که می‌خواند " // TODO: Initialize the Autocomplete widget ".

app.js - مقداردهی اولیه

 // Initialize the Places Autocomplete Widget
 initAutocompleteWidget();

سرور خود را با اجرای دستور زیر راه اندازی مجدد کنید، سپس پیش نمایش خود را تازه سازی کنید.

go run *.go

اکنون باید یک ویجت تکمیل خودکار را در گوشه سمت راست بالای نقشه خود مشاهده کنید، که آدرس‌های ایالات متحده را مطابق با آنچه تایپ می‌کنید، به سمت ناحیه قابل مشاهده نقشه به شما نشان می‌دهد.

58e9bbbcc4bf18d1.png

هنگامی که کاربر یک آدرس شروع را انتخاب می کند، نقشه را به روز کنید

اکنون، باید زمانی که کاربر یک پیش‌بینی را از ویجت تکمیل خودکار انتخاب می‌کند، رسیدگی کنید و از آن مکان به عنوان مبنایی برای محاسبه فاصله تا فروشگاه‌های خود استفاده کنید.

کد زیر را به انتهای initAutocompleteWidget در app.js و به جای نظر " // TODO: Respond when a user selects an address دهید".

app.js - initAutocompleteWidget

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

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

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

کد یک شنونده اضافه می کند به طوری که وقتی کاربر روی یکی از پیشنهادات کلیک می کند، نقشه آدرس انتخاب شده را مجدداً نشان می دهد و مبدا را به عنوان مبنایی برای محاسبات فاصله شما قرار می دهد. شما محاسبات فاصله را در مرحله آینده اجرا می کنید.

سرور خود را متوقف کرده و مجدداً راه اندازی کنید و پیش نمایش خود را بازخوانی کنید تا پس از وارد کردن آدرس در نوار جستجوی تکمیل خودکار، مرکز مجدد نقشه را مشاهده کنید.

8. مقیاس با Cloud SQL

تا کنون، ما یک مکان یاب فروشگاه بسیار عالی داریم. از این واقعیت استفاده می کند که برنامه فقط از صد مکان استفاده می کند، با بارگیری آنها در حافظه پشتیبان (به جای خواندن مکرر از فایل). اما اگر مکان یاب شما نیاز به عملکرد در مقیاس متفاوت داشته باشد، چه؟ اگر صدها مکان پراکنده در اطراف یک منطقه جغرافیایی بزرگ (یا هزاران مکان در سراسر جهان) دارید، حفظ همه آن مکان‌ها در حافظه دیگر بهترین ایده نیست، و تقسیم مناطق به فایل‌های جداگانه مشکلات خاص خود را ایجاد می‌کند.

وقت آن است که مکان های خود را از پایگاه داده بارگیری کنید. برای این مرحله، همه مکان‌های موجود در فایل GeoJSON شما را به یک پایگاه داده Cloud SQL منتقل می‌کنیم و پس‌زمینه Go را به‌روزرسانی می‌کنیم تا هر زمان که درخواستی وارد می‌شود، نتایج را از آن پایگاه داده به جای کش محلی آن استخراج کند.

با پایگاه داده PostGres یک نمونه Cloud SQL ایجاد کنید

می‌توانید یک نمونه Cloud SQL از طریق Google Cloud Console ایجاد کنید، اما استفاده از ابزار gcloud برای ایجاد یکی از خط فرمان حتی ساده‌تر است. در پوسته ابری، یک نمونه Cloud SQL با دستور زیر ایجاد کنید:

gcloud sql instances create locations \
--database-version=POSTGRES_12 \
--tier=db-custom-1-3840 --region=us-central1
  • locations آرگومان نامی است که برای دادن این نمونه از Cloud SQL انتخاب می‌کنیم.
  • پرچم tier راهی برای انتخاب از بین برخی از ماشین‌های از پیش تعریف‌شده راحت است.
  • مقدار db-custom-1-3840 نشان می دهد که نمونه در حال ایجاد باید دارای یک vCPU و حدود 3.75 گیگابایت حافظه باشد.

نمونه Cloud SQL با پایگاه داده PostGresSQL با postgres کاربر پیش‌فرض ایجاد و مقداردهی اولیه می‌شود. رمز عبور این کاربر چیست؟ سوال عالی! آنها یکی ندارند. قبل از اینکه بتوانید وارد شوید باید یکی را پیکربندی کنید.

رمز عبور را با دستور زیر تنظیم کنید:

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

سپس زمانی که از شما خواسته شد رمز عبور انتخابی خود را وارد کنید.

پسوند PostGIS را فعال کنید

PostGIS یک برنامه افزودنی برای PostGresSQL است که ذخیره انواع استاندارد داده های مکانی را آسان تر می کند. در شرایط عادی، برای افزودن PostGIS به پایگاه داده خود باید یک فرآیند نصب کامل را طی کنیم. خوشبختانه، این یکی از پسوندهای پشتیبانی شده Cloud SQL برای PostGresSQL است.

با استفاده از دستور زیر در postgres پوسته ابری، به نمونه پایگاه داده متصل شوید.

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

رمز عبوری را که ایجاد کرده اید وارد کنید. اکنون پسوند PostGIS را در خط فرمان postgres=> اضافه کنید.

CREATE EXTENSION postgis;

در صورت موفقیت آمیز بودن، خروجی باید مطابق شکل زیر خوانده شود CREATE EXTENSION.

نمونه خروجی فرمان

CREATE EXTENSION

در نهایت با وارد کردن دستور quit در خط فرمان postgres=> از اتصال پایگاه داده خارج شوید.

\q

وارد کردن داده های جغرافیایی به پایگاه داده

اکنون باید تمام آن داده های مکان را از فایل های GeoJSON به پایگاه داده جدید خود وارد کنیم.

خوشبختانه، این مشکلی است که به خوبی سفر کرده است و چندین ابزار را می توان در اینترنت پیدا کرد تا این کار را برای شما خودکار کند. ما قصد داریم از ابزاری به نام ogr2ogr استفاده کنیم که بین چندین فرمت رایج برای ذخیره داده های مکانی تبدیل می شود. در میان این گزینه ها، بله، درست حدس زدید، تبدیل فرم GeoJSON به یک فایل dump SQL است. سپس از فایل dump SQL می توان برای ایجاد جداول و ستون های خود برای پایگاه داده استفاده کرد و آن را با تمام داده های موجود در فایل های GeoJSON بارگیری کرد.

فایل dump SQL ایجاد کنید

ابتدا ogr2ogr را نصب کنید.

sudo apt-get install gdal-bin

سپس، از ogr2ogr برای ایجاد فایل dump SQL استفاده کنید. این فایل جدولی به نام austinrecycling ایجاد می کند.

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

دستور بالا بر اساس اجرا از پوشه austin-recycling است. اگر می‌خواهید آن را از دایرکتوری دیگری اجرا کنید، data با مسیر دایرکتوری که recycling-locations.geojson در آن ذخیره می‌شود جایگزین کنید.

پایگاه داده خود را با مکان های بازیافت پر کنید

پس از تکمیل آخرین دستور، اکنون باید یک فایل به نام datadump.sql, در همان فهرستی که دستور را اجرا کرده اید داشته باشید. اگر آن را باز کنید، کمی بیش از صد خط SQL را خواهید دید که جدولی را ایجاد می کند که در حال austinrecycling و آن را با مکان ها پر می کند.

حالا یک اتصال به دیتابیس باز کنید و با دستور زیر آن اسکریپت را اجرا کنید.

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

اگر اسکریپت با موفقیت اجرا شود، چند خط آخر خروجی به این صورت خواهد بود:

نمونه خروجی فرمان

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

برای استفاده از Cloud SQL، Go back end را به روز کنید

اکنون که همه این داده ها را در پایگاه داده خود داریم، وقت آن است که کد خود را به روز کنیم.

برای ارسال اطلاعات مکان، قسمت جلویی را به‌روزرسانی کنید

بیایید با یک به‌روزرسانی بسیار کوچک در قسمت جلویی شروع کنیم: از آنجایی که ما اکنون این برنامه را برای مقیاسی می‌نویسیم که نمی‌خواهیم هر زمان که درخواست اجرا می‌شود، هر مکان به جلو تحویل داده شود، باید برخی از اطلاعات اولیه را از قسمت جلویی در مورد مکانی که کاربر به آن اهمیت می دهد ارسال کنید.

app.js را باز کنید و تعریف تابع fetchStores را با این نسخه جایگزین کنید تا طول و عرض جغرافیایی مورد نظر را در URL لحاظ کنید.

app.js - fetchStores

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

پس از تکمیل این مرحله از Codelab، پاسخ تنها نزدیکترین فروشگاه ها به مختصات نقشه ارائه شده در پارامتر center را برمی گرداند. برای واکشی اولیه در تابع initialize ، کد نمونه ارائه شده در این آزمایشگاه از مختصات مرکزی آستین، تگزاس استفاده می کند.

از آنجایی که fetchStores اکنون فقط زیرمجموعه‌ای از مکان‌های فروشگاه را برمی‌گرداند، هر زمان که کاربر مکان شروع خود را تغییر داد، باید فروشگاه‌ها را دوباره واکشی کنیم.

تابع initAutocompleteWidget را به روز کنید تا هر زمان که یک مبدأ جدید تنظیم شد، مکان ها را به روز کنید. این نیاز به دو ویرایش دارد:

  1. در initAutocompleteWidget، پاسخ تماس شنونده place_changed را پیدا کنید. خطی را که حلقه‌های موجود را پاک می‌کند، لغو نظر کنید، به طوری که هر بار که کاربر آدرسی را از جستجوی تکمیل خودکار مکان انتخاب می‌کند، این خط اجرا می‌شود.

app.js - initAutocompleteWidget

  autocomplete.addListener("place_changed", async () => {
    circles.forEach((c) => c.setMap(null)); // clear existing stores
    // ...
  1. هر زمان که مبدا انتخاب شده تغییر کند، متغیر originLocation به روز می شود. در پایان فراخوانی " place_changed "، خط بالای خط " // TODO: Calculate the closest stores " را لغو نظر کنید تا این مبدا جدید را به یک فراخوانی جدید به تابع fetchAndRenderStores کنید.

app.js - initAutocompleteWidget

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

برای استفاده از CloudSQL به جای یک فایل JSON صاف، قسمت پشتی را به روز کنید

خواندن و کش کردن فایل های مسطح GeoJSON را حذف کنید

ابتدا main.go را تغییر دهید تا کدی که فایل فلت GeoJSON را بارگیری و کش می کند حذف کنید. ما همچنین می‌توانیم از تابع dropoffsHandler خلاص شویم، زیرا در حال نوشتن یکی از Cloud SQL در فایل دیگری هستیم.

main.go جدید شما بسیار کوتاهتر خواهد بود.

main.go

package main

import (

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

func main() {

        initConnectionPool()

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

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

یک کنترل کننده جدید برای درخواست های مکان ایجاد کنید

اکنون بیایید یک فایل دیگر به نام locations.go ایجاد کنیم، همچنین در فهرست راهنمای بازیافت آستین. با پیاده سازی مجدد کنترل کننده برای درخواست های مکان شروع کنید.

locations.go

package main

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

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

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

گرداننده وظایف مهم زیر را انجام می دهد:

  • طول و عرض جغرافیایی را از شی درخواست بیرون می کشد (به یاد دارید چگونه آنها را به URL اضافه کردیم؟)
  • این getGeoJsonFromDatabase را اجرا می کند، که یک رشته GeoJSON را برمی گرداند (این را بعداً خواهیم نوشت.)
  • از ResponseWriter برای چاپ رشته GeoJSON در پاسخ استفاده می کند.

در مرحله بعد، ما یک استخر اتصال ایجاد می کنیم تا به مقیاس استفاده از پایگاه داده با کاربران همزمان کمک کند.

یک استخر اتصال ایجاد کنید

استخر اتصال مجموعه ای از اتصالات پایگاه داده فعال است که سرور می تواند مجدداً برای خدمات رسانی به درخواست های کاربر استفاده کند. با افزایش تعداد کاربران فعال شما، هزینه های زیادی را حذف می کند، زیرا سرور نیازی به صرف زمان برای ایجاد و از بین بردن اتصالات برای هر کاربر فعال ندارد. ممکن است در بخش قبلی متوجه شده باشید که کتابخانه github.com/jackc/pgx/stdlib. این یک کتابخانه محبوب برای کار با استخرهای اتصال در Go است.

در انتهای locations.go ، یک تابع initConnectionPool (که از main.go می شود) ایجاد کنید که یک مخزن اتصال را مقداردهی اولیه می کند. برای وضوح، از چند روش کمکی در این قطعه استفاده شده است. configureConnectionPool مکانی مفید برای تنظیم تنظیمات استخر مانند تعداد اتصالات و طول عمر هر اتصال فراهم می کند. mustGetEnv تماس‌ها را برای دریافت متغیرهای محیطی مورد نیاز جمع می‌کند، بنابراین اگر نمونه اطلاعات مهمی را از دست داده باشد (مانند IP یا نام پایگاه داده برای اتصال به آن) می‌توان پیام‌های خطای مفیدی ارسال کرد.

locations.go

// The connection pool
var db *sql.DB

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

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

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

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

                configureConnectionPool(dbPool)

                if err != nil {

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

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

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

از پایگاه داده برای مکان ها پرس و جو کنید، در ازای آن JSON دریافت کنید.

اکنون می خواهیم یک کوئری پایگاه داده بنویسیم که مختصات نقشه را می گیرد و نزدیکترین 25 مکان را برمی گرداند. نه تنها این، بلکه به لطف برخی از عملکردهای مدرن پایگاه داده فانتزی، آن داده ها را به عنوان GeoJSON برمی گرداند. نتیجه نهایی همه اینها این است که تا آنجا که کد فرانت اند می تواند بگوید، هیچ چیز تغییر نکرده است. قبل از اینکه درخواستی را برای یک URL ارسال کند و یک دسته GeoJSON دریافت کند. اکنون درخواستی را به یک URL ارسال می کند و ... دسته ای از GeoJSON را دریافت می کند.

در اینجا تابع برای انجام آن جادو است. تابع زیر را بعد از کنترل کننده و کد ادغام اتصال که در پایین locations.go نوشتید اضافه کنید.

locations.go

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

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

        const tableName = "austinrecycling"

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

        log.Println(queryStr)

        rows, err := db.Query(queryStr)

        defer rows.Close()

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

این تابع عمدتاً فقط راه‌اندازی، حذف و مدیریت خطا برای ارسال درخواست به پایگاه داده است. بیایید به SQL واقعی نگاه کنیم، که کارهای بسیار جالبی را در لایه پایگاه داده انجام می دهد، بنابراین لازم نیست نگران پیاده سازی هر یک از آنها در کد باشید.

پرس و جوی خامی که پس از تجزیه رشته و درج تمام حروف الفبای رشته در جای مناسب خود، فعال می شود، به این صورت است:

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

این پرس و جو را می توان به عنوان یک پرس و جو اولیه و برخی از توابع بسته بندی JSON مشاهده کرد.

SELECT * ... LIMIT 25 همه فیلدها را برای هر مکان انتخاب می کند. سپس از تابع ST_DISTANCE (بخشی از مجموعه توابع اندازه گیری جغرافیایی PostGIS) برای تعیین فاصله بین هر مکان در پایگاه داده و جفت lat/long مکانی که کاربر در قسمت جلویی ارائه کرده است استفاده می کند. به یاد داشته باشید که بر خلاف Distance Matrix، که می تواند مسافت رانندگی را به شما بدهد، این فواصل GeoSpatial هستند. سپس برای کارایی از آن فاصله برای مرتب‌سازی استفاده می‌کند و 25 نزدیک‌ترین مکان را به مکان مشخص شده کاربر برمی‌گرداند.

** SELECT json_build_object('type', 'F **eature') کوئری قبلی را می‌پیچد، نتایج را می‌گیرد و از آنها برای ساختن یک شی GeoJSON Feature استفاده می‌کند. به طور غیرمنتظره، این درخواست همچنین جایی است که حداکثر شعاع اعمال می شود "16090" تعداد مترها در 10 مایل است، محدودیت سختی که توسط Go backend مشخص شده است. اگر تعجب می کنید که چرا این بند WHERE به پرس و جو داخلی (که در آن فاصله هر مکان تعیین می شود) اضافه نشده است، به این دلیل است که نحوه اجرای SQL در پشت صحنه، ممکن است آن فیلد در هنگام عبارت WHERE محاسبه نشده باشد. مورد بررسی قرار گرفت. در واقع اگر بخواهید این عبارت WHERE را به پرس و جو داخلی منتقل کنید، یک خطا ایجاد می کند.

** SELECT json_build_object('type', 'FeatureColl ') این پرس و جو تمام ردیف های حاصل از جستار تولید کننده JSON را در یک شی GeoJSON FeatureCollection می پیچد .

کتابخانه PGX را به پروژه خود اضافه کنید

ما باید یک وابستگی به پروژه شما اضافه کنیم: PostGres Driver & Toolkit ، که ادغام اتصال را فعال می کند. ساده ترین راه برای انجام این کار با Go Modules است. یک ماژول را با این دستور در پوسته ابری راه اندازی کنید:

go mod init my_locator

در مرحله بعد، این دستور را اجرا کنید تا کدها را برای وابستگی ها اسکن کنید، لیستی از وابستگی ها را به فایل مود اضافه کنید و آنها را دانلود کنید.

go mod tidy

در نهایت، این دستور را اجرا کنید تا وابستگی ها را مستقیماً به فهرست پروژه خود بکشید تا کانتینر به راحتی برای AppEngine Flex ساخته شود.

go mod vendor

خوب، شما برای آزمایش آن آماده هستید!

تستش کن

خوب، ما فقط کارهای زیادی انجام دادیم. بیایید کار آن را تماشا کنیم!

برای اینکه ماشین توسعه شما (بله، حتی پوسته ابری) به پایگاه داده متصل شود، باید از Cloud SQL Proxy برای مدیریت اتصال پایگاه داده استفاده کنیم. برای راه اندازی Cloud SQL Proxy:

  1. برای فعال کردن Cloud SQL Admin API به اینجا بروید
  2. اگر در یک ماشین توسعه محلی هستید، ابزار پروکسی ابری SQL را نصب کنید. اگر از پوسته ابری استفاده می کنید، می توانید این مرحله را رد کنید، قبلاً نصب شده است! توجه داشته باشید که دستورالعمل ها به یک حساب کاربری ارجاع می دهند. قبلاً یکی برای شما ایجاد شده است، و ما اضافه کردن مجوزهای لازم به آن حساب را در بخش زیر پوشش خواهیم داد.
  3. یک تب جدید (در پوسته ابری یا ترمینال خودتان) برای شروع پروکسی ایجاد کنید.

bcca42933bfbd497.png

  1. از https://console.cloud.google.com/sql/instances/locations/overview دیدن کنید و به پایین بروید تا قسمت نام اتصال را پیدا کنید. آن نام را برای استفاده در دستور بعدی کپی کنید.
  2. در آن تب، پروکسی Cloud SQL را با این دستور اجرا کنید و نام اتصال نشان داده شده در مرحله قبل را جایگزین CONNECTION_NAME کنید.
cloud_sql_proxy -instances=CONNECTION_NAME=tcp:5432

به اولین تب پوسته ابری خود برگردید و متغیرهای محیطی را که Go برای برقراری ارتباط با پایگاه داده نیاز دارد تعریف کنید و سپس سرور را به همان روشی که قبلا انجام دادید اجرا کنید:

اگر قبلاً آنجا نیستید، به دایرکتوری ریشه پروژه بروید.

cd YOUR_PROJECT_ROOT

پنج متغیر محیطی زیر را ایجاد کنید (پسوردی را که در بالا ایجاد کردید جایگزین YOUR_PASSWORD_HERE کنید).

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

نمونه محلی خود را اجرا کنید.

go run *.go

پنجره پیش‌نمایش را باز کنید، و باید طوری عمل کند که انگار هیچ چیز تغییر نکرده است: می‌توانید یک آدرس شروع را وارد کنید، روی نقشه زوم کنید و روی مکان‌های بازیافت کلیک کنید. اما اکنون توسط یک پایگاه داده پشتیبانی شده و برای مقیاس آماده شده است!

9. نزدیک ترین فروشگاه ها را فهرست کنید

Directions API بسیار شبیه تجربه درخواست مسیرها در برنامه Google Maps عمل می کند - وارد کردن یک مبدا و یک مقصد واحد برای دریافت مسیری بین این دو. Distance Matrix API این مفهوم را برای شناسایی جفت‌های بهینه بین مبداهای متعدد و چندین مقصد ممکن بر اساس زمان و مسافت‌های سفر بیشتر می‌کند. در این حالت، برای کمک به کاربر برای یافتن نزدیک‌ترین فروشگاه به آدرس انتخاب شده، یک مبدا و مجموعه‌ای از مکان‌های فروشگاه را به عنوان مقصد ارائه می‌کنید.

فاصله از مبدا را به هر فروشگاه اضافه کنید

At the beginning of the initMap function definition, replace the comment " // TODO: Start Distance Matrix service " with the following code:

app.js - initMap

distanceMatrixService = new google.maps.DistanceMatrixService();

Add a new function to the end of app.js called 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);
  });
};

The function calls the Distance Matrix API using the origin passed to it as a single origin and the store locations as an array of destinations. Then, it builds an array of objects storing the store's ID, distance expressed in a human-readable string, distance in meters as a numerical value, and sorts the array.

Update the initAutocompleteWidget function to calculate the store distances whenever a new origin is selected from the Place Autocomplete search bar. At the bottom of the initAutocompleteWidget function, replace the comment " // TODO: Calculate the closest stores " with the following code:

app.js - initAutocompleteWidget

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

Display a list view of stores sorted by distance

The user expects to see a list of the stores ordered from nearest to farthest. Populate a side-panel listing for each store using the list that was modified by the calculateDistances function to inform the display order of the stores.

Add a two new functions to the end of app.js called renderStoresPanel() and 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;
};

Restart your server and refresh your preview by running the following command.

go run *.go

Finally, enter an Austin, TX address into the Autocomplete search bar and click on one of the suggestions.

The map should center on that address and a sidebar should appear listing the store locations in order of distance from the selected address. One example is pictured as follows:

96e35794dd0e88c9.png

10. Style the map

One high-impact way to set your map apart visually is to add styling to it. With cloud-based map styling, the customization of your maps is controlled from the Cloud Console using Cloud-based Map Styling (beta). If you'd rather style your map with a non-beta feature, you can use the map styling documentation to help you generate json for programmatically styling the map. The instructions below guide you through Cloud-based Map Styling (beta).

Create a Map ID

First, open up Cloud Console and in the search box, and type in "Map Management" . Click the result that says "Map Management (Google Maps)". 64036dd0ed200200.png

You'll see a button near the top (right under the Search box) that says Create New Map ID . Click that, and fill in whatever name you want. For Map Type, be sure to select JavaScript , and when further options show up, select Vector from the list. The end result should look something like the image below.

70f55a759b4c4212.png

Click "Next" and you'll be graced with a brand new Map ID. You can copy it now if you want, but don't worry, it's easy to look up later.

Next we're going to create a style to apply to that map.

Create a Map Style

If you're still in the Maps section of the Cloud Console, click "Map Styles at the bottom of the navigation menu on the left. Otherwise, just like creating a Map ID, you can find the right page by typing "Map Styles" in the search box and selecting " Map Styles (Google Maps)" from the results, like in the picture below.

9284cd200f1a9223.png

Next click on the button near the top that says " + Create New Map Style "

  1. If you want to match the styling in the map shown in this lab, click the " IMPORT JSON " tab and paste the JSON blob below. Otherwise if you want to create your own, select the Map Style you want to start with. Then click Next .
  2. Select the Map ID you just created to associate that Map ID with this style, and click Next again.
  3. At this point you're given the option of further customizing the styling of your map. If this is something you want to explore, click Customize in Style Editor and play around with the colors & options until you have a map style you like. Otherwise click Skip .
  4. On the next step, enter your style's name and description, and then click Save And Publish .

Here is an optional JSON blob to import in the first step.

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

Add Map ID to your code

Now that you've gone through the trouble of creating this map style, how do you actually USE this map style in your own map? You need to make two small changes:

  1. Add the Map ID as a url parameter to the script tag in index.html
  2. Add the Map ID as a constructor argument when you create the map in your initMap() method.

Replace the script tag that loads the Maps JavaScript API in the HTML file with the loader URL below, replacing the placeholders for " YOUR_API_KEY " and " 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>
...

In the initMap method of app.js where the constant map is defined, uncomment the line for the mapId property and replace " YOUR_MAP_ID_HERE " with the Map ID you just created:

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

Restart your server.

go run *.go

Upon refreshing your preview, the map should look styled according to your preferences. Here is an example using the JSON styling above.

2ece59c64c06e9da.png

11. Deploy to production

If you want to see your app running from AppEngine Flex (and not just a local webserver on your development machine / Cloud Shell, which is what you've been doing), it's very easy. We just need to add a couple things in order for database access to work in the production environment. This is all outlined in the documentation page on Connecting from App Engine Flex to Cloud SQL .

Add Environment Variables to App.yaml

First, all those environment variables you were using to test locally need to be added to the bottom of your application's app.yaml file.

  1. Visit https://console.cloud.google.com/sql/instances/locations/overview to look up the instance connection name.
  2. Paste the following code at the end of app.yaml .
  3. Replace YOUR_DB_PASSWORD_HERE with the password you created for the postgres username earlier.
  4. Replace YOUR_CONNECTION_NAME_HERE with the value from step 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

Note that the DB_TCP_HOST should have the value 172.17.0.1 since this app connects via AppEngine Flex**.** This is because it will be communicating with Cloud SQL via a proxy, similar to the way you were.

Add SQL Client permissions to the AppEngine Flex service account

Go to the IAM-Admin page in Cloud Console and look for a service account whose name matches the format service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com . This is the service account App Engine Flex will use to connect to the database. Click the Edit button at the end of the row and add the role " Cloud SQL Client ".

b04ccc0b4022b905.png

Copy your project code to the Go path

In order for AppEngine to run your code, it needs to be able to find relevant files in the Go path. Make sure you are in your project root directory.

cd YOUR_PROJECT_ROOT

Copy the directory to the go path.

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

Change into that directory.

cd ~/gopath/src/austin-recycling

Deploy Your App

Use the gcloud CLI to deploy your app. It will take some time to deploy.

gcloud app deploy

Use the browse command to get a link that you can click on to see your fully deployed, enterprise-grade, aesthetically stunning store locator in action.

gcloud app browse

If you were running gcloud outside the cloud shell, then running gcloud app browse would open a new browser tab.

12. (Recommended) Clean up

Performing this codelab will stay within free tier limits for BigQuery processing and Maps Platform API calls, but if you performed this solely as an educational exercise and want to avoid incurring any future charges, the easiest way to delete the resources associated with this project is to delete the project itself.

Delete the Project

In the GCP Console, go to the Cloud Resource Manager page:

In the project list, select the project we've been working in and click Delete . You'll be prompted to type in the project ID. Enter it and click Shut Down.

Alternatively, you can delete the entire project directly from Cloud Shell with gcloud by running the following command and replacing the placeholder GOOGLE_CLOUD_PROJECT with your project ID:

gcloud projects delete GOOGLE_CLOUD_PROJECT

13. Congratulations

تبریک می گویم! You have successfully completed the codelab !

Or you skimmed to the last page. تبریک می گویم! You have skimmed to the last page !

Over the course of this codelab, you have worked with the following technologies:

Further Reading

There's still lots to learn about all of these technologies. Below are some helpful links for topics we didn't have time to cover in this codelab, but could certainly be useful to you in building out a store locator solution that fits your specific needs.