إنشاء أداة تخطيط مسارات باستخدام Place Autocomplete وRoutes API

1. نظرة عامة

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

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

يرشد هذا الدرس العملي المبرمجين خلال عملية إنشاء تطبيق ويب باستخدام Maps JavaScript API والإكمال التلقائي للأماكن وRoutes API. ستتعرّف على كيفية دمج واجهات Google Maps Platform API المتعددة من خلال برنامج تعليمي قابل للتخصيص.

ما ستنشئه

سيرشدك هذا الدرس التطبيقي حول الترميز إلى كيفية إنشاء تطبيق ويب باستخدام HTML وCSS وJavaScript وخادم Node.js.

بنية تطبيق الويب "مخطِّط المسار"

تطبيق ويب لمخطِّط المسار

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

  • كيفية تفعيل واجهات برمجة التطبيقات في Google Maps Platform
  • كيفية دمج خريطة ديناميكية في تطبيق ويب
  • كيفية دمج خدمة Places Autocomplete
  • كيفية طلب مسار من خلال Routes API
  • كيفية عرض المسار على خريطة ديناميكية
  • كيفية إنشاء معرّف خريطة
  • كيفية إضافة "علامات متقدّمة" إلى خريطة ديناميكية

المتطلبات

نموذج التعليمات البرمجية

يتوفّر الحل الكامل والرمز المفصّل خطوة بخطوة على GitHub. لا يتضمّن الرمز حِزم Node المطلوبة. ثبِّت التبعيات اللازمة قبل تنفيذ الرمز البرمجي. يمكن العثور على تفاصيل الحِزم المطلوبة في ملف package.json(موضّحة في الخطوة 3).

2. إعداد المشروع وتفعيل واجهات برمجة التطبيقات

في خطوة التفعيل، عليك تفعيل JavaScript API وPlace Autocomplete وRoutes API في "خرائط Google".

إعداد Google Maps Platform

إذا لم يكن لديك حساب على Google Cloud Platform ومشروع مفعَّل فيه نظام الفوترة، يُرجى الاطّلاع على دليل البدء باستخدام Google Maps Platform لإنشاء حساب فوترة ومشروع.

  1. في Cloud Console، انقر على القائمة المنسدلة الخاصة بالمشروع واختَر المشروع الذي تريد استخدامه في هذا الدرس العملي. اختيار مشروع
  2. فعِّل واجهات Google Maps Platform API المطلوبة لهذا الدرس العملي في صفحة "مكتبة Maps API". لإجراء ذلك، اتّبِع الخطوات الواردة في هذا الفيديو أو هذه المستندات.
  3. أنشئ مفتاح واجهة برمجة التطبيقات في صفحة بيانات الاعتماد في Cloud Console. يمكنك اتّباع الخطوات الواردة في هذا الفيديو أو هذه المستندات. تتطلّب جميع الطلبات إلى "منصة خرائط Google" مفتاح واجهة برمجة تطبيقات.

3- إعداد مشروع Node.js

في هذا التمرين العملي، سنستخدم Node.js لجمع نقطتَي الأصل والوجهة من الويب وطلب المسار من خلال Routes API.

بافتراض أنّك ثبّت Node.js، أنشئ دليلًا ستستخدمه لتنفيذ هذا المشروع:

$ mkdir ac_routes
$ cd ac_routes

ابدأ حزمة Node.js جديدة في دليل تطبيقك:

$ npm init

سيطلب منك هذا الأمر إدخال عدد من المعلومات، مثل اسم تطبيقك وإصداره. في الوقت الحالي، يمكنك ببساطة الضغط على RETURN لقبول الإعدادات التلقائية لمعظمها. نقطة الدخول التلقائية هي index.js، ويمكنك تغييرها إلى ملفك الرئيسي. في هذا الدرس العملي، الملف الرئيسي هو function/server.js(مزيد من التفاصيل في الخطوة 6).

بالإضافة إلى ذلك، يمكنك تثبيت إطار العمل والوحدات المفضّلة لديك. يستخدم هذا المختبر إطار عمل الويب(Express) ومحلّل النص الأساسي(body-parser). يمكنك الاطّلاع على مزيد من التفاصيل في ملف package.json.

4. إنشاء خريطة ديناميكية

بعد إعداد الخلفية المستندة إلى Node.js، لنستكشف الخطوات اللازمة من جهة العميل.

  • إنشاء صفحة HTML للتطبيق
  • إنشاء ملف CSS لتحديد النمط
  • تحميل Google Maps JavaScript API في صفحة HTML
  • لصق مفتاح واجهة برمجة التطبيقات في علامة النص البرمجي لمصادقة تطبيقك
  • إنشاء ملف JavaScript للتعامل مع وظائف التطبيق

إنشاء صفحة HTML

  1. أنشئ دليلاً جديدًا في مجلد مشروعك(ac_routes في هذه الحالة)
     $ mkdir public
     $ cd public
    
  2. في الدليل العام، أنشئ ملف index.html
  3. انسخ الرمز التالي إلى ملف index.html
     <!DOCTYPE html>
     <html>
     <head>
       <title>GMP Autocomplete + Routes</title>
       <meta charset="utf-8">
       <link rel="stylesheet" type="text/css" href="style.css">
     </head>
     <body>
       <div class="container">
         <!-- Start of the container for map -->
         <div class="main">
           <div id="map"></div>
         </div>
         <!-- End of the container for map -->
       </div>
       </body>
     </html>
    

إنشاء ملف CSS

  1. إنشاء ملف style.css في الدليل العام
  2. انسخ الرمز التالي والصقه في ملف style.css:
     html, body {height: 100%;}
     body {
       background: #fff;
       font-family: "Helvetica Neue", Helvetica, Roboto, Arial, sans-serif;
       font-style: normal;
       font-weight: normal;
       font-size:16px;
       line-height: 1.5;
       margin: 0;
       padding: 0;
     }
     .container {display:flex; width:90%; padding:100px 0; margin:0 auto;}
     .main {width:70%; height:800px;}
      #map {height:100%; border-radius:20px;}
    

تحميل Maps JavaScript API

في هذا التمرين المعملي، سنستخدم استيراد المكتبة الديناميكية لتحميل Maps JavaScript API. يمكنك الاطّلاع على المزيد من التفاصيل في هذه الصفحة.

في ملف index.html، انسخ الرمز التالي قبل علامة نص الإغلاق. استبدِل "YOUR_API_KEY" بمفتاح واجهة برمجة التطبيقات الخاص بك.

<script>(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({key: "YOUR_API_KEY", v: "weekly"});</script>

إنشاء ملف JavaScript

  1. في الدليل العام، أنشئ ملف app.js
  2. انسخ الرمز التالي إلى ملف app.js
     (function(){
       let map;
    
       async function initMap() {
           const { Map } = await google.maps.importLibrary('maps');
           map = new Map(document.getElementById('map'), {
               center: { lat: -34.397, lng: 150.644 },
               zoom: 8,
               mapId: 'DEMO_MAP_ID'
           });
       }
    
       initMap();
     }());
    

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

في ملف index.html، اربط ملف app.js قبل علامة نص الإغلاق وبعد علامة النص البرمجي لتحميل Maps JavaScript API.

<script type="text/JavaScript" src="app.js"></script>

نموذج الرمز الكامل

يتوفّر الرمز الكامل حتى هذه النقطة على GitHub:step1_createDynamicMap

5- إدخال عناوين المصدر والوجهة

  • إضافة حقلَي نص في ملف index.html لإدخال المصدر والوجهة
  • استيراد مكتبة الإكمال التلقائي
  • ربط خدمة الإكمال التلقائي بحقلَي النص الخاصَين بالمصدر والوجهة

إضافة حقول نصية

في ملف index.html، أضِف الرمز التالي كعنصر ثانوي أول للرمز div مع الفئة container.

<div class="aside">
  <div class="inputgroup">
    <label for="origin">Start</label>
    <input type="text" id="origin" name="origin" class="input-location" placeholder="Enter an address">
  </div>
  <div class="inputgroup">
    <label for="origin">End</label>
    <input type="text" id="destination" name="destination" class="input-location" placeholder="Enter an address">
  </div>
</div>

استيراد وتفعيل خدمة "الإكمال التلقائي"

الفئة google.maps.places.Autocomplete هي أداة توفّر توقّعات حول الأماكن استنادًا إلى إدخال نصي من المستخدم. يتم إرفاقه بعنصر إدخال من نوع نص، ويستمع إلى إدخال النص في هذا الحقل. يتم عرض قائمة التوقعات كقائمة منسدلة، ويتم تعديلها عند إدخال النص.

في ملف app.js، أضِف الرمز التالي بعد تهيئة الخريطة:

let placeIds = [];
async function initPlace() {
  const { Autocomplete } = await google.maps.importLibrary('places');
  let autocomplete = [];
  let locationFields = Array.from(document.getElementsByClassName('input-location'));
  //Enable autocomplete for input fields
  locationFields.forEach((elem,i) => {
      autocomplete[i] = new Autocomplete(elem);
      google.maps.event.addListener(autocomplete[i],"place_changed", () => {
          let place = autocomplete[i].getPlace();
          if(Object.keys(place).length > 0){
              if (place.place_id){
                  placeIds[i] = place.place_id; //We use Place Id in this example
              } else {
                  placeIds.splice(i,1); //If no place is selected or no place is found, remove the previous value from the placeIds.
                  window.alert(`No details available for input: ${place.name}`);
                  return;
              }
          }
      });
  });
}
initPlace();

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

إضافة أنماط ذات صلة

في ملف style.css، أضِف الرمز التالي:

.aside {width:30%; padding:20px;}
.inputgroup {margin-bottom:30px;}
.aside label {display:block; padding:0 10px; margin-bottom:10px; font-size:18px; color:#666565;}
.aside input[type=text] {width:90%;padding:10px; font-size:16px; border:1px solid #e6e8e6; border-radius:10px;}

نموذج الرمز الكامل

يتوفّر الرمز الكامل حتى هذه النقطة على GitHub:step2_inputAddress

6. طلب المسار

  • أضِف زر "الحصول على مسار" إلى ملف index.html لبدء طلب المسار
  • يؤدي هذا الزر إلى إرسال بيانات المصدر والوجهة إلى خدمة Node.js
  • ترسل خدمة Node.js طلبًا إلى Routes API
  • يتم عرض الردّ من واجهة برمجة التطبيقات على جهة العميل

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

في ملف index.html، أضِف زر "الحصول على مسار" (Get a route) قبل علامة الإغلاق div مع الفئة aside :

<div class="inputgroup">
  <button id="btn-getroute">Get a route</button>
</div>

في ملف style.css، أضِف السطر التالي:

.aside button {padding:20px 30px; font-size:16px; border:none; border-radius:50px; background-color:#1a73e8; color:#fff;}

في ملف app.js، أضِف الرمز البرمجي أدناه لإرسال بيانات المصدر والوجهة إلى خدمة Node.js:

function requestRoute(){
  let btn = document.getElementById('btn-getroute');
  btn.addEventListener('click', () => {
    //In this example, we will extract the Place IDs from the Autocomplete response
    //and use the Place ID for origin and destination
    if(placeIds.length == 2){
        let reqBody = {
            "origin": {
                "placeId": placeIds[0]
            },
            "destination": {
                "placeId": placeIds[1]
            }
        }

        fetch("/request-route", {
            method: 'POST',
            body: JSON.stringify(reqBody),
            headers: {
                "Content-Type": "application/json"
            }
        }).then((response) => {
            return response.json();
        }).then((data) => {
            //Draw the route on the map
            //Details will be covered in next step
            renderRoutes(data);
        }).catch((error) => {
            console.log(error);
        });
    } else {
        window.alert('Location must be set');
        return;
    }
  });
}

requestRoute();

renderRoutes() هي الدالة التي سنستخدمها لرسم المسار على الخريطة. سنتناول التفاصيل في الخطوة التالية.

إنشاء الخادم

في دليل المشروع(ac_routes في هذه الحالة)، أنشئ مجلدًا جديدًا باسم function. داخل هذا المجلد، أنشئ ملفًا باسم server.js. يعمل الملف كنقطة دخول لمشروعك يتم إعدادها عند إنشاء مشروع Node.js، ويتولّى ثلاث وظائف رئيسية:

  1. جمع البيانات من برنامج الويب
  2. إرسال طلبات إلى Routes API
  3. إرجاع استجابة واجهة برمجة التطبيقات إلى جانب العميل

انسخ الرمز التالي والصِقه في ملف server.js. استبدِل YOUR_API_KEY بمفتاح واجهة برمجة التطبيقات الخاص بك. لتعزيز أمان مفتاح واجهة برمجة التطبيقات، ننصحك بشدة باستخدام مفتاح مميّز للخادم الخلفي. اطّلِع على إرشادات الأمان.

const express = require('express');
const app = express();
const bodyParser = require('body-parser');

const port  = 8080;
const urlencodedParser = bodyParser.urlencoded({extended:true}); 

function main() {
  app.use('/', express.static('public'));
  app.use(urlencodedParser);
  app.use(express.json());

  app.post('/request-route', (req,res) => {    
    fetch("https://routes.googleapis.com/directions/v2:computeRoutes", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Goog-Api-Key": "YOUR_API_KEY",
        "X-Goog-FieldMask": "*"
      },
      body: JSON.stringify(req.body)
    }).then((response) => {
      return response.json();
    }).then((data) => {
      if('error' in data){
        console.log(data.error);
      } else if(!data.hasOwnProperty("routes")){
        console.log("No route round");
      } else {
        res.end(JSON.stringify(data));
      }
    }).catch((error) => {
      console.log(error)
    });
  });

  app.listen(port, () => {
      console.log('App listening on port ${port}: ' + port);
      console.log('Press Ctrl+C to quit.');
  });
}

main();

يمكنك الاطّلاع على مزيد من التفاصيل حول Routes API في مقالة الحصول على مسار باستخدام Routes API.

تشغيل الرمز

نفِّذ الرمز البرمجي أدناه في سطر الأوامر:

$ node function/server.js

افتح المتصفّح وانتقِل إلى http://127.0.0.1:8080/index.html. من المفترض أن تظهر لك صفحة التطبيق. حتى هذه المرحلة، يتم إرجاع الردّ من واجهة برمجة التطبيقات إلى عميل الويب. لنطّلِع على كيفية عرض المسار على الخريطة في الخطوة التالية.

نموذج الرمز الكامل

يتوفّر الرمز الكامل حتى هذه النقطة على GitHub:step3_requestRoute

7. عرض المسار على الخريطة

في الخطوة السابقة، نشير إلى renderRoutes() عندما نتلقّى الردّ بنجاح من خدمة Node.js. لنضِف الآن الرمز الفعلي لعرض المسار على الخريطة.

في ملف app.js، أضِف الرمز أدناه:

let paths = [];
async function renderRoutes(data) {
  const { encoding } = await google.maps.importLibrary("geometry");
  let routes = data.routes;
  let decodedPaths = [];

  ///Display routes and markers
  routes.forEach((route,i) => {
      if(route.hasOwnProperty('polyline')){
        //Decode the encoded polyline
        decodedPaths.push(encoding.decodePath(route.polyline.encodedPolyline));

        //Draw polyline on the map
        for(let i = decodedPaths.length - 1; i >= 0; i--){
            let polyline = new google.maps.Polyline({
                map: map,
                path: decodedPaths[i],
                strokeColor: "#4285f4",
                strokeOpacity: 1,
                strokeWeight: 5
            });
            paths.push(polyline);
        }
        
        //Add markers for origin/destination
        addMarker(route.legs[0].startLocation.latLng,"A");
        addMarker(route.legs[0].endLocation.latLng,"B");
        //Set the viewport
        setViewport(route.viewport);
      } else {
        console.log("Route cannot be found");
      }
  });
}

تعرض Routes API خطًا متعدد الأضلاع إما بتنسيق encodedPolyline(الإعداد التلقائي) أو geoJsonLinestring. في هذا التمرين العملي، سنستخدم تنسيق encodedPolyline وفكّ ترميزه باستخدام مكتبة الهندسة في Maps JavaScript.

سنستخدم addMarker() لإضافة علامات متقدّمة للمصدر والوجهة. في ملف app.js، أضِف الرمز التالي:

let markers = [];
async function addMarker(pos,label){
  const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
  const { PinElement } = await google.maps.importLibrary("marker");
  const { LatLng } = await google.maps.importLibrary("core");
  let pinGlyph = new PinElement({
      glyphColor: "#fff",
      glyph: label
  });
  let marker = new AdvancedMarkerElement({
      position: new LatLng({lat:pos.latitude,lng:pos.longitude}),
      gmpDraggable: false,
      content: pinGlyph.element,
      map: map
  });
  markers.push(marker);
}

هنا، ننشئ علامتَين متقدّمتَين، الأولى A للمصدر والثانية B للوجهة. مزيد من التفاصيل حول العلامات المتقدّمة

بعد ذلك، سنوسّط إطار عرض الخريطة على المسار الذي تم استرجاعه، وذلك باستخدام معلومات إطار العرض الملائمة التي توفّرها Routes API. في ملف app.js، أضِف الرمز التالي:

async function setViewport(viewPort) {
  const { LatLng } = await google.maps.importLibrary("core");
  const { LatLngBounds } = await google.maps.importLibrary("core");
  let sw = new LatLng({lat:viewPort.low.latitude,lng:viewPort.low.longitude});
  let ne = new LatLng({lat:viewPort.high.latitude,lng:viewPort.high.longitude});
  map.fitBounds(new LatLngBounds(sw,ne));
}

رمز العيّنة الكامل يتوفّر الرمز الكامل حتى هذه النقطة على GitHub:step4_displayRoute

8. إزالة عناصر من الخريطة

نريد هنا أن نخطو خطوة أخرى إلى الأمام. لنمسح الخريطة قبل رسم علامات ومسارات جديدة لتجنُّب الازدحام.

في ملف app.js، لنضِف دالة أخرى:

function clearUIElem(obj,type) {
  if(obj.length > 0){
      if(type == 'advMarker'){
          obj.forEach(function(item){
              item.map = null;
          });
      } else {
          obj.forEach(function(item){
              item.setMap(null);
          });
      }
  }
}

أضِف السطر التالي في بداية renderRoutes():

clearUIElem(paths,'polyline');

أضِف السطر التالي في بداية addMarker():

clearUIElem(markers,'advMarker');

نموذج الرمز الكامل

يتوفّر الرمز الكامل حتى هذه النقطة على GitHub:step5_removeElements

9- تهانينا

لقد نجحت في بناء الشيء.

ما تعلّمته

  • تفعيل واجهات برمجة التطبيقات في "منصة خرائط Google"
  • تحميل Google Maps JavaScript API في صفحة HTML
  • استيراد مكتبة Places في Maps JavaScript API
  • ربط خدمة "الإكمال التلقائي للأماكن" بالحقول النصية
  • طلب مسار من خلال Routes API
  • عرض المسار على خريطة ديناميكية
  • إنشاء رقم تعريف خريطة
  • إنشاء علامات متقدّمة

مزيد من المعلومات

ما هي جلسات الترميز الأخرى التي تريد المشاركة فيها؟

العرض المرئي للبيانات على الخرائط مزيد من المعلومات عن تخصيص نمط خرائطي إنشاء تفاعلات ثلاثية الأبعاد في الخرائط

هل لم يتم إدراج الدرس العملي الذي تريده أعلاه؟ يمكنك طلب ذلك من خلال تقديم مشكلة جديدة هنا.