יצירת מאתר מחסניות בערימה מלאה באמצעות מפות Google ו-Google Cloud

1. מבוא

מופשט

נניח שיש לכם הרבה מקומות להציב במפה, ואתם רוצים שמשתמשים יוכלו לראות את המקומות האלה ולזהות את המקום שהם רוצים לבקר בו. למשל:

  • מאתר חנויות באתר של קמעונאי
  • מפה של מיקומי סקרים לקראת בחירות קרובות
  • ספרייה של מיקומים מיוחדים כגון מיחזור מיחזור סוללות

מה תפתחו

ב-Codelab הזה, תיצרו ממקם שמתבסס על פיד נתונים פעיל של מיקומים מיוחדים ויעזור למשתמש למצוא את המיקום הקרוב ביותר לנקודת ההתחלה שלו. ממקם הקריסות המלא יכול לטפל במספרים גדולים יותר של מקומות מאשר ממקם החנות הפשוט, שמוגבל ל-25 מיקומי חנויות בלבד.

2ece59c64c06e9da.png

מה תלמדו

ב-Codelab הזה יש קבוצת נתונים פתוחה שמאפשרת לדמות מטא-נתונים מאוכלסים מראש על מספר גדול של מיקומי חנויות, כדי שתוכלו להתמקד בלמידה על המושגים הטכניים העיקריים.

  • ממשק API של JavaScript במפות Google: הצגת מספר רב של מיקומים במפת אינטרנט מותאמת אישית
  • GeoJSON: פורמט שבו נשמרים מטא-נתונים על מיקומים
  • השלמה אוטומטית של מקום: עוזרים למשתמשים לספק מיקומים מתחילים בצורה מהירה ומדויקת יותר
  • Go: שפת התכנות המשמשת לפיתוח היישום העורפי. הקצה העורפי יקיים אינטראקציה עם מסד הנתונים וישלח תוצאות שאילתה בחזרה לממשק הקצה בפורמט JSON.
  • App Engine: לאירוח אפליקציית האינטרנט

דרישות מוקדמות

  • ידע בסיסי ב-HTML ו-JavaScript
  • חשבון Google

2. להגדרה

בשלב 3 של הקטע הבא, יש להפעיל את API של מפות Google, את API של מקומות Google ואת API של מטריצת מרחק במעבד הקוד הזה.

תחילת העבודה עם הפלטפורמה של מפות Google

אם לא השתמשתם בעבר בפלטפורמה של מפות Google, יש לבצע את המדריך לתחילת העבודה עם מפות Google או לצפות בפלייליסט של הפלטפורמה של מפות Google כדי להשלים את השלבים הבאים:

  1. יוצרים חשבון לחיוב.
  2. יוצרים פרויקט.
  3. הפעלת ממשקי API וערכות SDK של מפות Google (מפורט בקטע הקודם).
  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, אתם כבר יכולים לראות שהאימות שלכם כבר הוגדר, והפרויקט כבר הוגדר כמזהה הפרויקט שבחרתם במהלך ההגדרה.

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

אם מסיבה כלשהי הפרויקט לא מוגדר, מריצים את הפקודה הבאה:

gcloud config set project <YOUR_PROJECT_ID>

הפעלת AppEngine Flex API

יש להפעיל ידנית את AppEngine Flex API ב-Cloud Console. פעולה זו לא רק תפעיל את ה-API וגם תיצור את חשבון השירות הגמיש של AppEngine, החשבון המאומת שיקיים אינטראקציה עם שירותי Google (כגון מסדי נתונים של SQL) בשם המשתמש.

3. שלום, עולם

קצה עורפי: Hello World in Go

במופע של Cloud Shell, תתחילו ליצור אפליקציה של Go App Engine Flex שתשמש כבסיס לשאר קוד שיעור ה-Lab.

בסרגל הכלים של Cloud Shell, לוחצים על הלחצן Open Editor כדי לפתוח את עורך הקוד בכרטיסייה חדשה. עורך הקוד הזה באינטרנט מאפשר לכם לערוך בקלות קבצים במכונה של Cloud Shell.

b63f7baad67b6601.png

לאחר מכן לוחצים על הסמל פתיחה בחלון חדש כדי להעביר את העורך והמסוף לכרטיסייה חדשה.

3f6625ff8461c551.png

במסוף, בחלק התחתון של הכרטיסייה החדשה, יוצרים ספרייה חדשה מסוג austin-recycling.

mkdir -p austin-recycling && cd $_

בשלב הבא, יוצרים אפליקציה קטנה ב-Go App Engine כדי לוודא שהכול פועל. שלום עולם!

הספרייה austin-recycling אמורה להופיע גם ברשימת התיקיות של Editor בצד ימין. בספרייה של 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, ומתעדת פונקציית handler של בקשות HTTP התואמות לנתיב "/".

פונקציית ה-handler, שנקראת באופן ידני handler, כותבת את מחרוזת הטקסט "Hello, world!". הטקסט הזה יועבר חזרה לדפדפן שלך, שבו תהיה לך אפשרות לקרוא אותו. בשלבים הבאים, תעשו גורמי handler שמגיבים באמצעות נתוני GeoJSON במקום מחרוזות פשוטות עם קוד קשיח.

לאחר ביצוע השלבים האלה, אמורה להיות לכם עורך שנראה כך:

2084fdd5ef594ece.png

רוצה לנסות?

כדי לבדוק את האפליקציה הזו, אפשר להריץ את שרת הפיתוח של App Engine בתוך המופע של Cloud Shell. חוזרים לשורת הפקודה ב-Cloud Shell ומקלידים את הפרטים הבאים:

go run *.go

יוצגו לכם מספר שורות של פלט יומן שמראה שאכן אתם מפעילים את שרת הפיתוח במופע של Cloud Shell, כשאפליקציית האינטרנט בשלום נמצאת ביציאה מ-localhost 8080. ניתן לפתוח כרטיסייה של דפדפן אינטרנט באפליקציה הזו על ידי לחיצה על הלחצן תצוגה מקדימה באינטרנט ובחירה בפריט תצוגה מקדימה ביציאה 8080 בסרגל הכלים של Cloud Shell.

4155fc1dc717ac67.png

לחיצה על פריט התפריט הזה תפתח כרטיסייה חדשה בדפדפן האינטרנט עם המילים "שלום, עולם!" מוגש משרת הפיתוח של App Engine.

בשלב הבא, תוסיפו לאפליקציה הזו את נתוני המיחזור של העיר אוסטין, ותתחילו להציג אותם באופן חזותי.

4. קבלת נתונים עדכניים

GeoJSON, לינגואה פרנקה של עולם ה-GIS

בשלב הקודם ציינת שרכיבי ה-handler בקוד של Go ישמשו לעיבוד נתוני GeoJSON בדפדפן האינטרנט. אבל מה זה JSONJSON?

בעולם מערכת המידע הגיאוגרפי (GIS), עלינו להיות מסוגלים להעביר ידע על יישויות גיאוגרפיות בין מערכות מחשבים. מפות הן נהדרות לבני אדם, אבל מחשבים בדרך כלל מעדיפים את הנתונים שלהם בפורמט קל יותר לתקציר.

geoJSON הוא פורמט לקידוד של מבני נתונים גיאוגרפיים, כגון הקואורדינטות של מיחזור מיקומים למחזור באוסטין, טקסס. GeoJSON סטנדרטי לפי תקן Internet Task Force שנקרא RFC7946. GeoJSON מוגדר ב-JSON, JavaScript Object Notation, עצמו, התקני בתקן ECMA-404, על ידי אותו ארגון שהגדיר את JavaScript כ-Ecma International.

מה שחשוב הוא שה-geoJSON הוא פורמט כבלים שיש בו תמיכה נרחבת להעברת ידע גיאוגרפי. Codelab זה משתמש ב-geoJSON בדרכים הבאות:

  • משתמשים בחבילות Go כדי לנתח את נתוני אוסטין למבנה נתונים פנימי ספציפי ב-GIS שבו תשתמשו כדי לסנן נתונים מבוקשים.
  • טורי הנתונים המבוקשים לצורך העברה בין שרת האינטרנט ודפדפן האינטרנט.
  • יש להשתמש בספריית JavaScript כדי להמיר את התגובה לסמנים במפה.

פעולה זו תחסוך לכם כמות משמעותית של הקלדה בקוד, כי אין צורך לכתוב מנתחים וגנרטורים כדי להמיר את מקור הנתונים הפנימי לכבלים לייצוגים בזיכרון.

אחזור הנתונים

הפורטל של אוסטין, טקסס Open Data מציג מידע גיאוגרפי-מרחבי על משאבים ציבוריים הזמינים לשימוש ציבורי. במעבדה זו, יוצג לכם מערך הנתונים של מיקומי המחזור.

ניתן להציג את הנתונים באופן חזותי באמצעות סמנים במפה, באמצעות שכבת הנתונים של ה-API של JavaScript במפות.

התחל בהורדת הנתונים של 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 כך שישקף את האפליקציה &החזקה יותר, ולא רק את אפליקציית World hello, גם אם אתם מתכננים להעלות.

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. כך תפחית את עומס השרת ותשפר את מהירות ההצגה.

הגיע הזמן לפתח את הקצה העורפי של האפליקציה!

יצירת קצה עורפי

אולי שמת לב שמשהו אחד מעניין את קובץ 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"])
}

הקצה העורפי כבר מספק לנו תכונה חשובה: המכונה של AppEngine שומרת במטמון את כל המיקומים האלה ברגע שהיא מופעלת. כך אפשר לחסוך זמן כי הקצה העורפי לא צריך לקרוא את הקובץ מהדיסק בכל רענון מכל משתמש!

יצירת ממשק קצה

הדבר הראשון שעלינו לעשות הוא ליצור תיקייה שכוללת את כל הנכסים הסטטיים שלנו. מתיקיית ההורה של הפרויקט, יוצרים תיקיית static.

mkdir -p static && cd static

אנחנו ניצור 3 קבצים בתיקייה הזו.

  • המדיניות index.html מכילה את כל קובצי ה-HTML של אפליקציית מאתר החנויות בדף אחד.
  • style.css , כפי שציפיתם – יכלול את הסגנון
  • app.js יהיה אחראי לאחזר את האזור הגיאוגרפי הגיאוגרפי, לבצע קריאות ל-API של מפות Google ולהציב סמנים במפה המותאמת אישית.

יוצרים את 3 הקבצים האלה, ומקפידים להעלות אותם ל-static/.

style.css

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

body {
  display: flex;
}

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

index.html

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

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

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

יש לשים לב במיוחד לכתובת האתר src בתג הסקריפט של הרכיב head.

  • מחליפים את הטקסט של ה-placeholder &YOUR_API_KEY" במפתח ה-API שיצרתם בשלב ההגדרה. אפשר להיכנס אל APIs & Services -> Credentials; Credentials (דף של ממשקי API ו- -> פרטי כניסה) ב-Cloud Console כדי לאחזר את מפתח ה-API או ליצור מפתח חדש.
  • לתשומת ליבך, כתובת ה-URL מכילה את הפרמטר callback=initialize. ועכשיו אנחנו יוצרים את קובץ ה-JavaScript שמכיל את פונקציית הקריאה החוזרת. זה המקום שבו האפליקציה תטען את המיקומים מהקצה העורפי, תשלח אותם ל-API של מפות Google ותשתמש בתוצאה כדי לסמן מיקומים מותאמים אישית במפה, והכל יהיה יפהפה בדף האינטרנט שלך.
  • הפרמטר 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

כבר מתבצע עיבוד של מיקומי מפות, ואנחנו נמצאים רק באמצע הדרך ב-codelab! מדהים! עכשיו נוסיף קצת אינטראקטיביות.

6. הצגת פרטים על פי דרישה

תגובה לאירועי לחיצה בסמני המפה

הצגת כמה סמנים במפה היא התחלה טובה, אבל אנחנו באמת צריכים שמבקר יוכל ללחוץ על אחד מהסמנים ולראות מידע על המיקום (כמו שם העסק, הכתובת שלו וכו'). השם של חלון המידע הקטן שמופיע בדרך כלל כשלוחצים על סמן של מפות Google הוא חלון מידע.

יוצרים אובייקט 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 – storeTo המעגל

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, פרט לחיזויים של כולם.

יצירה של שדה להזנת משתמש

חוזרים לעריכת 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 - initcompleteWidget

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

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

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

הקוד מוסיף פונקציות מסוג listener כך שכאשר המשתמש לוחץ על אחת מההצעות, המפה מתריעה על הכתובת שנבחרה ומגדירה את המקור כבסיס לחישובי המרחק. מטמיעים את חישובי המרחק בשלב עתידי.

יש לעצור ולהפעיל מחדש את השרת ולרענן את התצוגה המקדימה כדי לראות את המפה מחדש, לאחר שמזינים כתובת בסרגל החיפוש של ההשלמה האוטומטית.

8. התאמה ל-Cloud SQL

עד עכשיו, יש לנו ממקם חנויות די יפה. ומנצלים את העובדה שיש רק כמאה מיקומים שהאפליקציה תשתמש בהם. הם נטענים בזיכרון בזיכרון בקצה העורפי (במקום לקרוא שוב ושוב מהקובץ). אבל מה קורה אם הממקם שלכם צריך לפעול בקנה מידה שונה? אם יש לכם מאות מיקומים שמפוזרים באזור גיאוגרפי גדול (או אלפי אנשים בכל העולם), השמירה על כל המיקומים האלה בזיכרון היא לא הרעיון הטוב ביותר, ופירוט האזורים בקבצים שונים יגרום לבעיות משלו.

הגיע הזמן לטעון את המיקומים שלך ממסד נתונים. לשלב זה, אנחנו נעביר את כל המיקומים שבקובץ GeoJSON למסד נתונים של Cloud SQL, ונעדכן את הקצה העורפי של Go כדי למשוך תוצאות ממסד הנתונים הזה במקום מהמטמון המקומי שלו בכל פעם שמתקבלת בקשה.

יצירה של מופע Cloud SQL עם PostGres Database

אפשר ליצור מופע ב-Cloud SQL באמצעות Google Cloud Console, אבל קל יותר להשתמש בכלי השירות gcloud כדי ליצור מופע משורת הפקודה. ב-Cloud Cloud יוצרים מופע של 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.75GB.

המכונה Cloud SQL תיווצר ותופעל בעזרת מסד נתונים של PostGresSQL, עם משתמש ברירת המחדל postgres. מה הסיסמה של המשתמש הזה? שאלה מעולה! אין להם חשבון. עליך להגדיר סיסמה כדי להתחבר.

מגדירים את הסיסמה עם הפקודה הבאה:

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

לאחר מכן מזינים את הסיסמה שבחרתם כשמוצגת בקשה לעשות זאת.

הפעלת תוסף PostGIS

PostGIS הוא תוסף של PostGresSQL שמקל על אחסון סוגים רגילים של נתונים גיאו-מרחביים. בנסיבות רגילות, נצטרך לעבור תהליך התקנה מלא כדי להוסיף את PostGIS למסד הנתונים שלנו. למרבה המזל, זהו אחד מהתוספים הנתמכים של Cloud SQL' ל-PostGresSQL .

מתחברים למופע מסד הנתונים על ידי התחברות בתור המשתמש postgres באמצעות הפקודה הבאה ב-Cloud Shell Console.

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

מזינים את הסיסמה שיצרתם הרגע. כעת יש להוסיף את התוסף PostGIS בשורת הפקודה של postgres=>.

CREATE EXTENSION postgis;

אם ההפעלה תתבצע בהצלחה, הפלט אמור להופיע כ'יצירת תוסף', כפי שמוצג למטה.

דוגמה לפלט פקודה

CREATE EXTENSION

לבסוף, יוצאים מהחיבור של מסד הנתונים על ידי הזנת הפקודה 'יציאה' בשורת הפקודה של postgres=>.

\q

ייבוא של נתונים גיאוגרפיים למסד הנתונים

עכשיו אנחנו צריכים לייבא את כל נתוני המיקום האלה מקובצי GeoJSON אל מסד הנתונים החדש שלנו.

למרבה המזל, הבעיה הזו טופלה היטב, וניתן למצוא באינטרנט כמה כלים כדי להתאים אותה באופן אוטומטי לצרכים שלכם. אנחנו נשתמש בכלי שנקרא ogr2ogr שימיר בין פורמטים נפוצים לאחסון נתונים גיאו-מרחביים. אחת מהאפשרויות היא, למשל, ניחחתם שהמרתם את הטופס GeoJSON לקובץ Dump של SQL. לאחר מכן, אתם יכולים להשתמש בקובץ ה-SQL של ה-SQL כדי ליצור את הטבלאות והעמודות של מסד הנתונים, ולטעון אותו עם כל הנתונים שהיו קיימים בקובצי ה-geoJSON.

יצירת קובץ נתונים בפורמט SQL

תחילה, מתקינים את ogr2ogr.

sudo apt-get install gdal-bin

לאחר מכן, השתמש ב-ogr2ogr כדי ליצור את קובץ ה-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 יש לעדכן את הקצה האחורי

כעת, כשכל הנתונים נמצאים במסד הנתונים שלנו, הגיע הזמן לעדכן את הקוד שלנו.

עדכון ממשק הקצה לשליחת פרטי מיקום

נתחיל בעדכון קטן מאוד לממשק הקצה: מאחר שאנחנו כותבים כעת את האפליקציה בקנה מידה שבו אנחנו לא רוצים שכל המיקום היחיד יועבר לממשק הקצה בכל פעם שהשאילתה מופעלת, אנחנו צריכים להעביר מידע בסיסי ממשק הקצה לגבי המיקום שחשוב למשתמש.

יש לפתוח את app.js ולהחליף את ההגדרה של הפונקציה fetchStores בגרסה הזו, כך שתכלול את קווי האורך והרוחב של כתובת ה-URL.

app.js – אחזור חנויות

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

לאחר השלמת שלב זה של הקוד, התגובה תחזיר רק את החנויות הקרובות ביותר לקואורדינטות במפה שסופקו בפרמטר center. בשליפה הראשונית בפונקציה initialize, הקוד לדוגמה שסופק בשיעור ה-Lab הזה משתמש בקואורדינטות המרכזיות של אוסטין, טקסס.

מאחר ש-fetchStores יחזיר עכשיו רק קבוצת משנה של מיקומי החנויות, נצטרך לאחזר מחדש את החנויות בכל פעם שהמשתמש ישנה את מיקום ההתחלה שלו.

יש לעדכן את הפונקציה initAutocompleteWidget כדי לרענן את המיקומים בכל פעם שהוגדר מקור חדש. יש צורך בשתי עריכות:

  1. בתוך ה-InotcompleteWidget, מחפשים את הקריאה החוזרת להאזנה של place_changed. לבטל את ההערה של השורה שמוחקת מעגלים קיימים, כך שהשורה הזו תפעל בכל פעם שהמשתמש בוחר כתובת מ'חיפוש השלמה אוטומטית'.

app.js – ווידג'ט inincomplete

  autocomplete.addListener("place_changed", async () => {
    circles.forEach((c) => c.setMap(null)); // clear existing stores
    // ...
  1. בכל פעם שהמקור שנבחר משתנה, משתנה originLocation מתעדכן. בסוף השורה " place_changed" callback, מבטלים את סימון השורה מעל השורה &&quot ;// TODO: Calculate the closest stores" כדי להעביר את המקור החדש אל קריאה חדשה לפונקציה fetchAndRenderStores.

app.js – ווידג'ט inincomplete

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

יצירת handler חדש לבקשות מיקום

עכשיו יש ליצור קובץ נוסף, locations.go, שנמצא גם בספריית מיחזור אוסטין. מתחילים בהטמעה מחדש של ה-handler עבור בקשות מיקום.

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.

כאן הפונקציה לבצע את הקסם. צריך להוסיף את הפונקציה הבאה אחרי הקוד של ה-handler והמאגר של החיבור לרשת, שאותו כתבת בתחתית של 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 בפועל, וזה מבצע הרבה דברים מעניינים בשכבת מסד הנתונים.

השאילתה הגולמית שמופעלת, לאחר ניתוח המחרוזת וכל ליטרלים של מחרוזת שנוספו במקומות המתאימים, נראים כך:

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

ניתן להציג את השאילתה הזו כשאילתה ראשית אחת, וגם כפונקציות wrapper מסוימות של JSON.

SELECT * ... LIMIT 25 בוחרת את כל השדות בכל מיקום בנפרד. לאחר מכן, היא משתמשת בפונקציה ST_DISTANCE (חלק מחבילה של פונקציות מדידה גיאוגרפיות) כדי לקבוע את המרחק בין כל מיקום במסד הנתונים ואת צמד הרוחב/האורך של המיקום שהמשתמש סיפק בממשק הקצה. חשוב לזכור שבניגוד למטריצה של המרחק, שיכולה לספק לכם מרחק נסיעה, אלה מרחקים גיאוגרפיים-מרחביים. כדי לשמור על יעילות, המערכת משתמשת במרחק הזה כדי למיין ולהחזיר את 25 המיקומים הקרובים ביותר למיקום שצוין על ידי המשתמש.

**SELECT json_build_object(‘type', ‘F**ספרות') מכסה את השאילתה הקודמת, לוקחת את התוצאות ומשתמשת בהן כדי ליצור אובייקט תכונה גיאוגרפית מסוג JSON. באופן בלתי צפוי, השאילתה הזו היא גם המקום שבו הרדיוס חל אם לא ברור לך למה פסקה זו (WHERE) לא נוספה לשאילתה הפנימית (כאשר המרחק של כל מיקום נקבע), היא נובעת מהאופן שבו SQL מסתיים מאחורי הקלעים, ולכן ייתכן שהשדה הזה לא חושב כאשר נבדק הסעיף WHERE. למעשה, אם תנסו להעביר את המשפט WHERE לשאילתה הפנימית, הוא יגרום לשגיאה.

**SELECT json_build_object(‘type', ‘FeatureColl**סריקה') שאילתה זו מכסה את כל השורות המתקבלות מהשאילתה שיוצרת JSON באובייקט GeoJSON FeatureCollection.

הוספת ספריית PGX לפרויקט

עלינו להוסיף תלות אחת בפרויקט: PostGres Driver & Toolkit, המאפשר מאגר של חיבורים. הדרך הקלה ביותר לעשות זאת היא באמצעות Go מודולים. מפעילים מודול עם הפקודה הבאה במעטפת בענן:

go mod init my_locator

לאחר מכן, מריצים את הפקודה הזו כדי לסרוק את הקוד לתלויים, מוסיפים רשימה של יחסי תלות לקובץ ה-mod ומורידים אותם.

go mod tidy

לבסוף, מריצים את הפקודה הזו כדי למשוך יחסי תלות ישירות לספריית הפרויקט שלכם, כך שניתן יהיה לבנות בקלות את המאגר עבור AppEngine Flex.

go mod vendor

בסדר, הגיע הזמן לנסות!

רוצה לנסות?

אוקיי, הצלחנו. בואו נפעל!

כדי שמכונת הפיתוח (כן, גם מעטפת בענן) תוכל להתחבר למסד הנתונים, נצטרך להשתמש ב-Cloud SQL Proxy כדי לנהל את החיבור למסד הנתונים. כדי להגדיר שרת proxy ל-Cloud SQL:

  1. כאן אפשר להפעיל את Cloud SQL Admin API
  2. אם משתמשים במחשב פיתוח מקומי, מתקינים את הכלי Cloud SQL proxy. אם אתה משתמש במעטפת בענן, אפשר לדלג על השלב הזה, הוא כבר מותקן! לתשומת ליבך, ההוראות יתייחסו לחשבון שירות. כבר סיימנו ליצור עבורך, ונעבור על הוספת ההרשאות הדרושות לחשבון בקטע הבא.
  3. יצירת כרטיסייה חדשה (ב-Cloud Shell או בטרמינל משלכם) כדי להתחיל את שרת ה-proxy.

bcca42933bfbd497.png

  1. נכנסים לכתובת https://console.cloud.google.com/sql/instances/locations/overview וגוללים למטה כדי למצוא את השדה שם החיבור. אפשר להעתיק את השם כדי להשתמש בו בפקודה הבאה.
  2. בכרטיסייה זו, מריצים את שרת ה-proxy של 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. להצגת החנויות הקרובות ביותר

ה-API של כיוונים דומה מאוד לחוויית השימוש במסלולים באפליקציה מפות Google – הזנת מקור יחיד ויעד יחיד לקבלת מסלול בין השניים. ה-API של מטריצת הנתונים מתייחס לקונספט הזה עוד יותר כדי לזהות את ההתאמה האופטימלית בין כמה מקורות אפשריים לכמה יעדים אפשריים, על סמך זמני הנסיעה והמרחקים. במקרה כזה, כדי לעזור למשתמש למצוא את החנות הקרובה ביותר לכתובת שנבחרה, צריך לספק מקור אחד ומערך של מיקומי חנויות בתור היעדים.

יש להוסיף את המרחק מהמקור לכל חנות

בתחילת הגדרת הפונקציה initMap יש להחליף את התגובה "// TODO: Start Distance Matrix service" בקוד הבא:

app.js - initmap

distanceMatrixService = new google.maps.DistanceMatrixService();

צריך להוסיף פונקציה חדשה לסוף של app.js שנקראת calculateDistances.

app.js

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

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

הפונקציה מפעיל את ה-API של Matrix API באמצעות המקור שמועבר אליו כמקור יחיד, ואת מיקומי החנויות כמערך יעדים. לאחר מכן, הוא בונה מערך של אובייקטים השומרים את מזהה החנות, המרחק שמופיע במחרוזת הקריאה לבני אדם, המרחק במטרים כערך מספרי וממיין את המערך.

יש לעדכן את הפונקציה initAutocompleteWidget כדי לחשב את המרחקים מהחנות בכל פעם שנבחר מקור חדש בסרגל החיפוש של 'השלמה אוטומטית'. בחלק התחתון של הפונקציה initAutocompleteWidget, מחליפים את התגובה "// TODO: Calculate the closest stores" בקוד הבא:

app.js - initcompleteWidget

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

הצגת תצוגת רשימה של חנויות הממוינות לפי מרחק

המשתמש מצפה לראות רשימה של חנויות מסודרות בסביבה הקרובה ביותר. אכלוס דף בחלונית צדדית לכל חנות באמצעות הרשימה ששונה על ידי הפונקציה calculateDistances כדי ליידע את סדר התצוגה של החנויות.

יש להוסיף שתי פונקציות חדשות לסוף של app.js שנקראות renderStoresPanel() ו-storeToPanelRow().

app.js

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

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

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

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

מפעילים מחדש את השרת ומרעננים את התצוגה המקדימה. כדי לעשות את זה מריצים את הפקודה הבאה.

go run *.go

לבסוף, מזינים את כתובת אוסטין, TX בסרגל החיפוש של ההשלמה האוטומטית, ולוחצים על אחת מההצעות.

המפה צריכה להתמקד בכתובת הזו, וסרגל צד אמור להופיע ברשימה של מיקומי החנויות לפי המרחק מהכתובת שנבחרה. לדוגמה:

96e35794dd0e88c9.png

10. עיצוב המפה

אחת הדרכים האפקטיביות ביותר לבדל את המפה באופן חזותי היא להוסיף לה סגנון. בעזרת עיצוב מפה מבוסס-ענן, ההתאמה האישית של המפות שלך נשלטת מ-Cloud Console באמצעות עיצוב מפה מבוסס ענן (בטא). אם אתם מעדיפים לעצב את המפה בעזרת תכונה שאינה בטא, תוכלו להשתמש בתיעוד של סגנון המפה כדי ליצור json לסגנון פרוגרמטי של המפה. ההוראות הבאות מנחות אותך במיפוי מפות מבוסס ענן (בטא)

יצירת מזהה מפה

קודם כל, פותחים את Cloud Console ובתיבת החיפוש ומקלידים "מפת ניהול" . לוחצים על התוצאה שבה כתוב "ניהול המפה (מפות Google)". 64036dd0ed200200.png

'יופיע בחלק העליון של המסך (מתחת לתיבת החיפוש) הכיתוב יצירת מזהה מפה חדש. לוחצים על השדה וממלאים את השם הרצוי. בסוג המפה, חשוב לבחור באפשרות JavaScript. כדי להציג אפשרויות נוספות בוחרים באפשרות וקטור מהרשימה. התוצאה הסופית אמורה להיראות בערך כמו התמונה הבאה.

70f55a759b4c4212.png

לחצו על "Next" ותוצג לכם מזהה מפה חדש לגמרי. אפשר להעתיק אותו עכשיו אם רוצים, אבל אפשר לחפש אותו בקלות במועד מאוחר יותר.

בשלב הבא, ניצור סגנון שיחול על המפה הזו.

יצירת סגנון מפה

אם אתם עדיין נמצאים בקטע 'מפות' ב-Cloud Console, יש ללחוץ על &ציטוט; מיפוי סגנונות בחלק התחתון של תפריט הניווט מימין. אחרת, בדיוק כמו שאתם יוצרים מזהה מפה, אתם יכולים למצוא את הדף הנכון על ידי הקלדת "מיפוי סגנונות;& בתיבת החיפוש ובחירה ב-" סגנונות המפה (מפות Google)" מהתוצאות, כמו בתמונה שלמטה.

9284cd200f1a9223.png

לוחצים על הלחצן בחלק העליון שבחלק העליון כתוב ""+ יצירת סגנון מפה חדש"

  1. אם רוצים להתאים את הסגנון במפה שמוצגת במעבדה זו, לוחצים על הכרטיסייה '"ייבוא JSON&QUOTE; ומדביקים למטה את blob ה-JSON. אחרת, אם רוצים ליצור סגנון משלכם, בוחרים את סגנון המפה שאיתו רוצים להתחיל. לאחר מכן לוחצים על הבא.
  2. בוחרים את מזהה המפה שיצרתם כדי לשייך את מזהה המפה לסגנון הזה, ולוחצים על הבא שוב.
  3. בשלב זה תהיה לך אפשרות להתאים אישית את סגנון המפה. אם אתם רוצים להכיר את האפשרויות האלה, לחצו על התאמה אישית בעורך הסגנונות ושחקו עם האפשרויות של הצבעים & עד שתראו את סגנון המפה הרצוי. אחרת, לוחצים על דילוג.
  4. בשלב הבא, מזינים את השם והתיאור של הסגנון, ולוחצים על שמירה ופרסום.

זהו קובץ blob אופציונלי לייבוא בשלב הראשון.

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

הוספת מזהה מפה לקוד

עכשיו, לאחר שעברת על הבעיות של יצירת סגנון המפה הזה, כיצד אתה משתמש בפועל בסגנון המפה הזה במפה שלך? יש שני שינויים קלים:

  1. יש להוסיף את מזהה המפה כפרמטר של כתובת URL לתג הסקריפט ב-index.html
  2. Add מזהה המפה כארגומנט של קבלן כשיוצרים את המפה בשיטת initMap().

מחליפים את תג הסקריפט שטוען את ממשק ה-API של JavaScript במפות Google בקובץ ה-HTML בכתובת ה-URL של הטענה שבהמשך, ומחליף את ה-placeholders עבור " YOUR_API_KEY" & "YOUR_MAP_ID"

index.html

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

בשיטה initMap של app.js שבה מוגדר ה-map הקבוע, מבטלים את סימון השורה של הנכס mapId ומחליפים את "YOUR_MAP_ID_HERE" באמצעות מזהה המפה שיצרת עכשיו:

app.js – initmap

...

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

מפעילים מחדש את השרת.

go run *.go

לאחר רענון התצוגה המקדימה, המפה אמורה להיות מעוצבת בהתאם להעדפות שלכם. לפניכם דוגמה לשימוש בסגנון JSON למעלה.

2ece59c64c06e9da.png

11. פריסה לסביבת ייצור

אם ברצונך לראות את האפליקציה שלך פועלת מ-AppEngine Flex (ולא רק משרת אינטרנט מקומי במכונת הפיתוח / Cloud Shell, זו הפעולה קלה מאוד). צריך רק להוסיף כמה דברים כדי שהגישה למסד הנתונים תפעל בסביבת הייצור. כל זה מופיע בדף התיעוד בנושא התחברות מ-App Engine Flex ל-Cloud SQL.

הוספת משתני סביבה ל-App.yaml

תחילה יש להוסיף את כל משתני הסביבה שבהם השתמשת לצורך בדיקה באופן מקומי לתחתית קובץ app.yaml של האפליקציה שלך.

  1. נכנסים לכתובת https://console.cloud.google.com/sql/instances/locations/overview כדי לחפש את שם החיבור למכונה.
  2. צריך להדביק את הקוד הבא בסוף של app.yaml.
  3. יש להחליף את YOUR_DB_PASSWORD_HERE בסיסמה שיצרת קודם לשם המשתמש ב-postgres.
  4. החליפו את YOUR_CONNECTION_NAME_HERE בערך משלב 1.

app.yaml

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

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

לתשומת ליבכם, הערך של DB_TCP_HOST צריך להיות 172.17.0.1 כי האפליקציה הזו מתחברת באמצעות AppEngine Flex**.** הסיבה לכך היא שהתקשורת עם Cloud SQL היא דרך שרת proxy, בדומה לאופן שבו השתמשתם בו.

הוספת הרשאות של לקוח SQL אל חשבון השירות AppEngine Flex

נכנסים לדף IAM-Admin ב-Cloud Console ומחפשים חשבון שירות שהשם שלו תואם לפורמט service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com. זהו חשבון השירות App Engine Flex שישמש להתחברות למסד הנתונים. לוחצים על הלחצן Edit (עריכה) שבסוף השורה ומוסיפים את התפקיד && הלקוחות;Cloud SQL Client"

b04sandbox0b4022b905.png

העתקת קוד הפרויקט לנתיב 'נסיעה'

כדי ש-AppEngine יוכל להריץ את הקוד שלכם, הוא צריך להיות מסוגל למצוא קבצים רלוונטיים בנתיב Go. יש לוודא שנמצאים בספריית הבסיס של הפרויקט.

cd YOUR_PROJECT_ROOT

מעתיקים את הספרייה לנתיב Go.

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

צריך לעבור לספרייה הזו.

cd ~/gopath/src/austin-recycling

פריסת האפליקציה

אפשר להשתמש ב-CLI של gcloud כדי לפרוס את האפליקציה. תהליך הפריסה יימשך קצת זמן.

gcloud app deploy

אפשר להשתמש בפקודה browse כדי ללחוץ על הקישור שניתן לראות

gcloud app browse

אם הפעלת את gcloud מחוץ למעטפת בענן, הפעלת gcloud app browse תפתח כרטיסייה חדשה בדפדפן.

12. (מומלץ) ניקיון

ביצוע קוד Lab זה יישאר במסגרת של דרגות חינם של עיבוד מידע ב-BigQuery וקריאות ל-Maps Platform API, אבל אם ביצעתם את השלב הזה לתרגיל חינוכי בלבד, ואתם רוצים להימנע מהחיוב העתידי, הדרך הקלה ביותר למחוק את המשאבים המשויכים לפרויקט הזה היא למחוק את הפרויקט עצמו.

מחיקת הפרויקט

במסוף GCP, נכנסים לדף Cloud Resource Manager:

ברשימת הפרויקטים, בחר את הפרויקט שבו אנחנו עובדים ולוחצים על מחיקה. תתבקשו להקליד את מזהה הפרויקט. מזינים אותו ולוחצים על כיבוי.

לחלופין, אפשר למחוק את כל הפרויקט ישירות מ-Cloud Shell באמצעות gcloud על ידי הרצת הפקודה הבאה והחלפת ה-placeholder GOOGLE_CLOUD_PROJECT במזהה הפרויקט שלך:

gcloud projects delete GOOGLE_CLOUD_PROJECT

13. מזל טוב

מעולה! השלמתם את Lablab בהצלחה!

לחלופין, גלשתם לדף האחרון. מעולה! דלגתם לדף האחרון!

במהלך שיעור Lab זה, עבדתם עם הטכנולוגיות הבאות:

קריאה נוספת

נותר עוד הרבה מידע על כל הטכנולוגיות האלה. בהמשך מופיעים קישורים שימושיים לנושאים שלא היה לנו זמן לטפל בהם במעבדה זו, אבל בהחלט נוכל לסייע לכם בבניית פתרון למאתר חנויות שמתאים לצרכים הספציפיים שלכם.