دليلك الشامل: بناء تطبيق خرائط رحلات صيفية تفاعلي بـ Gatsby و Leaflet

دقائق القراءة: 13
استعد لمغامرات الصيف القادمة بإنشاء تطبيق خرائط رحلاتك البرية الخاص بك، من خلال هذا الدليل الشامل الذي يقدم لك خطوات عملية ومفصلة. سنأخذك في رحلة بناء تطبيق خرائط تفاعلي يعرض مسار رحلتك ومحطاتها، مع إمكانية إضافة صور وتفاصيل لكل موقع. سواء كنت مطوراً متمرساً أو بدأت للتو في عالم تطوير الويب، سيمنحك هذا الدليل الأدوات والمعرفة اللازمة لإنشاء مشروعك الخاص والمميز.

ماذا سنبني؟

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

تطبيق خرائط رحلات صيفية يوضح المسار والمحطات

للبدء، سنستعين بمشروع Leaflet Gatsby Starter الذي يوفر إعداداً أولياً سلساً. بعد تهيئة التطبيق، سنقوم بإنشاء قائمة بمواقعنا واستخدام واجهة برمجة تطبيقات Leaflet API لرسم مسار رحلتنا على الخريطة.

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

ماذا نحتاج قبل البدء؟

إذا كنت قد تابعت دليلي السابق حول بناء تطبيق Santa Tracker، يمكنك اتباع نفس الخطوات للبدء. أما إذا لم تكن كذلك، فسنحتاج إلى التأكد من توفر الإعدادات التالية:

  • Node.js أو Yarn: سأستخدم Yarn في هذا الدليل، ولكن يمكنك استبداله بـ npm عند الاقتضاء.
  • واجهة سطر الأوامر (CLI) الخاصة بـ Gatsby: يمكنك تثبيتها باستخدام الأمر yarn global add gatsby-cli.

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

سنحتاج أيضاً إلى إعداد أساس لتطبيق الخرائط الخاص بنا. يمكننا تحقيق ذلك بالاستفادة من مشروع Leaflet Gatsby Starter الذي يوفر إعداداً أولياً مع مكتبتي Leaflet و React Leaflet.

gatsby new my-road-trip https://github.com/colbyfayock/gatsby-starter-leaflet

إنشاء تطبيق Gatsby جديد باستخدام Leaflet Starter عبر سطر الأوامر

بعد اكتمال عملية الإنشاء، يمكنك الانتقال إلى مجلد المشروع الجديد وتشغيل خادم التطوير المحلي الخاص بك:

cd my-road-trip
yarn develop

تشغيل خادم تطوير Gatsby في الطرفية

إذا سارت الأمور كما هو مخطط لها، سيبدأ خادمك وستتمكن الآن من رؤية تطبيق الخرائط الأساسي الخاص بك في متصفح الويب!

تطبيق Leaflet Gatsby الجديد يعمل في المتصفح

الخطوة 1: تنظيف الكود غير الضروري

يأتي مشروع Gatsby Starter الذي نستخدمه لتشغيل هذا التطبيق مع بعض الأكواد التجريبية التي لا نحتاجها. سنقوم بإجراء جميع التغييرات التالية في الملف src/pages/index.js، وهو الصفحة الرئيسية لتطبيقنا.

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

// In src/pages/index.js
async function mapEffect ( { leafletElement } = {} ) {
  // Get rid of everything in here
}

بعد ذلك، لا نحتاج إلى علامة (marker) هذه المرة، لذا لنقم بإزالة المكون <Marker> من مكون <Map> الخاص بنا. يجب أن يبدو مكون <Map> بعد الإزالة كالتالي:

<Map {…mapSettings} />

الآن بعد أن أزلنا تلك الأجزاء، يمكننا إزالة جميع الاستيرادات (imports) والمتغيرات التالية من أعلى ملفنا: useRef، Marker، promiseToFlyTo، getCurrentLocation، gatsby_astronaut، timeToZoom، timeToOpenPopupAfterZoom، timeToUpdatePopupAfterZoom، ZOOM، popupContentHello، popupContentGatsby، markerRef.

بعد ذلك، يجب أن تستمر خريطتنا في العمل، ولكن دون أن تقوم بأي وظيفة محددة.

تطبيق خرائط جديد فارغ لا يعرض أي عناصر

الخطوة 2: إنشاء مواقع رحلتنا البرية

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

أولاً، أنشئ ملفاً جديداً في المجلد src/data باسم locations.js. داخل هذا الملف، سنقوم بإنشاء وتصدير مصفوفة (array) جديدة.

export const locations = [
  {
    placename: ‘Herndon, VA’,
    date: ‘August 1, 2015’,
    location: {
      lat: 38.958988,
      lng: -77.417320
    },
    todo: [
      ‘Where we start! ?’
    ]
  },
  {
    placename: ‘Middlesboro, KY ',
    date: ‘August 1, 2015’,
    location: {
      lat: 36.627517,
      lng: -83.621635
    },
    todo: [
      ‘Cumberland Gap ?’
    ]
  }
];

يمكنك استخدام الكود أعلاه للبدء، ولكن ستحتاج في النهاية إلى تغيير التفاصيل لتتناسب مع اختياراتك. إذا أردت إضافة صورة إلى موقعك، يمكنك فعل ذلك بتضمين خاصية image للكائن (object). يمكنك استخدام رابط URL للصورة أو استيراد ملف محلي إذا كان متاحاً لديك، كما هو موضح في هذا المثال:

import imgHerndonStart from 'assets/images/herndon-start.jpg’;

export const locations = [
  {
    placename: ‘Herndon, VA’,
    date: ‘August 1, 2015’,
    image: imgHerndonStart,
    location: {
      lat: 38.958988,
      lng: -77.417320
    },
    todo: [
      ‘Where we start! ?’
    ]
  }
]

بمجرد إنشاء هذا الملف، يمكننا الآن استيراد مواقعنا إلى ملف src/pages/index.js لاستخدامها في الخطوة التالية:

import { locations } from 'data/locations’;

إذا أضفت تعليمة console.log(locations) داخل صفحتك، يجب أن ترى الآن جميع بيانات مواقعك في مصفوفة (array)!

الخطوة 3: تجهيز تطبيقنا ببعض الدوال المساعدة

للحفاظ على البساطة والتركيز، قمت بتجميع ثلاثة مكونات أساسية لإنشاء خريطتنا في دوال (functions) منفصلة. على الرغم من إمكانية نسخها ولصقها مباشرة، إلا أننا سنتناول شرح ما يحدث في كل دالة. يمكنك وضع كل من هذه الدوال في الجزء السفلي من ملف src/pages/index.js لتكون جاهزة للاستخدام في الخطوة التالية.

الدالة createTripPointsGeoJson

ستأخذ دالتنا الأولى مصفوفة (array) مواقعنا وتعيد مستند GeoJSON، حيث يتم تحويل كل موقع إلى Feature فردي. سنستخدم هذه الدالة لإنشاء النقاط الفردية على خريطتنا.

ما هو مستند GeoJSON؟ إنه في الأساس كائن JavaScript أو مستند JSON ذو بنية محددة تضمن الاتساق مع البيانات الجغرافية.

function createTripPointsGeoJson ( { locations } = {} ) {
  return {
    “type”: “FeatureCollection”,
    “features”: locations.map( ( { placename, location = {}, image, date, todo = [] } = {} ) => {
      const { lat, lng } = location;
      return {
        “type”: “Feature”,
        “properties”: {
          placename,
          todo,
          date,
          image
        },
        “geometry”: {
          “type”: “Point”,
          “coordinates”: [
            lng,
            lat
          ]
        }
      }
    })
  }
}

ماذا يحدث في الكود أعلاه؟

  • نحن نأخذ وسيطاً (argument) هو locations، والذي سيكون مصفوفة (array) وجهاتنا.
  • نحن نعيد كائناً (object) ببعض الخصائص الديناميكية المرتبطة به.
  • داخل الكائن، نقوم بتحويل مواقعنا إلى كائنات Feature فردية.
  • يحتوي كل كائن على شكل Point باستخدام إحداثياتنا.
  • يتضمن أيضاً خصائصنا التي تخزن بياناتنا الوصفية (metadata).

عند استدعاء هذه الدالة، سيكون لدينا كائن JavaScript جديد يتضمن مصفوفة من النقاط (Points) التي تمثل المواقع التي سنتوقف عندها في رحلتنا البرية.

الدالة createTripLinesGeoJson

سنقوم بإنشاء دالة أخرى مشابهة للدالة السابقة. ولكن هذه المرة، بدلاً من النقاط، نريد إنشاء خطوط تمثل الانتقال من نقطة إلى أخرى.

function createTripLinesGeoJson ( { locations } = {} ) {
  return {
    “type”: “FeatureCollection”,
    “features”: locations.map( ( stop = {}, index ) => {
      const prevStop = locations[index - 1 ];

      if ( !prevStop ) return [];

      const { placename, location = {}, date, todo = [] } = stop;
      const { lat, lng } = location;
      const properties = {
        placename,
        todo,
        date
      };

      const { location : prevLocation = {} } = prevStop;
      const { lat : prevLat, lng : prevLng } = prevLocation;

      return {
        type : ‘Feature’,
        properties,
        geometry : {
          type : ‘LineString’,
          coordinates : [
            [ prevLng, prevLat ],
            [ lng, lat ]
          ]
        }
      }
    })
  }
}

ستلاحظ على الفور أن هذه الدالة مشابهة جداً لدالتنا الأخيرة. نحن نعيد كائناً (object) ونحدد خصائص البيانات الوصفية (metadata) الخاصة بنا على قائمة من Features. الفرق الكبير، مع ذلك، هو أننا ننشئ خطاً (Line). للقيام بذلك، نبحث ونشير إلى prevStop، والذي سيكون المحطة السابقة. سنستخدم كلاً من المحطة السابقة والمحطة الحالية للحصول على نقطتين يمكننا استخدامهما لرسم الخط. إذا لم يكن لدينا محطة سابقة، فإننا نعيد مصفوفة فارغة، مما يعني أننا في بداية رحلتنا دون خط يسبقها. باستخدام المحطة السابقة والمحطة الحالية، ننشئ Feature من نوع LineString بنقطتينا.

الدالة tripStopPointToLayer

ستمكننا دالتنا الأخيرة من إنشاء محتوى مخصص لكل نقطة سنضيفها إلى خريطتنا. سنستخدم هذه الدالة ضمن خاصية (property) في Leaflet، لذا سنقوم بتكييف وسائطنا (arguments) لتتوافق مع هذه المواصفات.

function tripStopPointToLayer ( feature = {}, latlng ) {
  const { properties = {} } = feature;
  const { placename, todo = [], image, date } = properties;

  const list = todo.map( what => `<li> ${ what } </li>` );

  let listString = ‘’;
  let imageString = ‘’;

  if ( Array.isArray(list) && list.length > 0 ) {
    listString = list.join(‘’);
    listString = `
      <p>Things we will or have done…</p>
      <ul>
        ${listString}
      </ul>
    `
  }

  if ( image ) {
    imageString = `
      <span class=“trip-stop-image” style=“background-image: url( ${image} )”>
        ${placename}
      </span>
    `
  }

  const text = `
    <div class=“trip-stop”>
      ${ imageString }
      <div class=“trip-stop-content”>
        <h2> ${placename} </h2>
        <p class=“trip-stop-date”> ${date} </p>
        ${ listString }
      </div>
    </div>
  `

  const popup = L.popup({
    maxWidth : 400
  }).setContent(text);

  const layer = L.marker(
    latlng,
    {
      icon : L.divIcon({
        className : ‘icon’,
        html : `<span class=“icon-trip-stop”></span>`,
        iconSize : 20
      }),
      riseOnHover : true
    }
  ).bindPopup(popup);

  return layer;
}

أحد الأمور التي ستلاحظها أثناء عملنا على هذه الدالة هو أننا ننشئ سلاسل نصية (strings) من HTML. نظراً لأن واجهة برمجة تطبيقات Leaflet API التي نستخدمها لا تتفاعل مباشرة مع React، يجب علينا بناء HTML يدوياً لتمريره إلى دوالنا. بدءاً من الأعلى:

  • نحن نستقبل وسيطين (arguments): feature و latlng. تقوم مكتبة Leaflet بتمرير هاتين القيمتين لنا لاستخدامهما في دالتنا.
  • نقوم بفك هيكلة (destructure) الكائن feature، مما يسمح لنا بتعيين بياناتنا الوصفية (metadata) إلى متغيرات.
  • يتم تهيئة متغيرين نصيين (string variables) سنستخدمهما لبناء HTML الخاص بنا.
  • إذا قمنا بتضمين خاصية todo كمصفوفة (array)، فإننا نضيف قائمة جديدة تتضمن كل عنصر.
  • إذا قمنا بتضمين صورة، فإننا ننشئ وسم صورة (image tag).
  • باستخدام سلاسل HTML التي أنشأناها حديثاً، نقوم ببناء بطاقة النافذة المنبثقة (popup card) بالكامل لكل محطة.
  • باستخدام HTML الخاص بالنافذة المنبثقة، ننشئ نسخة (instance) من popup في Leaflet.
  • باستخدام الوسيط latlng ونافذتنا المنبثقة، ننشئ نسخة جديدة من marker في Leaflet. سيمثل هذا النقطة على الخريطة.
  • داخل عملية إنشاء الـ Marker، ننشئ وسم HTML أساسي سنستخدمه لتصميم الـ marker.
  • بعد ذلك، نقوم بربط نافذتنا المنبثقة بنسخة الـ Marker الجديدة هذه. سيسمح هذا بربط النافذة المنبثقة بذلك الـ Marker الفردي.
  • أخيراً، نعيد الطبقة (layer) التي أنشأناها حديثاً.

تذكر أن تتأكد من وضع جميع الدوال المذكورة أعلاه في الجزء السفلي من ملف src/pages/index.js. بمجرد إضافة جميع هذه الدوال، يجب أن تظل خريطتنا كما هي، أي لا يحدث شيء بشكل أساسي.

الخطوة 4: بناء مسار رحلتنا

هنا تبدأ الأمور بالإثارة. سنقوم الآن بالاستفادة من الدوال التي أنشأناها لبناء مسار رحلتنا البرية. سيتم تنفيذ كل عملنا هنا داخل الدالة mapEffect الموجودة في ملف src/pages/index.js.

للتوضيح، تتضمن دالتنا mapEffect وسيطاً (argument) يسمى leafletElement. تشير هذه القيمة إلى نسخة الخريطة (Map instance) التي تتعرف عليها Leaflet. تتضمن نسخة الخريطة هذه حالة خريطتنا والعديد من الدوال المساعدة للتعامل مع خريطتنا.

أولاً، في الجزء العلوي من الدالة، نريد التأكد من وجود خريطة. إذا لم يكن الأمر كذلك، يمكننا استخدام return للخروج من الدالة.

if ( !leafletElement ) return ;

بعد ذلك، نريد استخدام الدالة المساعدة eachLayer وإزالة كل layer من عنصر الخريطة الخاص بنا. نقوم بذلك للتأكد من أن لدينا دائماً حالة طبقة الخريطة الصحيحة.

leafletElement.eachLayer( ( layer ) => leafletElement.removeLayer(layer));

بعد تنظيف خريطتنا، يمكننا الاستفادة من اثنتين من الدوال التي أنشأناها لإنشاء كائنات GeoJSON جديدة.

const tripPoints = createTripPointsGeoJson({ locations });
const tripLines = createTripLinesGeoJson({ locations });

باستخدام كائنات GeoJSON الخاصة بنا، نحتاج إلى تحويلها إلى نسخ Leaflet GeoJSON، والتي سنستخدمها لإضافتها إلى الخريطة.

const tripPointsGeoJsonLayers = new L.geoJson(tripPoints, {
  pointToLayer : tripStopPointToLayer
});
const tripLinesGeoJsonLayers = new L.geoJson(tripLines);

إذا لاحظت في الكود أعلاه، فإننا نستخدم دالتنا tripStopPointToLayer. كما أشرت سابقاً، تتضمن نسخة geoJson التي ننشئها خاصية (property) تسمح لنا بتمرير دالة، مما يمنحنا القدرة على التلاعب بإنشاء الطبقة. هذه هي الطريقة التي ننشئ بها نقطتنا ومحتوى النافذة المنبثقة (popup).

يمكننا المضي قدماً في إضافة كلتا الطبقتين الجديدتين إلى خريطتنا باستخدام الدالة addTo.

tripPointsGeoJsonLayers.addTo(leafletElement);
tripLinesGeoJsonLayers.addTo(leafletElement);

بعد ذلك، للتأكد من أننا نقوم بالتكبير والتركيز على الموقع الصحيح، نريد الحصول على حدود الخريطة باستخدام الدالة getBounds على نسخة طبقة GeoJSON الخاصة بنا.

const bounds = tripPointsGeoJsonLayers.getBounds();

أخيراً، نقوم بتكييف عرض خريطتنا مع تلك الحدود باستخدام الدالة fitBounds على نسخة الخريطة (Map instance) الخاصة بنا.

leafletElement.fitBounds(bounds);

بمجرد حفظ الصفحة وإعادة تحميلها، يجب أن ترى الآن مساراً أزرق يمثل الانتقال بين كل من مواقعنا على الخريطة!

تطبيق خرائط يعرض مسار الرحلة البرية

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

الخطوة 5: تصميم مكونات الخريطة

ستكون خطوتنا الأخيرة هي إضافة بعض الأنماط التي ستسمح بظهور علاماتنا (markers) وستجعل النوافذ المنبثقة (popups) تبدو بالشكل الصحيح. في هذه الخطوة، سنعمل داخل ملف _home.scss، الذي يمكنك العثور عليه في المسار src/assets/stylesheets/pages. يمكننا البدء بنسخ ولصق كتلة الأنماط هذه في الجزء السفلي من ذلك الملف. بعد الانتهاء من ذلك، يمكننا استعراض ما يحدث.

.trip-stop {
  width : 400px ;
  overflow : hidden;

  h2 {
    font-size : 1.4em ;
    margin-top : 0 ;
    margin-bottom : . 2em ;
  }

  p ,
  ul ,
  h3 {
    font-size : 1.2em ;
    font-weight : normal;
  }

  p {
    margin : . 2em 0 ;
  }

  .trip-stop-date {
    color : $grey-600 ;
    font-size : 1em ;
  }

  ul {
    padding : 0 0 0 1.4em ;
    margin : 0 ;
  }
}

.trip-stop-image {
  display : block;
  float : left;
  overflow : hidden;
  width : 150px ;
  height : 150px ;
  text-indent : 100% ;
  color : transparent;
  background-position : center;
  background-size : cover;
}

.trip-stop-content {
  float : left;
  width : 250px ;
  padding-left : 1em ;
}

.icon-trip-stop {
  display : block;
  width : 1.5em ;
  height : 1.5em ;
  background-color : $orange-500 ;
  border-radius : 100% ;
  box-shadow : 0 2px 5px rgba( 0 , 0 , 0 ,. 5 );

  & :hover {
    background-color : $deep-orange-400 ;
  }
}

تتكون أنماطنا أعلاه من ثلاثة مكونات رئيسية:

  • .trip-stop-images: داخل النافذة المنبثقة (popup) للعلامة (marker)، يمكننا اختيارياً تضمين صورة. تحدد هذه الأنماط الحجم، وتجعل النص شفافاً (لأغراض إمكانية الوصول)، وتجعله يطفو إلى اليسار ليتسنى لمحتوى النافذة المنبثقة أن يتراصف بشكل صحيح جنباً إلى جنب.
  • .trip-stop-content: يشير هذا إلى النصف الآخر من محتوى نافذتنا المنبثقة. كل ما نحتاجه هنا هو التأكد من أن حجمه مناسب وأنه يطفو بجانب صورتنا.
  • .icon-trip-stop: يتم تصميم وسم HTML الذي نستخدمه كرمز هنا. نقوم بتحديد حجمه، وتعيين لون باستخدام متغير Scss محدد مسبقاً، وبذلك نكون جاهزين.

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

تطبيق خرائط الرحلات البرية مع النوافذ المنبثقة للمحطات

خطوة إضافية اختيارية: تعديلات الأنماط

الخطوة الأخيرة، وهي اختيارية تماماً، هي إجراء بعض التعديلات الطفيفة على الأنماط لإضفاء لمسة شخصية على موقعك. لن أخوض في تفاصيل هذه التعديلات هنا، ولكن إذا كنت ترغب في متابعة وتجميل التصميم قليلاً، يمكنك الاطلاع على هذا الالتزام (commit) الذي يعرض كل تغيير قمت به في الكود.

النسخة النهائية لتطبيق خرائط الرحلات البرية

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

هل تريد تعلم المزيد عن الخرائط؟

يمكنك الاطلاع على بعض المصادر الأخرى للبدء:

  • كيفية إنشاء لوحة تحكم وتطبيق خرائط لفيروس كورونا (COVID-19) في React باستخدام Gatsby و Leaflet
  • كيفية إعداد نمط خريطة أساس Mapbox مخصص باستخدام React Leaflet و Leaflet Gatsby Starter
  • الجميع يستطيع رسم الخرائط! إلهام ومقدمة إلى عالم الخرائط
  • كيفية إنشاء تطبيق Santa Tracker الخاص بك باستخدام Gatsby و React Leaflet
  • كيفية بناء تطبيق خرائط في React بطريقة سهلة باستخدام Leaflet

الخلاصة التقنية

لقد نجحنا في بناء تطبيق خرائط تفاعلي باستخدام مزيج قوي من Gatsby و Leaflet. يبرز هذا المشروع قدرة هذه التقنيات على إنشاء تطبيقات ويب عالية الأداء وغنية بالميزات الجغرافية. من خلال هيكلة البيانات باستخدام GeoJSON وتخصيص الطبقات والعلامات عبر دوال Leaflet، تمكنا من تقديم تجربة مستخدم سلسة وغنية بالمعلومات. هذا الدليل لم يقدم فقط خطوات بناء التطبيق، بل أكد أيضاً على أهمية تنظيم الكود، واستخدام الأنماط بفعالية، وفتح الباب أمام إمكانيات لا حصر لها لتكييف هذا المشروع مع احتياجات مختلفة، مما يجعله نموذجاً ممتازاً للمطورين الراغبين في استكشاف عالم الخرائط التفاعلية على الويب.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *