יום שלישי, נובמבר 16

על המפה


אפליקציות הקשורות למיקום ולמפות הן מהדברים הכי מעניינים את בעלי הסמארטפונים. לפיכך, נקדיש לנושא זה מספר פוסטים. כוונתי להתחיל מהפשוט מאד ולהוסיף דוגמאות מורכבות יותר בהדרגה. בפוסט הזה נדגים הצגה של מפות+  הצגה של אתרים על המפה. בפוסט שאחריו, נדגים ציור על גבי המפה (overlay), ע"י ציור של סיכה המסמנת מיקום. באותו פוסט, או אולי בפוסט מאוחר יותר (נשקול את העניין, כשהמטרה היא ללמוד בשלבים), נדגים גם את נושא ה-Geocoding, כשנהפוך שם של מקום לקואורדינטות על המפה.
אגב, לכל המעוניינים, באתר המפתחים של גוגל יש מדריך מבוא למפות לא רע. בכל זאת, אני מקוה שהפוסט הזה ואלה שאחריו יהיו אולי יעילים יותר. הקישור - Google Map View.
עוד  נושא די מצער לפני שמתחילים: המפות של גוגל אמנם כוללות גם את ישראל, אבל ה-API של גוגל מאפשר הצגה של ישראל ברזולוציה מאד נמוכה - ראה את התמונה הבאה להמחשה:



רואים את הסיכה שעל המפה? זו עפולה. ומימינה - לא, זה לא כביש 65 של ישראל. זו כבר ירדן.  מפת ישראל ברזולוציה גבוהה אמנם זמינה בגוגל, אבל אפשר לגשת אליה רק דרך ה- Browser. הסיבות לכך קשורות לעניינים מסחריים, כנראה בין גוגל לבין הבעלים של מפות הישראל.  חבל. יש סיכוי שזה ישתנה?  מקווה שכן אבל אין (לי) שום מידע בקשר לזה. מצד שני לא לשכוח את חצי הכוס המלאה: יש מפות לשאר העולם. הקישור הבא מפרט את המדינות עבורן google Map API נתמך:
http://gmaps-samples.googlecode.com/svn/trunk/mapcoverage_filtered.html
נעבור לדוגמא שלנו. נציג על המסך מפה + כפתורי שליטה. המיקום הראשוני שיקבע יהיה פאריס. נוסיף מספר כפתורים כדי להציג יכולות: כפתור למעבר בין תמונת לויין למפה, כפתורי זום, כפתור  clickable שיבחר בין תמונה סטטית לבין אפשרות לתזוזה.
קישור להורדת קבצי הפרויקט - למטה בסוף.
הנה ה-UI:



כמה הערות אדמיניסטרטיביות חשובות: העבודה עם המפות מחייבת רשיונות\הרשאות. מדובר בהרשאה לשימוש בספריות של גוגל, הרשאה כללית לגישה לאינטרנט ו-  API Key עליו נדון כשנסקור את קובץ ה-Layout.  זאת מפני שגוגל שומרת על הפרדה בין הספריות של אנדרואיד לבין ספריות ה-API שלה. האם הם זוממים משהו שאנחנו לא יודעים? מקווה מאד שלא.
מכאן ואילך אם כן נתאר את הנושאים הבאים:
  1. דגשים מיוחדים הקשורים לתמיכה במפות בזמן בבניית הפרויקט.
  2. דגשים מיוחד הקשורים למפות בקובץ ה-manifest.xml.
  3. קובץ ה-layout.
  4. תוכנית ה-java.
1. דגשים בבניית הפרוייקט :
הדגש היחידי, בחירת build target מסוג Google API. לכל ורסיה החל מ-1.5 יש ורסיה תואמת. למה? מפני שגוגל שומרת על הפרדה בין הספריות של אנדרואיד לבין ספריות ה-API שלה. האם הם זוממים משהו שאנחנו לא יודעים? מקווה מאד שלא. ליצירת הפרויקט ב-Eclipse נבחר: File->New->Android Project. בחלון הדוגמא שלפניכם מצגת הבחירה עבור 2.2 - Froyo.


יתר המאפיינים - כרגיל.
2. נעבור לקובץ ה-manifest. נציג אותו כאן בשלמותו. האלמנטים המיוחדים כאן מודגשים (שורות 7 ו-17):
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <manifest xmlns:android="http://schemas.android.com/apk/res/android"
  3.       package="com.ahs.adroheb.mapdisplay"
  4.       android:versionCode="1"
  5.       android:versionName="1.0">
  6.     <application android:icon="@drawable/icon" android:label="@string/app_name">
  7.      <uses-library android:name="com.google.android.maps" />
  8.     
  9.         <activity android:name=".MyMapDisplay"
  10.                   android:label="@string/app_name">
  11.             <intent-filter>
  12.                 <action android:name="android.intent.action.MAIN" />
  13.                 <category android:name="android.intent.category.LAUNCHER" />
  14.             </intent-filter>
  15.         </activity>
  16.     </application>
  17. <uses-permission android:name="android.permission.INTERNET"/>
  18.     <uses-sdk android:minSdkVersion="3" />
  19. </manifest>
השורות המעניינות:
שורה 7: הכללת ספריית ההמפות.
שורה 17 היא הרשאה לגישה לאינטרנט.
מעבר לזה, אפשר ללמוד מהקובץ הנ"ל שהאפליקציה שלנו מכילה Activity אחד בלדבד.
3. נציג את קובץ ה-Layout בשלמותו:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <!-- This file is /res/layout/geocode.xml -->
  3. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  4. android:layout_width="fill_parent"
  5. android:layout_height="fill_parent">
  6. <com.google.android.maps.MapView
  7. android:id="@+id/geoMap" android:clickable="true"
  8. android:layout_width="fill_parent"
  9. android:layout_height="wrap_content"
  10. android:apiKey="Put your API Key here"
  11. />
  12. <LinearLayout android:layout_width="fill_parent"
  13. android:layout_alignParentBottom="true"
  14. android:layout_height="wrap_content"
  15. android:orientation="horizontal" >
  16. <Button
  17. android:id="@+id/button_sat_map"
  18. android:layout_width="wrap_content"
  19. android:layout_height="wrap_content"
  20. android:text="@string/button_sat_map"/>
  21. <Button
  22. android:id="@+id/button_zoom_out"
  23. android:layout_width="wrap_content"
  24. android:layout_height="wrap_content"
  25. android:text="@string/button_zoom_out"/>
  26. <Button
  27. android:id="@+id/button_zoom_in"
  28. android:layout_width="wrap_content"
  29. android:layout_height="wrap_content"
  30. android:text="@string/button_zoom_in"/>
  31. <Button
  32. android:id="@+id/button_clickable"
  33. android:layout_width="wrap_content"
  34. android:layout_height="wrap_content"
  35. android:text="@string/button_clickable"/>
  36. </LinearLayout>
  37. </RelativeLayout>


מעבר להגדרת הכפתורים עליהם לא נתעכב, נסתכל על השורות המודגשות: 6-10.
שורות אלה מגדירות את האלמנט MapView בו נשתמש לצירת המפה. את ה-API Key יש לשתול בשורה 10. הקלק על הקישור לקבלת מדריך ליצירת ה-API Key. ללא ה-Key לא ניתן לצפות במפות.

4.  שתי  מחלקות (classes) עושות את עיקר העבודה הקשורה למפות: MapView ו- MapActivity.
MapActivity היא הרחבה (subclass) של Activity (לטובת טיפול במפות, וכדי להשתמש ב MapView חייבים להשתמש בה.
נציג תחילה את השלד של האוביקט והמתודות שלו:

  1. public class MyMapDisplay extends MapActivity implements OnClickListener{
  2.     MapView mapView;
  3.     private Button     buttonSatMap;
  4.     private    Button    buttonZoomIn;
  5.     private    Button    buttonZoomOut;
  6.     private    Button    buttonClick;
  7.     @Override
  8.     protected void onCreate(Bundle savedInstanceState)
  9.     {
  10.      }
  11.    
  12.     private void controlButtonManage(){
  13.    }
  14.     public void onClick(View view) {
  15.  
  16.     }
  17.     @Override
  18.     protected boolean isLocationDisplayed() {
  19.         return false;
  20.     }
  21.     @Override
  22.     protected boolean isRouteDisplayed() {
  23.         return false;
  24.     }


 שורה 1: ה-class כאמור   extends MapActivity.
הפעם הוספנו גם את: implements OnClickListener. המשמעות: ה-callback שמופעל עם לחיצת הכפתור - onClick ימומש כמתודה בתוך ה-class (ראו שורה 14 + הסברים בהמשך). שימו לב, זהו מימוש מעט שונה מזה שהשתמשנו בו בחלק מהדוגמאות הקודמות.
 לגבי שורות 17-24: שתי המתודות הנ"ל: isLocationDisplayed ו- isRouteDisplayed חייבות להיות ממומשות עבור classes של ה-MapActivity. זה חוק מחייב מעוגן ברישיון של גוגל, שנועד לאפשר לשרתים שלהם לעקוב אחרי אפליקציות אלה. מה הם זוממים שם??
  isLocationDisplayed - יחזיר true אם האובייקט מציג את מיקומו על סמך שימוש בסנסורים - למשל GPS. במקרה שלנו נחזיר false.
isRouteDisplayed - יחזיר true אם מוצגת אינפורמציה של הכוונת נתיב (routing), למשל - הוראות כיוון נסיעה.  גם כאן נחזיר false.



המתודה בה מתבצעת עיקר הפעילות היא onCreate, נציג אותה שלמותה:


  1.     @Override
  2.     protected void onCreate(Bundle savedInstanceState)
  3.     {
  4.         super.onCreate(savedInstanceState);
  5.         setContentView(R.layout.main);
  6.         mapView = (MapView)findViewById(R.id.geoMap);
  7.         mapView.setBuiltInZoomControls(true);
  8. // Coordinates of Paris:        
  9.         int lat = (int)(48.856826*1000000);
  10.         int lng = (int)(2.351317*1000000);
  11.         GeoPoint loc = new GeoPoint(lat,lng);
  12.         mapView.getController().setZoom(15);
  13.         mapView.getController().setCenter(loc);
  14.         controlButtonManage();
  15.      }

שורה 6 שולפת את האלמנט MapView .
שורה 7, מפעילה את הכפתור המובנה במפה לשליטה בזום. אמנם הוספנו כפתור זום משלנו אך הוא די מיותר. למעשה, הוספנו אותו כדי להציג את המתודה של הזום.
שורה 11 יוצרת את האובייקט  loc מסוג GeoPoint. הוא מחזיק מיקום על המסך על סמך קואורדינטות. במקרה הנ"ל הכנסנו את הקואורדינטות של פאריס.
שורה 12 קובעת את רמת הזום שתוצג.
שורה 13 מעבירה את תצוגת המפה לפי loc.

זה עיקר העניין. שורה 14 מפעילה מתודת שרות שמפעילה את הכפתורים ומחברת אותם לפונקציונליות שלהם. הנה המתודה:
  1.     private void controlButtonManage(){        buttonSatMap         = (Button)findViewById(R.id.button_sat_map); 
  2.         buttonZoomIn         = (Button)findViewById(R.id.button_zoom_in); 
  3.         buttonZoomOut         = (Button)findViewById(R.id.button_zoom_out);
  4.         buttonClick         = (Button)findViewById(R.id.button_clickable);
  5.         buttonSatMap.setOnClickListener(this);
  6.         buttonZoomIn.setOnClickListener(this);
  7.         buttonZoomOut.setOnClickListener(this);
  8.         buttonClick.setOnClickListener(this);
      
    }

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



  1.     public void onClick(View view) {
  2.         switch(view.getId()){
  3.             case     R.id.button_sat_map:
  4.                 if(mapView.isSatellite())
  5.                     mapView.setSatellite(false);
  6.                 else
  7.                     mapView.setSatellite(true);
  8.             break;
  9.             case    R.id.button_zoom_in:
  10.                 mapView.getController().setZoom(mapView.getZoomLevel() + 1);
  11.             break;
  12.             case    R.id.button_zoom_out:
  13.                 mapView.getController().setZoom(mapView.getZoomLevel() - 1);
  14.             break;
  15.             case    R.id.button_clickable:
  16.                 if(mapView.isClickable())
  17.                     mapView.setClickable(false);
  18.                 else
  19.                     mapView.setClickable(true);
  20.             break;
  21.         }
  22.     }


לא נתעכב עליה יותר מדי. בשורה 2 מתבצע פיצול פי הכפתור שנלחץ. בהמשך כל כפתור מקבל טיפול בהתאם. שימו לב למתודות המטפלות בהצגת תמונת לווין (satellite), זום, ו-click.
הקלק להורדת קבצי הפרויקט.







14 תגובות:

  1. היי , יש לך טעות בהגדרת קובץ הXML השם שלו לא נכון לפי ההערה שלך.

    ורציתי להוסיף שיש עוד דרך להעלות מפה (בגלל שישראל לא מופיעה)

    Intent browserIntent = new Intent("android.intent.action.VIEW", Uri.parse("geo:0,0?q=my+street+address"));
    startActivity(browserIntent);

    השבמחק
  2. הי לאוניד, לגבי הצעתך להעלאת המפה עם browser: בהחלט רעיון טוב להצגת מפה בהנתן שהמפות של ישראל לא זמינות ב-Google API. מובן שהיכולות של ה-API חסרות כאן.אני מתכנן "אחד קטן" על הצגת מפות וגם street view.אולי בזכות ההערה שלך אזרז את זה.

    השבמחק
  3. נ.ב.
    לגבי הערתך על הטעות בקובץ ה-xml: אתה מתייחס לעובדה שה-comment בתוך הקובץ לא תואם את השם האמיתי? אם כן, אשתדל לתקן.

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

    השבמחק
  5. מצאתי איך אפשר להציג דרך מאיפה לאיפה :

    String addr = http://maps.google.com/maps?saddr=48.856826,2.351317&daddr=my+street+address

    Intent browserIntent = new Intent("android.intent.action.VIEW", Uri.parse(addr));
    startActivity(browserIntent);

    אם יהיה צורך אני יכול להוסיף איך מוציאים את את המיקום מהGPS

    השבמחק
  6. הי לאוניד, תודה על התוספת. ה-intent מפעיל הצגת המפה ב-browser או ב-maps.

    השבמחק
  7. הבחירה היא בידי המשתמש , אחרי הקריאה המערכת מזהה איזה אפליקציה יכולות להפעיל את הקישור וניתנת אופציה למשתמש לבחור איך הוא מפעיל , כאשר בין הבחירות (אצלי לפחות) יש MAPS , WAZE , BROWSER .

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

    השבמחק
  9. אתה יכול לקלות את הנקודה עלידי GPS , ולהכניס לשאילתה :
    http://maps.google.com/?q=48.856826,2.351317

    השבמחק
  10. אולי שאלתי לגבי ה-FAVORITES לא הובנה: כשהמשתמש בוחר נקודה מתוך רשימת ה-FAVORITES שלו ומבקש לקבל אליה הוראות הכוונה מאפליקציית המפה, באותו רגע גם אפליקציה שלי רוצה לקבל את מיקום ה-FAVORITE הנ"ל.

    השבמחק
  11. יש לי בעייה עם קבלת מפתח עבור GOOGLE MAPS: במערכת הפעלה WINDOWS 7, לאחר טעינת JDK1.6.0_23 וגם בגירסא 24, כאשר אני מבצע את פקודת ה-KEYTOOL כדי לקבל מפתח שישמש את האנדרויד בגישה למפות, אני מקבל הודעה כי הקובץ KEYSTORE לא נמצא (הוא נמצא, ואני נותן את ה-PATH המלא אליו בפקודת ה- KEYTOOL) או שהתשובה היא שהקובץ פגום/התעסקו איתו. לא עזר להתקין מחדש את ה- JDK. להלן פקודת ה-KEYTOOL שאני מקיש ב- COMMAND PROMPT. כמובן שקודם אני משנה את הסיפרייה ל: c:\program files\java\jdk1.6.0_24\bin
    להלן פקודת ה-KEYTOOL:
    keytool.exe -list -alias androiddebugkey -keystore "c:\program files\java\jdk1.6.0_24\ jre\jmx\jmx-samples\.keystore" -keypass android -storepass android
    מה עושים?

    השבמחק
  12. הי בועז,
    נסה להוריד את המרכאות.

    השבמחק
  13. זה לא עזר כיוון שעתה, בגלל ששם הספריה PROGRAM FILES מורכב משתי מילים, המחשב מתייחס לשם ספרייה רק החל מה-FILES

    השבמחק
  14. תעתיק את ה.keystore ל C , ואז רק
    c:\.keystore

    השבמחק