понеділок, 30 липня 2012 р.

Google Maps API

Всем привет! Хочу поделиться с вами моим опытом работы с этим API. Значит мне известны два способа отображение карт от гугла, это: при помощи ихнего api и через kml-файл. Сначала пару слов о kml, вы делаете post/get запрос на maps.google.com с соответствующими параметрами и получаете kml (в виде xml) и делаете отображение на SurfaceView, как то так...
Но мы сейчас говорим о api который нам так любезно предоставляет google. Но здесь как всегда есть "но". Первое что смущает, так это у них есть обычная версия и biznes. Чем они отличаются думаю не трудно догодаться, обычная просто урезанная. Второе что возмутило (но это лишь по слухам и мною не проверенно) это то что количество обращение в обычной версии является максимум 5000 запросов (и здесь я не понял, что имеется в виду под запросами, то ли кеширование то ли ещё что то), в общем кто что знает по этому поводу отпишитесь.
Начнём с того что api довольно таки грамотно написано. Первым делом что нам нужно сделать  так это зарегистрировать себя (да-да, вы не раслышали, что бы пользоваться ихними либами нужно сгенерировать ключ и зарегистрировать). Идём в папку
C:\Users\<your username>\.android\
и ищем там файл debug.keystore , а так же находжим папку с keytool в вашем jdk, у меня она расположена вот так:
C:\Program Files\Java\jdk1.7.0_05\bin\
и запускаем всё это дело таким макаром:
 C:\Program Files\Java\jdk1.7.0_05\bin>keytool -list -v -keystore C:\Users\admin\.android\debug.keystore -storepass android -keypass android
 это всё одна команда. После вы увидите на экране несколько ключей, вам нужен только MD5. Копируете его и идёте на страницу https://developers.google.com/maps/documentation/android/maps-api-signup где и вставляете этот ключ. Ставите галочку на соглашение соблюдения лицензии и нажимаете generate. Далее вы увидите ваш сгенерированный ключик
0jZ4KPIPuzHQcoaucLvVk6IveljjqPZenTPdOiw
и сразу вам любезно google пишет как стоит объявить MapView:
<com.google.android.maps.MapView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:apiKey="0jZ4KPIPuzHQcoaucLvVk6IveljjqPZenTPdOiw" />
И так, размещаете этот компонент в main.xml , это и будет ваш единственный компонент в вашем приложении. Далее нужно подправить манифест. Добавте такие строки:
<uses-permission android:name="android.permission.INTERNET" /> - для доступа в интернет
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> - позволяет поулчить доступ к GPS
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> - позволяет получить доступ к Cell-ID, WiFi
потом в application добавте атрибут, который удаляет заголовок:
android:theme="@android:style/Theme.NoTitleBar" 
а так же в application перед activity добавте используемую библиотеку:
<uses-library android:name="com.google.android.maps"/>
Так же главное активити будет наследоваться от MapActivity.
Теперь можите смело запускать приложение. Пока что вы можете перемещаться по карте. И так первое что вам необходимо так это сделать возможность масштибировать карту. Для этого в метод onCreate добавте строку:
mapView.setBuiltInZoomControls(true);
 и на вашей карте появится две кнопочки "+/-".
Теперь пора заглянуть по адресу https://developers.google.com/maps/documentation/android/reference/ Это описание всех классов которые вы можете использовать. Попытаюсь вам как можно подробнее рассказать о каждом классе.
Как мы видим существует три интерфейса, коротко о них:
  • Projection - преобразует координаты экрана в координаты на карте;
  • Overlay.Snappable -  привязывает элементы;
  • ItemizedOverlayOnFocusChangeListener - интерфейс обработчик происходит когда элемент входит в фокус.
 По интерфейсам думаю всё ясно, переходим к классам:

Видим что классов не так уж и много (как я говорил урезанная версия). А теперь подробно о каждом классе:

  • GeoPoint - класс для работы с координатами (они представленны в виде долготы и широты);
  • MapActivity - абстрактный класс который расширяет activity;
  • MapController - класс, служащий для масштабирования и панорамирования карты;
  • MapView - вьюшка для отображения карты;
  • OverlayItem - класс для работы с маркером;
  • TrackballGestureDetector - класс для работы с MotionEvent (его не использовал, так что не могу много о нём сказать, он особо и не нужен);
  • Overlay - базовый класс позволяющий работать с маркерами;
  • MapView.LayoutParams - в основном содержит статик константы для отображения на карте (по центру, с лева и т.д.);
  • ItimizedOverlay - абстрактный класс который содержит список маркеров;
  • MyLocationOverlay - класс для отображения вашего местоположения.
И так теперь переходим к вкусностям. Я рекомендую переопределять все (или хотя бы большинство классов), так как это добавляет гибкости приложению и конечно же это является хорошим тоном программирования.
Начнём с того что бы отобразить нашу текущую позицию. Для этого нужно в первую очередь переопределить класс MyLocationOverlay. В нём нам нужно переопределить только один метод - drawMyLocation:

public class MyCurrentLocation extends MyLocationOverlay {
 
 private Context context;
 private MapView mapView;
 
 public MyCurrentLocation(Context _context, MapView _mapView) {
  super(_context, _mapView);
  mapView = _mapView;
  context = _context;
 }
 
 @Override
 protected void drawMyLocation(Canvas _canvas, MapView _mapView, Location _lastFix, GeoPoint _myLocation, long _when) {
  Point screen = mapView.getProjection().toPixels(_myLocation, null); //переводим GeoPoint в пиксели
  Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.map_marker_red);
  _canvas.drawBitmap(bitmap, screen.x - (bitmap.getWidth()/2), screen.y - (bitmap.getHeight()/2), null); //добавляем маркер
  Log.i("MyTag", "широта: " + _lastFix.getLatitude() + " | долгота: " + _lastFix.getLongitude() + " | speed: " + _lastFix.getSpeed()
    + " | provider: " + _lastFix.getProvider() + " | time: " + new Time(_lastFix.getTime()).getSeconds() + "s");
  }
 
}

И так рассмотрим подробнее что же мы тут написали. В конструкторе передаём текущий контекст и ссылку на MapView. По названию метода ясно что в нём будет отрисовываться ваше местоположение. В нём получаем текущую позицию и приобразовываем в GeoPoint(координаты), далее формируем картинку и отрисовываем её на канве. В лог решил для наглядности вывести текущие координаты, скорость опредиления, провайдера, и время опрдиления. Но для того что бы опредилить ваше местоположение этого не достаточно. Вам ещё нужно зарегистрировать слушатель LocationListener который будет реагировать на изменение вашей точки.

public class MyLocationListener implements LocationListener {
 
 public void onLocationChanged(Location location) {
  //вызывается когда локация изменилась
  Log.i("MyTag", "onLocationChanged - широта: " + location.getLatitude() + " | долгота: " + location.getLongitude() + " | speed: " + location.getSpeed()
    + " | provider: " + location.getProvider() + " | time: " + new Time(location.getTime()).getSeconds() + "s");
 }
 
 public void onProviderDisabled(String provider) {
  //вызывается когда провайдер отключается от пользователя
  Log.i("MyTag", "Provider disabled. GPS is off");
 }
 
 public void onProviderEnabled(String provider) {
  //вызывается когда провайдер включается
  Log.i("MyTag", "Provider enabled. GPS is on");
 }
 
 public void onStatusChanged(String provider, int status, Bundle extras) {
  //вызывается при изменении статуса провайдера
  Log.i("MyTag", "Provider " + provider + " status " + status + " changed");
 }
 
}
Так же переопределяем для больше наглядности класс LocationListener. Как видим мы здесь ничего не меняем, а лишь пишем в лог что бы видеть когда какой метод вызывается. И теперь в MapActivity реализовываем всё выше перечисленное:

  mapView = (MapView) findViewById(R.id.mapview);
  mapView.setBuiltInZoomControls(true); //включает возможность масштабирования карты
  lManager = (LocationManager) getSystemService(LOCATION_SERVICE);
  MyLocationListener myLocationListener = new MyLocationListener();
  lManager.requestLocationUpdates(lManager.getBestProvider(new Criteria(), true), 1, 1000, myLocationListener);
  MyCurrentLocation myLocationOverlay = new MyCurrentLocation(this, mapView);
  mapView.getOverlays().add(myLocationOverlay);
  myLocationOverlay.runOnFirstFix(new Runnable() {
   public void run() {
    mapView.getController().animateTo(myLocationOverlay.getMyLocation());
   }
  });
  mapView.setReticleDrawMode(MapView.ReticleDrawMode.DRAW_RETICLE_UNDER);
Теперь разжуём. Метод requestLocationUpdates имеет пять разных вариантов опредиления, мы используем следующий: первым параметром передаём провайдера (в данном случае передаём лучшего, но на практике могу с казать что не всегда возвращает лучшего), 1 означает минимальное время изменения в милисекундах (так как мы тестируем на эмуляторе и интернет у нас хороший то я поставил 1 милисекунду, но если вы будете тестировать на реальном девайсе то ставте хотя бы минуту, тем более если у вас мобильный интернет), третий параметр  это дистанция в метрах через которую будет происходить обновление, и последний параметр собственно наш обработчик. Далее методом add добавляем к MapView маркер (собственно тот маркер который отображает наше местоположение). Так же стоит остановиться на методе runOnFirstFix, при изменении локации изменения на карте будет происходить в отдельном потоке.
Ещё нужно переопределить методы onPause() и onResume() в главном активити:

 protected void onPause() {
  super.onPause();
  lManager.removeUpdates(myLocationListener);
  myLocationOverlay.disableCompass();
  myLocationOverlay.disableMyLocation();
 }
 
 protected void onResume() {
  super.onResume();
  lManager.requestLocationUpdates(lManager.getBestProvider(new Criteria(), true), 1, 1000, myLocationListener);
  myLocationOverlay.enableCompass();
  myLocationOverlay.enableMyLocation();
 }
в этих метода как вы видите мы отключаем и возобновляем локационную работу. Нужно ж заботиться о мобильном траффике =)
Что бы помотреть всё в действии в Eclipse нужно открыть окно Emulator Control (Window - Show View - Emulator Control), вкладка Manual поставте галочку на Decimal и введите широту и долготу и нажмите Send.
Теперь попробуем добавить маркер на любую точку. Для этого нам нужно переопределить два класса OverlayItem - собственно сам маркер, и ItemizedOverlay - будет содержать все маркеры которые есть на карте.
public class MyOverlayItem extends OverlayItem {
 
 protected String mTitle, mSnippet;
 protected GeoPoint mPoint;
 protected Drawable mMarker;
 private Context context;
 
 public MyOverlayItem(GeoPoint _point, String _title, String _snippet, Context _context) {
  super(_point, _title, _snippet);
  mTitle = _title;
  mSnippet = _snippet;
  mPoint = _point;
  context = _context;
 }
 
 public Drawable getMarker(int stateBitset) {
  return super.getMarker(stateBitset);
  //возвращает маркер который должен быть использован при stateBitset
 }
 
 public GeoPoint getPoint() {
  return mPoint;
 }
 
 public String getSnippet() {
  return mSnippet;
 }
 
 public String getTitle() {
  return mTitle;
 }
 
 public String routableAddress() {
  return mPoint.getLatitudeE6() + "," + mPoint.getLongitudeE6();
 }
 
 public void setMarker(Drawable marker) {
  mMarker = marker;
 }
 
 public void draw(Canvas canvas, MapView mapView) {
  Point screen = mapView.getProjection().toPixels(mPoint, null); //переводим GeoPoint в пиксели
  Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.map_marker_red);
  canvas.drawBitmap(bitmap, screen.x - (bitmap.getWidth()/2), screen.y - (bitmap.getHeight()/2), null); //добавляем маркер
 }
 
}
 
Ничего сложного. В конструктор передаём точку, шапку и тело подсказки которые отображаются если нажать на маркер, контекст. Остановимся на методе draw - рисует маркер. Первым делом преобразуем координаты в пиксели, загружаем желаемую картинку и рисуем её на канве.

public class MyItemizedOverlay extends ItemizedOverlay<MyOverlayItem> { //класс для работы с маркерами
 
 private ArrayList<MyOverlayItem> mOverlays = new ArrayList<MyOverlayItem>(); //список марекров
 private Context mContext;
 
 public MyItemizedOverlay(Drawable defaultMarker, Context context) {
  //конструктор в который передаём:
  //defaultMarker - картинка маркера;
  //context - текущий контекст
  super(boundCenter(defaultMarker)); //boundCenter ставит координаты маркера (0,0) в центр экрана - то есть маркер будет отображаться в центре вашего экрана
  mContext = context;
  //контекст можно не добавлять, здесь он нужен лишь для отображения диалога
 }
 
 public void addOverlay(MyOverlayItem overlay) {
  mOverlays.add(overlay); //добавляем маркер в колекцию
  populate(); //этот метод читает каждый объект(маркер) который есть в колекции и отрисовывает их на карте
 }
 
 protected MyOverlayItem createItem(int idx) {
  //когда вызывается метод populate() то он будет в свою очередь вызывать метод createItem где по индексу будет вытаскивать маркер
  return mOverlays.get(idx);
 }
 
 public int size() {
  //так же нужно переопределить метод size() который возвращает количество маркеров
  return mOverlays.size();
 }
 
 public void draw(Canvas canvas, MapView mapView, boolean shadow) {
  //shadow - true рисует изображение с тенью
  populate();
  if(!shadow) {
   MyOverlayItem item;
   for(int i=0; i<mOverlays.size(); i++) {
    item = mOverlays.get(i);
    item.draw(canvas, mapView);
   }
  }
 
 }
 
 protected boolean onTap(int index) {
  //этот метод можно переопределять по желанию
  //он срабатывает тогда когда вы нажимаете на маркер
  //в метод передаётся индекс нажатого маркера
  OverlayItem item = mOverlays.get(index);
  AlertDialog.Builder dialog = new AlertDialog.Builder(mContext);
  dialog.setTitle(item.getTitle());
  dialog.setMessage(item.getSnippet());
  dialog.show();
  return true;
 }
 
 public boolean onTouchEvent(MotionEvent event, MapView mapView) {
  GeoPoint point = mapView.getProjection().fromPixels((int)event.getX(), (int)event.getY());
  //Log.i("MyTag", "onTouchEvent event: x=" + event.getX() + " | y=" + event.getY());
  //Log.i("MyTag", "onTouchEvent point: x=" + point.getLatitudeE6() + " | y=" + point.getLongitudeE6());
  return false;
 }
 
}
Здесь поле список в котором хранятся маркеры. Собственно из комментариев в классе всё ясно, остановимся лишь на двух моментах. Первое это стоит запомнить что метод draw вызывается каждый раз когда происходит перерисовка карты, а она происходит почти всегда (когда вы добавляете маркер, удаляете, делаете любые манипуляции с картой будь то масштабирование или перетаскивание). Второй момент это метод populate, насколько я понял он поочерёдно вытаскивает из коллекции "маркер" и отрисовывает его. Его везде нужно вызывать там где идёт какое либо обновление маркеров. В документации сказано что его желательно ставить в начале метода. Убедиться можно убрав вызов populate в каком нибудь из методов то сразу возникнет критическая ошибка.
Теперь переходим в главное активити и пишем та такой метод addOverlay:

 private void addOverlay(GeoPoint point, String title, String snippet) {
  Drawable drawable = this.getResources().getDrawable(R.drawable.map_marker_red); //загружаем из ресурсов маркер
  MyItemizedOverlay itemizedoverlay = new MyItemizedOverlay(drawable, this); //определяем свой объект с маркером
 
  MyOverlayItem overlayitem = new MyOverlayItem(point, title, snippet, this);
  itemizedoverlay.addOverlay(overlayitem);
  mapView.getOverlays().add(itemizedoverlay);
 }
Этот метод можете прикрутить например к кнопке меню. Посмотрим что в нём происходит. Входными параметрами являются координаты, шапка и тело подсказки. В теле метода получаем объект Drawable из ресурсов, определяем методы которые мы переопределили и добавляем к mapView обновлённый список маркеров. Что бы увидить результат добавте в конец метода onCreate такую строчку:

addOverlay(new GeoPoint(49076019, 33.421274), "Hello", "I'm here");

Со всем вроде бы разобрались, но хочется что бы маркер добавлялся по клику на экране. Как такое сделать? Всё казалось бы просто на первый взгляд, взять и определить интерфейс OnTouchListener и переопределить в активити метод onTouch. Но не всё оказалось так просто. Когда я реализовал всё выше перечисленное то добавлялся только первый маркер и всё, дальше даже в лог ничего не передовалось, значит метод даже не вызывался. Немного полистав гугл пришёл к выводу пойти инным путём. Есть такой метод dispatchTouchEvent он непосредственно определён в активити и его нужно лишь переопределить.


 public boolean dispatchTouchEvent(MotionEvent event) {
  int action = event.getAction();
  switch(action) {
   case MotionEvent.ACTION_UP:
    GeoPoint point = mapView.getProjection().fromPixels((int)event.getX(), (int)event.getY());
    Log.i("MyTag", "onTouch point: x=" + point.getLatitudeE6() + " | y=" + point.getLongitudeE6());
    addOverlay(point, "title", "snippet");
    break;
  }
  return super.dispatchTouchEvent(event);
 }
Для убедительности получаем action и проверяем отпущен ли палец. Когда отпускается палец то получаем текущую координату и спокойно передаём её в метод addOverlay.
Скачать проект с вышеперечисленными функциями можно по ссылкам:

http://файлообменник.рф/04jivi1ngtik.html
или
http://uafile.com.ua/get/38152/

Об основных вкусностях Google Maps API я рассказал. Если кому что непонятно - спрашивайте. Если заметили ошибки - пишите, исправлю. Есть предложения - пишите, рассмотрю!
С ув. Вячеслав.