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

للبدء، سنستعين بمشروع 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

بعد اكتمال عملية الإنشاء، يمكنك الانتقال إلى مجلد المشروع الجديد وتشغيل خادم التطوير المحلي الخاص بك:
cd my-road-trip
yarn develop

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

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