אלול

הרפתקאות מפת בתי הספר באתר אורט – פרק ג’, המפה: הוספת עיר

הפוסט הזה הוא חלק מסדרה על הקוד של עמוד בתי הספר של אורט באתר אורט החדש, עמוד שמכיל רשימה של כל בתי הספר של אורט לפי ערים, ומפה עם סימוני כל בתי הספר.
בפרק ההקדמה סיפרתי על הרקע הכללי של האתר ומבנה הסדרה, בפרק הראשון סיפרתי על סינון הערים לפי מחרוזת חיפוש, בפרק הקודם דיברנו על אתגרי המפה ואיך מתחילים איתה, ובפרק הזה אראה איך הכנו את התשתית לסנכרון.

מה זה בעצם סנכרון בין רשימת הערים למפה? בכיוון האחד – כשגולש לוחץ על שם של עיר המפה עושה זום לעיר הזאת. ובכיוון השני – כשגולש עושה זום במפה או גורר אותה לאזור מסוים, רשימת הערים מצטמצמת או משתנה בהתאם.

כדי לאפשר את ההתנהגות הזאת, הקוד צריך לדעת את הקואורדינטות של כל עיר, ולשם כך קיים ב-API של גוגל אובייקט בשם Geocode. לאובייקט המופלא הזה אפשר לתת שם של עיר, והוא מחזיר את כל המידע הגאוגרפי על העיר. את המידע הגאוגרפי הזה מעבירים למפה באמצעות הפונקציה fitBounds() (או, אם אין המידע שהפונקציה הזאת צריכה, אפשר להשתמש בפונקציה setCenter() ) והיא משתנה בהתאם. יש לציין שהפונקציה geocode() שבעזרתה מקבלים את המידע עובדת באופן א-סינכרוני וצריך לחכות לתוצאות שלה ורק אז להשתמש בהן.

בהתחלה חשבתי שאשתמש באובייקט הזה בכל לחיצה של עיר, אבל אז הבנתי שני דברים: 1) שזה יכול ליצור בעיית ביצועים אם כל לחיצה תצריך הליכה לשרתים של גוגל כדי להביא את המידע, ו-2) שכל שימוש ב-API עולה כסף, וחבל.

לכן החלטתי לאגור את כל המידע הגאוגרפי על כל עיר בקובץ JSON, ולהשתמש בו בעת לחיצה על שם של עיר במקום להשתמש ב-API. לכבוד זה כתבתי פונקציה שעם עליית העמוד עוברת על כל הערים באקורדיון ובודקת אם הן כבר נמצאות בקובץ ה-JSON. אם עיר כלשהי לא נמצאת בקובץ, היא כנראה עיר חדשה. במקרה כזה הקוד שולח את שם העיר ל-API, ואת המידע המוחזר הוא שומר בקובץ. הפונקציה הזאת עובדת עם AJAX מפני ששמירת הקובץ נעשית בשרת.

<span class="token comment">/**
 * Needs to called before the map is created,
 * so any new cities are inserted into the json file and are shown in the list
 */</span>
<span class="token function">updateCitiesFile</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

    <span class="token comment">/**
     * If the city doesn't exist in the JSON file, we use the geocoder to get its ccordinates info from google, and insert it into the JSON file
     */</span>
    <span class="token keyword">function</span> <span class="token function">updateCitiesFile</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span>schools_and_map_filter_ajax_obj<span class="token punctuation">.</span>is_user_admin<span class="token punctuation">)</span> <span class="token punctuation">{</span>
            $<span class="token punctuation">.</span><span class="token function">getJSON</span><span class="token punctuation">(</span><span class="token template-string"><span class="token string">`</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>schools_and_map_filter_ajax_obj<span class="token punctuation">.</span>json_file<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">?ver=</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>Date<span class="token punctuation">.</span><span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">`</span></span><span class="token punctuation">,</span> <span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token punctuation">{</span>
                <span class="token keyword">let</span> isThereANewCity <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span>
                <span class="token comment">// filter out cities that don't appear in bounds</span>
                cityNameLinkElements<span class="token punctuation">.</span><span class="token function">each</span><span class="token punctuation">(</span><span class="token punctuation">(</span>key<span class="token punctuation">,</span> cityElement<span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token punctuation">{</span>
                    <span class="token comment">// if the innerHTML, which is the city name, doesn't exist in the list of filtered cities, hide it</span>
                    <span class="token keyword">let</span> cityExists <span class="token operator">=</span> data<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span>item <span class="token operator">=</span><span class="token operator">></span> item<span class="token punctuation">.</span>name<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">===</span> cityElement<span class="token punctuation">.</span>innerHTML<span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
                    <span class="token comment">// if the city doesn't appear in bounds, hide it</span>
                    <span class="token keyword">if</span> <span class="token punctuation">(</span>cityExists<span class="token punctuation">.</span>length <span class="token operator">===</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
                        <span class="token comment">// then check if it has a geocode and simply didn't appear in the file</span>
                        <span class="token keyword">let</span> address <span class="token operator">=</span> cityElement<span class="token punctuation">.</span>innerHTML<span class="token punctuation">;</span> <span class="token comment">// get the name of the current city</span>
                        <span class="token comment">/**** prepare the geocode request and send it to the geocode function ****/</span>
                        <span class="token keyword">var</span> geocodeRequest <span class="token operator">=</span> <span class="token punctuation">{</span>
                            address<span class="token punctuation">:</span> address
                        <span class="token punctuation">}</span><span class="token punctuation">;</span>
                        geocoder <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">google<span class="token punctuation">.</span>maps<span class="token punctuation">.</span>Geocoder</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
                        geocoder<span class="token punctuation">.</span><span class="token function">geocode</span><span class="token punctuation">(</span>geocodeRequest<span class="token punctuation">,</span> <span class="token punctuation">(</span>results<span class="token punctuation">,</span> status<span class="token punctuation">)</span> <span class="token operator">=</span><span class="token operator">></span> <span class="token punctuation">{</span>
                            <span class="token keyword">if</span> <span class="token punctuation">(</span>status <span class="token operator">===</span> google<span class="token punctuation">.</span>maps<span class="token punctuation">.</span>GeocoderStatus<span class="token punctuation">.</span>OK<span class="token punctuation">)</span> <span class="token punctuation">{</span>
                                <span class="token keyword">if</span> <span class="token punctuation">(</span>results<span class="token punctuation">.</span>length <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
                                    <span class="token comment">/* Insert the result from the geocode function into the file*/</span>
                                    result <span class="token operator">=</span> results<span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span><span class="token punctuation">;</span>
                                    data <span class="token operator">=</span> <span class="token function">insertCityIntoJson</span><span class="token punctuation">(</span>data<span class="token punctuation">,</span> address<span class="token punctuation">,</span> result<span class="token punctuation">)</span><span class="token punctuation">;</span>
                                    <span class="token comment">/* must update the file in this loop because it's async, and outside it we don't know if it has finished */</span>
                                    <span class="token comment">/* if there is a new city, update the json file */</span>
                                    <span class="token function">updateJsonFile</span><span class="token punctuation">(</span>data<span class="token punctuation">)</span><span class="token punctuation">;</span>
                                <span class="token punctuation">}</span>
                            <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
                                console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token string">'Could not gecode: '</span> <span class="token operator">+</span> status<span class="token punctuation">)</span><span class="token punctuation">;</span>
                            <span class="token punctuation">}</span>
                        <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
                    <span class="token punctuation">}</span> <span class="token comment">// END if (cityExists.length === 0)</span>
                <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// END cityNameLinkElementsiteration</span>
            <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// END json file iteration</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>

אפשר לראות בקוד שכשחוזרת התשובה מה-Geocoder, אנחנו עושים שני דברים: מכניסים עיר חדשה לאובייקט ה-JSON, ומעדכנים את הקובץ.

זאת הפונקציה insertCityIntoJson, שמכניסה עיר חדשה ויוצרת node חדש ב-JSON:

    <span class="token comment">/**
     * Adds the new city to the city array and sends it to the backend via AJAX.
     * The backend inserts it into the json file
     * @param cities
     * @param newCityName
     * @param geocodeResult
     * @returns array - the cities array with the new city
     */</span>
    <span class="token keyword">function</span> <span class="token function">insertCityIntoJson</span><span class="token punctuation">(</span>cities<span class="token punctuation">,</span> newCityName<span class="token punctuation">,</span> geocodeResult<span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token keyword">let</span> newCity <span class="token operator">=</span> <span class="token punctuation">{</span>
            <span class="token string">"name"</span><span class="token punctuation">:</span> newCityName<span class="token punctuation">,</span>
            <span class="token string">"location"</span><span class="token punctuation">:</span> geocodeResult<span class="token punctuation">.</span>geometry<span class="token punctuation">.</span>location<span class="token punctuation">,</span>
            <span class="token string">"zoom"</span><span class="token punctuation">:</span> <span class="token number">8</span><span class="token punctuation">,</span>
            <span class="token string">"bounds"</span><span class="token punctuation">:</span> geocodeResult<span class="token punctuation">.</span>geometry<span class="token punctuation">.</span>bounds<span class="token punctuation">,</span>
            <span class="token string">"southWestViewport"</span><span class="token punctuation">:</span> result<span class="token punctuation">.</span>geometry<span class="token punctuation">.</span>viewport<span class="token punctuation">.</span><span class="token function">getSouthWest</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
            <span class="token string">"northEastViewport"</span><span class="token punctuation">:</span> result<span class="token punctuation">.</span>geometry<span class="token punctuation">.</span>viewport<span class="token punctuation">.</span><span class="token function">getNorthEast</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token punctuation">}</span><span class="token punctuation">;</span>
        cities<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>newCity<span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">return</span> cities<span class="token punctuation">;</span>
    <span class="token punctuation">}</span>

וזאת הפונקציה updateJsonFile שמעדכנת את קובץ ה-JSON:

<span class="token comment">/**
 *
 * @param cities
 */</span>
<span class="token keyword">function</span> <span class="token function">updateJsonFile</span><span class="token punctuation">(</span>cities<span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">let</span> newData <span class="token operator">=</span> JSON<span class="token punctuation">.</span><span class="token function">stringify</span><span class="token punctuation">(</span>cities<span class="token punctuation">)</span><span class="token punctuation">;</span>
    $<span class="token punctuation">.</span><span class="token function">post</span><span class="token punctuation">(</span>
        schools_and_map_filter_ajax_obj<span class="token punctuation">.</span>ajax_url<span class="token punctuation">,</span>
        <span class="token punctuation">{</span>
            <span class="token comment">//POST request</span>
            _ajax_nonce<span class="token punctuation">:</span> schools_and_map_filter_ajax_obj<span class="token punctuation">.</span>nonce<span class="token punctuation">,</span>
            action<span class="token punctuation">:</span> <span class="token string">"schools_map_locations_update"</span><span class="token punctuation">,</span>
            newData<span class="token punctuation">:</span> newData <span class="token comment">// cities</span>
        <span class="token punctuation">}</span><span class="token punctuation">,</span>
        <span class="token keyword">function</span> <span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token punctuation">{</span>
            <span class="token comment">//nothing to do with data</span>
        <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

בצד השרת, אנחנו יושבים על hook בהתאם לשם שנתנו בקריאת ה-AJAX:

<span class="token function">add_action</span><span class="token punctuation">(</span> <span class="token string">'wp_ajax_schools_map_locations_update'</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>
			<span class="token variable">$this</span><span class="token punctuation">,</span>
			<span class="token string">'schools_map_locations_update'</span>
		<span class="token punctuation">]</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// only run the ajax for users of type admin</span>

וזו הפונקציה:

	<span class="token comment">/**
	 * This function is called via ajax, and updates the JSON file
	 * It's only called for admins, because we don't ant every user to be able to update the file
	 */</span>
	<span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function">schools_map_locations_update</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
		<span class="token function">check_ajax_referer</span><span class="token punctuation">(</span> <span class="token string">'title_example'</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
		<span class="token comment">// Handle the ajax request</span>
		<span class="token variable">$updatedData</span> <span class="token operator">=</span> <span class="token variable">$_POST</span><span class="token punctuation">[</span><span class="token string">'newData'</span><span class="token punctuation">]</span><span class="token punctuation">;</span>

		<span class="token variable">$success</span> <span class="token operator">=</span> <span class="token function">file_put_contents</span><span class="token punctuation">(</span> <span class="token function">plugin_dir_path</span><span class="token punctuation">(</span> <span class="token constant">__FILE__</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> <span class="token string">'js/cities_map.json'</span><span class="token punctuation">,</span>
			<span class="token variable">$updatedData</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>
		<span class="token function">wp_die</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment">// All ajax handlers die when finished</span>
	<span class="token punctuation">}</span>
<span class="token punctuation">}</span>

זכינו אפילו לבדוק את זה לפני העליה לאוויר: העיר היקרה והחשובה  נצרת עלית החליפה את שמה לנוף הגליל. הקוד עבד מעולה והקובץ עודכן. ראיתי שהיה חשוב שאת שם העיר לקחתי מהאקורדיון ולא מהמידע שגוגל שלח, מפני שכששלחנו את השם נוף הגליל, גוגל החזיר נצרת עלית… כלומר אחלה שהוא זיהה, אבל אנחנו צריכים שהמידע יישמר תחת השם החדש.

יוצאים מן הכלל:

היו כמה מועצות אזוריות שהזום שלהן במפה לא היה מדויק מספיק. כלומר, התשובה שחזרה מה-Geocoder הייתה אמנם בתחום המועצה, אבל בחלק שאין בו בתי ספר של אורט. מדובר היה בעיקר באזורים בנגב שבהם המועצה האזורית היא ענקית, וכוללת שטחים נרחבים מעבר לבתי הספר. במקרים האלה החלטנו שנכניס ידנית את הקואורדינטות שאנחנו רוצים שהמפה תראה כשלוחצים על שמות המועצות האלה.

אלה היו האזורים שהיו בהם בעיות:

  1. באל-קאסום לא ראו את כל בתי הספר שבמועצה
  2. בנווה מדבר לא ראו אף סמן בגבול המעצה
  3. בערד יש שני בתי ספר, אך כיון שהם קרובים (אך לא זהים) רואים רק אחד מהם בזום הנתון
  4. ביקנעם אחד הסמנים היה קצת קטוע למטה, ולא ממש ראו אותו
  5. אחווה

איך נמצא את הקואורדינטות כדי להכניס ידנית? בהתחלה חשבתי להשתמש בכלי מקוון – של גוגל או של חברות אחרות. בכלים האלה אפשר לחפש שם של עיר ולקבל את הקואורדינטות. אבל מה שאני רציתי בנוסף זה שאוכל להזיז ולהגדיל את המפה כרצוני, ולקבל את הקואורדינטות אחרי השינויים, ואף אחד מהכלים האלה לא איפשר את זה. חשבתי שאולי כלי שעושה הפוך – מקבל קואורדינטות ומראה את המפה – יעזור, אך התבדיתי.

בסוף החלטתי פשוט להשתמש בקוד שלי. בפונקציה boundsChangedHandler() אחרי שקוראים ל-map.getBounds() ומקבלים תוצאה שאינה null, הכנסתי כתיבה לקונסול:

<span class="token keyword">let</span> mapBounds <span class="token operator">=</span> map<span class="token punctuation">.</span><span class="token function">getBounds</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token keyword">if</span> <span class="token punctuation">(</span>mapBounds <span class="token operator">!==</span> <span class="token keyword">null</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
console<span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span>mapBounds<span class="token punctuation">)</span><span class="token punctuation">;</span>

וכך הזזתי את המפה לאן שרציתי, והעתקתי את הערכים מהקונסול לקובץ ה-JSON. האובייקט בקונסול היה אמנם קצת שונה במבנהו מהאובייקט ב-JSON, אבל הצלחתי להתגבר:

{…}
​ 
ga: {…}
​​j: 35.06010750963742
​​l: 35.14701108171994
​​ 
na: {…}
​​j: 32.62576946263114
​​l: 32.661905895023025

עד עצם היום הזה אני לא מבינה מה הפירוש של האותיות na ו-ga, אבל הבנתי שב-ga ישנן הנקודות המערבית (הנקודה עם הערך הנמוך) והמזרחית (הנקודה עם הערך הגבוה) וב-na ישנן הנקודות הדרומית (הנקודה עם הערך הנמוך) והצפונית (הנקודה עם הערך הגבוה). העתקתי אותן לשדות lat ו-lng של southWestViewport ו-northEastViewport לפי ההתאמה, והפלא ופלא, זה עבד. בלחיצה על המועצות האזוריות האלה, המפה הסתנכרנה לערכים החדשים.

נ.ב
למי שמעוניין, יש פוסט שמסביר את GeoJSON. זה יכול להועיל עוד קצת בהבנה של ההתממשקות עם המפות של גוגל

עכשיו התשתית מוכנה. בפרק הבא נצלול לסנכרון עצמו. בואו בהמוניכם.

גלובוס ומצפן

כתבו תגובה

כתובת הדוא"ל שלכם לא תוצג.