1. מבוא
מופשט
נניח שיש לכם הרבה מקומות שאתם רוצים להוסיף למפה, ואתם רוצים שהמשתמשים יוכלו לראות איפה המקומות האלה נמצאים ולזהות את המקום שבו הם רוצים לבקר. דוגמאות נפוצות:
- תוסף מפה באתר של קמעונאי
- מפה של מיקומי קלפיות לבחירות הקרובות
- מדריך למיקומים מיוחדים, כמו מיכלי מיחזור סוללות
מה תפַתחו
ב-codelab הזה תיצרו כלי לאיתור מיקום שמבוסס על פיד נתונים בזמן אמת של מיקומים מיוחדים, ועוזר למשתמש למצוא את המיקום הקרוב ביותר לנקודת ההתחלה שלו. הכלי המלא לאיתור חנויות יכול לטפל במספרים גדולים בהרבה של מקומות בהשוואה לכלי הפשוט לאיתור חנויות, שמגביל את מספר מיקומי החנויות ל-25 או פחות.
מה תלמדו
ב-codelab הזה נעשה שימוש במערך נתונים פתוח כדי לדמות מטא-נתונים שאוכלסו מראש לגבי מספר גדול של מיקומי חנויות, כדי שתוכלו להתמקד בלימוד המושגים הטכניים העיקריים.
- Maps JavaScript API: הצגת מספר גדול של מיקומים במפה אינטרנטית בהתאמה אישית
- GeoJSON: פורמט שמאחסן מטא-נתונים על מיקומים
- השלמה אוטומטית למקומות: עוזרת למשתמשים לספק מיקומי התחלה מהר יותר ובצורה מדויקת יותר
- Go: שפת התכנות שמשמשת לפיתוח הקצה העורפי של האפליקציה. הקצה העורפי יקיים אינטראקציה עם מסד הנתונים וישלח את תוצאות השאילתה בחזרה לקצה הקדמי בפורמט JSON.
- App Engine: לאירוח אפליקציית האינטרנט
דרישות מוקדמות
- ידע בסיסי ב-HTML וב-JavaScript
- חשבון Google
2. להגדרה
בשלב 3 בקטע הבא, מפעילים את Maps JavaScript API, Places API ו-Distance Matrix API בשביל ה-codelab הזה.
תחילת העבודה עם הפלטפורמה של מפות Google
אם לא השתמשתם בפלטפורמה של מפות Google בעבר, תוכלו לעיין במדריך לתחילת העבודה עם הפלטפורמה של מפות Google או לצפות בפלייליסט לתחילת העבודה עם הפלטפורמה של מפות Google כדי לבצע את השלבים הבאים:
- יוצרים חשבון לחיוב.
- יוצרים פרויקט.
- מפעילים את ממשקי ה-API וערכות ה-SDK של הפלטפורמה של מפות Google (שמופיעים בקטע הקודם).
- יוצרים מפתח API.
הפעלת Cloud Shell
ב-codelab הזה משתמשים ב-Cloud Shell, סביבת שורת פקודה שפועלת ב-Google Cloud ומספקת גישה למוצרים ולמשאבים שפועלים ב-Google Cloud, כך שתוכלו לארח ולהריץ את הפרויקט שלכם באופן מלא מדפדפן האינטרנט.
כדי להפעיל את Cloud Shell ממסוף Cloud, לוחצים על הפעלת Cloud Shell (הקצאת המשאבים והחיבור לסביבה אמורים להימשך רק כמה רגעים).
ייפתח Shell חדש בחלק התחתון של הדפדפן, אחרי שיוצג מעברון מבוא.
אישור הפרויקט
אחרי שמתחברים ל-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 Flexible, החשבון המאומת שיקיים אינטראקציה עם שירותי Google (כמו מסדי נתונים של SQL) מטעם המשתמש.
3. שלום עולם
קצה עורפי: Hello World ב-Go
במופע Cloud Shell, מתחילים ביצירת אפליקציית Go App Engine Flex שתשמש כבסיס לשאר התרגיל.
בסרגל הכלים של Cloud Shell, לוחצים על הלחצן Open editor כדי לפתוח עורך קוד בכרטיסייה חדשה. עורך הקוד הזה מבוסס-אינטרנט ומאפשר לערוך בקלות קבצים במופע Cloud Shell.
לאחר מכן, לוחצים על הסמל Open in new window (פתיחה בחלון חדש) כדי להעביר את העורך ואת הטרמינל לכרטיסייה חדשה.
במסוף בחלק התחתון של הכרטיסייה החדשה, יוצרים ספרייה חדשה 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. מידע רקע על המשמעות של פריטי ההגדרה בקובץ הזה זמין במסמכי התיעוד של סביבת Go Standard ב-Google App Engine.
לאחר מכן, יוצרים קובץ 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!"
. הטקסט הזה יועבר חזרה לדפדפן שלכם, ושם תוכלו לקרוא אותו. בשלבים הבאים תיצרו פונקציות לטיפול בבקשות שיגיבו עם נתוני GeoJSON במקום מחרוזות פשוטות עם קידוד קשיח.
אחרי שמבצעים את השלבים האלה, עורך התמונות אמור להיראות כך:
אני רוצה לנסות
כדי לבדוק את האפליקציה הזו, אפשר להריץ את שרת הפיתוח של App Engine בתוך מכונת Cloud Shell. חוזרים לשורת הפקודה של Cloud Shell ומקלידים את הפקודה הבאה:
go run *.go
יוצגו כמה שורות של פלט יומן שיראו שאתם מריצים את שרת הפיתוח במופע Cloud Shell, ואפליקציית האינטרנט hello world מאזינה ליציאה 8080 ב-localhost. כדי לפתוח כרטיסייה בדפדפן אינטרנט באפליקציה הזו, לוחצים על הלחצן תצוגה מקדימה באינטרנט ובוחרים באפשרות תצוגה מקדימה ביציאה 8080 בתפריט של סרגל הכלים ב-Cloud Shell.
אם תלחצו על פריט התפריט הזה, תיפתח כרטיסייה חדשה בדפדפן האינטרנט עם המילים Hello, world! שמוצגות משרת הפיתוח של App Engine.
בשלב הבא תוסיפו לאפליקציה הזו את נתוני המיחזור של העיר אוסטין ותתחילו להציג אותם באופן חזותי.
4. קבלת נתונים עדכניים
GeoJSON, השפה המשותפת של עולם ה-GIS
בשלב הקודם צוין שתיצרו פונקציות handler בקוד Go שיעבדו נתוני GeoJSON בדפדפן האינטרנט. אבל מה זה GeoJSON?
בעולם של מערכות מידע גיאוגרפי (GIS), אנחנו צריכים להיות מסוגלים להעביר ידע על ישויות גיאוגרפיות בין מערכות מחשב. מפות נועדו לקריאה על ידי בני אדם, אבל מחשבים בדרך כלל מעדיפים נתונים בפורמטים שקל יותר לעבד.
GeoJSON הוא פורמט לקידוד של מבני נתונים גיאוגרפיים, כמו הקואורדינטות של מיקומי איסוף למיחזור באוסטין, טקסס. פורמט GeoJSON עבר סטנדרטיזציה בתקן של ארגון התקינה בנושאי האינטרנט שנקרא RFC7946. פורמט GeoJSON מוגדר במונחים של JSON, JavaScript Object Notation, שבעצמו עבר סטנדרטיזציה ב-ECMA-404 על ידי אותו ארגון שביצע סטנדרטיזציה של JavaScript, Ecma International.
הדבר החשוב הוא ש-GeoJSON הוא פורמט נתונים נפוץ שנתמך באופן נרחב להעברת ידע גיאוגרפי. ב-codelab הזה נעשה שימוש ב-GeoJSON בדרכים הבאות:
- משתמשים בחבילות Go כדי לנתח את הנתונים של אוסטין למבנה נתונים פנימי ספציפי של GIS, שבו תשתמשו כדי לסנן את הנתונים המבוקשים.
- הסדרת הנתונים המבוקשים להעברה בין שרת האינטרנט לדפדפן האינטרנט.
- משתמשים בספריית JavaScript כדי להמיר את התגובה לסמנים במפה.
כך תוכלו לחסוך כמות משמעותית של הקלדה בקוד, כי לא תצטרכו לכתוב מנתחים וגנרטורים כדי להמיר את זרם הנתונים בחיבור לייצוגים בזיכרון.
אחזור הנתונים
ב-City of Austin, Texas Open Data Portal (פורטל הנתונים הפתוחים של העיר אוסטין בטקסס) אפשר למצוא מידע גיאוגרפי על משאבים ציבוריים שזמין לשימוש הציבור. ב-codelab הזה תלמדו איך ליצור ויזואליזציה של מערך הנתונים recycling drop-off locations.
הנתונים יוצגו באמצעות סמנים במפה, שיוצגו באמצעות שכבת הנתונים של Maps JavaScript API.
מתחילים בהורדת נתוני GeoJSON מאתר העיר אוסטין לאפליקציה.
- בחלון שורת הפקודה של מכונת Cloud Shell, משביתים את השרת על ידי הקלדת [CTRL] + [C].
- יוצרים ספרייה בשם
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"
"log"
"net/http"
"os"
"path/filepath"
)
var GeoJSON = make(map[string][]byte)
// cacheGeoJSON loads files under data into `GeoJSON`.
func cacheGeoJSON() {
filenames, err := filepath.Glob("data/*")
if err != nil {
log.Fatal(err)
}
for _, f := range filenames {
name := filepath.Base(f)
dat, err := os.ReadFile(f)
if err != nil {
log.Fatal(err)
}
GeoJSON[name] = dat
}
}
func main() {
// Cache the JSON so it doesn't have to be reloaded every time a request is made.
cacheGeoJSON()
// Request for data should be handled by Go. Everything else should be directed
// to the folder of static files.
http.HandleFunc("/data/dropoffs", dropoffsHandler)
http.Handle("/", http.FileServer(http.Dir("./static/")))
// Open up a port for the webserver.
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("Listening on port %s", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal(err)
}
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
// Writes Hello, World! to the user's web browser via `w`
fmt.Fprint(w, "Hello, world!")
}
func dropoffsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-type", "application/json")
w.Write(GeoJSON["recycling-locations.geojson"])
}
ה-backend של Go כבר מספק לנו תכונה חשובה: המופע של AppEngine שומר במטמון את כל המיקומים האלה ברגע שהוא מתחיל לפעול. כך נחסך זמן כי ה-backend לא צריך לקרוא את הקובץ מהדיסק בכל רענון של כל משתמש.
בניית הקצה הקדמי
קודם כל צריך ליצור תיקייה שתכיל את כל הנכסים הסטטיים. בתיקיית ההורה של הפרויקט, יוצרים תיקייה בשם 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
בתג script של רכיב head
.
- מחליפים את הטקסט של הפלייסהולדר
YOUR_API_KEY
במפתח ה-API שיצרתם במהלך שלב ההגדרה. אפשר להיכנס לדף APIs & Services -> Credentials במסוף Cloud כדי לאחזר את מפתח ה-API או ליצור מפתח חדש. - שימו לב שכתובת ה-URL מכילה את הפרמטר
callback=initialize.
. עכשיו ניצור את קובץ ה-JavaScript שמכיל את פונקציית הקריאה החוזרת הזו. כאן האפליקציה תטען את המיקומים מהקצה העורפי, תשלח אותם אל 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
צופים בתצוגה מקדימה כמו קודם. אמורה להופיע מפה עם עיגולים ירוקים קטנים כמו אלה.
כבר הצלחתם להציג מיקומים במפה, ואנחנו רק באמצע ה-codelab! מדהים. עכשיו נוסיף קצת אינטראקטיביות.
6. הצגת פרטים לפי דרישה
תגובה לאירועי קליק על סמני מיקום במפה
הצגת הרבה סמנים במפה היא התחלה טובה, אבל אנחנו באמת צריכים שהמבקר יוכל ללחוץ על אחד מהסמנים האלה ולראות מידע על המיקום (כמו שם העסק, הכתובת וכו'). השם של חלון המידע הקטן שמופיע בדרך כלל כשלוחצים על סמן במפות Google הוא חלון מידע.
יוצרים אובייקט infoWindow. מוסיפים את השורה הבאה לפונקציה initialize
, במקום השורה עם ההערה // TODO: Initialize an info window
.
app.js – initialize
// Add an info window that pops up when user clicks on an individual
// location. Content of info window is entirely up to us.
infowindow = new google.maps.InfoWindow();
מחליפים את הגדרת הפונקציה 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
עם פרטי החנות שנבחרה בכל פעם שלוחצים על סמן של חנות במפה.
אם השרת עדיין פועל, צריך לעצור אותו ולהפעיל אותו מחדש. רעננו את דף המפה ונסו ללחוץ על סמן במפה. חלון קטן עם פרטי העסק, כולל השם והכתובת, אמור להיפתח. הוא ייראה בערך כך:
7. קבלת מיקום ההתחלה של המשתמש
משתמשים במאתרי חנויות רוצים בדרך כלל לדעת איזו חנות הכי קרובה אליהם או לכתובת שבה הם מתכננים להתחיל את המסע שלהם. מוסיפים סרגל חיפוש של השלמה אוטומטית של מקומות כדי לאפשר למשתמש להזין בקלות כתובת התחלתית. ההשלמה האוטומטית של מקומות מספקת פונקציונליות של הקלדה מראש, בדומה לאופן שבו ההשלמה האוטומטית פועלת בסרגלי חיפוש אחרים של Google, אלא שהחיזויים הם כולם מקומות ב-Google Maps Platform.
יצירה של שדה להזנת משתמש
חוזרים לעריכה style.css
כדי להוסיף עיצוב לסרגל החיפוש של ההשלמה האוטומטית ולחלונית הצדדית של התוצאות שקשורות אליו. במהלך העדכון של סגנונות ה-CSS, נוסיף גם סגנונות לסרגל צד עתידי שבו יוצג מידע על החנות כרשימה לצד המפה.
מוסיפים את הקוד הזה לסוף הקובץ.
style.css
#panel {
height: 100%;
flex-basis: 0;
flex-grow: 0;
overflow: auto;
transition: all 0.2s ease-out;
}
#panel.open {
flex-basis: auto;
}
#panel .place {
font-family: "open sans", arial, sans-serif;
font-size: 1.2em;
font-weight: 500;
margin-block-end: 0px;
padding-left: 18px;
padding-right: 18px;
}
#panel .distanceText {
color: silver;
font-family: "open sans", arial, sans-serif;
font-size: 1em;
font-weight: 400;
margin-block-start: 0.25em;
padding-left: 18px;
padding-right: 18px;
}
/* Styling for Autocomplete search bar */
#pac-card {
background-color: #fff;
border-radius: 2px 0 0 2px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
box-sizing: border-box;
font-family: Roboto;
margin: 10px 10px 0 0;
-moz-box-sizing: border-box;
outline: none;
}
#pac-container {
padding-top: 12px;
padding-bottom: 12px;
margin-right: 12px;
}
#pac-input {
background-color: #fff;
font-family: Roboto;
font-size: 15px;
font-weight: 300;
margin-left: 12px;
padding: 0 11px 0 13px;
text-overflow: ellipsis;
width: 400px;
}
#pac-input:focus {
border-color: #4d90fe;
}
#pac-title {
color: #fff;
background-color: #acbcc9;
font-size: 18px;
font-weight: 400;
padding: 6px 12px;
}
.hidden {
display: none;
}
גם סרגל החיפוש עם ההשלמה האוטומטית וגם החלונית הנפתחת מוסתרים בהתחלה עד שצריך אותם.
כדי להכין רכיב div לווידג'ט ההשלמה האוטומטית, מחליפים את התגובה בקובץ index.html שכתוב בה "<!-- Autocomplete div goes here -->
בקוד הבא. במהלך העריכה הזו, נוסיף גם את ה-div של החלונית הנשלפת.
index.html
<div id="panel" class="closed"></div>
<div class="hidden">
<div id="pac-card">
<div id="pac-title">Find the nearest location</div>
<div id="pac-container">
<input
id="pac-input"
type="text"
placeholder="Enter an address"
class="pac-target-input"
autocomplete="off"
/>
</div>
</div>
</div>
עכשיו מגדירים פונקציה להוספת הווידג'ט של ההשלמה האוטומטית למפה על ידי הוספת הקוד הבא לסוף הקובץ app.js
.
app.js
const initAutocompleteWidget = () => {
// Add search bar for auto-complete
// Build and add the search bar
const placesAutoCompleteCardElement = document.getElementById("pac-card");
const placesAutoCompleteInputElement = placesAutoCompleteCardElement.querySelector(
"input"
);
const options = {
types: ["address"],
componentRestrictions: { country: "us" },
map,
};
map.controls[google.maps.ControlPosition.TOP_RIGHT].push(
placesAutoCompleteCardElement
);
// Make the search bar into a Places Autocomplete search bar and select
// which detail fields should be returned about the place that
// the user selects from the suggestions.
const autocomplete = new google.maps.places.Autocomplete(
placesAutoCompleteInputElement,
options
);
autocomplete.setFields(["address_components", "geometry", "name"]);
map.addListener("bounds_changed", () => {
autocomplete.setBounds(map.getBounds());
});
// TODO: Respond when a user selects an address
};
הקוד מגביל את ההצעות להשלמה אוטומטית כך שיחזירו רק כתובות (כי השלמה אוטומטית של מקומות יכולה להתאים גם לשמות של עסקים ולמיקומים אדמיניסטרטיביים), ומגביל את הכתובות שמוחזרות רק לכתובות בארה"ב. הוספה של המפרטים האופציונליים האלה תצמצם את מספר התווים שהמשתמש צריך להזין כדי לצמצם את התחזיות ולהציג את הכתובת שהוא מחפש.
לאחר מכן, הוא מעביר את ההשלמה האוטומטית div
שיצרתם לפינה השמאלית העליונה של המפה ומציין אילו שדות צריכים להיות מוחזרים לגבי כל מקום בתגובה.
לבסוף, קוראים לפונקציה initAutocompleteWidget
בסוף הפונקציה initialize
, ומחליפים את התגובה // TODO: Initialize the Autocomplete widget
.
app.js - initialize
// Initialize the Places Autocomplete Widget
initAutocompleteWidget();
מריצים את הפקודה הבאה כדי להפעיל מחדש את השרת, ואז מרעננים את התצוגה המקדימה.
go run *.go
עכשיו אמור להופיע בווידג'ט של השלמה אוטומטית בפינה השמאלית העליונה של המפה, שבו מוצגות כתובות בארה"ב שתואמות למה שמקלידים, עם הטיה לאזור הגלוי של המפה.
עדכון המפה כשמשתמש בוחר כתובת התחלה
עכשיו צריך לטפל במצב שבו המשתמש בוחר תחזית מווידג'ט ההשלמה האוטומטית, ולהשתמש במיקום הזה כבסיס לחישוב המרחקים לחנויות.
מוסיפים את הקוד הבא לסוף הקובץ 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
עד עכשיו, יצרנו כלי די טוב לאיתור חנויות. האפליקציה מנצלת את העובדה שיש רק כ-100 מיקומים שבהם היא תשתמש, וטוענת אותם לזיכרון בקצה העורפי (במקום לקרוא מהקובץ שוב ושוב). אבל מה קורה אם כלי איתור המיקום צריך לפעול בקנה מידה שונה? אם יש לכם מאות מיקומים שפזורים באזור גיאוגרפי גדול (או אלפים בכל העולם), כבר לא מומלץ לשמור את כל המיקומים האלה בזיכרון, ופיצול אזורים לקבצים נפרדים יוביל לבעיות משלו.
הגיע הזמן לטעון את המיקומים ממסד נתונים. בשלב הזה נעביר את כל המיקומים בקובץ ה-GeoJSON למסד נתונים של Cloud SQL, ונעדכן את קצה העורפי של Go כך שיאחזר תוצאות ממסד הנתונים הזה במקום מהמטמון המקומי שלו בכל פעם שתתקבל בקשה.
יצירת מכונה של Cloud SQL עם מסד נתונים של PostGres
אפשר ליצור מכונת Cloud SQL דרך מסוף Google Cloud, אבל הרבה יותר קל ליצור אותה משורת הפקודה באמצעות כלי השירות gcloud
. ב-Cloud Shell, יוצרים מכונת Cloud SQL באמצעות הפקודה הבאה:
gcloud sql instances create locations \ --database-version=POSTGRES_12 \ --tier=db-custom-1-3840 --region=us-central1
- הארגומנט
locations
הוא השם שבחרנו לתת למופע הזה של Cloud SQL. - הדגל
tier
מאפשר לבחור מתוך כמה מכונות שהוגדרו מראש. - הערך
db-custom-1-3840
מציין שהמופע שנוצר צריך לכלול מעבד וירטואלי אחד וזיכרון של כ-3.75GB.
המכונה של Cloud SQL תיצור ותאתחל מסד נתונים של PostGresSQL, עם משתמש ברירת המחדל postgres
. מה הסיסמה של המשתמש הזה? שאלה מצוינת! אין להם חשבון. כדי להתחבר, צריך להגדיר אחת כזו.
מגדירים את הסיסמה באמצעות הפקודה הבאה:
gcloud sql users set-password postgres \ --instance=locations --prompt-for-password
כשתוצג הבקשה, מזינים את הסיסמה שבחרתם.
הפעלת התוסף PostGIS
PostGIS היא תוסף ל-PostGresSQL שמקל על אחסון של סוגים סטנדרטיים של נתונים גיאו-מרחביים. בדרך כלל, כדי להוסיף את PostGIS למסד הנתונים, צריך לעבור תהליך התקנה מלא. למזלנו, זהו אחד מהתוספים הנתמכים של Cloud SQL ל-PostgreSQL.
מתחברים למכונה של מסד הנתונים על ידי התחברות כמשתמש postgres
באמצעות הפקודה הבאה במסוף Cloud Shell.
gcloud sql connect locations --user=postgres --quiet
מזינים את הסיסמה שיצרתם. עכשיו מוסיפים את התוסף PostGIS בשורת הפקודה postgres=>
.
CREATE EXTENSION postgis;
אם הפעולה בוצעה ללא שגיאות, הפלט אמור להיות CREATE EXTENSION, כמו שמוצג בהמשך.
פלט לדוגמה של פקודה
CREATE EXTENSION
לסיום, מזינים את הפקודה quit בשורת הפקודה postgres=>
כדי לצאת מחיבור מסד הנתונים.
\q
ייבוא נתונים גיאוגרפיים למסד נתונים
עכשיו צריך לייבא את כל נתוני המיקום האלה מקובצי ה-GeoJSON למסד הנתונים החדש שלנו.
לשמחתנו, זו בעיה מוכרת, ויש באינטרנט כמה כלים שיכולים לבצע את הפעולה הזו באופן אוטומטי. אנחנו נשתמש בכלי שנקרא ogr2ogr, שמבצע המרה בין כמה פורמטים נפוצים לאחסון נתונים גיאו-מרחביים. בין האפשרויות האלה יש גם, ניחשתם נכון, המרה מ-GeoJSON לקובץ SQL dump. אחר כך אפשר להשתמש בקובץ ה-SQL dump כדי ליצור את הטבלאות והעמודות במסד הנתונים, ולטעון לתוכו את כל הנתונים שהיו בקובצי ה-GeoJSON.
יצירת קובץ SQL מוכן לשימוש
קודם כול, מתקינים את ogr2ogr.
sudo apt-get install gdal-bin
בשלב הבא, משתמשים ב-ogr2ogr כדי ליצור את קובץ ה-SQL dump. הקובץ הזה ייצור טבלה בשם 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
עדכון הקצה העורפי של Go לשימוש ב-Cloud SQL
עכשיו, אחרי שיש לנו את כל הנתונים האלה במסד הנתונים, הגיע הזמן לעדכן את הקוד.
עדכון החלק הקדמי של האתר כדי לשלוח נתוני מיקום
נתחיל עם עדכון קטן מאוד בחלק הקדמי: עכשיו אנחנו כותבים את האפליקציה הזו עבור קנה מידה שבו אנחנו לא רוצים שכל מיקום יועבר לחלק הקדמי בכל פעם שהשאילתה מופעלת, ולכן אנחנו צריכים להעביר מהחלק הקדמי מידע בסיסי על המיקום שמעניין את המשתמש.
פותחים את app.js
ומחליפים את הגדרת הפונקציה fetchStores
בגרסה הזו כדי לכלול בכתובת ה-URL את קו הרוחב וקו האורך הרלוונטיים.
app.js – fetchStores
const fetchStores = async (center) => {
const url = `/data/dropoffs?centerLat=${center.lat}¢erLng=${center.lng}`;
const response = await fetch(url);
return response.json();
};
אחרי שתשלימו את השלב הזה ב-codelab, התשובה תחזיר רק את החנויות הכי קרובות לקואורדינטות המפה שצוינו בפרמטר center
. בפונקציה initialize
, כדי לאחזר את הנתונים בפעם הראשונה, קוד הדוגמה שמופיע בשיעור ה-Lab הזה משתמש בקואורדינטות המרכזיות של אוסטין, טקסס.
מעכשיו, fetchStores
יחזיר רק קבוצת משנה של מיקומי החנויות, ולכן נצטרך לאחזר מחדש את החנויות בכל פעם שהמשתמש ישנה את מיקום ההתחלה שלו.
מעדכנים את הפונקציה initAutocompleteWidget
כדי לרענן את המיקומים בכל פעם שמגדירים מקור חדש. כדי לעשות את זה, צריך לבצע שני שינויים:
- ב-initAutocompleteWidget, מחפשים את הקריאה החוזרת של מאזין
place_changed
. מבטלים את ההערה בשורה שמנקה את העיגולים הקיימים, כדי שהשורה הזו תפעל בכל פעם שהמשתמש בוחר כתובת מתיבת החיפוש של השלמה אוטומטית של מקומות.
app.js - initAutocompleteWidget
autocomplete.addListener("place_changed", async () => {
circles.forEach((c) => c.setMap(null)); // clear existing stores
// ...
- בכל פעם שמשנים את המיקום המקורי שנבחר, המשתנה originLocation מתעדכן. בסוף הקריאה החוזרת (callback) של
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)
}
}
יצירת handler חדש לבקשות למיקום
עכשיו ניצור קובץ נוסף, locations.go
, גם הוא בספרייה austin-recycling. מתחילים בהטמעה מחדש של רכיב ה-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)
}
ה-handler מבצע את המשימות המשמעותיות הבאות:
- הוא שולף את קווי הרוחב והאורך מאובייקט הבקשה (זוכרים איך הוספנו אותם לכתובת ה-URL? )
- הוא מפעיל את הקריאה
getGeoJsonFromDatabase
, שמחזירה מחרוזת GeoJSON (נכתוב את זה בהמשך). - הוא משתמש ב-
ResponseWriter
כדי להדפיס את מחרוזת ה-GeoJSON הזו בתגובה.
בשלב הבא ניצור מאגר חיבורים כדי לעזור להרחיב את השימוש במסד הנתונים בהתאם למספר המשתמשים בו-זמנית.
יצירת מאגר חיבורים
מאגר חיבורים הוא אוסף של חיבורים פעילים למסד נתונים שהשרת יכול לעשות בהם שימוש חוזר כדי לטפל בבקשות של משתמשים. הוא מפחית הרבה תקורה ככל שמספר המשתמשים הפעילים גדל, כי השרת לא צריך להשקיע זמן ביצירה ובהשמדה של חיבורים לכל משתמש פעיל. יכול להיות ששמתם לב שבקטע הקודם ייבאנו את הספרייה github.com/jackc/pgx/stdlib.
זו ספרייה פופולרית לעבודה עם מאגרי חיבורים ב-Go.
בסוף locations.go
, צריך ליצור פונקציה initConnectionPool
(שנקראת מ-main.go
) שמפעילה מאגר חיבורים. לצורך הבהרה, בקטע הקוד הזה נעשה שימוש בכמה שיטות עזר. configureConnectionPool
הוא מקום נוח להתאמת הגדרות המאגר, כמו מספר החיבורים ומשך החיים של כל חיבור. mustGetEnv
wraps calls כדי לקבל משתני סביבה נדרשים, כך שאפשר להציג הודעות שגיאה שימושיות אם חסר במופע מידע קריטי (כמו כתובת ה-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 וקוד ה-connection pooling שכתבתם זה עתה בתחתית של 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) כדי לקבוע את המרחק בין כל מיקום במסד הנתונים לבין זוג קווי הרוחב והאורך של המיקום שהמשתמש סיפק בחלק הקדמי של האתר. חשוב לזכור שבניגוד ל-Distance Matrix, שנותן את מרחק הנהיגה, המרחקים האלה הם מרחקים גיאוגרפיים. כדי לייעל את התהליך, המערכת משתמשת במרחק הזה כדי למיין את המיקומים ולהחזיר את 25 המיקומים הכי קרובים למיקום שצוין על ידי המשתמש.
**SELECT json_build_object(‘type', ‘F
**eature') עוטף את השאילתה הקודמת, לוקח את התוצאות ומשתמש בהן כדי ליצור אובייקט GeoJSON Feature. באופן לא צפוי, השאילתה הזו היא גם המקום שבו מוחל הרדיוס המקסימלי. הערך 16090 הוא מספר המטרים ב-10 מיילים, המגבלה הקשיחה שצוינה ב-backend של Go. אם אתם תוהים למה לא הוספנו את פסוקית ה-WHERE הזו לשאילתה הפנימית (שבה נקבע המרחק של כל מיקום), הסיבה לכך היא שכשפסוקית ה-WHERE נבדקה, יכול להיות שהשדה הזה לא חושב, בגלל האופן שבו SQL מופעלת ברקע. למעשה, אם תנסו להעביר את פסוקית ה-WHERE הזו לשאילתה הפנימית, תופיע שגיאה.
**SELECT json_build_object(‘type', ‘FeatureColl
**ection') השאילתה הזו עוטפת את כל השורות שמתקבלות מהשאילתה ליצירת JSON באובייקט GeoJSON FeatureCollection.
הוספת ספריית PGX לפרויקט
צריך להוסיף לפרויקט תלות אחת: PostGres Driver & Toolkit, שמאפשרת איגום חיבורים. הדרך הקלה ביותר לעשות זאת היא באמצעות Go Modules. מפעילים מודול באמצעות הפקודה הבאה ב-Cloud Shell:
go mod init my_locator
לאחר מכן, מריצים את הפקודה הזו כדי לסרוק את הקוד ולחפש יחסי תלות, להוסיף רשימה של יחסי תלות לקובץ mod ולהוריד אותם.
go mod tidy
לבסוף, מריצים את הפקודה הזו כדי למשוך יחסי תלות ישירות לספריית הפרויקט, כך שאפשר יהיה לבנות את הקונטיינר בקלות עבור AppEngine Flex.
go mod vendor
אוקיי, עכשיו אפשר לבדוק את זה.
אני רוצה לנסות
אוקיי, סיימנו הרבה דברים. בואו נראה איך זה עובד!
כדי שמכונת הפיתוח (כן, גם Cloud Shell) תוכל להתחבר למסד הנתונים, נשתמש ב-Cloud SQL Proxy כדי לנהל את החיבור למסד הנתונים. כדי להגדיר את Cloud SQL Proxy:
- כאן אפשר להפעיל את Cloud SQL Admin API
- אם אתם משתמשים במחשב פיתוח מקומי, מתקינים את כלי ה-proxy של Cloud SQL. אם אתם משתמשים ב-Cloud Shell, אתם יכולים לדלג על השלב הזה כי הוא כבר מותקן. שימו לב שההוראות מתייחסות לחשבון שירות. כבר יצרנו בשבילכם חשבון כזה, ובקטע הבא נסביר איך להוסיף לחשבון הזה את ההרשאות הנדרשות.
- יוצרים כרטיסייה חדשה (ב-Cloud Shell או במסוף שלכם) כדי להפעיל את ה-proxy.
- נכנסים אל
https://console.cloud.google.com/sql/instances/locations/overview
וגוללים למטה עד לשדה שם החיבור. מעתיקים את השם הזה כדי להשתמש בו בפקודה הבאה. - בכרטיסייה הזו, מריצים את Cloud SQL Proxy באמצעות הפקודה הזו, ומחליפים את
CONNECTION_NAME
בשם החיבור שמופיע בשלב הקודם.
cloud_sql_proxy -instances=CONNECTION_NAME=tcp:5432
חוזרים לכרטיסייה הראשונה של Cloud Shell ומגדירים את משתני הסביבה שנדרשים ל-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 – מזינים נקודת מוצא אחת ויעד אחד כדי לקבל מסלול בין שתי הנקודות. ה-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);
});
};
הפונקציה קוראת ל-Distance Matrix API באמצעות המקור שמועבר אליה כמקור יחיד ומיקומי החנויות כמערך של יעדים. לאחר מכן, הפונקציה יוצרת מערך של אובייקטים שמאחסנים את מזהה החנות, המרחק שמוצג כמחרוזת שנוחה לקריאה, המרחק במטרים כערך מספרי וממיינת את המערך.
מעדכנים את הפונקציה initAutocompleteWidget
כדי לחשב את המרחקים מהחנות בכל פעם שמקור חדש נבחר מסרגל החיפוש של השלמה אוטומטית של מקומות. בתחתית הפונקציה initAutocompleteWidget
, מחליפים את ההערה // TODO: Calculate the closest stores
בקוד הבא:
app.js - initAutocompleteWidget
// Use the selected address as the origin to calculate distances
// to each of the store locations
await calculateDistances(originLocation, stores);
renderStoresPanel();
הצגת רשימה של חנויות במיון לפי מרחק
המשתמש מצפה לראות רשימה של חנויות, מסודרת מהקרובה ביותר לרחוקה ביותר. מאכלסים רשימה בחלונית הצדדית של כל חנות באמצעות הרשימה ששונתה על ידי הפונקציה calculateDistances
כדי לקבוע את סדר ההצגה של החנויות.
מוסיפים שתי פונקציות חדשות לסוף של 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
לבסוף, מזינים כתובת באוסטין, טקסס, בסרגל החיפוש של ההשלמה האוטומטית ולוחצים על אחת מההצעות.
הכתובת הזו תוצג במרכז המפה, ובסרגל הצד יופיעו מיקומי החנויות לפי המרחק שלהם מהכתובת שנבחרה. דוגמה אחת מוצגת כך:
10. הגדרת סגנון המפה
דרך אחת להבליט את המפה שלכם מבחינה ויזואלית היא להוסיף לה סגנון. בעזרת עיצוב מפות מבוסס-ענן, אפשר לשלוט בהתאמה האישית של המפות דרך Cloud Console באמצעות עיצוב מפות מבוסס-ענן (בטא). אם אתם מעדיפים לעצב את המפה באמצעות תכונה שלא נמצאת בשלב בטא, תוכלו להיעזר במסמכי התיעוד בנושא עיצוב מפות כדי ליצור קובץ JSON לעיצוב המפה באופן פרוגרמטי. ההוראות הבאות מתייחסות לעיצוב מפות מבוסס-ענן (בטא).
יצירת מזהה מפה
קודם פותחים את Cloud Console ובתיבת החיפוש מקלידים Map Management. לוחצים על התוצאה ניהול מפות (מפות Google).
בסמוך לחלק העליון (מתחת לתיבת החיפוש), יופיע לחצן עם הכיתוב יצירת מזהה מפה חדש. לוחצים על האפשרות הזו ומזינים את השם הרצוי. בקטע Map Type (סוג המפה), חשוב לבחור באפשרות JavaScript, וכשמוצגות אפשרויות נוספות, בוחרים באפשרות Vector (וקטור) מהרשימה. התוצאה הסופית אמורה להיראות כמו בתמונה שלמטה.
לוחצים על 'הבא' ויוצג מזהה מפה חדש. אם רוצים, אפשר להעתיק אותו עכשיו, אבל אל דאגה, אפשר לחפש אותו בקלות גם מאוחר יותר.
בשלב הבא ניצור סגנון להחלה על המפה.
יצירת סגנון מפה
אם אתם עדיין בקטע 'מפות' ב-Cloud Console, לוחצים על 'סגנונות מפה' בחלק התחתון של תפריט הניווט בצד ימין. אפשרות אחרת היא להקליד 'סגנונות מפה' בתיבת החיפוש ולבחור באפשרות סגנונות מפה (מפות Google) מתוך התוצאות, כמו בתמונה שלמטה.
לוחצים על הלחצן '+ יצירת סגנון מפה חדש' בחלק העליון של המסך.
- אם רוצים שהסגנון של המפה יהיה כמו במפה שמוצגת במעבדה הזו, לוחצים על הכרטיסייה IMPORT JSON ומדביקים את ה-JSON blob שבהמשך. אם רוצים ליצור סגנון משלכם, בוחרים את סגנון המפה שרוצים להתחיל איתו. לאחר מכן לוחצים על Next.
- בוחרים את מזהה המפה שיצרתם כדי לשייך אותו לסגנון הזה, ולוחצים שוב על הבא.
- בשלב הזה, יש לכם אפשרות להתאים אישית את הסגנון של המפה. אם אתם רוצים לנסות את האפשרות הזו, לוחצים על התאמה אישית בכלי לעריכת סגנונות ומשחקים עם הצבעים והאפשרויות עד שתמצאו סגנון מפה שמוצא חן בעיניכם. אחרת, לוחצים על דילוג.
- בשלב הבא, מזינים את השם והתיאור של הסגנון ולוחצים על שמירה ופרסום.
זוהי דוגמה ל-blob של JSON שאפשר לייבא בשלב הראשון.
[
{
"elementType": "geometry",
"stylers": [
{
"color": "#d6d2c4"
}
]
},
{
"elementType": "labels.icon",
"stylers": [
{
"visibility": "off"
}
]
},
{
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#616161"
}
]
},
{
"elementType": "labels.text.stroke",
"stylers": [
{
"color": "#f5f5f5"
}
]
},
{
"featureType": "administrative.land_parcel",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#bdbdbd"
}
]
},
{
"featureType": "landscape.man_made",
"elementType": "geometry.fill",
"stylers": [
{
"color": "#c0baa5"
},
{
"visibility": "on"
}
]
},
{
"featureType": "landscape.man_made",
"elementType": "geometry.stroke",
"stylers": [
{
"color": "#9cadb7"
},
{
"visibility": "on"
}
]
},
{
"featureType": "poi",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#757575"
}
]
},
{
"featureType": "poi.park",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#9e9e9e"
}
]
},
{
"featureType": "road",
"elementType": "geometry",
"stylers": [
{
"color": "#ffffff"
}
]
},
{
"featureType": "road.arterial",
"elementType": "geometry",
"stylers": [
{
"weight": 1
}
]
},
{
"featureType": "road.arterial",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#757575"
}
]
},
{
"featureType": "road.highway",
"elementType": "geometry",
"stylers": [
{
"color": "#bf5700"
}
]
},
{
"featureType": "road.highway",
"elementType": "geometry.stroke",
"stylers": [
{
"visibility": "off"
}
]
},
{
"featureType": "road.highway",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#616161"
}
]
},
{
"featureType": "road.local",
"elementType": "geometry",
"stylers": [
{
"weight": 0.5
}
]
},
{
"featureType": "road.local",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#9e9e9e"
}
]
},
{
"featureType": "transit.line",
"elementType": "geometry",
"stylers": [
{
"color": "#e5e5e5"
}
]
},
{
"featureType": "transit.station",
"elementType": "geometry",
"stylers": [
{
"color": "#eeeeee"
}
]
},
{
"featureType": "water",
"elementType": "geometry",
"stylers": [
{
"color": "#333f48"
}
]
},
{
"featureType": "water",
"elementType": "labels.text.fill",
"stylers": [
{
"color": "#9e9e9e"
}
]
}
]
הוספת מזהה מפה לקוד
אחרי שטרחתם ויצרתם סגנון מפה, איך משתמשים בו במפה שלכם? צריך לבצע שני שינויים קטנים:
- מוסיפים את מזהה המפה כפרמטר של כתובת URL לתג הסקריפט ב-
index.html
Add
את מזהה המפה כארגומנט של constructor כשיוצרים את המפה בשיטהinitMap()
.
מחליפים את תג הסקריפט שבו נטען ממשק ה-API של JavaScript במפות Google בקובץ ה-HTML בכתובת ה-URL של כלי הטעינה שמופיעה בהמשך, ומחליפים את ה-placeholder YOUR_API_KEY
ואת ה-placeholder 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 שלמעלה.
11. פריסה בסביבת הייצור
אם רוצים לראות את האפליקציה פועלת מ-AppEngine Flex (ולא רק משרת אינטרנט מקומי במחשב הפיתוח או ב-Cloud Shell, כמו שעשיתם עד עכשיו), זה מאוד פשוט. כדי שהגישה למסד הנתונים תפעל בסביבת הייצור, צריך להוסיף כמה דברים. הסבר מפורט על כך מופיע בדף התיעוד בנושא התחברות מ-App Engine Flex ל-Cloud SQL.
הוספת משתני סביבה לקובץ app.yaml
קודם כול, צריך להוסיף את כל משתני הסביבה שבהם השתמשתם כדי לבדוק את האפליקציה באופן מקומי לחלק התחתון של קובץ app.yaml
של האפליקציה.
- כדי לחפש את שם החיבור של המופע, נכנסים לכתובת https://console.cloud.google.com/sql/instances/locations/overview.
- מדביקים את הקוד הבא בסוף
app.yaml
. - מחליפים את
YOUR_DB_PASSWORD_HERE
בסיסמה שיצרתם קודם לשם המשתמשpostgres
. - מחליפים את
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 לחשבון השירות של App Engine Flex
עוברים אל הדף IAM-Admin במסוף Cloud ומחפשים חשבון שירות שהשם שלו תואם לפורמט service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com
. זהו חשבון השירות שבו ישתמש App Engine Flex כדי להתחבר למסד הנתונים. לוחצים על הלחצן Edit בסוף השורה ומוסיפים את התפקיד Cloud SQL Client.
העתקת קוד הפרויקט לנתיב Go
כדי ש-AppEngine יוכל להריץ את הקוד שלכם, הוא צריך למצוא את הקבצים הרלוונטיים בנתיב Go. מוודאים שאתם נמצאים בספריית הבסיס של הפרויקט.
cd YOUR_PROJECT_ROOT
מעתיקים את הספרייה לנתיב go.
mkdir -p ~/gopath/src/austin-recycling cp -r ./ ~/gopath/src/austin-recycling
עוברים לספרייה הזו.
cd ~/gopath/src/austin-recycling
פריסת האפליקציה
משתמשים ב-gcloud
CLI כדי לפרוס את האפליקציה. הפריסה תיקח זמן מה.
gcloud app deploy
אפשר להשתמש בפקודה browse
כדי לקבל קישור שאפשר ללחוץ עליו ולראות את מפת הסניפים המדהימה שלכם בפעולה. מפת הסניפים הזו היא באיכות גבוהה ומוכנה לפריסה מלאה.
gcloud app browse
אם הפעלתם את gcloud
מחוץ ל-Cloud Shell, הפעלת gcloud app browse
תפתח כרטיסייה חדשה בדפדפן.
12. (מומלץ) ניקוי
הביצוע של ה-codelab הזה לא יחרוג מהמגבלות של רמת השירות בחינם לעיבוד ב-BigQuery ולשיחות ל-API של הפלטפורמה של מפות Google. אבל אם ביצעתם אותו רק כתרגיל לימודי ואתם רוצים להימנע מחיובים עתידיים, הדרך הכי קלה למחוק את המשאבים שמשויכים לפרויקט הזה היא למחוק את הפרויקט עצמו.
מחיקת הפרויקט
במסוף GCP, נכנסים לדף Cloud Resource Manager:
ברשימת הפרויקטים, בוחרים את הפרויקט שעבדנו עליו ולוחצים על Delete (מחיקה). תתבקשו להקליד את מזהה הפרויקט. מזינים את הסיסמה ולוחצים על כיבוי.
לחלופין, אפשר למחוק את כל הפרויקט ישירות מ-Cloud Shell באמצעות gcloud
על ידי הפעלת הפקודה הבאה והחלפת placeholder GOOGLE_CLOUD_PROJECT
במזהה הפרויקט:
gcloud projects delete GOOGLE_CLOUD_PROJECT
13. מזל טוב
מעולה! סיימתם את ה-Codelab!
או שדילגתם לדף האחרון. מעולה! דילגתם לדף האחרון!
במהלך ה-Codelab הזה, עבדתם עם הטכנולוגיות הבאות:
- Maps JavaScript API
- Distance Matrix Service, Maps JavaScript API (יש גם את Distance Matrix API)
- Places Library, Maps JavaScript API (נקרא גם Places API)
- הסביבה הגמישה של App Engine (Go)
- Cloud SQL API
קריאה נוספת
יש עוד הרבה מה ללמוד על כל הטכנולוגיות האלה. בהמשך תמצאו כמה קישורים שימושיים לנושאים שלא היה לנו זמן לעסוק בהם בסדנת ה-codelab הזו, אבל הם יכולים לעזור לכם ליצור פתרון לאיתור חנויות שמתאים לצרכים הספציפיים שלכם.