יום ראשון, פברואר 27

שרותי מיקום - GPS

גי.פי.אס - המצאה כ"כ חכמה.  גי.פי.אס בסמארטפון - זה כבר ממש גאוני.
בפוסט הזה נדגים טיפול בשרותי מיקום,  שמסופקים ע"י ה-GPS וגם ע"י ה-AGPS ורשת המחשבים.
למי שאינו מכיר את ה-Map API, מומלץ לקרוא  את הפוסטים שעסקו במפות וב-Maps Overlay.

אפליקציה שמשתמשת ב-GPS צריכה להכיל את המרכיבים הבאים:
  1. איתחול ה-GPS - ה-setup. בתהליך זה יבחר ספק שרותי המיקום, עפ"י הקריטריונים אותם נבחר.
  2. הגדרת פעולת עידכון המיקום: הגדרת ה-LocationListener וחיבורו לאפליקציה.
  • בדוגמא הבאה נציג את נתוני  המיקום שיתקבלו מה-GPS על גבי המפה עם סימון של overlays.
  • בשונה מה-overlay שהצגנו בעבר, כאן לא נציב Drawable אלא נצייר עיגול קטן.
  • ליד העיגול נוסיף טקסט. הטקסט יכיל  את המספר הסידורי של הנקודה.
  • בעזרת ה-onKeyDown, נממש מתודה שתמחוק את הנקודה האחרונה מהמפה עם לחיצה על המקש "d".
הנה תצלום של מסך עם נקודות שהועברו מה-GPS- טיול בקליפורניה:






ונעבור לקוד התוכנית. נציג 4 קבצים:
  1. ה-Manifest - כולל מספר הרשאות דרושות.
  2. Layout - כולל את ה-Key עבור ה-Map API.
  3. class ה-JAVA הראשי - ה-MapActivity
  4. class המטפל ב-overlay.


קובץ המניפסט.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.ahs.gpsandmaps"
      android:versionCode="1"
      android:versionName="1.0">
    <application android:icon="@drawable/icon" android:label="@string/app_name">
        <uses-library android:name="com.google.android.maps"/>
    
        <activity android:name=".GpsFindLocation"
                  android:label="@string/app_name">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>
     <uses-permission android:name="android.permission.INTERNET"/>
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    
    <uses-sdk android:minSdkVersion="8" />

</manifest> 


דגשים  בקובץ הנ"ל:
רישיון לשימוש במפות - צבוע אדום.
רישיון לגישה לאינטרנט - צבע סגול (?).
רישיון לשרותי המיקום, בכחול:
ACCESS_FINE_LOCATION= GPS,
ACCESS_COARSE_LOCATION =Network, Cell-ID. 


קובץ ה-Layout


  1. <?xml version="1.0" encoding="utf-8"?>
  2. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  3.     android:orientation="vertical"
  4.     android:layout_width="fill_parent"
  5.     android:layout_height="fill_parent"
  6.     >
  7.        <com.google.android.maps.MapView
  8.     android:id="@+id/map_view"
  9.     android:layout_width="fill_parent"
  10.     android:layout_height="fill_parent"
  11.     android:enabled="true"
  12.     android:clickable="true"
  13.     android:apiKey="put your map key here"
  14.         />
  15.     
  16. </LinearLayout>



הפקת ה-MapKey הוסברה בפוסט של המפות - להכנסה בשורה 13.

וכעת הג'אוות: שני קבצים עם שני classes עיקריים.
נתחיל עם ה-class העיקרי, ה-MapActivity.

 הוא אחראי על:
  • איתחול המערכת - כולל ה-gps,
  • ה-UI-כולל הצגת המפה - לכן הוא MapActivity extention.
  • ה-Location Listeners. 

הנה תוכן הקובץ:



import java.util.List;

import android.content.Context;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.os.Bundle;
import android.view.KeyEvent;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapActivity;
import com.google.android.maps.MapController;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
public class GpsFindLocation extends MapActivity {
MapController mapController;
MapView mapView;
int pointIndex=1;
MapLocationOverlay mapLocationOverlay;
List<Overlay> overlays;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mapSetup();
gpsSetup();
}
private void mapSetup(){
mapView = (MapView)findViewById(R.id.map_view);
mapController = mapView.getController();
mapView.displayZoomControls(true);
mapView.setClickable(true);
mapView.setBuiltInZoomControls(true);
mapController.setZoom(5);
}
private void gpsSetup(){
LocationManager locationManager = (LocationManager)getSystemService( Context.LOCATION_SERVICE);
Criteria criteria = new Criteria();
criteria.setAccuracy(Criteria.ACCURACY_FINE);
criteria.setAltitudeRequired(false);
criteria.setBearingRequired(false);
criteria.setCostAllowed(true);
criteria.setPowerRequirement(Criteria.POWER_LOW);
String provider = locationManager.getBestProvider(criteria, true);
Location location = 
 locationManager.getLastKnownLocation(provider);
markLocationOnMap(location);
locationManager.requestLocationUpdates(provider, 2000, 10,
                                        locationListener);
}
private final LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
markLocationOnMap(location);
 }
 
 public void onProviderDisabled(String provider){
  markLocationOnMap(null);
 }

 public void onProviderEnabled(String provider)
                       { }
 public void onStatusChanged(String provider, int status, 
                             Bundle extras){ }
};
private void markLocationOnMap(Location location) {
if (location != null) {
MapLocationOverlay mapLocationOverlay =  new MapLocationOverlay(location);
overlays = mapView.getOverlays();
overlays.add(mapLocationOverlay);
mapLocationOverlay.setOverlayTipText(Integer.toString(pointIndex++));
Double geoLat = location.getLatitude()*1E6;
Double geoLng = location.getLongitude()*1E6;
GeoPoint point = new GeoPoint(geoLat.intValue(),
                             geoLng.intValue());
mapController.animateTo(point);
}
}
@Override
protected boolean isRouteDisplayed() {
return false;
}
private void lastOverlayDel(){
if(overlays.size() > 0){
overlays.remove(overlays.size()-1);
pointIndex--;
mapView.postInvalidate();
}
}
public boolean onKeyDown(int keyCode, KeyEvent event) {
if ( keyCode == KeyEvent.KEYCODE_D) {
    lastOverlayDel();
    return true;
   }
   return false;
}
}


הנה מעט אוריינטציה  נתחיל ב-onCreate:

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mapSetup();
gpsSetup();
}


מופעלות שתי מתודות עזר:
mapSetup -  איתחול המפות.
gpsSetp -  איתחול ה-gps.


נתייחס למתודה השניה. הנה היא שוב:

  1. private void gpsSetup(){
  2. LocationManager locationManager = (LocationManager)getSystemService( Context.LOCATION_SERVICE);
  3. Criteria criteria = new Criteria();
  4. criteria.setAccuracy(Criteria.ACCURACY_FINE);
  5. criteria.setAltitudeRequired(false);
  6. criteria.setBearingRequired(false);
  7. criteria.setCostAllowed(true);
  8. criteria.setPowerRequirement(Criteria.POWER_LOW);
  9. String provider = locationManager.getBestProvider(criteria, true);
  10. Location location = 
  11.   locationManager.getLastKnownLocation(provider);
  12. markLocationOnMap(location);
  13. locationManager.requestLocationUpdates(provider, 2000, 10,
  14.                                          locationListener);
  15. }
  • המתודה מחולקת ל4 חלקים, המובדלים בצבע הרקע:
  • החלק הראשון - שורה 2- שליפת האובייקט של ה-LocationManager. זהו ה-class שמטפל בנושא שרותי המיקום. בעזרתו נבצע  את הפעולות הבאות.
  • החלק השני - שורה 4-10 - קביעת שרותי המיקום. עד שורה 9, מוכנסים הקריטריונים הדרושים. בשורה 10 שולפים את שם נותן השרות. במקרה הנ"ל התוצאה שהתקבלה כמצופה היא: provider = gps.
  • החלק השלישי - 11-13 - העברה לתצוגה של המיקום האחרון שקיים ב-GPS. לא ברור אם המיקום הנ"ל רלוונטי, אבל זה מה שיש בינתיים בנמצא:
    •  שורה 11 שולפת את המיקום האחרון.
    • שורה 13 מפעילה את מתודת העזר markLocationOnMap, בה נשתמש להצגת מיקום על גבי המפה. מיד נפרט אותה.
  • החלק הרביעי- שורות 14-15 - הפעלת ה-LocationListener. המתודה נ"ל מקבלת את הפרמטרים הבאים:
    • provider - מצאנו אותו למעלה.
    • הזמן המינימלי במילישניה בין עידכוני מיקום. פרמטר זה רק מהווה אינדיקציה לגבי הדרישה לחיסכון בהספק. זמן העידכון בפועל עשוי להיות גדול או קטן מזה.
    • המרחק המינימלי במטרים שצריך לעבור בין עידכוני מיקום.
    • ה-LocationListener Class שמחובר למערכת הנ"ל. בכל פעם שיגיע עידכון מקום, תופעל המתודה onLocationChanged של ה-class הזה. מיד נגיע אליו.

הגענו ל-LocationListener. ממומשות בו 4 מתודות callback:
  1. onLocationChanged - מופעלת עם כל עידכון מיקום.
  2. onProviderDisabled - ספק שרותי מקום חדל לפעול.
  3. onProviderEnabled - ספק שרותי מקום התחיל לפעול.
  4. onStatusChanged - מופעלת כש-provider משנה מצב ל-enabled או disabled. ה-extras כוללים זוגות של key/value. הערך הידוע כרגע key=satellites, מחזיר את מס הלויינים על פיהם מתבצע האיכון.
הנה ה-LocationListener שוב:


private final LocationListener locationListener = new LocationListener() {
public void onLocationChanged(Location location) {
markLocationOnMap(location);
 }
 
 public void onProviderDisabled(String provider){
 
 }

 public void onProviderEnabled(String provider){ }
 public void onStatusChanged(String provider, int status, 
                             Bundle extras){ }
};


עד כאן ה-LocationListener.

המתודה הבאה: markLocationOnMap.
המתודה הזו תופעל בכל מקרה של שינוי מיקום - מתוךonLocationChanged - וגם בהתחלת הפעולה מתוך gpsSetup. 
תפקידה - העברת יצירת התצוגה על המפה.

הנה המתודה:

  1. private void markLocationOnMap(Location location) {
  2. if (location != null) {
  3. MapLocationOverlay mapLocationOverlay =  new MapLocationOverlay(location);
  4. overlays = mapView.getOverlays();
  5. overlays.add(mapLocationOverlay);
  6. mapLocationOverlay.setOverlayTipText(Integer.toString(pointIndex++));
  7. Double geoLat = location.getLatitude()*1E6;
  8. Double geoLng = location.getLongitude()*1E6;
  9. GeoPoint point = new GeoPoint(geoLat.intValue(),
  10.                               geoLng.intValue());
  11. mapController.animateTo(point);
  12. }
  13. }
שורות 3-6: יצירת אובייקט של ה-overlay class.  על מנת לשייך את ה-overlay הזה למפה, צריך  להוסיף אותו לרשימת ה-overlays הכללית של המפה: שורה 4 שולפת את הרשימה, שורה 5 מוסיפה את ה-overlay החדש. שורה 6 מפעילה מתודה של ה-overlay class, בה מועבר האינדקס של הנקודה. מדובר פשוט באינדקס רץ שיוצג ליד הנקודה על גבי המפה - נראה את בניית התצוגה כשנעבור על ה-overlay class.
שורות 7-9: המטרה - העברת תצוגת המפה למקום החדש. לצורך זה מבצעים התמרהמהקורדינטות שנמסרו מה-GPS לנקודה מסוג -GeoPoint.
שורה 11: ה-MapController מזיז את התצוגה לפי מיקום הנקודה.

עד כאן המתודה markLocationOnMap.
ה-class הנ"ל מכיל עוד מספר מתודות קטנות:
  • isRouteDisplayed - מתודה שחייבים לממש ב-class מסוג MapActivity. אם ניתן שרות Route, למשל מתן כיווני דרך, יש להחזיר true.
  • lastOverlayDel - מוחקת את ה-overlay האחרון שהוסף למפה. המתודה מופעלת מתוך onKeyDown. שימו לב לקריאה ל-postInvalidate, לצורך עידכון מיידי של התצוגה.
  • onKeyDown - זהו callback שמופעל עם לחיצה על מקשי המקלדת. במקרה של לחיצה על האות 'd', תופעל lastOverlayDel.


 נעבור ל-overlay class , שנמצא ב קובץ הג'אווה השני.


import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.location.Location;

import com.google.android.maps.GeoPoint;
import com.google.android.maps.MapView;
import com.google.android.maps.Overlay;
import com.google.android.maps.Projection;

  1. public class MapLocationOverlay extends Overlay {
  2. Location location;
  3. String mOverlayText = "No Text";
  4. public MapLocationOverlay(Location location) {
  5. this.location = location;
  6. }
  7. @Override
  8. public boolean onTap(GeoPoint point, MapView mapView) {
  9. return false;
  10. }
  11. @Override
  12. public void draw(Canvas canvas, MapView mapView, boolean shadow) {
  13. if(null != location){
  14. Double latitude = location.getLatitude()*1E6;
  15. Double longitude = location.getLongitude()*1E6;
  16. GeoPoint geoPoint = new GeoPoint(latitude.intValue(),longitude.intValue());
  17.      Projection projection = mapView.getProjection();
  18. Point point = new Point();
  19.      projection.toPixels(geoPoint, point);
  20.    Paint paint = new Paint();
  21.    paint.setARGB(250, 0, 0, 0);
  22.    paint.setAntiAlias(true);
  23.    paint.setFakeBoldText(true);
  24. final int markRadius = 2;
  25.    canvas.drawCircle(point.x,point.y, markRadius, paint);
  26.    canvas.drawText(mOverlayText, point.x, point.y - 2*markRadius , paint);
  27.  }
  28.  super.draw(canvas, mapView, shadow);
  29. }
  30. public void setOverlayTipText(String overlayText){
  31.  mOverlayText = overlayText;
  32. }
  33. }

שלא כמו בפוסט הקודם,  ה-class הוא מסוג: MapLocationOverlay extends Overlay. בפעם הקודמת התמשנו ב-class מסוג  ה- ItemizedOverlay. 
הסיבה להבדל- ה-overlays לא יהיו Drawables אלא נייצר אותם בתוך ה-class, כך שאפשר בקלות לגוון. 

נתחיל עם ה-constructor. הוא מקבל ושומר את ה-Location עליו יצויר ה-overlay.
  1. public MapLocationOverlay(Location location) {
  2. this.location = location;
  3. }


נעבור למתודה העיקרית ב-class: ה-draw, שמציירת את ה-overlays.
החלק הראשון של המתודה - שורות 14-19: תרגום הקואורדינטות של ה-GPS למיקום על גבי הפיקסלים שעל הצג. זה נעשה בשני שלבים:
המרה ל-GeoPoint, כפי שעשינו תמיד כדי לקבל מיקום על המפה.
projection.toPixels: תרגום בין ה-GeoPoint למיקום בפיקסלים שעל הצג.
כעת האובייקט point מחזיק את המיקום על גבי הצג בו נצב את ה-overlay.
החלק השני של המתודה - שורות 20-26 - ציור על המפה. נצייר עיגול שחור (שורה 25), ונוסיף טקסט שיציג את האינדקס של הנקודה (שורה 26). 
שורה 28 - ה-super constructor.
עד כאן draw.
נשארה מתודת עזר קטנה - setOverlayTipText, דרכה מועבר הטקסט שהוספנו ל-overlay. היה אפשר להכניס את הפרמטר ב-constructor.

הכנסת נתוני מיקום ב-Emulator
אפשר לדמות הכנסת נתונים ע"י ה-DDMS - ראשי תבות של Dalvik Debug Monitor Server. ה-DDMS משולב בתוך ההתקנה של ה-Eclipse.
הפעלה מתוך Eclipse:
Window->Open Perspective->DDMS. בנוסף יש כפתור open perspective ב-toolbar.
אחרי הפעלת ה-DDMS אפשר להכניס נתוני מיקום מה-Emulator Control. הנה צילום המסך:






אפשר להכניס נקודה בודדת באופן ידני, או רשימה של נקודות מתוך קבצי GPX או KML.


קישור להורדת קבצי הפרויקט. הערה: לא לשכוח להכניס את ה-API Key לתוך ה-main.xml. פרטים על יצירת ה-Key בפוסט המפות הראשון.
(בעבודה - הקישור לקבצים יתווסף בהקדם)

נ.ב.
מתכנן להוסיף תוך כמה ימים  ProximityAlert לתוך הדוגמא הנ"ל - TBD.




2 תגובות:

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

    השבמחק
  2. תודה.
    אם אתה מתכנן תוכנה דומה לwaze, אולי כדאי שתעיף מבט על הקוד שלהם.

    השבמחק