إنشاء محدّد مواقع متاجر كامل باستخدام Google Maps Platform وGoogle Cloud

1. مقدمة

ملخص

لنفترض أنّ لديك العديد من الأماكن التي تريد وضعها على الخريطة وتريد أن يتمكّن المستخدمون من معرفة أماكنها وتحديد المكان الذي يريدون زيارته. تشمل الأمثلة الشائعة على ذلك ما يلي:

  • أداة تحديد مواقع المتاجر على الموقع الإلكتروني لبائع تجزئة
  • خريطة لمراكز الاقتراع في الانتخابات القادمة
  • دليل للمواقع المتخصصة، مثل حاويات إعادة تدوير البطاريات

ما ستنشئه

في هذا الدرس العملي، ستنشئ أداة تحديد مواقع جغرافية تستند إلى خلاصة بيانات مباشرة خاصة بمواقع جغرافية محدّدة وتساعد المستخدم في العثور على الموقع الجغرافي الأقرب إلى نقطة البداية. يمكن أن يتعامل أداة تحديد الموقع الجغرافي المتكاملة هذه مع عدد أكبر بكثير من الأماكن مقارنةً بأداة تحديد الموقع الجغرافي البسيطة للمتجر التي تقتصر على 25 موقعًا جغرافيًا للمتجر أو أقل.

2ece59c64c06e9da.png

أهداف الدورة التعليمية

يستخدم هذا الدرس التطبيقي مجموعة بيانات مفتوحة لمحاكاة البيانات الوصفية التي تمّت تعبئتها مسبقًا حول عدد كبير من مواقع المتاجر، وذلك لكي تتمكّن من التركيز على تعلُّم المفاهيم الفنية الأساسية.

  • Maps JavaScript API: عرض عدد كبير من المواقع الجغرافية على خريطة ويب مخصّصة
  • ‫GeoJSON: تنسيق يخزِّن البيانات الوصفية حول المواقع الجغرافية
  • الإكمال التلقائي للأماكن: مساعدة المستخدمين في تقديم مواقع جغرافية للبدء بشكل أسرع وأكثر دقة
  • ‫Go: لغة البرمجة المستخدَمة لتطوير الخلفية البرمجية للتطبيق سيتفاعل الخلفية مع قاعدة البيانات ويرسل نتائج طلب البحث إلى الواجهة الأمامية بتنسيق JSON.
  • ‫App Engine: لاستضافة تطبيق الويب

المتطلبات الأساسية

  • معرفة أساسية بلغتَي HTML وJavaScript
  • حساب Google

2. طريقة الإعداد

في الخطوة 3 من القسم التالي، فعِّل Maps JavaScript API وPlaces API وDistance Matrix API لهذا الدرس العملي.

بدء استخدام "منصة خرائط Google"

إذا لم يسبق لك استخدام "منصة خرائط Google"، اتّبِع دليل "البدء باستخدام منصة خرائط Google" أو شاهِد قائمة تشغيل "البدء باستخدام منصة خرائط Google" لإكمال الخطوات التالية:

  1. أنشئ حساب فوترة.
  2. إنشاء مشروع
  3. فعِّل واجهات برمجة التطبيقات وحِزم تطوير البرامج (SDK) في "منصة خرائط Google" (المدرَجة في القسم السابق).
  4. أنشئ مفتاح واجهة برمجة تطبيقات.

تفعيل Cloud Shell

في هذا الدرس العملي، ستستخدم Cloud Shell، وهي بيئة سطر أوامر تعمل في Google Cloud وتتيح الوصول إلى المنتجات والموارد التي تعمل على Google Cloud، ما يتيح لك استضافة مشروعك وتشغيله بالكامل من متصفّح الويب.

لتفعيل Cloud Shell من Cloud Console، انقر على تفعيل Cloud Shell 89665d8d348105cd.png (يجب أن تستغرق عملية توفير البيئة والاتصال بها بضع لحظات فقط).

5f504766b9b3be17.png

يؤدي هذا الإجراء إلى فتح نافذة shell جديدة في الجزء السفلي من المتصفّح بعد عرض إعلان بيني تمهيدي.

d3bb67d514893d1f.png

تأكيد مشروعك

بعد الاتصال بـ Cloud Shell، من المفترض أن ترى أنّه تم إثبات هويتك وأنّ المشروع تم ضبطه مسبقًا على رقم تعريف المشروع الذي اخترته أثناء عملية الإعداد.

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

إذا لم يتم ضبط المشروع لسبب ما، شغِّل الأمر التالي:

gcloud config set project <YOUR_PROJECT_ID>

تفعيل واجهة برمجة التطبيقات AppEngine Flex

يجب تفعيل واجهة برمجة التطبيقات AppEngine Flex يدويًا من Cloud Console. لن يؤدي ذلك إلى تفعيل واجهة برمجة التطبيقات فحسب، بل سيؤدي أيضًا إلى إنشاء حساب خدمة "بيئة App Engine المرنة"، وهو الحساب الذي تمّت المصادقة عليه والذي سيتفاعل مع خدمات Google (مثل قواعد بيانات SQL) نيابةً عن المستخدم.

3- Hello, World

الخلفية: Hello World في Go

في مثيل Cloud Shell، ستبدأ بإنشاء تطبيق Go App Engine Flex الذي سيكون أساس بقية الدرس العملي.

في شريط أدوات Cloud Shell، انقر على الزر فتح المحرِّر لفتح محرِّر رموز في علامة تبويب جديدة. يتيح لك محرّر الرموز البرمجية المستند إلى الويب هذا تعديل الملفات بسهولة في مثيل Cloud Shell.

b63f7baad67b6601.png

بعد ذلك، انقر على رمز الفتح في نافذة جديدة لنقل المحرّر ونافذة الأوامر إلى علامة تبويب جديدة.

3f6625ff8461c551.png

في نافذة الوحدة الطرفية في أسفل علامة التبويب الجديدة، أنشئ دليل austin-recycling جديدًا.

mkdir -p austin-recycling && cd $_

بعد ذلك، ستنشئ تطبيقًا صغيرًا على Go App Engine للتأكّد من أنّ كل شيء يعمل بشكل صحيح. مرحبًا أيها العالم!

يجب أن يظهر الدليل austin-recycling أيضًا في قائمة مجلدات المحرّر على يمين الصفحة. في الدليل austin-recycling، أنشئ ملفًا باسم app.yaml. ضَع المحتوى التالي في ملف app.yaml:

app.yaml

runtime: go
env: flex

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

يضبط ملف الإعداد هذا تطبيق App Engine لاستخدام وقت تشغيل Go Flex. للحصول على معلومات أساسية حول معنى عناصر الإعداد في هذا الملف، يُرجى الاطّلاع على مستندات بيئة Google App Engine Go العادية.

بعد ذلك، أنشِئ ملف main.go بجانب ملف app.yaml:

main.go

package main

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

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

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

من المفيد التوقّف هنا لحظة لفهم ما يفعله هذا الرمز، على الأقل على مستوى عالٍ. لقد حدّدت حزمة main تبدأ تشغيل خادم http يستمع إلى المنفذ 8080، وتسجّل دالة معالجة لطلبات HTTP التي تتطابق مع المسار "/".

تكتب دالة المعالجة، التي يُطلق عليها اسم handler، السلسلة النصية "Hello, world!". سيتم نقل هذا النص مرة أخرى إلى المتصفّح، حيث ستتمكّن من قراءته. في الخطوات المستقبلية، ستنشئ معالِجات تستجيب ببيانات GeoJSON بدلاً من سلاسل بسيطة مبرمَجة بشكل ثابت.

بعد تنفيذ هذه الخطوات، من المفترض أن يظهر لك محرّر بالشكل التالي:

2084fdd5ef594ece.png

تجربة الميزة

لاختبار هذا التطبيق، يمكنك تشغيل خادم تطوير App Engine داخل مثيل Cloud Shell. ارجع إلى سطر أوامر Cloud Shell واكتب ما يلي:

go run *.go

ستظهر لك بعض أسطر ناتج السجلّ التي توضّح أنّك تشغّل خادم التطوير على مثيل Cloud Shell، مع استماع تطبيق الويب "مرحبًا بالعالم" على المنفذ 8080 للمضيف المحلي. يمكنك فتح علامة تبويب متصفّح ويب في هذا التطبيق من خلال النقر على الزر معاينة الويب واختيار عنصر القائمة المعاينة على المنفذ 8080 في شريط أدوات Cloud Shell.

4155fc1dc717ac67.png

سيؤدي النقر على عنصر القائمة هذا إلى فتح علامة تبويب جديدة في متصفّح الويب تحتوي على العبارة "Hello, world!" (مرحبًا بالعالم) التي يعرضها خادم تطوير App Engine.

في الخطوة التالية، ستضيف بيانات إعادة التدوير في مدينة أوستن إلى هذا التطبيق، وستبدأ في عرضها بشكل مرئي.

4. الحصول على البيانات الحالية

‫GeoJSON، اللغة المشتركة في عالم نظم المعلومات الجغرافية

ذكرت الخطوة السابقة أنّك ستنشئ معالجات في رمز Go تعرض بيانات GeoJSON في متصفّح الويب. ولكن ما هو GeoJSON؟

في عالم نظام المعلومات الجغرافية (GIS)، يجب أن نتمكّن من نقل المعلومات حول الكيانات الجغرافية بين أنظمة الكمبيوتر. تُعد الخرائط وسيلة رائعة لقراءة المعلومات، ولكن أجهزة الكمبيوتر تفضّل عادةً أن تكون البيانات بتنسيقات أسهل في الاستيعاب.

GeoJSON هو تنسيق لترميز بنى البيانات الجغرافية، مثل إحداثيات مواقع تسليم مواد إعادة التدوير في أوستن، تكساس. تم توحيد معايير GeoJSON في معيار فريق عمل هندسة الإنترنت يُعرف باسم RFC7946. يتم تحديد GeoJSON من خلال JSON، أي JavaScript Object Notation، الذي تم توحيد معاييره في ECMA-404 من قِبل المؤسسة نفسها التي وضعت المعايير الموحّدة للغة JavaScript، وهي Ecma International.

الأمر المهم هو أنّ GeoJSON هو تنسيق سلكي متوافق على نطاق واسع لنقل المعلومات الجغرافية. يستخدم هذا الدرس التطبيقي حول الترميز GeoJSON بالطرق التالية:

  • استخدِم حِزم Go لتحليل بيانات أوستن إلى بنية بيانات داخلية خاصة بنظام المعلومات الجغرافية ستستخدمها لفلترة البيانات المطلوبة.
  • تسلسل البيانات المطلوبة لنقلها بين خادم الويب ومتصفّح الويب
  • استخدِم مكتبة JavaScript لتحويل الردّ إلى علامات على خريطة.

سيوفّر لك ذلك الكثير من كتابة الرموز البرمجية، لأنّك لن تحتاج إلى كتابة محلّلات ومولّدات لتحويل دفق البيانات عبر الإنترنت إلى تمثيلات في الذاكرة.

استرداد البيانات

يتيح بوابة البيانات المفتوحة لمدينة أوستن في تكساس استخدام المعلومات الجغرافية المكانية المتعلّقة بالموارد العامة. في هذا الدرس التطبيقي حول الترميز، ستعرض مجموعة بيانات أماكن تسليم المواد القابلة لإعادة التدوير.

سيتم عرض البيانات باستخدام علامات على الخريطة، ويتم عرضها باستخدام طبقة البيانات في Maps JavaScript API.

ابدأ بتنزيل بيانات GeoJSON من موقع مدينة أوستن الإلكتروني إلى تطبيقك.

  1. في نافذة سطر الأوامر الخاصة بمثيل Cloud Shell، أوقِف الخادم عن طريق كتابة [CTRL] + [C].
  2. أنشئ الدليل data داخل الدليل austin-recycling، ثم انتقِل إلى هذا الدليل:
mkdir -p data && cd data

الآن، استخدِم curl لاسترداد مواقع إعادة التدوير:

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

أخيرًا، ارجع إلى الدليل الرئيسي.

cd ..

5- تعيين المواقع الجغرافية

أولاً، عدِّل ملف app.yaml ليعكس التطبيق الأكثر قوة الذي ستنشئه، والذي "لم يعُد مجرد تطبيق بسيط".

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. يعود السبب إلى أنّ الخلفية المستندة إلى Go ستعالج GeoJSON وترسلها، ما يتيح لنا إنشاء بعض الميزات الرائعة في الخطوات اللاحقة. غيِّر ملف 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"])
}

يوفّر لنا الخلفية البرمجية في Go ميزة قيّمة: تخزّن مثيل AppEngine جميع المواقع الجغرافية مؤقتًا فور بدء تشغيله. يوفّر ذلك الوقت لأنّ الخلفية لن تضطر إلى قراءة الملف من القرص عند كل عملية إعادة تحميل من كل مستخدم.

إنشاء الواجهة الأمامية

أول ما علينا فعله هو إنشاء مجلد لتخزين جميع مواد العرض الثابتة. من المجلد الرئيسي لمشروعك، أنشئ مجلدًا باسم static.

mkdir -p static && cd static

سننشئ 3 ملفات في هذا المجلد.

  • سيحتوي index.html على كل رمز HTML الخاص بتطبيق أداة البحث عن المتاجر المكوّن من صفحة واحدة.
  • سيحتوي style.css، كما هو متوقّع، على التصميم.
  • سيكون app.js مسؤولاً عن استرداد GeoJSON، وإجراء طلبات إلى Maps API، ووضع علامات على خريطتك المخصّصة.

أنشئ هذه الملفات الثلاثة، وتأكَّد من وضعها في static/ .

style.css

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

body {
  display: flex;
}

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

index.html

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

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

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

أولِ اهتمامًا خاصًا لعنوان URL الخاص بـ src في علامة النص البرمجي الخاصة بالعنصر head.

  • استبدِل نص العنصر النائب "YOUR_API_KEY" بمفتاح واجهة برمجة التطبيقات الذي أنشأته أثناء خطوة الإعداد. يمكنك الانتقال إلى صفحة "واجهات برمجة التطبيقات والخدمات" -> "بيانات الاعتماد" في Cloud Console لاسترداد مفتاح واجهة برمجة التطبيقات أو إنشاء مفتاح جديد.
  • يُرجى العِلم أنّ عنوان 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

يمكنك معاينته كما فعلت من قبل. من المفترض أن تظهر خريطة تحتوي على دوائر خضراء صغيرة على النحو التالي.

58a6680e9c8e7396.png

أنت تعرض حاليًا المواقع الجغرافية على الخريطة، ولم نصل بعد إلى منتصف الدرس التطبيقي حول الترميز. رائع. لنضِف الآن بعض التفاعلية.

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 مع معلومات المتجر المحدّد كلّما تم النقر على علامة متجر على الخريطة.

إذا كان الخادم لا يزال يعمل، أوقِفه وأعِد تشغيله. أعِد تحميل صفحة الخريطة وحاوِل النقر على محدّد موقع مكان على الخريطة. يجب أن تظهر نافذة معلومات صغيرة تتضمّن اسم المؤسسة وعنوانها، على النحو التالي:

1af0ab72ad0eadc5.png

7. الحصول على الموقع الجغرافي للمستخدم عند البدء

يريد مستخدمو أدوات البحث عن المتاجر عادةً معرفة أقرب متجر إليهم أو عنوان المكان الذي يخطّطون لبدء رحلتهم منه. أضِف شريط بحث "الإكمال التلقائي لاسم المكان" للسماح للمستخدم بإدخال عنوان بداية بسهولة. توفّر خدمة Place Autocomplete وظيفة البحث المسبق المشابهة لطريقة عمل ميزة "الإكمال التلقائي" في أشرطة بحث Google الأخرى، باستثناء أنّ جميع العبارات المقترَحة هي أماكن في "منصة خرائط Google".

إنشاء حقل إدخال خاص بالمستخدم

ارجع إلى تعديل style.css لإضافة أنماط إلى شريط البحث الخاص بميزة "الإكمال التلقائي" واللوحة الجانبية المرتبطة بالنتائج. أثناء تعديل أنماط CSS، سنضيف أيضًا أنماطًا لشريط جانبي مستقبلي يعرض معلومات المتجر على شكل قائمة مصاحبة للخريطة.

أضِف هذا الرمز إلى نهاية الملف.

style.css

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

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

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

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

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

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

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

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

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

.hidden {
  display: none;
}

يتم إخفاء كلّ من شريط البحث "الإكمال التلقائي" واللوحة المنزلقة في البداية إلى أن تكون هناك حاجة إليهما.

جهِّز عنصر div لأداة Autocomplete عن طريق استبدال التعليق في ملف 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

من المفترض أن يظهر لك الآن تطبيق مصغّر للإكمال التلقائي في أعلى يسار الخريطة، يعرض لك عناوين في الولايات المتحدة تتطابق مع ما تكتبه، مع التركيز على المنطقة المرئية من الخريطة.

58e9bbbcc4bf18d1.png

تعديل الخريطة عندما يختار المستخدم عنوان بدء

الآن، عليك التعامل مع الحالات التي يختار فيها المستخدم نتيجة بحث مقترَحة من أداة "الإكمال التلقائي" واستخدام هذا الموقع الجغرافي كأساس لاحتساب المسافات إلى متاجرك.

أضِف الرمز التالي إلى نهاية initAutocompleteWidget في app.js، واستبدِل التعليق "// TODO: Respond when a user selects an address".

app.js - initAutocompleteWidget

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

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

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

يضيف الرمز البرمجي أداة معالجة الأحداث حتى عندما ينقر المستخدم على أحد الاقتراحات، تتم إعادة توسيط الخريطة على العنوان المحدّد ويتم ضبط نقطة البداية كأساس لاحتساب المسافات. ستنفّذ عمليات حساب المسافة في خطوة لاحقة.

أوقِف الخادم وأعِد تشغيله، ثم أعِد تحميل المعاينة لملاحظة إعادة توسيط الخريطة بعد إدخال عنوان في شريط البحث الذي يتيح الإكمال التلقائي.

8. التوسّع باستخدام Cloud SQL

حتى الآن، لدينا أداة رائعة لتحديد مواقع المتاجر. يستفيد هذا التطبيق من حقيقة أنّه لن يستخدم سوى حوالي مائة موقع جغرافي، وذلك من خلال تحميلها في الذاكرة على الخلفية (بدلاً من القراءة من الملف بشكل متكرر). ولكن ماذا لو كان محدّد الموقع الجغرافي بحاجة إلى العمل على نطاق مختلف؟ إذا كان لديك مئات المواقع الجغرافية المنتشرة في منطقة جغرافية كبيرة (أو الآلاف في جميع أنحاء العالم)، لن يكون الاحتفاظ بكل هذه المواقع الجغرافية في الذاكرة فكرة جيدة، وسيؤدي تقسيم المناطق إلى ملفات فردية إلى حدوث مشاكل.

حان الوقت لتحميل مواقعك الجغرافية من قاعدة بيانات. في هذه الخطوة، سننقل جميع المواقع الجغرافية في ملف GeoJSON إلى قاعدة بيانات Cloud SQL، وسنعدّل الخلفية المستندة إلى Go لاسترداد النتائج من قاعدة البيانات هذه بدلاً من ذاكرة التخزين المؤقت المحلية كلما وصل طلب.

إنشاء مثيل Cloud SQL باستخدام قاعدة بيانات PostGres

يمكنك إنشاء مثيل Cloud SQL من خلال Google Cloud Console، ولكن من الأسهل استخدام الأداة المساعدة 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.75 غيغابايت تقريبًا.

سيتم إنشاء مثيل Cloud SQL وتهيئته باستخدام قاعدة بيانات PostGresSQL، مع المستخدم التلقائي postgres. ما هي كلمة مرور هذا المستخدم؟ نشكرك على طرح هذا السؤال. ليس لديهم حساب. يجب إعداد أحدها قبل تسجيل الدخول.

اضبط كلمة المرور باستخدام الأمر التالي:

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

بعد ذلك، أدخِل كلمة المرور التي اخترتها عندما يُطلب منك ذلك.

تفعيل إضافة PostGIS

PostGIS هي إضافة إلى PostGresSQL تسهّل تخزين أنواع موحّدة من البيانات الجغرافية المكانية. في الظروف العادية، علينا اتّباع عملية تثبيت كاملة لإضافة PostGIS إلى قاعدة البيانات. لحسن الحظ، إنّها إحدى الإضافات المتوافقة مع Cloud SQL لـ PostGresSQL.

اتّصِل بمثيل قاعدة البيانات عن طريق تسجيل الدخول كمستخدم postgres باستخدام الأمر التالي في وحدة Cloud Shell الطرفية.

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

أدخِل كلمة المرور التي أنشأتها للتوّ. الآن، أضِف إضافة PostGIS في موجّه الأوامر postgres=>.

CREATE EXTENSION postgis;

في حال نجاح العملية، يجب أن تظهر النتيجة CREATE EXTENSION، كما هو موضّح أدناه.

مثال على ناتج الأمر

CREATE EXTENSION

أخيرًا، اخرج من اتصال قاعدة البيانات عن طريق إدخال أمر الخروج في موجّه الأوامر postgres=>.

\q

استيراد البيانات الجغرافية إلى قاعدة البيانات

الآن، علينا استيراد جميع بيانات الموقع الجغرافي هذه من ملفات GeoJSON إلى قاعدة البيانات الجديدة.

لحسن الحظ، هذه مشكلة شائعة، ويمكن العثور على العديد من الأدوات على الإنترنت لأتمتة هذه العملية نيابةً عنك. سنستخدم أداة تُسمى ogr2ogr تحوّل بين عدة تنسيقات شائعة لتخزين البيانات الجغرافية المكانية. من بين هذه الخيارات، يمكنك تحويل GeoJSON إلى ملف SQL. يمكن بعد ذلك استخدام ملف SQL dump لإنشاء الجداول والأعمدة لقاعدة البيانات، وتحميل جميع البيانات التي كانت موجودة في ملفات GeoJSON.

إنشاء ملف تفريغ SQL

أولاً، ثبِّت أداة ogr2ogr.

sudo apt-get install gdal-bin

بعد ذلك، استخدِم ogr2ogr لإنشاء ملف تفريغ SQL. سينشئ هذا الملف جدولاً باسم austinrecycling.

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

يستند الأمر أعلاه إلى التشغيل من المجلد austin-recycling. إذا كنت بحاجة إلى تشغيله من دليل آخر، استبدِل data بمسار الدليل الذي يتم تخزين recycling-locations.geojson فيه.

ملء قاعدة البيانات بمواقع إعادة التدوير

بعد إكمال هذا الأمر الأخير، من المفترض أن يكون لديك الآن ملف datadump.sql, في الدليل نفسه الذي نفّذت فيه الأمر. إذا فتحت الملف، ستجد أكثر من مئة سطر من لغة SQL، ما يؤدي إلى إنشاء جدول austinrecycling وتعبئته بالمواقع الجغرافية.

الآن، افتح اتصالاً بقاعدة البيانات وشغِّل هذا النص البرمجي باستخدام الأمر التالي.

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

إذا تم تشغيل النص البرمجي بنجاح، ستظهر الأسطر القليلة الأخيرة من الناتج على النحو التالي:

نموذج لناتج الأمر

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

تعديل الخلفية المستندة إلى Go لاستخدام Cloud SQL

بعد أن جمعنا كل هذه البيانات في قاعدة البيانات، حان الوقت لتعديل الرمز.

تعديل الواجهة الأمامية لإرسال معلومات الموقع الجغرافي

لنبدأ بتعديل صغير جدًا على الواجهة الأمامية: بما أنّنا نكتب هذا التطبيق الآن بمقياس لا نريد فيه أن يتم إرسال كل موقع جغرافي إلى الواجهة الأمامية في كل مرة يتم فيها تنفيذ طلب البحث، نحتاج إلى تمرير بعض المعلومات الأساسية من الواجهة الأمامية حول الموقع الجغرافي الذي يهتم به المستخدم.

افتح app.js واستبدِل تعريف الدالة fetchStores بهذا الإصدار لتضمين خطوط الطول والعرض التي تهمّك في عنوان URL.

app.js - fetchStores

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

بعد إكمال هذه الخطوة من الدرس العملي، لن يعرض الرد سوى المتاجر الأقرب إلى إحداثيات الخريطة المقدَّمة في المَعلمة center. بالنسبة إلى عملية الجلب الأولية في الدالة initialize، يستخدم نموذج الرمز البرمجي المقدَّم في هذا المختبر الإحداثيات المركزية لمدينة أوستن، تكساس.

بما أنّ fetchStores ستعرض الآن مجموعة فرعية فقط من مواقع المتاجر، علينا إعادة جلب المتاجر كلما غيّر المستخدم الموقع الجغرافي الأولي.

تعديل الدالة initAutocompleteWidget لإعادة تحميل المواقع الجغرافية كلما تم ضبط مصدر جديد يتطلّب ذلك إجراء تعديلَين:

  1. في initAutocompleteWidget، ابحث عن دالة معالجة البيانات الخاصة بمعالج place_changed. أزِل التعليق من السطر الذي يمحو الدوائر الحالية، وبذلك سيتم تنفيذ هذا السطر في كل مرة يختار فيها المستخدم عنوانًا من شريط البحث في خدمة "الإكمال التلقائي للأماكن".

app.js - initAutocompleteWidget

  autocomplete.addListener("place_changed", async () => {
    circles.forEach((c) => c.setMap(null)); // clear existing stores
    // ...
  1. عند تغيير المصدر المحدّد، يتم تعديل قيمة المتغيّر originLocation. في نهاية عملية رد الاتصال "place_changed"، أزِل التعليق من السطر الذي يسبق السطر "// TODO: Calculate the closest stores" لتمرير المصدر الجديد هذا إلى عملية استدعاء جديدة للدالة fetchAndRenderStores.

app.js - initAutocompleteWidget

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

تعديل الخلفية لاستخدام CloudSQL بدلاً من ملف JSON مسطّح

إزالة ميزة قراءة ملفات GeoJSON المسطّحة وتخزينها مؤقتًا

أولاً، غيِّر main.go لإزالة الرمز الذي يحمّل ملف GeoJSON المسطّح ويخزّنه مؤقتًا. يمكننا أيضًا التخلّص من الدالة dropoffsHandler، لأنّنا سنكتب دالة تستند إلى Cloud SQL في ملف مختلف.

سيكون main.go الجديد أقصر بكثير.

main.go

package main

import (

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

func main() {

        initConnectionPool()

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

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

إنشاء معالج جديد لطلبات تحديد الموقع الجغرافي

لننشئ الآن ملفًا آخر، locations.go، في دليل austin-recycling أيضًا. ابدأ بإعادة تنفيذ معالج طلبات الموقع الجغرافي.

locations.go

package main

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

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

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

ينفِّذ المعالج المهام المهمة التالية:

  • يستخرج خطوط الطول والعرض من عنصر الطلب (تذكّر كيف أضفنا هذه البيانات إلى عنوان URL؟ )
  • يتم تشغيل طلب getGeoJsonFromDatabase، الذي يعرض سلسلة GeoJSON (سنكتبها لاحقًا).
  • يستخدم ResponseWriter لطباعة سلسلة GeoJSON هذه في الرد.

بعد ذلك، سننشئ مجموعة اتصالات للمساعدة في توسيع نطاق استخدام قاعدة البيانات بشكل جيد مع المستخدمين المتزامنين.

إنشاء مجموعة اتصالات

مجموعة الاتصالات هي مجموعة من اتصالات قواعد البيانات النشطة التي يمكن للخادم إعادة استخدامها لتلبية طلبات المستخدمين. تؤدي هذه الطريقة إلى إزالة الكثير من النفقات العامة مع زيادة عدد المستخدمين النشطين، لأنّ الخادم لا يحتاج إلى تخصيص وقت لإنشاء عمليات الربط وإزالتها لكل مستخدم نشط. ربما لاحظت في القسم السابق أنّنا استوردنا المكتبة github.com/jackc/pgx/stdlib.. هذه مكتبة شائعة للتعامل مع مجموعات الاتصال في Go.

في نهاية locations.go، أنشئ الدالة initConnectionPool (التي يتم استدعاؤها من main.go) التي تهيئ مجموعة الاتصال. للتوضيح، يتم استخدام بعض الطرق المساعدة في هذا المقتطف. توفّر configureConnectionPool مكانًا مناسبًا لتعديل إعدادات مجموعة الخوادم، مثل عدد الاتصالات ومدة كل اتصال. تتضمّن mustGetEnv عمليات استدعاء للحصول على متغيرات البيئة المطلوبة، وبالتالي يمكن عرض رسائل خطأ مفيدة في حال كانت المعلومات الأساسية غير متوفّرة في المثيل (مثل عنوان IP أو اسم قاعدة البيانات المطلوب الاتصال بها).

locations.go

// The connection pool
var db *sql.DB

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

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

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

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

                configureConnectionPool(dbPool)

                if err != nil {

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

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

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

طلب البحث من قاعدة البيانات عن المواقع الجغرافية والحصول على ملف JSON في المقابل

سنكتب الآن طلب بحث في قاعدة البيانات يأخذ إحداثيات الخريطة ويعرض أقرب 25 موقعًا جغرافيًا. بالإضافة إلى ذلك، وبفضل بعض وظائف قواعد البيانات الحديثة الرائعة، ستعرض هذه البيانات بتنسيق GeoJSON. والنتيجة النهائية لكل ذلك هي أنّه بقدر ما يمكن أن يوضّح الرمز البرمجي للواجهة الأمامية، لم يحدث أي تغيير. قبل أن يرسل طلبًا إلى عنوان URL ويحصل على مجموعة من GeoJSON. الآن، يتم إرسال طلب إلى عنوان URL، ويتم تلقّي مجموعة من GeoJSON.

إليك الدالة التي تنفّذ هذه العملية السحرية. أضِف الدالة التالية بعد رمز معالج الطلبات ورمز تجميع الاتصالات الذي كتبته للتو في أسفل locations.go.

locations.go

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

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

        const tableName = "austinrecycling"

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

        log.Println(queryStr)

        rows, err := db.Query(queryStr)

        defer rows.Close()

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

تقتصر وظيفة هذا الإجراء في الغالب على الإعداد والإيقاف ومعالجة الأخطاء لإرسال طلب إلى قاعدة البيانات. لنلقِ نظرة على SQL الفعلي الذي ينفّذ الكثير من الإجراءات المثيرة للاهتمام على مستوى قاعدة البيانات، لذا لن تحتاج إلى تنفيذ أيّ منها في الرمز.

يبدو الاستعلام الأولي الذي يتم تنفيذه، بعد تحليل السلسلة وإدراج جميع القيم الحرفية للسلسلة في مواضعها المناسبة، على النحو التالي:

parsed.sql

SELECT jsonb_build_object(
        'type',
        'FeatureCollection',
        'features',
        jsonb_agg(feature)
    )
FROM (
        SELECT jsonb_build_object(
                'type',
                'Feature',
                'id',
                ogc_fid,
                'geometry',
                ST_AsGeoJSON(wkb_geometry)::jsonb,
                'properties',
                to_jsonb(row) - 'ogc_fid' - 'wkb_geometry'
            ) AS feature
        FROM (
                SELECT *,
                    ST_Distance(
                        ST_GEOGFromWKB(wkb_geometry),
                        -- Los Angeles (LAX)
                        ST_GEOGFromWKB(st_makepoint(-97.7624043, 30.523725))
                    ) as distance
                from austinrecycling
                order by distance
                limit 25
            ) row
        where distance < 16090
    ) features

يمكن عرض هذا الطلب على أنّه طلب بحث أساسي واحد وبعض دوال التضمين بتنسيق JSON.

يؤدي النقر على SELECT * ... LIMIT 25 إلى اختيار جميع الحقول لكل موقع جغرافي. بعد ذلك، تستخدم الدالة ST_DISTANCE (وهي جزء من مجموعة دوال قياس الجغرافيا في PostGIS) لتحديد المسافة بين كل موقع جغرافي في قاعدة البيانات وزوج خطوط الطول والعرض الخاص بالموقع الجغرافي الذي قدّمه المستخدم في الواجهة الأمامية. يُرجى العِلم أنّ هذه المسافات هي مسافات جغرافية مكانية، على عكس خدمة "مصفوفة المسافة" التي يمكنها أن تقدّم لك مسافة القيادة. ولتحقيق الكفاءة، تستخدم هذه المسافة بعد ذلك لترتيب المواقع الجغرافية وعرض أقرب 25 موقعًا جغرافيًا إلى الموقع الجغرافي الذي حدّده المستخدم.

يغلّف **SELECT json_build_object(‘type', ‘F**eature') طلب البحث السابق، ويأخذ النتائج ويستخدمها لإنشاء عنصر GeoJSON Feature. بشكل غير متوقّع، يتم تطبيق الحد الأقصى لنصف القطر أيضًا في طلب البحث هذا، حيث إنّ "16090" هو عدد الأمتار في 10 أميال، وهو الحدّ الأقصى الثابت الذي يحدّده الخلفية البرمجية في Go. إذا كنت تتساءل عن سبب عدم إضافة عبارة WHERE هذه إلى الاستعلام الداخلي (حيث يتم تحديد مسافة كل موقع جغرافي) بدلاً من ذلك، فذلك لأنّه أثناء تنفيذ SQL في الخلفية، قد لا يكون قد تم احتساب هذا الحقل عند فحص عبارة WHERE. في الواقع، إذا حاولت نقل عبارة WHERE هذه إلى الاستعلام الداخلي، سيظهر لك خطأ.

**SELECT json_build_object(‘type', ‘FeatureColl**ection') يغلّف هذا الطلب جميع الصفوف الناتجة من طلب البحث الذي ينشئ JSON في كائن GeoJSON FeatureCollection.

إضافة مكتبة PGX إلى مشروعك

علينا إضافة تبعية واحدة إلى مشروعك، وهي PostGres Driver & Toolkit التي تتيح تجميع الاتصالات. أسهل طريقة لإجراء ذلك هي باستخدام وحدات Go. ابدأ وحدة باستخدام هذا الأمر في Cloud Shell:

go mod init my_locator

بعد ذلك، شغِّل هذا الأمر لفحص الرمز بحثًا عن التبعيات وإضافة قائمة بالتبعيات إلى ملف الوحدة وتنزيلها.

go mod tidy

أخيرًا، شغِّل هذا الأمر لجلب التبعيات مباشرةً إلى دليل مشروعك حتى يمكن إنشاء الحاوية بسهولة في AppEngine Flex.

go mod vendor

حسنًا، أنت جاهز لتجربتها.

تجربة الميزة

حسنًا، لقد أنجزنا الكثير. لنرى كيف تعمل هذه الميزة.

لكي يتمكّن جهاز التطوير (نعم، حتى Cloud Shell) من الاتصال بقاعدة البيانات، علينا استخدام Cloud SQL Proxy لإدارة اتصال قاعدة البيانات. لإعداد Cloud SQL Proxy، اتّبِع الخطوات التالية:

  1. انتقِل إلى هنا لتفعيل Cloud SQL Admin API
  2. إذا كنت تستخدم جهاز تطوير محليًا، عليك تثبيت أداة Cloud SQL Proxy. إذا كنت تستخدم Cloud Shell، يمكنك تخطي هذه الخطوة لأنّ الأداة مثبّتة مسبقًا. يُرجى العِلم أنّ التعليمات ستشير إلى حساب خدمة. تم إنشاء حساب لك مسبقًا، وسنتناول في القسم التالي كيفية إضافة الأذونات اللازمة إلى هذا الحساب.
  3. أنشئ علامة تبويب جديدة (في Cloud Shell أو في نافذة طرفية خاصة بك) لبدء الخادم الوكيل.

bcca42933bfbd497.png

  1. انتقِل إلى https://console.cloud.google.com/sql/instances/locations/overview ومرِّر للأسفل للعثور على حقل اسم الاتصال. انسخ هذا الاسم لاستخدامه في الأمر التالي.
  2. في علامة التبويب هذه، شغِّل 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"، أي إدخال نقطة بداية واحدة ووجهة واحدة لتلقّي مسار بين النقطتين. تتوسّع واجهة برمجة التطبيقات Distance Matrix 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

أخيرًا، أدخِل عنوانًا في أوستن، تكساس في شريط بحث "الإكمال التلقائي" وانقر على أحد الاقتراحات.

يجب أن تتوسّط الخريطة هذا العنوان وأن يظهر شريط جانبي يعرض مواقع المتاجر حسب المسافة من العنوان المحدّد. في ما يلي مثال على ذلك:

96e35794dd0e88c9.png

10. تحديد نمط الخريطة

إحدى الطرق الفعّالة لإبراز خريطتك بصريًا هي إضافة أنماط إليها. باستخدام ميزة "تصميم الخرائط باستخدام السحابة الإلكترونية"، يمكنك التحكّم في تخصيص خرائطك من Cloud Console باستخدام ميزة "تصميم الخرائط باستخدام السحابة الإلكترونية" (إصدار تجريبي). إذا كنت تفضّل تصميم خريطتك باستخدام ميزة غير تجريبية، يمكنك استخدام مستندات تصميم الخرائط لمساعدتك في إنشاء ملف JSON لتصميم الخريطة آليًا. ترشدك التعليمات أدناه إلى كيفية استخدام ميزة "تصميم الخرائط باستخدام السحابة الإلكترونية" (إصدار تجريبي).

إنشاء رقم تعريف خريطة

أولاً، افتح Cloud Console وفي مربّع البحث، اكتب "إدارة الخرائط" (Map Management). انقر على النتيجة التي تحمل العنوان "إدارة الخرائط (خرائط Google)". 64036dd0ed200200.png

سيظهر لك زر بالقرب من أعلى الصفحة (أسفل مربّع البحث مباشرةً) مكتوب عليه إنشاء معرّف خريطة جديد. انقر على ذلك، ثم املأ الاسم الذي تريده. بالنسبة إلى "نوع الخريطة"، احرص على اختيار JavaScript، وعند ظهور المزيد من الخيارات، اختَر Vector من القائمة. يجب أن تبدو النتيجة النهائية مشابهة للصورة أدناه.

70f55a759b4c4212.png

انقر على "التالي" وسيتم منحك معرّف خريطة جديدًا. يمكنك نسخها الآن إذا أردت، ولكن لا داعي للقلق، فمن السهل البحث عنها لاحقًا.

بعد ذلك، سننشئ نمطًا لتطبيقه على تلك الخريطة.

إنشاء نمط خريطة

إذا كنت لا تزال في قسم "خرائط Google" في Cloud Console، انقر على "أنماط الخرائط" في أسفل قائمة التنقّل على يمين الصفحة. بخلاف ذلك، يمكنك العثور على الصفحة المناسبة من خلال كتابة "أنماط الخرائط" في مربّع البحث واختيار " أنماط الخرائط (خرائط Google)" من النتائج، كما هو موضّح في الصورة أدناه، تمامًا كما تفعل عند إنشاء معرّف خريطة.

9284cd200f1a9223.png

بعد ذلك، انقر على الزرّ بالقرب من أعلى الصفحة الذي يحمل عبارة "+ إنشاء نمط خريطة جديد".

  1. إذا كنت تريد مطابقة التصميم في الخريطة المعروضة في هذا المختبر، انقر على علامة التبويب استيراد JSON والصق كائن JSON أدناه. في حال أردت إنشاء نمط خاص بك، اختَر نمط الخريطة الذي تريد البدء به. ثم انقر على التالي.
  2. اختَر رقم تعريف الخريطة الذي أنشأته للتو لربط رقم تعريف الخريطة هذا بهذا النمط، ثم انقر على التالي مرة أخرى.
  3. في هذه المرحلة، يمكنك تخصيص نمط الخريطة بشكل أكبر. إذا أردت استكشاف هذه الميزة، انقر على تخصيص في "محرّر الأنماط" وجرِّب الألوان والخيارات إلى أن تحصل على نمط خريطة يعجبك. بخلاف ذلك، انقر على تخطّي.
  4. في الخطوة التالية، أدخِل اسم النمط ووصفه، ثم انقر على حفظ ونشر.

في ما يلي كائن ثنائي كبير اختياري بتنسيق 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"
      }
    ]
  }
]

إضافة معرّف الخريطة إلى الرمز

بعد أن بذلت جهدًا لإنشاء نمط الخريطة هذا، كيف يمكنك استخدامه في خريطتك؟ عليك إجراء تغييرَين صغيرَين:

  1. أضِف معرّف الخريطة كمعلَمة عنوان URL إلى علامة البرنامج النصي في index.html
  2. Add معرّف الخريطة كمعلَمة في الدالة الإنشائية عند إنشاء الخريطة في طريقة initMap()

استبدِل علامة النص البرمجي التي تحمّل واجهة برمجة تطبيقات JavaScript لـ "خرائط Google" في ملف HTML بعنوان URL الخاص بأداة التحميل أدناه، مع استبدال العناصر النائبة لـ "YOUR_API_KEY" و"YOUR_MAP_ID":

index.html

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

في طريقة initMap من app.js حيث يتم تعريف الثابت map، أزِل التعليق من السطر الخاص بالسمة mapId واستبدِل "YOUR_MAP_ID_HERE" بمعرّف الخريطة الذي أنشأته للتو:

app.js - initMap

...

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

أعِد تشغيل الخادم.

go run *.go

بعد إعادة تحميل المعاينة، من المفترض أن تظهر الخريطة منمّقة وفقًا لإعداداتك المفضّلة. في ما يلي مثال باستخدام تنسيق JSON أعلاه.

2ece59c64c06e9da.png

11. النشر في مرحلة الإنتاج

إذا أردت رؤية تطبيقك يعمل من AppEngine Flex (وليس مجرد خادم ويب محلي على جهاز التطوير أو Cloud Shell، وهو ما كنت تفعله)، سيكون ذلك سهلاً للغاية. ما علينا سوى إضافة بعض العناصر لكي يعمل الوصول إلى قاعدة البيانات في بيئة الإنتاج. يتم توضيح كل ذلك في صفحة المستندات حول الاتصال من App Engine Flex بخدمة Cloud SQL.

إضافة متغيرات البيئة إلى ملف App.yaml

أولاً، يجب إضافة جميع متغيرات البيئة التي كنت تستخدمها للاختبار محليًا إلى أسفل ملف app.yaml الخاص بتطبيقك.

  1. انتقِل إلى https://console.cloud.google.com/sql/instances/locations/overview للبحث عن اسم اتصال المثيل.
  2. ألصِق الرمز التالي في نهاية app.yaml.
  3. استبدِل YOUR_DB_PASSWORD_HERE بكلمة المرور التي أنشأتها لاسم المستخدم postgres في وقت سابق.
  4. استبدِل YOUR_CONNECTION_NAME_HERE بالقيمة من الخطوة 1.

app.yaml

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

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

يُرجى العِلم أنّ DB_TCP_HOST يجب أن تكون قيمته 172.17.0.1 لأنّ هذا التطبيق يتصل عبر AppEngine Flex**.** ويرجع ذلك إلى أنّه سيتواصل مع Cloud SQL من خلال خادم وكيل، على غرار الطريقة التي كنت تستخدمها.

إضافة أذونات SQL Client إلى حساب خدمة AppEngine Flex

انتقِل إلى صفحة "إدارة الهوية وإمكانية الوصول" في Cloud Console وابحث عن حساب خدمة يتطابق اسمه مع التنسيق service-PROJECT_NUMBER@gae-api-prod.google.com.iam.gserviceaccount.com. هذا هو حساب الخدمة الذي سيستخدمه App Engine Flexible للاتصال بقاعدة البيانات. انقر على الزر تعديل في نهاية الصف وأضِف الدور عميل Cloud SQL.

b04ccc0b4022b905.png

نسخ رمز مشروعك إلى مسار 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 واجهة سطر الأوامر لنشر تطبيقك، وسيستغرق ذلك بعض الوقت.

gcloud app deploy

استخدِم الأمر browse للحصول على رابط يمكنك النقر عليه لمشاهدة أداة تحديد المواقع الجغرافية للمتاجر التي تم نشرها بالكامل والمناسبة للمؤسسات والمذهلة من الناحية الجمالية أثناء عملها.

gcloud app browse

إذا كنت تنفّذ gcloud خارج نافذة Cloud Shell، سيؤدي تنفيذ gcloud app browse إلى فتح علامة تبويب جديدة في المتصفّح.

12. (يُنصح به) التنظيف

سيظلّ تنفيذ هذا الدرس العملي ضمن حدود الطبقة المجانية لمعالجة BigQuery وطلبات البيانات من Maps Platform API، ولكن إذا نفّذت هذا الدرس العملي فقط كتدريب تعليمي وأردت تجنُّب تكبُّد أي رسوم مستقبلية، فإنّ أسهل طريقة لحذف الموارد المرتبطة بهذا المشروع هي حذف المشروع نفسه.

حذف المشروع

في "وحدة تحكّم Google Cloud Platform"، انتقِل إلى صفحة Cloud Resource Manager:

في قائمة المشاريع، اختَر المشروع الذي كنا نعمل فيه وانقر على حذف. سيُطلب منك كتابة رقم تعريف المشروع. أدخِلها وانقر على إيقاف.

بدلاً من ذلك، يمكنك حذف المشروع بأكمله مباشرةً من Cloud Shell باستخدام gcloud من خلال تنفيذ الأمر التالي واستبدال العنصر النائب GOOGLE_CLOUD_PROJECT برقم تعريف مشروعك:

gcloud projects delete GOOGLE_CLOUD_PROJECT

13. تهانينا

تهانينا! لقد أكملت الدرس التطبيقي حول الترميز بنجاح.

أو أنّك انتقلت بسرعة إلى الصفحة الأخيرة. تهانينا! لقد انتقلت بسرعة إلى الصفحة الأخيرة.

خلال هذا الدرس التطبيقي حول الترميز، عملت على التقنيات التالية:

محتوى إضافي للقراءة

ولا يزال هناك الكثير لنتعلّمه عن كل هذه التقنيات. في ما يلي بعض الروابط المفيدة حول المواضيع التي لم يتسنَّ لنا تناولها في هذا الدرس العملي، ولكنّها قد تكون مفيدة لك بالتأكيد في إنشاء حلّ للعثور على المتاجر يناسب احتياجاتك المحدّدة.