{"id":"l-rn-4-4","courseId":"react-native-apps","lang":"ar","kind":"lesson","summary":"تتبّع مسار الرحلة باستخدام expo-location، وأرفق صور الدروب باستخدام expo-image-picker، وجدول تذكيرًا باستخدام expo-notifications، مع تنظيف الاشتراكات وتصفية بيانات GPS المشوّشة.","html":"<p>ثلاث ميزات من الجهاز تجعل Trailhead أكثر من مجرّد قائمة: المسار الذي مشيته، والصور التي التقطتها، وتنبيه لطيف في نهاية اليوم لتسجيل الرحلة التي نسيتها. تستخدم هذه الميزات ثلاث وحدات من Expo بينها الكثير من القواسم المشتركة: كلٌّ منها يحتاج إلى إذن (يُطلب بالطريقة التي عرضها الدرس السابق)، وكلٌّ منها يُرجع بيانات ستخزّنها بالأدوات التي تعلّمتها قبل درسين، ولكلٍّ منها تفصيلة واحدة تؤذيك إن تجاهلتها.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">تتبّع المسار</h2>\n<p>يعطيك <code dir=\"ltr\">getCurrentPositionAsync</code> قراءة واحدة، وهذا يكفي لوسم نقطة بداية الرحلة. أما التتبّع فيحتاج إلى بثّ متواصل: يستدعي <code dir=\"ltr\">watchPositionAsync</code> دالتك مع كل موقع جديد، ويُرجع <strong>اشتراكًا</strong> (subscription) يُبقي الـ GPS يعمل حتى تزيله.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">src/hooks/useRoute.ts</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> *</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Location </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'expo-location'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { useEffect, useState } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> type</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Point</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#953800;--shiki-dark:#FFA657\">latitude</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">longitude</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">accuracy</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> null</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#953800;--shiki-dark:#FFA657\">timestamp</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useRoute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">active</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> boolean</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">points</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">setPoints</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> useState</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#953800;--shiki-dark:#FFA657\">Point</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[]>([]);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  useEffect</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(() </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">active) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> subscription</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> Location</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#953800;--shiki-dark:#FFA657\">LocationSubscription</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> |</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> undefined</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    let</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> cancelled </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    Location.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">watchPositionAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      { accuracy: Location.Accuracy.High, distanceInterval: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">10</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, timeInterval: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      ({ </span><span style=\"color:#953800;--shiki-dark:#FFA657\">coords</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">timestamp</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> }) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">        setPoints</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">prev</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">...</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">prev, { latitude: coords.latitude, longitude: coords.longitude, accuracy: coords.accuracy, timestamp }]);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    ).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">then</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">sub</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (cancelled) sub.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">remove</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">      else</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> subscription </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sub;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    });</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      cancelled </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      subscription?.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">remove</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    };</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }, [active]);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> points;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>تعالج العلامة <code dir=\"ltr\">cancelled</code> حالة سباق دقيقة: الاشتراك يصل بشكل غير متزامن، فإن أُزيلت الشاشة قبل أن يُحَلّ الـ promise، لا تجد دالة التنظيف شيئًا تزيله بعد. والعلامة تجعل الاشتراك المتأخّر يزيل نفسه.</p>\n<p>إلحاق العناصر بمصفوفة في الـ state مع كل قراءة لا بأس به لبضعة آلاف من النقاط، وهذا يغطّي رحلة نهارية طويلة. أما للمسارات متعدّدة الأيام، فاكتب النقاط إلى SQLite على دفعات واحتفظ في الذاكرة بالحديثة منها فقط، حتى لا ينسخ تحديث state واحد عشرات الآلاف من الكائنات.</p>\n<p>الخيارات ميزانية للبطارية. يطلب <code dir=\"ltr\">Accuracy.High</code> دقّة بنحو عشرة أمتار؛ و<code dir=\"ltr\">Balanced</code> بنحو مئة متر وأرخص بكثير. و<code dir=\"ltr\">distanceInterval: 10</code> تعني «لا تستدعِني إلا بعد التحرّك عشرة أمتار»، و<code dir=\"ltr\">timeInterval</code> يحدّد سقف التكرار على Android. مسار المشي يحتاج إلى دقّة عالية، لكنه لا يحتاج إلى نقطة كل ثانية.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>الثقة بكل قراءة GPS</span></p>\n<p>تحت الأشجار، وفي الأودية، وبجوار المنحدرات الصخرية، تتيه قراءات الـ GPS. كل <code dir=\"ltr\">coords.accuracy</code> هي نصف قطر عدم اليقين بالأمتار، وقراءة واحدة بدقّة 80 مترًا يمكن أن تضيف مئات الأمتار من المسافة الزائفة بينما يتعرّج المسار. أسقط القراءات التي تتجاوز حدًّا معيّنًا (25 مترًا يناسب المشي) قبل حساب المسافة أو رسم الخط. وهذا تمرين هذا الدرس.</p>\n</aside>\n<p>التتبّع والشاشة مقفلة يحتاج إلى موقع في الخلفية ومهمّة (task) مسجّلة عبر <code dir=\"ltr\">expo-task-manager</code>، إضافةً إلى الإذن الإضافي والتدقيق في المراجعة اللذين تحدّث عنهما الدرس السابق. أطلق التتبّع في المقدّمة أولًا؛ فكثير من المتنزّهين يُبقون الشاشة مضاءة لرؤية الخريطة على أي حال.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">صور الدروب</h2>\n<p>للصور، يفتح <code dir=\"ltr\">expo-image-picker</code> واجهة مكتبة الصور أو الكاميرا الخاصة بالنظام نفسه، وهي مألوفة للمستخدمين وتتولّى التحرير نيابةً عنك:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"tsx\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">TSX</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> *</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ImagePicker </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'expo-image-picker'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> addPhoto</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">fromCamera</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> boolean</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> options</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">:</span><span style=\"color:#953800;--shiki-dark:#FFA657\"> ImagePicker</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#953800;--shiki-dark:#FFA657\">ImagePickerOptions</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { mediaTypes: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'images'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">], quality: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0.7</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, allowsEditing: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> };</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (fromCamera) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> permission</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ImagePicker.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">requestCameraPermissionsAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">permission.granted) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> result</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> fromCamera</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    ?</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ImagePicker.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">launchCameraAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(options)</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    :</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ImagePicker.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">launchImageLibraryAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(options);</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (result.canceled) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> uri</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> result.assets[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">].uri;</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">  // copy into the app's document directory, then save the new path on the hike</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>تضغط <code dir=\"ltr\">quality: 0.7</code> صورة JPEG، وهذا مهم حين يكون للرحلة عشرون صورة. والإلغاء ليس خطأ: يُحَلّ الـ promise مع <code dir=\"ltr\">canceled: true</code>. والـ <code dir=\"ltr\">uri</code> المُرجَع يشير إلى مجلّد مؤقت قد يمسحه النظام، لذا انسخ الملف إلى مجلّد المستندات الخاص بتطبيقك باستخدام <code dir=\"ltr\">expo-file-system</code> وخزّن ذلك المسار في SQLite، كما وصف درس التخزين. ومحاكي iOS ليس فيه كاميرا، فاختبر التقاط الصور على هاتف حقيقي.</p>\n<p>وحين تحتاج إلى واجهة كاميرا خاصة بك، مثل مسح رموز QR على علامات الدروب، استخدم <code dir=\"ltr\">expo-camera</code>، ففيه الـ component المسمّى <code dir=\"ltr\">CameraView</code> الذي يعرض معاينة حيّة داخل تخطيطك.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">إشعار تذكير</h2>\n<p>المتنزّهون ينسون تسجيل رحلاتهم. وتذكير يومي في السابعة مساءً، فقط في الأيام التي فعّلوه فيها، هو إشعار محلي: يُجدوَل على الجهاز، دون أي خادم.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">src/lib/reminders.ts</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> *</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> as</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Notifications </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'expo-notifications'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { Platform } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'react-native'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">Notifications.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setNotificationHandler</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({</span></span>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  handleNotification</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> ({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    shouldShowBanner: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    shouldShowList: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    shouldPlaySound: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    shouldSetBadge: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }),</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">export</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> async</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> enableDailyReminder</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">() {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (Platform.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">OS</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'android'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Notifications.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setNotificationChannelAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'reminders'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      name: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Hike reminders'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      importance: Notifications.AndroidImportance.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">DEFAULT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    });</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">granted</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Notifications.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">requestPermissionsAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">granted) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> false</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Notifications.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">cancelAllScheduledNotificationsAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> Notifications.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">scheduleNotificationAsync</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">({</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    content: { title: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Out on a trail today?'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, body: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'Log it while you remember the details.'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, data: { url: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/new-hike'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> } },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    trigger: { type: Notifications.SchedulableTriggerInputTypes.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">DAILY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, hour: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">19</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, minute: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, channelId: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'reminders'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> },</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  });</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  return</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> true</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>يقرّر <code dir=\"ltr\">setNotificationHandler</code> هل يظهر الإشعار بينما التطبيق مفتوح. وعلى Android، تنتمي الإشعارات إلى <strong>قنوات</strong> (channels) يستطيع المستخدمون كتم كلٍّ منها على حدة في الإعدادات، لذا أنشئ قناة باسم واضح. والإلغاء قبل الجدولة يتجنّب تكديس تذكيرات مكرّرة في كل مرة يُبدَّل فيها الإعداد.</p>\n<p>وتؤتي <code dir=\"ltr\">data.url</code> ثمارها مع Expo Router: في الـ root layout، استمع عبر <code dir=\"ltr\">Notifications.addNotificationResponseReceivedListener</code>، واقرأ <code dir=\"ltr\">response.notification.request.content.data.url</code>، واستدعِ <code dir=\"ltr\">router.push(url)</code>. عندها يفتح النقر على التذكير نافذة إضافة الرحلة (الـ modal) مباشرة، عبر المسارات نفسها التي تستخدمها الـ deep links.</p>\n<p>أما الإشعارات الفورية (<strong>push</strong>)، التي تُرسَل من خادم، فتحتاج إلى أكثر من ذلك: development build، وبيانات اعتماد لخدمات الإشعارات لدى Apple وGoogle (يديرها EAS)، وpush token يُرسَل إلى الـ backend الخاص بك. أما كود الجدولة والمعالجة الذي كتبته للتوّ فيبقى كما هو.</p>\n<p>القسم 5 عن إيصال كل هذا إلى أيدي المستخدمين: تصحيح الأخطاء، والاختبار، والإطلاق.</p>\n","toc":[{"id":"sec-1","text":"تتبّع المسار","level":2},{"id":"sec-2","text":"صور الدروب","level":2},{"id":"sec-3","text":"إشعار تذكير","level":2}],"takeaways":["يبثّ <code dir=\"ltr\">watchPositionAsync</code> المواقع حتى تستدعي <code dir=\"ltr\">remove()</code> على الاشتراك (subscription) الذي يُرجعه، لذا أزله في دالة التنظيف الخاصة بالـ effect.","الدقّة وفترات التحديث تقايض الضبط بالبطارية؛ اختر أقل الإعدادات دقّةً تتحمّلها الميزة.","قراءات GPS مشوّشة، فأسقط النقاط ضعيفة الدقّة قبل حساب المسافة.","يُرجع expo-image-picker القيمة <code dir=\"ltr\">canceled</code> ومصفوفة <code dir=\"ltr\">assets</code>؛ والملفات المختارة تبدأ في مجلّد مؤقت، فانسخ أي شيء تريد الاحتفاظ به.","الإشعارات المحلية تُجدوَل على الجهاز بمُطلِق (trigger)، ويمكن أن تحمل <code dir=\"ltr\">data</code> المسار الذي يُفتح حين ينقر المستخدم عليها."],"further":[{"title":"Location","url":"https://docs.expo.dev/versions/latest/sdk/location/"},{"title":"ImagePicker","url":"https://docs.expo.dev/versions/latest/sdk/imagepicker/"},{"title":"Notifications","url":"https://docs.expo.dev/versions/latest/sdk/notifications/"}],"quiz":[{"q":"<p>تستدعي شاشة التتبّع في Trailhead الدالة <code dir=\"ltr\">watchPositionAsync</code> داخل effect لكنها لا تخزّن النتيجة أبدًا. ماذا يحدث بعد أن يغادر المستخدم الشاشة؟</p>","options":[{"text":"يتوقّف الاشتراك تلقائيًا حين يُزال الـ component.","why":"الاشتراكات الأصلية لا تعرف شيئًا عن الـ component الخاص بك. وحده <code dir=\"ltr\">remove()</code> يوقفها."},{"text":"ينهار التطبيق في المرة التالية التي يصل فيها موقع.","why":"الـ callback ما زال يعمل؛ وكل ما في الأمر أن React تتجاهل تحديثات الـ state للشاشة المُزالة. إنه تسرّب صامت، لا انهيار."},{"text":"لا شيء، لأن الموقع لا يتحدّث إلا ما دامت الشاشة ظاهرة.","why":"موقع المقدّمة يستمر في التحديث ما دام التطبيق مفتوحًا، أيًّا كانت الشاشة المعروضة."},{"text":"يستمر الـ GPS في العمل واستنزاف البطارية، لأن أحدًا لم يستدعِ <code dir=\"ltr\">remove()</code> على الاشتراك.","why":"صحيح. خزّن الاشتراك واستدعِ <code dir=\"ltr\">subscription.remove()</code> في دالة التنظيف الخاصة بالـ effect."}],"answer":3},{"q":"<p>في مسار مسجّل، تقفز نقطة واحدة 400 متر جانبًا بينما كان المتنزّه واقفًا تحت الأشجار. ما أفضل ردّ في الكود؟</p>","options":[{"text":"اطلب <code dir=\"ltr\">Accuracy.BestForNavigation</code> حتى لا يخطئ الـ GPS أبدًا.","why":"إعدادات الدقّة الأعلى تقلّل الخطأ لكنها لا تلغيه، وتكلّف بطارية أكثر."},{"text":"تجاهل النقاط التي تكون قيمة <code dir=\"ltr\">coords.accuracy</code> المبلَّغ عنها فيها أسوأ من حدّ معيّن، مثل 25 مترًا، قبل إضافتها إلى المسار.","why":"صحيح. كل قراءة تُبلغ عن نصف قطر عدم اليقين الخاص بها؛ وتصفية القراءات السيئة تُبقي المسار والمسافة صادقين."},{"text":"قرّب كل الإحداثيات إلى منزلتين عشريتين.","why":"منزلتان عشريتان من خط العرض تعنيان دقّة بنحو 1 km، وهذا يدمّر المسار."}],"answer":1},{"q":"<p>بعد <code dir=\"ltr\">launchImageLibraryAsync</code>، يتراجع المستخدم دون أن يختار شيئًا. ماذا يرى كودك؟</p>","options":[{"text":"يُرفض الـ promise بخطأ “cancelled”.","why":"الإلغاء ليس خطأ. يُحَلّ الـ promise بشكل طبيعي."},{"text":"لا يُحَلّ الـ promise أبدًا.","why":"يُحَلّ فور إغلاق أداة الاختيار، أيًّا كان ما فعله المستخدم."},{"text":"نتيجة فيها <code dir=\"ltr\">canceled: true</code> ولا assets، وينبغي أن تتعامل معها بألّا تفعل شيئًا.","why":"صحيح. تحقّق بـ <code dir=\"ltr\">if (result.canceled) return;</code> قبل قراءة <code dir=\"ltr\">result.assets[0].uri</code>."}],"answer":2}],"exercise":{"kind":"web","mode":"js","starter":{"js":"// Total distance of a recorded route in km, rounded to 2 decimals.\n// Ignore points whose accuracy (meters) is worse than maxAccuracyM.\nfunction trackDistanceKm(points, maxAccuracyM = 25) {\n  let total = 0;\n  for (let i = 1; i < points.length; i++) {\n    const dLat = points[i].latitude - points[i - 1].latitude;\n    const dLon = points[i].longitude - points[i - 1].longitude;\n    total += Math.sqrt(dLat * dLat + dLon * dLon);\n  }\n  return total;\n}\n\nconst route = [\n  { latitude: 46.5, longitude: 7.9, accuracy: 8 },\n  { latitude: 46.51, longitude: 7.9, accuracy: 12 },\n  { latitude: 46.53, longitude: 7.95, accuracy: 90 },\n  { latitude: 46.51, longitude: 7.92, accuracy: 10 },\n];\nconsole.log(trackDistanceKm(route), 'km');\n"},"solution":{"js":"const EARTH_RADIUS_KM = 6371;\nconst toRad = (deg) => (deg * Math.PI) / 180;\n\nfunction haversineKm(a, b) {\n  const dLat = toRad(b.latitude - a.latitude);\n  const dLon = toRad(b.longitude - a.longitude);\n  const h = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(a.latitude)) * Math.cos(toRad(b.latitude)) * Math.sin(dLon / 2) ** 2;\n  return 2 * EARTH_RADIUS_KM * Math.asin(Math.sqrt(h));\n}\n\nfunction trackDistanceKm(points, maxAccuracyM = 25) {\n  const good = points.filter((p) => p.accuracy == null || p.accuracy <= maxAccuracyM);\n  let total = 0;\n  for (let i = 1; i < good.length; i++) total += haversineKm(good[i - 1], good[i]);\n  return Math.round(total * 100) / 100;\n}\n\nconst route = [\n  { latitude: 46.5, longitude: 7.9, accuracy: 8 },\n  { latitude: 46.51, longitude: 7.9, accuracy: 12 },\n  { latitude: 46.53, longitude: 7.95, accuracy: 90 },\n  { latitude: 46.51, longitude: 7.92, accuracy: 10 },\n];\nconsole.log(trackDistanceKm(route), 'km');\n"},"checks":[{"id":"one-step","test":"const p = (latitude, longitude, accuracy = 5) => ({ latitude, longitude, accuracy }); return typeof trackDistanceKm === 'function' && trackDistanceKm([p(0, 0), p(0.01, 0)]) === 1.11;"},{"id":"sums-route","test":"const p = (latitude, longitude, accuracy = 5) => ({ latitude, longitude, accuracy }); return trackDistanceKm([p(46.5, 7.9), p(46.51, 7.9), p(46.51, 7.92)]) === 2.64;"},{"id":"drops-inaccurate","test":"const p = (latitude, longitude, accuracy = 5) => ({ latitude, longitude, accuracy }); return trackDistanceKm([p(46.5, 7.9, 8), p(46.51, 7.9, 12), p(46.53, 7.95, 90), p(46.51, 7.92, 10)]) === 2.64;"},{"id":"custom-threshold","test":"const p = (latitude, longitude, accuracy = 5) => ({ latitude, longitude, accuracy }); return trackDistanceKm([p(0, 0, 5), p(0.01, 0, 40), p(0.02, 0, 5)], 50) === 2.22 && trackDistanceKm([p(0, 0, 5), p(0.01, 0, 40), p(0.02, 0, 5)], 30) === 2.22;"},{"id":"short-input","test":"const p = (latitude, longitude, accuracy = 5) => ({ latitude, longitude, accuracy }); return trackDistanceKm([]) === 0 && trackDistanceKm([p(1, 1)]) === 0 && trackDistanceKm([p(1, 1, 99), p(1.01, 1, 99)]) === 0;"},{"id":"null-accuracy","test":"const p = (latitude, longitude, accuracy = 5) => ({ latitude, longitude, accuracy }); return trackDistanceKm([p(0, 0, null), p(0.01, 0, null)]) === 1.11;"}],"title":"قِس مسار GPS","prompt":"<p>أصلح <code dir=\"ltr\">trackDistanceKm(points, maxAccuracyM = 25)</code> ليعرض Trailhead الطول الحقيقي للرحلة المسجّلة. لكل نقطة <code dir=\"ltr\">latitude</code> و<code dir=\"ltr\">longitude</code> (بالدرجات) و<code dir=\"ltr\">accuracy</code> (بالأمتار، أو <code dir=\"ltr\">null</code> حين تكون مجهولة).</p>\n<ul>\n<li>تجاهل النقاط التي تكون قيمة <code dir=\"ltr\">accuracy</code> فيها <strong>أكبر من</strong> <code dir=\"ltr\">maxAccuracyM</code>. واحتفظ بالنقاط التي دقّتها <code dir=\"ltr\">null</code>.</li>\n<li>اجمع مسافة الدائرة العظمى بين كل نقطة متبقّية والتي تليها، باستخدام معادلة haversine بنصف قطر للأرض قدره <strong>6371 km</strong>.</li>\n<li>أرجع الكيلومترات مقرّبة إلى <strong>منزلتين عشريتين</strong>. وأقل من نقطتين صالحتين يعني <code dir=\"ltr\">0</code>.</li>\n</ul>\n<p>يعامل الكود المبدئي الدرجات على أنها إحداثيات مسطّحة، وهذا خاطئ في كل مكان، وخاطئ جدًا بعيدًا عن خط الاستواء.</p>\n","hints":["صفِّ أولًا: <code dir=\"ltr\">points.filter((p) =&gt; p.accuracy == null || p.accuracy &lt;= maxAccuracyM)</code>.","معادلة haversine: حوّل فرقَي خط العرض وخط الطول إلى راديان، ثم <code dir=\"ltr\">h = sin²(dLat/2) + cos(lat1)·cos(lat2)·sin²(dLon/2)</code> و<code dir=\"ltr\">distance = 2 · 6371 · asin(√h)</code>.","قرّب مرة واحدة في النهاية باستخدام <code dir=\"ltr\">Math.round(total * 100) / 100</code>، لا بعد كل خطوة، وإلا تراكمت أخطاء التقريب."],"explanation":"<p>الدرجات ليست شبكة مسطّحة: درجة خط الطول الواحدة تنكمش من نحو 111 km عند خط الاستواء إلى صفر عند القطبين، لذا أعطت نظرية فيثاغورس على الدرجات الخام في الكود المبدئي نتائج بلا معنى. أما معادلة haversine فتقيس على سطح الأرض، وهي دقيقة لمسافات المشي بهامش أصغر بكثير من خطأ قراءة الـ GPS نفسها. والتصفية بحسب <code dir=\"ltr\">accuracy</code> قبل الجمع تُزيل التعرّجات التي كانت ستضيف كيلومترات زائفة تحت الأشجار. والدالة نقية، فيمكن لشاشة التتبّع استدعاؤها مع كل نقطة جديدة، أو يمكنك حسابها مرة واحدة حين تُحفظ الرحلة.</p>\n","checkLabels":{"one-step":"خطوة واحدة بمقدار 0.01° من خط العرض تساوي نحو 1.11 km","sums-route":"تُجمع المسافات على امتداد المسار","drops-inaccurate":"تُتجاهَل النقاط ضعيفة الدقّة","custom-threshold":"يمكن تغيير حدّ الدقّة","short-input":"أقل من نقطتين صالحتين يعطي 0","null-accuracy":"يُحتفَظ بالنقاط ذات الدقّة المجهولة (null)"}},"words":600,"readMinutes":4}