1. Giới thiệu
Bản tóm tắt
Hãy tưởng tượng bạn có nhiều địa điểm cần đặt trên bản đồ và bạn muốn người dùng có thể xem những địa điểm này ở đâu và xác định địa điểm mà họ muốn ghé thăm. Các ví dụ phổ biến về vấn đề này bao gồm:
- công cụ định vị cửa hàng trên trang web của nhà bán lẻ
- bản đồ các địa điểm bỏ phiếu cho một cuộc bầu cử sắp tới
- một danh mục các địa điểm chuyên biệt, chẳng hạn như thùng tái chế pin
Sản phẩm bạn sẽ tạo ra
Trong lớp học lập trình này, bạn sẽ tạo một công cụ định vị lấy dữ liệu từ nguồn cấp dữ liệu trực tiếp về các vị trí chuyên biệt và giúp người dùng tìm thấy vị trí gần nhất với điểm bắt đầu của họ. Công cụ định vị toàn diện này có thể xử lý số lượng địa điểm lớn hơn nhiều so với công cụ định vị cửa hàng đơn giản, chỉ giới hạn ở 25 địa điểm cửa hàng trở xuống.
Kiến thức bạn sẽ học được
Lớp học lập trình này sử dụng một tập dữ liệu mở để mô phỏng siêu dữ liệu được điền sẵn về một số lượng lớn vị trí cửa hàng để bạn có thể tập trung vào việc tìm hiểu các khái niệm kỹ thuật chính.
- Maps JavaScript API: hiển thị một số lượng lớn vị trí trên bản đồ web tuỳ chỉnh
- GeoJSON: một định dạng lưu trữ siêu dữ liệu về vị trí
- Tính năng Tự động hoàn thành địa điểm: giúp người dùng cung cấp vị trí bắt đầu nhanh chóng và chính xác hơn
- Go: Ngôn ngữ lập trình dùng để phát triển phần phụ trợ của ứng dụng. Phần phụ trợ sẽ tương tác với cơ sở dữ liệu và gửi kết quả truy vấn trở lại giao diện người dùng ở định dạng JSON.
- App Engine: để lưu trữ ứng dụng web
Điều kiện tiên quyết
- Kiến thức cơ bản về HTML và JavaScript
- Tài khoản Google
2. Bắt đầu thiết lập
Trong Bước 3 của phần sau, hãy bật Maps JavaScript API, Places API và Distance Matrix API cho lớp học lập trình này.
Bắt đầu sử dụng Nền tảng Google Maps
Nếu bạn chưa từng sử dụng Nền tảng Google Maps, hãy làm theo hướng dẫn Bắt đầu sử dụng Nền tảng Google Maps hoặc xem danh sách phát Bắt đầu sử dụng Nền tảng Google Maps để hoàn tất các bước sau:
- Tạo tài khoản thanh toán.
- Tạo dự án.
- Bật các API và SDK của Nền tảng Google Maps (được liệt kê trong phần trước).
- Tạo khoá API.
Kích hoạt Cloud Shell
Trong lớp học lập trình này, bạn sẽ sử dụng Cloud Shell, một môi trường dòng lệnh chạy trong Google Cloud, cho phép truy cập vào các sản phẩm và tài nguyên chạy trên Google Cloud, nhờ đó bạn có thể lưu trữ và chạy dự án hoàn toàn từ trình duyệt web.
Để kích hoạt Cloud Shell từ Cloud Console, hãy nhấp vào Kích hoạt Cloud Shell (chỉ mất vài giây để cung cấp và kết nối với môi trường).
Thao tác này sẽ mở một shell mới ở phần dưới của trình duyệt sau khi có thể hiển thị một quảng cáo xen kẽ giới thiệu.
Xác nhận dự án
Sau khi kết nối với Cloud Shell, bạn sẽ thấy rằng mình đã được xác thực và dự án đã được đặt thành mã dự án mà bạn chọn trong quá trình thiết lập.
$ gcloud auth list Credentialed Accounts: ACTIVE ACCOUNT * <myaccount>@<mydomain>.com
$ gcloud config list project [core] project = <YOUR_PROJECT_ID>
Nếu vì lý do nào đó mà dự án chưa được thiết lập, hãy chạy lệnh sau:
gcloud config set project <YOUR_PROJECT_ID>
Bật AppEngine Flex API
Bạn cần bật AppEngine Flex API theo cách thủ công trong Cloud Console. Thao tác này không chỉ cho phép API mà còn tạo Tài khoản dịch vụ Môi trường linh hoạt AppEngine, tài khoản được xác thực sẽ tương tác với các dịch vụ của Google (chẳng hạn như cơ sở dữ liệu SQL) thay cho người dùng.
3. Xin chào
Phụ trợ: Xin chào thế giới bằng Go
Trong phiên bản Cloud Shell, bạn sẽ bắt đầu bằng cách tạo một ứng dụng Go App Engine linh hoạt. Ứng dụng này sẽ đóng vai trò là cơ sở cho phần còn lại của lớp học lập trình.
Trong thanh công cụ của Cloud Shell, hãy nhấp vào nút Mở trình chỉnh sửa để mở một trình chỉnh sửa mã trong thẻ mới. Trình chỉnh sửa mã dựa trên web này giúp bạn dễ dàng chỉnh sửa các tệp trong phiên bản Cloud Shell.
Tiếp theo, hãy nhấp vào biểu tượng Mở trong cửa sổ mới để di chuyển trình chỉnh sửa và thiết bị đầu cuối sang một thẻ mới.
Trong cửa sổ dòng lệnh ở cuối thẻ mới, hãy tạo một thư mục austin-recycling
mới.
mkdir -p austin-recycling && cd $_
Tiếp theo, bạn sẽ tạo một ứng dụng Go App Engine nhỏ để đảm bảo mọi thứ đều hoạt động. Xin chào!
Thư mục austin-recycling
cũng sẽ xuất hiện trong danh sách thư mục của Trình chỉnh sửa ở bên trái. Trong thư mục austin-recycling
, hãy tạo một tệp có tên là app.yaml
. Đặt nội dung sau vào tệp app.yaml
:
app.yaml
runtime: go
env: flex
manual_scaling:
instances: 1
resources:
cpu: 1
memory_gb: 0.5
disk_size_gb: 10
Tệp cấu hình này định cấu hình ứng dụng App Engine để sử dụng thời gian chạy Go linh hoạt. Để biết thông tin cơ bản về ý nghĩa của các mục cấu hình trong tệp này, hãy xem Tài liệu về môi trường chuẩn Go của Google App Engine.
Tiếp theo, hãy tạo một tệp main.go
cùng với tệp 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!")
}
Bạn nên tạm dừng một chút để tìm hiểu chức năng của mã này, ít nhất là ở mức tổng quan. Bạn đã xác định một gói main
khởi động một máy chủ http đang theo dõi trên cổng 8080 và đăng ký một hàm trình xử lý cho các yêu cầu HTTP khớp với đường dẫn "/"
.
Hàm trình xử lý, được gọi là handler
, ghi chuỗi văn bản "Hello, world!"
. Văn bản này sẽ được chuyển tiếp lại cho trình duyệt của bạn để bạn có thể đọc. Trong các bước tiếp theo, bạn sẽ tạo các trình xử lý phản hồi bằng dữ liệu GeoJSON thay vì các chuỗi được mã hoá cứng đơn giản.
Sau khi thực hiện các bước này, bạn sẽ có một trình chỉnh sửa có dạng như sau:
Dùng thử
Để kiểm thử ứng dụng này, bạn có thể chạy máy chủ phát triển App Engine trong phiên bản Cloud Shell. Quay lại dòng lệnh Cloud Shell rồi nhập nội dung sau:
go run *.go
Bạn sẽ thấy một số dòng đầu ra nhật ký cho thấy rằng bạn thực sự đang chạy máy chủ phát triển trên phiên bản Cloud Shell, với ứng dụng web hello world đang theo dõi trên cổng localhost 8080. Bạn có thể mở một thẻ trình duyệt web trên ứng dụng này bằng cách nhấn vào nút Web Preview (Xem trước trên web) rồi chọn mục trình đơn Preview on port 8080 (Xem trước trên cổng 8080) trong thanh công cụ Cloud Shell.
Khi bạn nhấp vào mục trình đơn này, một thẻ mới sẽ mở ra trong trình duyệt web của bạn với dòng chữ "Xin chào thế giới!" được phân phát từ máy chủ phát triển App Engine.
Trong bước tiếp theo, bạn sẽ thêm dữ liệu tái chế của Thành phố Austin vào ứng dụng này và bắt đầu trực quan hoá dữ liệu đó.
4. Lấy dữ liệu hiện tại
GeoJSON, ngôn ngữ chung của thế giới GIS
Bước trước đó có đề cập đến việc bạn sẽ tạo các trình xử lý trong mã Go để hiển thị dữ liệu GeoJSON cho trình duyệt web. Nhưng GeoJSON là gì?
Trong thế giới Hệ thống thông tin địa lý (GIS), chúng ta cần có khả năng truyền đạt kiến thức về các thực thể địa lý giữa các hệ thống máy tính. Bản đồ rất phù hợp để con người đọc, nhưng máy tính thường thích dữ liệu ở định dạng dễ hiểu hơn.
GeoJSON là một định dạng để mã hoá các cấu trúc dữ liệu địa lý, chẳng hạn như toạ độ của các địa điểm thu gom tái chế ở Austin, Texas. GeoJSON đã được chuẩn hoá theo tiêu chuẩn của Lực lượng đặc trách kỹ thuật Internet có tên là RFC7946. GeoJSON được xác định theo JSON, Ký hiệu đối tượng JavaScript, bản thân được chuẩn hoá trong ECMA-404, bởi chính tổ chức đã chuẩn hoá JavaScript, Ecma International.
Điều quan trọng là GeoJSON là một định dạng truyền dữ liệu được hỗ trợ rộng rãi để truyền đạt kiến thức địa lý. Lớp học lập trình này sử dụng GeoJSON theo những cách sau:
- Sử dụng các gói Go để phân tích cú pháp dữ liệu Austin thành một cấu trúc dữ liệu cụ thể của GIS nội bộ mà bạn sẽ dùng để lọc dữ liệu được yêu cầu.
- Chuyển đổi dữ liệu được yêu cầu thành chuỗi để truyền giữa máy chủ web và trình duyệt web.
- Sử dụng một thư viện JavaScript để chuyển đổi phản hồi thành các điểm đánh dấu trên bản đồ.
Điều này sẽ giúp bạn tiết kiệm đáng kể thời gian nhập mã, vì bạn không cần viết trình phân tích cú pháp và trình tạo để chuyển đổi luồng dữ liệu trên đường truyền thành các biểu thị trong bộ nhớ.
Truy xuất dữ liệu
Cổng dữ liệu mở của Thành phố Austin, Texas cung cấp thông tin không gian địa lý về các tài nguyên công cộng để mọi người sử dụng. Trong lớp học lập trình này, bạn sẽ trực quan hoá tập dữ liệu vị trí thu gom tái chế.
Bạn sẽ trực quan hoá dữ liệu bằng các điểm đánh dấu trên bản đồ, được kết xuất bằng Lớp dữ liệu của Maps JavaScript API.
Bắt đầu bằng cách tải dữ liệu GeoJSON từ trang web của Thành phố Austin xuống ứng dụng.
- Trong cửa sổ dòng lệnh của phiên bản Cloud Shell, hãy tắt máy chủ bằng cách nhập [CTRL] + [C].
- Tạo thư mục
data
bên trong thư mụcaustin-recycling
rồi chuyển sang thư mục đó:
mkdir -p data && cd data
Giờ đây, hãy dùng curl để truy xuất các vị trí tái chế:
curl "https://data.austintexas.gov/resource/qzi7-nx8g.geojson" -o recycling-locations.geojson
Cuối cùng, hãy thay đổi lại thành thư mục mẹ.
cd ..
5. Lập bản đồ các vị trí
Trước tiên, hãy cập nhật tệp app.yaml
để phản ánh ứng dụng mạnh mẽ hơn "không chỉ là một ứng dụng Hello World nữa" mà bạn sắp tạo.
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
Cấu hình app.yaml
này chuyển hướng các yêu cầu cho /
, /*.js
, /*.css
và /*.html
đến một tập hợp các tệp tĩnh. Điều này có nghĩa là thành phần HTML tĩnh của ứng dụng sẽ được cơ sở hạ tầng phân phát tệp của App Engine phân phát trực tiếp, chứ không phải ứng dụng Go của bạn. Điều này giúp giảm tải cho máy chủ và tăng tốc độ phân phát.
Giờ là lúc bạn xây dựng phần phụ trợ của ứng dụng bằng Go!
Xây dựng phần phụ trợ
Có thể bạn đã nhận thấy một điều thú vị là tệp app.yaml
của bạn không hiển thị tệp GeoJSON. Đó là vì GeoJSON sẽ được xử lý và gửi bởi phần phụ trợ Go của chúng tôi, cho phép chúng tôi tạo một số tính năng nâng cao ở các bước sau. Thay đổi tệp main.go
để có nội dung như sau:
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"])
}
Phần phụ trợ Go đã cung cấp cho chúng ta một tính năng có giá trị: phiên bản AppEngine đang lưu vào bộ nhớ đệm tất cả những vị trí đó ngay khi khởi động. Điều này giúp tiết kiệm thời gian vì phần phụ trợ sẽ không phải đọc tệp từ đĩa mỗi khi người dùng làm mới!
Xây dựng giao diện người dùng
Điều đầu tiên chúng ta cần làm là tạo một thư mục để lưu trữ tất cả các thành phần tĩnh. Trong thư mục mẹ của dự án, hãy tạo một thư mục static
.
mkdir -p static && cd static
Chúng ta sẽ tạo 3 tệp trong thư mục này.
index.html
sẽ chứa tất cả HTML cho ứng dụng công cụ định vị cửa hàng một trang của bạn.style.css
, như bạn mong đợi, sẽ chứa kiểuapp.js
sẽ chịu trách nhiệm truy xuất GeoJSON, thực hiện các lệnh gọi đến Maps API và đặt điểm đánh dấu trên bản đồ tuỳ chỉnh của bạn.
Tạo 3 tệp này và nhớ đặt chúng vào 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>
Hãy đặc biệt chú ý đến URL src
trong thẻ tập lệnh của phần tử head
.
- Thay thế văn bản giữ chỗ "
YOUR_API_KEY
" bằng khoá API mà bạn đã tạo trong bước thiết lập. Bạn có thể truy cập vào trang API và Dịch vụ -> Thông tin xác thực trong Cloud Console để truy xuất khoá API hoặc tạo một khoá API mới. - Xin lưu ý rằng URL này có chứa tham số
callback=initialize.
. Giờ đây, chúng ta sẽ tạo tệp javascript chứa hàm gọi lại đó. Đây là nơi ứng dụng của bạn sẽ tải vị trí từ phần phụ trợ, gửi vị trí đó đến Maps API và sử dụng kết quả để đánh dấu vị trí tuỳ chỉnh trên bản đồ, tất cả đều được hiển thị đẹp mắt trên trang web của bạn. - Tham số
libraries=places
tải Thư viện Địa điểm. Đây là tham số cần thiết cho các tính năng như tự động hoàn thành địa chỉ sẽ được thêm sau.
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;
};
Mã này hiển thị vị trí cửa hàng trên bản đồ. Để kiểm thử những gì chúng ta đã làm cho đến nay, hãy quay lại thư mục mẹ từ dòng lệnh:
cd ..
Bây giờ, hãy chạy lại ứng dụng ở chế độ phát triển bằng cách sử dụng:
go run *.go
Xem trước như bạn đã làm trước đây. Bạn sẽ thấy một bản đồ có các vòng tròn nhỏ màu xanh lục như thế này.
Bạn đã kết xuất các vị trí trên bản đồ và chúng ta mới chỉ đi được nửa chặng đường của lớp học lập trình này! Tuyệt vời. Bây giờ, hãy thêm một số tính năng tương tác.
6. Hiện chi tiết theo yêu cầu
Phản hồi sự kiện nhấp vào điểm đánh dấu trên bản đồ
Việc hiển thị một loạt điểm đánh dấu trên bản đồ là một khởi đầu tuyệt vời, nhưng chúng ta thực sự cần khách truy cập có thể nhấp vào một trong những điểm đánh dấu đó và xem thông tin về vị trí đó (chẳng hạn như tên doanh nghiệp, địa chỉ, v.v.). Tên của cửa sổ thông tin nhỏ thường xuất hiện khi bạn nhấp vào một điểm đánh dấu trên Google Maps là Cửa sổ thông tin.
Tạo một đối tượng infoWindow. Thêm đoạn mã sau vào hàm initialize
, thay thế dòng được nhận xét có nội dung "// 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();
Thay thế định nghĩa hàm fetchAndRenderStores
bằng phiên bản hơi khác này. Phiên bản này thay đổi dòng cuối cùng để gọi storeToCircle
bằng một đối số bổ sung, 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));
};
Thay thế định nghĩa storeToCircle
bằng phiên bản dài hơn một chút này. Phiên bản này hiện lấy Cửa sổ thông tin làm đối số thứ ba:
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;
};
Đoạn mã mới ở trên hiển thị một infoWindow
có thông tin của cửa hàng được chọn bất cứ khi nào người dùng nhấp vào một điểm đánh dấu cửa hàng trên bản đồ.
Nếu máy chủ vẫn đang chạy, hãy dừng rồi khởi động lại máy chủ. Làm mới trang bản đồ rồi thử nhấp vào một điểm đánh dấu trên bản đồ. Một cửa sổ thông tin nhỏ sẽ bật lên cùng với tên và địa chỉ của doanh nghiệp, trông như thế này:
7. Lấy vị trí bắt đầu của người dùng
Người dùng công cụ định vị cửa hàng thường muốn biết cửa hàng nào gần họ nhất hoặc địa chỉ nơi họ dự định bắt đầu hành trình. Thêm một thanh tìm kiếm tính năng Tự động hoàn thành địa điểm để người dùng dễ dàng nhập địa chỉ bắt đầu. Tính năng Tự động hoàn thành địa điểm cung cấp chức năng nhập liệu dự đoán tương tự như cách hoạt động của tính năng Tự động hoàn thành trong các thanh tìm kiếm khác của Google, ngoại trừ việc tất cả các cụm từ dự đoán đều là Địa điểm trong Google Maps Platform.
Tạo một trường nhập dữ liệu của người dùng
Quay lại bước chỉnh sửa style.css
để thêm kiểu cho thanh tìm kiếm Tự động hoàn thành và bảng điều khiển bên liên kết của kết quả. Trong khi cập nhật các kiểu CSS, chúng tôi cũng sẽ thêm các kiểu cho một thanh bên trong tương lai. Thanh bên này sẽ hiển thị thông tin cửa hàng dưới dạng danh sách đi kèm với bản đồ.
Thêm đoạn mã này vào cuối tệp.
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;
}
Thanh tìm kiếm Tự động hoàn thành và bảng điều khiển trượt ra ban đầu bị ẩn cho đến khi cần thiết.
Chuẩn bị một div cho tiện ích Tự động hoàn thành bằng cách thay thế chú thích trong index.html có nội dung "<!-- Autocomplete div goes here -->
" bằng mã sau. Trong khi chỉnh sửa, chúng ta cũng sẽ thêm div cho bảng điều khiển trượt ra.
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>
Bây giờ, hãy xác định một hàm để thêm tiện ích Autocomplete vào bản đồ bằng cách thêm đoạn mã sau vào cuối 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
};
Đoạn mã này hạn chế tính năng Gợi ý tự động chỉ trả về địa chỉ (vì tính năng Tự động hoàn thành địa điểm cũng có thể so khớp tên cơ sở và vị trí hành chính) và giới hạn số lượng địa chỉ được trả về chỉ ở Hoa Kỳ. Việc thêm các quy cách không bắt buộc này sẽ giảm số lượng ký tự mà người dùng cần nhập để thu hẹp các kết quả dự đoán nhằm hiển thị địa chỉ mà họ đang tìm kiếm.
Sau đó, thao tác này sẽ di chuyển div
Tự động hoàn thành mà bạn đã tạo vào góc trên cùng bên phải của bản đồ và chỉ định những trường cần trả về về từng Địa điểm trong phản hồi.
Cuối cùng, hãy gọi hàm initAutocompleteWidget
ở cuối hàm initialize
, thay thế nhận xét có nội dung "// TODO: Initialize the Autocomplete widget
".
app.js – initialize
// Initialize the Places Autocomplete Widget
initAutocompleteWidget();
Khởi động lại máy chủ bằng cách chạy lệnh sau, sau đó làm mới bản xem trước.
go run *.go
Giờ đây, bạn sẽ thấy một tiện ích Tự động hoàn thành ở góc trên cùng bên phải của bản đồ. Tiện ích này cho biết các địa chỉ ở Hoa Kỳ khớp với nội dung bạn nhập, thiên về khu vực hiển thị của bản đồ.
Cập nhật bản đồ khi người dùng chọn địa chỉ bắt đầu
Bây giờ, bạn cần xử lý khi người dùng chọn một kết quả dự đoán trong tiện ích Autocomplete (Tự động hoàn thành) và sử dụng vị trí đó làm cơ sở để tính khoảng cách đến các cửa hàng của bạn.
Thêm mã sau vào cuối initAutocompleteWidget
trong app.js
, thay thế cho chú thích "// 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
});
Mã này sẽ thêm một trình nghe để khi người dùng nhấp vào một trong các đề xuất, bản đồ sẽ đặt lại tâm vào địa chỉ đã chọn và đặt vị trí ban đầu làm cơ sở cho các phép tính khoảng cách. Bạn sẽ triển khai các phép tính khoảng cách ở một bước sau này.
Dừng và khởi động lại máy chủ, đồng thời làm mới bản xem trước để quan sát bản đồ được căn giữa lại sau khi bạn nhập một địa chỉ vào thanh tìm kiếm tự động hoàn thành.
8. Mở rộng quy mô bằng Cloud SQL
Hiện tại, chúng tôi có một công cụ định vị cửa hàng khá tốt. Ứng dụng này tận dụng thực tế là chỉ có khoảng một trăm vị trí mà ứng dụng sẽ sử dụng, bằng cách tải các vị trí đó vào bộ nhớ trên phần phụ trợ (thay vì đọc từ tệp nhiều lần). Nhưng nếu bộ định vị của bạn cần hoạt động ở một quy mô khác thì sao? Nếu bạn có hàng trăm vị trí rải rác trên một khu vực địa lý rộng lớn (hoặc hàng nghìn vị trí trên khắp thế giới), thì việc lưu giữ tất cả những vị trí đó trong bộ nhớ không còn là ý tưởng hay nữa và việc chia các vùng thành các tệp riêng lẻ sẽ gây ra những vấn đề riêng.
Đã đến lúc tải các vị trí của bạn từ cơ sở dữ liệu. Trong bước này, chúng ta sẽ di chuyển tất cả vị trí trong tệp GeoJSON vào cơ sở dữ liệu Cloud SQL và cập nhật phần phụ trợ Go để kéo kết quả từ cơ sở dữ liệu đó thay vì từ bộ nhớ đệm cục bộ bất cứ khi nào có yêu cầu.
Tạo một phiên bản Cloud SQL bằng cơ sở dữ liệu PostGres
Bạn có thể tạo một phiên bản Cloud SQL thông qua Google Cloud Console, nhưng sẽ dễ dàng hơn nếu bạn sử dụng tiện ích gcloud
để tạo một phiên bản từ dòng lệnh. Trong Cloud Shell, hãy tạo một phiên bản Cloud SQL bằng lệnh sau:
gcloud sql instances create locations \ --database-version=POSTGRES_12 \ --tier=db-custom-1-3840 --region=us-central1
- Đối số
locations
là tên mà chúng ta chọn để đặt cho phiên bản Cloud SQL này. - Cờ
tier
là một cách để chọn trong số một số máy được xác định trước một cách thuận tiện. - Giá trị
db-custom-1-3840
cho biết phiên bản đang được tạo phải có một vCPU và khoảng 3,75 GB bộ nhớ.
Phiên bản Cloud SQL sẽ được tạo và khởi tạo bằng cơ sở dữ liệu PostGresSQL, với người dùng mặc định là postgres
. Mật khẩu của người dùng này là gì? Câu hỏi rất hay! Họ không có. Bạn cần định cấu hình một phương thức trước khi có thể đăng nhập.
Đặt mật khẩu bằng lệnh sau:
gcloud sql users set-password postgres \ --instance=locations --prompt-for-password
Sau đó, hãy nhập mật khẩu bạn chọn khi được nhắc.
Bật tiện ích PostGIS
PostGIS là một tiện ích cho PostGresSQL, giúp bạn dễ dàng lưu trữ các loại dữ liệu không gian địa lý được chuẩn hoá. Trong trường hợp bình thường, chúng ta sẽ phải trải qua toàn bộ quy trình cài đặt để thêm PostGIS vào cơ sở dữ liệu. Rất may, đây là một trong những tiện ích được Cloud SQL hỗ trợ cho PostGresSQL.
Kết nối với phiên bản cơ sở dữ liệu bằng cách đăng nhập với tư cách người dùng postgres
bằng lệnh sau trong cửa sổ dòng lệnh trên đám mây.
gcloud sql connect locations --user=postgres --quiet
Nhập mật khẩu bạn vừa tạo. Bây giờ, hãy thêm tiện ích PostGIS tại dấu nhắc lệnh postgres=>
.
CREATE EXTENSION postgis;
Nếu thành công, kết quả đầu ra sẽ là CREATE EXTENSION, như minh hoạ bên dưới.
Ví dụ về đầu ra của lệnh
CREATE EXTENSION
Cuối cùng, hãy thoát khỏi kết nối cơ sở dữ liệu bằng cách nhập lệnh thoát tại dấu nhắc lệnh postgres=>
.
\q
Nhập dữ liệu địa lý vào cơ sở dữ liệu
Bây giờ, chúng ta cần nhập tất cả dữ liệu vị trí đó từ các tệp GeoJSON vào cơ sở dữ liệu mới.
Rất may là đây là một vấn đề thường gặp và bạn có thể tìm thấy một số công cụ trên Internet để tự động hoá việc này cho mình. Chúng ta sẽ sử dụng một công cụ có tên là ogr2ogr. Công cụ này chuyển đổi giữa nhiều định dạng phổ biến để lưu trữ dữ liệu không gian địa lý. Trong số các lựa chọn đó, bạn có thể chuyển đổi biểu mẫu GeoJSON thành tệp kết xuất SQL. Sau đó, bạn có thể dùng tệp kết xuất SQL để tạo các bảng và cột cho cơ sở dữ liệu, đồng thời tải tất cả dữ liệu có trong tệp GeoJSON vào cơ sở dữ liệu đó.
Tạo tệp kết xuất SQL
Trước tiên, hãy cài đặt ogr2ogr.
sudo apt-get install gdal-bin
Tiếp theo, hãy dùng ogr2ogr để tạo tệp kết xuất SQL. Tệp này sẽ tạo một bảng có tên là austinrecycling
.
ogr2ogr --config PG_USE_COPY YES -f PGDump datadump.sql \ data/recycling-locations.geojson -nln austinrecycling
Lệnh ở trên dựa trên việc chạy từ thư mục austin-recycling
. Nếu bạn cần chạy công cụ này từ một thư mục khác, hãy thay thế data
bằng đường dẫn đến thư mục lưu trữ recycling-locations.geojson
.
Điền thông tin về các địa điểm tái chế vào cơ sở dữ liệu
Sau khi hoàn tất lệnh cuối cùng đó, bạn sẽ có một tệp datadump.sql,
trong cùng thư mục nơi bạn chạy lệnh. Nếu mở tệp này, bạn sẽ thấy hơn 100 dòng SQL, tạo một bảng austinrecycling
và điền thông tin về các vị trí vào bảng đó.
Bây giờ, hãy mở một kết nối đến cơ sở dữ liệu và chạy tập lệnh đó bằng lệnh sau.
gcloud sql connect locations --user=postgres --quiet < datadump.sql
Nếu tập lệnh chạy thành công, thì đây là những dòng cuối cùng của đầu ra:
Ví dụ về đầu ra của lệnh
ALTER TABLE ALTER TABLE ATLER TABLE ALTER TABLE COPY 103 COMMIT WARNING: there is no transaction in progress COMMIT
Cập nhật phần phụ trợ Go để sử dụng Cloud SQL
Giờ đây, chúng ta đã có tất cả dữ liệu này trong cơ sở dữ liệu, đã đến lúc cập nhật mã.
Cập nhật giao diện người dùng để gửi thông tin vị trí
Hãy bắt đầu với một bản cập nhật rất nhỏ cho giao diện người dùng: Vì hiện tại, chúng ta đang viết ứng dụng này cho một quy mô mà chúng ta không muốn mọi vị trí được gửi đến giao diện người dùng mỗi khi truy vấn được chạy, nên chúng ta cần truyền một số thông tin cơ bản từ giao diện người dùng về vị trí mà người dùng quan tâm.
Mở app.js
rồi thay thế định nghĩa hàm fetchStores
bằng phiên bản này để đưa vĩ độ và kinh độ mà bạn quan tâm vào 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();
};
Sau khi hoàn tất bước này của lớp học lập trình, phản hồi sẽ chỉ trả về những cửa hàng gần với toạ độ trên bản đồ được cung cấp trong tham số center
. Đối với lần tìm nạp ban đầu trong hàm initialize
, mã mẫu được cung cấp trong lớp học này sẽ dùng toạ độ trung tâm cho Austin, Texas.
Vì fetchStores
giờ đây sẽ chỉ trả về một nhóm nhỏ các vị trí cửa hàng, nên chúng ta sẽ cần tìm nạp lại các cửa hàng bất cứ khi nào người dùng thay đổi vị trí bắt đầu.
Cập nhật hàm initAutocompleteWidget
để làm mới các vị trí mỗi khi bạn đặt một nguồn gốc mới. Bạn cần chỉnh sửa 2 điểm:
- Trong initAutocompleteWidget, hãy tìm lệnh gọi lại cho trình nghe
place_changed
. Huỷ chú thích dòng xoá các vòng tròn hiện có để dòng đó sẽ chạy mỗi khi người dùng chọn một địa chỉ trong thanh tìm kiếm của tính năng Tự động hoàn thành địa điểm.
app.js – initAutocompleteWidget
autocomplete.addListener("place_changed", async () => {
circles.forEach((c) => c.setMap(null)); // clear existing stores
// ...
- Bất cứ khi nào điểm xuất phát được chọn thay đổi, biến originLocation sẽ được cập nhật. Ở cuối lệnh gọi lại "
place_changed
", hãy bỏ chú thích dòng phía trên dòng "// TODO: Calculate the closest stores
" để truyền nguồn gốc mới này vào một lệnh gọi mới cho hàmfetchAndRenderStores
.
app.js – initAutocompleteWidget
await fetchAndRenderStores(originLocation.toJSON());
// TODO: Calculate the closest stores
Cập nhật phần phụ trợ để sử dụng CloudSQL thay vì tệp JSON đơn giản
Xoá tính năng đọc và lưu vào bộ nhớ đệm GeoJSON dạng tệp phẳng
Trước tiên, hãy thay đổi main.go
để xoá mã tải và lưu vào bộ nhớ đệm tệp GeoJSON phẳng. Chúng ta cũng có thể loại bỏ hàm dropoffsHandler
vì chúng ta sẽ viết một hàm do Cloud SQL cung cấp trong một tệp khác.
main.go
mới của bạn sẽ ngắn hơn nhiều.
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)
}
}
Tạo một trình xử lý mới cho Yêu cầu về vị trí
Bây giờ, hãy tạo một tệp khác, locations.go
, cũng trong thư mục austin-recycling. Bắt đầu bằng cách triển khai lại trình xử lý cho các yêu cầu về vị trí.
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)
}
Trình xử lý thực hiện những tác vụ quan trọng sau:
- Nó lấy vĩ độ và kinh độ từ đối tượng yêu cầu (Bạn có nhớ cách chúng ta thêm những thông tin đó vào URL không? )
- Thao tác này sẽ kích hoạt lệnh gọi
getGeoJsonFromDatabase
, lệnh gọi này sẽ trả về một chuỗi GeoJSON (Chúng ta sẽ viết chuỗi này sau.) - Phương thức này sử dụng
ResponseWriter
để in chuỗi GeoJSON đó vào phản hồi.
Tiếp theo, chúng ta sẽ tạo một nhóm kết nối để giúp việc sử dụng cơ sở dữ liệu mở rộng tốt với nhiều người dùng đồng thời.
Tạo một Nhóm kết nối
Nhóm kết nối là một tập hợp các kết nối cơ sở dữ liệu đang hoạt động mà máy chủ có thể sử dụng lại để phục vụ các yêu cầu của người dùng. Việc này giúp giảm đáng kể lượng tài nguyên tiêu hao khi số lượng người dùng đang hoạt động tăng lên, vì máy chủ không phải tốn thời gian tạo và huỷ các kết nối cho mọi người dùng đang hoạt động. Trong phần trước, có thể bạn đã nhận thấy chúng ta nhập thư viện github.com/jackc/pgx/stdlib.
. Đây là một thư viện phổ biến để làm việc với các nhóm kết nối trong Go.
Ở cuối locations.go
, hãy tạo một hàm initConnectionPool
(được gọi từ main.go
) để khởi tạo một nhóm kết nối. Để cho rõ ràng, một số phương thức trợ giúp được dùng trong đoạn mã này. configureConnectionPool
cung cấp một nơi thuận tiện để điều chỉnh các chế độ cài đặt nhóm như số lượng kết nối và thời gian tồn tại cho mỗi kết nối. mustGetEnv
bao bọc các lệnh gọi để lấy các biến môi trường bắt buộc, vì vậy, các thông báo lỗi hữu ích có thể được đưa ra nếu phiên bản thiếu thông tin quan trọng (chẳng hạn như IP hoặc tên của cơ sở dữ liệu cần kết nối).
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
}
Truy vấn Cơ sở dữ liệu để biết vị trí, nhận JSON khi trả về.
Bây giờ, chúng ta sẽ viết một truy vấn cơ sở dữ liệu lấy toạ độ trên bản đồ và trả về 25 vị trí gần nhất. Không chỉ vậy, nhờ một số chức năng cơ sở dữ liệu hiện đại, ứng dụng này sẽ trả về dữ liệu đó dưới dạng GeoJSON. Kết quả cuối cùng của tất cả những điều này là theo như mã giao diện người dùng có thể cho biết, không có gì thay đổi. Trước khi nó gửi yêu cầu đến một URL và nhận được một loạt GeoJSON. Giờ đây, nó sẽ gửi một yêu cầu đến một URL và... nhận lại một loạt GeoJSON.
Sau đây là hàm thực hiện thao tác đó. Thêm hàm sau vào sau mã xử lý và mã gộp kết nối mà bạn vừa viết ở cuối 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
}
Hàm này chủ yếu chỉ thiết lập, huỷ thiết lập và xử lý lỗi để kích hoạt một yêu cầu đến cơ sở dữ liệu. Hãy xem SQL thực tế. SQL này đang thực hiện rất nhiều việc thú vị ở lớp cơ sở dữ liệu, vì vậy bạn không phải lo lắng về việc triển khai bất kỳ việc nào trong số đó trong mã.
Truy vấn thô được kích hoạt, sau khi chuỗi đã được phân tích cú pháp và tất cả các ký tự chuỗi được chèn vào đúng vị trí, sẽ có dạng như sau:
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
Bạn có thể xem truy vấn này dưới dạng một truy vấn chính và một số hàm bao bọc JSON.
SELECT * ... LIMIT 25
chọn tất cả các trường cho từng vị trí. Sau đó, ứng dụng này sử dụng hàm ST_DISTANCE (một phần trong bộ hàm đo lường địa lý của PostGIS) để xác định khoảng cách giữa từng vị trí trong cơ sở dữ liệu và cặp vĩ độ/kinh độ của vị trí mà người dùng cung cấp ở giao diện người dùng. Xin lưu ý rằng không giống như Distance Matrix (Ma trận khoảng cách) có thể cho bạn biết khoảng cách lái xe, đây là khoảng cách không gian địa lý. Để tăng hiệu quả, sau đó, hệ thống sẽ dùng khoảng cách đó để sắp xếp và trả về 25 vị trí gần nhất với vị trí mà người dùng chỉ định.
**SELECT json_build_object(‘type', ‘F
**eature') bao bọc truy vấn trước đó, lấy kết quả và sử dụng chúng để tạo một đối tượng GeoJSON Feature. Ngoài dự kiến, truy vấn này cũng là nơi áp dụng bán kính tối đa "16090" là số mét trong 10 dặm, hạn mức cố định do phần phụ trợ Go chỉ định. Nếu bạn thắc mắc vì sao mệnh đề WHERE này không được thêm vào truy vấn bên trong (nơi xác định khoảng cách của từng vị trí), thì đó là do cách SQL thực thi ở chế độ nền. Trường đó có thể chưa được tính toán khi mệnh đề WHERE được kiểm tra. Trên thực tế, nếu bạn cố gắng di chuyển mệnh đề WHERE này sang truy vấn bên trong, thì hệ thống sẽ gửi ra lỗi.
**SELECT json_build_object(‘type', ‘FeatureColl
**ection') Truy vấn này bao bọc tất cả các hàng kết quả từ truy vấn tạo JSON trong một đối tượng GeoJSON FeatureCollection.
Thêm thư viện PGX vào dự án của bạn
Chúng ta cần thêm một phần phụ thuộc vào dự án của bạn: PostGres Driver & Toolkit (Trình điều khiển và bộ công cụ PostGres), cho phép nhóm kết nối. Cách dễ nhất để làm việc này là sử dụng Go Modules. Khởi chạy một mô-đun bằng lệnh này trong Cloud Shell:
go mod init my_locator
Tiếp theo, hãy chạy lệnh này để quét mã tìm phần phụ thuộc, thêm danh sách phần phụ thuộc vào tệp mod và tải các phần phụ thuộc đó xuống.
go mod tidy
Cuối cùng, hãy chạy lệnh này để kéo các phần phụ thuộc trực tiếp vào thư mục dự án của bạn để có thể dễ dàng tạo vùng chứa cho AppEngine Flex.
go mod vendor
Được rồi, bạn đã sẵn sàng thử nghiệm!
Dùng thử
Được rồi, chúng ta vừa hoàn thành RẤT NHIỀU việc. Hãy xem cách hoạt động của tính năng này!
Để máy phát triển của bạn (kể cả Cloud Shell) kết nối với cơ sở dữ liệu, bạn sẽ phải sử dụng Cloud SQL Proxy để quản lý kết nối cơ sở dữ liệu. Cách thiết lập Cloud SQL Proxy:
- Truy cập vào đây để bật Cloud SQL Admin API
- Nếu bạn đang sử dụng máy phát triển cục bộ, hãy cài đặt công cụ proxy Cloud SQL. Nếu đang sử dụng cloud shell, bạn có thể bỏ qua bước này vì công cụ này đã được cài đặt! Xin lưu ý rằng hướng dẫn này sẽ đề cập đến một tài khoản dịch vụ. Một tài khoản đã được tạo cho bạn và chúng ta sẽ đề cập đến việc thêm các quyền cần thiết vào tài khoản đó trong phần sau.
- Tạo một thẻ mới (trong cloud shell hoặc thiết bị đầu cuối của riêng bạn) để bắt đầu proxy.
- Truy cập vào
https://console.cloud.google.com/sql/instances/locations/overview
rồi di chuyển xuống để tìm trường Tên mối kết nối. Sao chép tên đó để dùng trong lệnh tiếp theo. - Trong thẻ đó, hãy chạy proxy Cloud SQL bằng lệnh này, thay thế
CONNECTION_NAME
bằng tên kết nối xuất hiện ở bước trước.
cloud_sql_proxy -instances=CONNECTION_NAME=tcp:5432
Quay lại thẻ đầu tiên của cloud shell và xác định các biến môi trường mà Go sẽ cần để giao tiếp với phần phụ trợ cơ sở dữ liệu, sau đó chạy máy chủ theo cách bạn đã làm trước đây:
Chuyển đến thư mục gốc của dự án nếu bạn chưa ở đó.
cd YOUR_PROJECT_ROOT
Tạo 5 biến môi trường sau (thay thế YOUR_PASSWORD_HERE
bằng mật khẩu bạn đã tạo ở trên).
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
Chạy phiên bản cục bộ.
go run *.go
Mở cửa sổ xem trước và cửa sổ này sẽ hoạt động như thể không có gì thay đổi: Bạn có thể nhập địa chỉ bắt đầu, thu phóng bản đồ và nhấp vào các địa điểm tái chế. Nhưng giờ đây, nó được hỗ trợ bởi một cơ sở dữ liệu và sẵn sàng mở rộng quy mô!
9. Liệt kê các cửa hàng gần nhất
Directions API hoạt động tương tự như trải nghiệm yêu cầu chỉ đường trong ứng dụng Google Maps – nhập một điểm xuất phát và một điểm đến để nhận được tuyến đường giữa hai điểm đó. Distance Matrix API mở rộng khái niệm này để xác định các cặp tối ưu giữa nhiều điểm khởi hành có thể và nhiều điểm đến có thể dựa trên thời gian di chuyển và khoảng cách. Trong trường hợp này, để giúp người dùng tìm thấy cửa hàng gần nhất với địa chỉ đã chọn, bạn cung cấp một điểm xuất phát và một mảng vị trí cửa hàng làm điểm đến.
Thêm khoảng cách từ điểm xuất phát đến từng cửa hàng
Ở đầu định nghĩa hàm initMap
, hãy thay thế chú thích "// TODO: Start Distance Matrix service
" bằng đoạn mã sau:
app.js – initMap
distanceMatrixService = new google.maps.DistanceMatrixService();
Thêm một hàm mới vào cuối app.js
có tên là 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);
});
};
Hàm này gọi Distance Matrix API bằng cách sử dụng nguồn gốc được truyền đến hàm dưới dạng một nguồn gốc duy nhất và vị trí cửa hàng dưới dạng một mảng đích đến. Sau đó, hàm này sẽ tạo một mảng các đối tượng lưu trữ mã nhận dạng của cửa hàng, khoảng cách được biểu thị bằng một chuỗi mà con người có thể đọc được, khoảng cách tính bằng mét dưới dạng một giá trị số và sắp xếp mảng.
Cập nhật hàm initAutocompleteWidget
để tính khoảng cách đến cửa hàng bất cứ khi nào một điểm xuất phát mới được chọn trong thanh tìm kiếm của tính năng Tự động hoàn thành địa điểm. Ở cuối hàm initAutocompleteWidget
, hãy thay thế nhận xét "// TODO: Calculate the closest stores
" bằng mã sau:
app.js – initAutocompleteWidget
// Use the selected address as the origin to calculate distances
// to each of the store locations
await calculateDistances(originLocation, stores);
renderStoresPanel();
Hiển thị chế độ xem danh sách các cửa hàng được sắp xếp theo khoảng cách
Người dùng mong muốn thấy danh sách các cửa hàng được sắp xếp theo thứ tự từ gần nhất đến xa nhất. Điền thông tin vào danh sách trên bảng điều khiển bên cho từng cửa hàng bằng danh sách đã được sửa đổi bằng hàm calculateDistances
để thông báo thứ tự hiển thị của các cửa hàng.
Thêm hai hàm mới vào cuối app.js
có tên là renderStoresPanel()
và 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;
};
Khởi động lại máy chủ và làm mới bản xem trước bằng cách chạy lệnh sau.
go run *.go
Cuối cùng, hãy nhập một địa chỉ ở Austin, TX vào thanh tìm kiếm Tự động hoàn thành rồi nhấp vào một trong các đề xuất.
Bản đồ sẽ căn giữa vào địa chỉ đó và một thanh bên sẽ xuất hiện, liệt kê các vị trí cửa hàng theo thứ tự khoảng cách từ địa chỉ đã chọn. Sau đây là một ví dụ minh hoạ:
10. Tạo kiểu cho bản đồ
Một cách hiệu quả để làm cho bản đồ của bạn trở nên khác biệt về mặt hình ảnh là thêm kiểu cho bản đồ. Với tính năng định kiểu bản đồ dựa trên đám mây, bạn có thể kiểm soát việc tuỳ chỉnh bản đồ của mình từ Cloud Console bằng tính năng Định kiểu bản đồ dựa trên đám mây (thử nghiệm). Nếu muốn tạo kiểu cho bản đồ bằng một tính năng không phải là phiên bản thử nghiệm, bạn có thể sử dụng tài liệu về tạo kiểu cho bản đồ để giúp bạn tạo JSON nhằm tạo kiểu cho bản đồ theo phương thức lập trình. Các hướng dẫn bên dưới sẽ hướng dẫn bạn sử dụng tính năng Định kiểu bản đồ dựa trên đám mây (bản thử nghiệm).
Tạo mã bản đồ
Trước tiên, hãy mở Cloud Console rồi nhập "Map Management" (Quản lý bản đồ) vào hộp tìm kiếm. Nhấp vào kết quả có nội dung "Quản lý bản đồ (Google Maps)".
Bạn sẽ thấy một nút ở gần phía trên cùng (ngay bên dưới hộp Tìm kiếm) có nội dung Tạo mã bản đồ mới. Nhấp vào đó rồi điền tên bạn muốn. Đối với Loại bản đồ, hãy nhớ chọn JavaScript và khi các lựa chọn khác xuất hiện, hãy chọn Vector (Véc tơ) trong danh sách. Kết quả cuối cùng sẽ có dạng như hình bên dưới.
Nhấp vào "Tiếp theo" và bạn sẽ nhận được một mã bản đồ hoàn toàn mới. Bạn có thể sao chép ngay bây giờ nếu muốn, nhưng đừng lo lắng, bạn có thể dễ dàng tìm lại sau này.
Tiếp theo, chúng ta sẽ tạo một kiểu để áp dụng cho bản đồ đó.
Tạo kiểu bản đồ
Nếu bạn vẫn đang ở mục Maps của Cloud Console, hãy nhấp vào "Kiểu bản đồ " ở cuối trình đơn điều hướng bên trái. Nếu không, giống như khi tạo mã bản đồ, bạn có thể tìm thấy trang phù hợp bằng cách nhập "Kiểu bản đồ" vào hộp tìm kiếm rồi chọn " Kiểu bản đồ (Google Maps)" trong kết quả, như trong hình bên dưới.
Tiếp theo, hãy nhấp vào nút "+ Tạo kiểu bản đồ mới" ở gần trên cùng
- Nếu bạn muốn điều chỉnh kiểu dáng cho phù hợp với bản đồ xuất hiện trong lớp học này, hãy nhấp vào thẻ "NHẬP JSON" rồi dán khối JSON bên dưới. Nếu không, nếu bạn muốn tạo kiểu bản đồ của riêng mình, hãy chọn Kiểu bản đồ mà bạn muốn bắt đầu. Nhấp vào Tiếp theo.
- Chọn Mã bản đồ mà bạn vừa tạo để liên kết Mã bản đồ đó với kiểu này, rồi nhấp lại vào Tiếp theo.
- Tại thời điểm này, bạn có thể tuỳ chỉnh thêm kiểu dáng của bản đồ. Nếu bạn muốn khám phá, hãy nhấp vào Tuỳ chỉnh trong Trình chỉnh sửa kiểu và thử nghiệm các màu sắc cũng như lựa chọn cho đến khi bạn có được một kiểu bản đồ mà mình thích. Nếu không, hãy nhấp vào Bỏ qua.
- Ở bước tiếp theo, hãy nhập tên và nội dung mô tả cho kiểu của bạn, rồi nhấp vào Lưu và xuất bản.
Sau đây là một blob JSON không bắt buộc để nhập trong bước đầu tiên.
[
{
"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"
}
]
}
]
Thêm mã bản đồ vào mã của bạn
Giờ đây, bạn đã trải qua quá trình tạo kiểu bản đồ này. Vậy làm cách nào để SỬ DỤNG kiểu bản đồ này trong bản đồ của riêng bạn? Bạn cần thực hiện 2 thay đổi nhỏ:
- Thêm Mã bản đồ làm tham số URL vào thẻ tập lệnh trong
index.html
Add
Mã bản đồ làm đối số hàm khởi tạo khi bạn tạo bản đồ trong phương thứcinitMap()
.
Thay thế thẻ tập lệnh tải Maps JavaScript API trong tệp HTML bằng URL trình tải bên dưới, thay thế phần giữ chỗ cho "YOUR_API_KEY
" và "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>
...
Trong phương thức initMap
của app.js
nơi hằng số map
được xác định, hãy bỏ chú thích dòng cho thuộc tính mapId
và thay thế "YOUR_MAP_ID_HERE
" bằng Mã bản đồ mà bạn vừa tạo:
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',
// ...
});
...
Khởi động lại máy chủ.
go run *.go
Sau khi bạn làm mới bản xem trước, bản đồ sẽ có kiểu dáng theo ý bạn. Sau đây là một ví dụ sử dụng kiểu JSON ở trên.
11. Triển khai cho kênh phát hành công khai
Nếu muốn xem ứng dụng của mình chạy từ AppEngine Flex (chứ không chỉ là một máy chủ web cục bộ trên máy phát triển / Cloud Shell, tức là những gì bạn đã làm), thì bạn có thể thực hiện rất dễ dàng. Chúng ta chỉ cần thêm một vài thứ để quyền truy cập vào cơ sở dữ liệu hoạt động trong môi trường phát hành. Tất cả những điều này đều được trình bày trong trang tài liệu về Kết nối từ App Engine linh hoạt đến Cloud SQL.
Thêm biến môi trường vào App.yaml
Trước tiên, bạn cần thêm tất cả các biến môi trường mà bạn đã dùng để kiểm thử cục bộ vào cuối tệp app.yaml
của ứng dụng.
- Truy cập vào https://console.cloud.google.com/sql/instances/locations/overview để tra cứu tên kết nối phiên bản.
- Dán mã sau vào cuối
app.yaml
. - Thay thế
YOUR_DB_PASSWORD_HERE
bằng mật khẩu bạn đã tạo cho tên người dùngpostgres
trước đó. - Thay thế
YOUR_CONNECTION_NAME_HERE
bằng giá trị ở bước 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
Xin lưu ý rằng DB_TCP_HOST
phải có giá trị 172.17.0.1 vì ứng dụng này kết nối thông qua AppEngine Flex**.** Điều này là do ứng dụng sẽ giao tiếp với Cloud SQL thông qua một proxy, tương tự như cách bạn đã làm.
Thêm quyền SQL Client vào tài khoản dịch vụ AppEngine Flex
Chuyển đến trang IAM-Admin trong Cloud Console rồi tìm một tài khoản dịch vụ có tên theo định dạng service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com
. Đây là tài khoản dịch vụ mà App Engine linh hoạt sẽ dùng để kết nối với cơ sở dữ liệu. Nhấp vào nút Chỉnh sửa ở cuối hàng rồi thêm vai trò "Cloud SQL Client".
Sao chép mã dự án vào đường dẫn Go
Để AppEngine chạy mã của bạn, AppEngine cần tìm thấy các tệp có liên quan trong đường dẫn Go. Đảm bảo bạn đang ở trong thư mục gốc của dự án.
cd YOUR_PROJECT_ROOT
Sao chép thư mục vào đường dẫn go.
mkdir -p ~/gopath/src/austin-recycling cp -r ./ ~/gopath/src/austin-recycling
Thay đổi thành thư mục đó.
cd ~/gopath/src/austin-recycling
Triển khai ứng dụng
Sử dụng CLI gcloud
để triển khai ứng dụng. Quá trình triển khai sẽ mất một khoảng thời gian.
gcloud app deploy
Sử dụng lệnh browse
để nhận một đường liên kết mà bạn có thể nhấp vào để xem công cụ định vị cửa hàng đã được triển khai đầy đủ, cấp doanh nghiệp và có tính thẩm mỹ cao đang hoạt động.
gcloud app browse
Nếu bạn đang chạy gcloud
bên ngoài Cloud Shell, thì việc chạy gcloud app browse
sẽ mở một thẻ trình duyệt mới.
12. (Nên dùng) Dọn dẹp
Việc thực hiện lớp học lập trình này sẽ nằm trong giới hạn của bậc miễn phí đối với hoạt động xử lý BigQuery và các lệnh gọi Maps Platform API. Tuy nhiên, nếu bạn chỉ thực hiện việc này như một bài tập giáo dục và muốn tránh phát sinh bất kỳ khoản phí nào trong tương lai, thì cách dễ nhất để xoá các tài nguyên liên kết với dự án này là xoá chính dự án đó.
Xoá dự án
Trong Bảng điều khiển GCP, hãy chuyển đến trang Cloud Resource Manager (Trình quản lý tài nguyên trên đám mây):
Trong danh sách dự án, hãy chọn dự án mà chúng ta đang thực hiện rồi nhấp vào Xoá. Bạn sẽ được nhắc nhập mã dự án. Nhập mật khẩu rồi nhấp vào Tắt.
Ngoài ra, bạn có thể xoá toàn bộ dự án ngay trong Cloud Shell bằng gcloud
bằng cách chạy lệnh sau và thay thế phần giữ chỗ GOOGLE_CLOUD_PROJECT
bằng mã dự án của bạn:
gcloud projects delete GOOGLE_CLOUD_PROJECT
13. Xin chúc mừng
Xin chúc mừng! Bạn đã hoàn tất thành công lớp học lập trình này!
Hoặc bạn đã lướt nhanh đến trang cuối cùng. Xin chúc mừng! Bạn đã lướt đến trang cuối cùng!
Trong suốt lớp học lập trình này, bạn đã làm việc với các công nghệ sau:
- Maps JavaScript API
- Dịch vụ ma trận khoảng cách, API Maps JavaScript (cũng có Distance Matrix API)
- Thư viện địa điểm, Maps JavaScript API (còn gọi là Places API)
- Môi trường linh hoạt của App Engine (Go)
- Cloud SQL API
Tài liệu đọc thêm
Vẫn còn rất nhiều điều cần học về tất cả những công nghệ này. Dưới đây là một số đường liên kết hữu ích cho những chủ đề mà chúng tôi không có thời gian đề cập đến trong lớp học lập trình này, nhưng chắc chắn có thể hữu ích cho bạn khi xây dựng giải pháp định vị cửa hàng phù hợp với nhu cầu cụ thể của bạn.