Статьи

Android SDK: работа с Google Maps — отображение достопримечательностей

С помощью Карт Google в ваших приложениях для Android вы можете предоставлять пользователям функции локализации, такие как географическая информация. На протяжении всей этой серии мы создавали приложение для Android, в котором Google Maps Android API v2 сочетается с Google Places API. До сих пор мы отображали карту, на которой пользователь может видеть свое текущее местоположение, и мы отправили запрос в Google Адреса, чтобы получить данные о близлежащих достопримечательностях. Это требовало настройки доступа API для обеих служб. В заключительной части серии мы проанализируем данные Google Places JSON и используем их, чтобы показать пользователю близлежащие достопримечательности. Мы также заставим приложение обновлять маркеры при изменении местоположения пользователя.

Это последняя из четырех частей серии руководств по использованию карт Google и Google Places в приложениях для Android :

Приложение, к которому мы работаем
Это снимок финального приложения.

Вам нужно будет добавить следующие операторы импорта в ваш класс Activity для этого урока:

1
2
3
4
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.util.Log;

В последнем уроке мы создали внутренний класс AsyncTask для обработки выборки данных из Google Places в фоновом режиме. Мы добавили метод doInBackground для запроса и получения данных. Теперь мы можем реализовать метод onPostExecute для анализа строки JSON, возвращаемой из doInBackground, внутри вашего класса AsyncTask после метода doInBackground :

1
2
3
protected void onPostExecute(String result) {
    //parse place data returned from Google Places
}

Во второй части этой серии мы создали объект «Маркер», чтобы указать последнее записанное местоположение пользователя на карте. Мы также собираемся использовать маркеры, чтобы показать близлежащие достопримечательности. Мы будем использовать массив для хранения этих маркеров. В верхней части объявления класса Activity добавьте следующую переменную экземпляра:

1
private Marker[] placeMarkers;

По умолчанию API Google Places возвращает максимум 20 мест, поэтому давайте определим это также как константу:

1
private final int MAX_PLACES = 20;

Когда мы создаем маркеры для каждого места, мы будем использовать объекты MarkerOptions для настройки деталей маркера. Создайте другую переменную экземпляра массива для них:

1
private MarkerOptions[] places;

Теперь давайте создадим экземпляр массива. В вашем методе Activity onCreate после строки, в которой мы устанавливаем тип карты, создайте массив максимально необходимого размера:

1
placeMarkers = new Marker[MAX_PLACES];

Теперь давайте обратимся к методу onPostExecute, который мы создали. Сначала выполните цикл по массиву маркеров, удалив все существующие маркеры. Этот метод будет выполняться несколько раз, когда пользователь меняет местоположение:

1
2
3
4
5
6
if(placeMarkers!=null){
    for(int pm=0; pm<placeMarkers.length; pm++){
        if(placeMarkers[pm]!=null)
            placeMarkers[pm].remove();
    }
}

Когда код приложения выполняется впервые, будут созданы новые маркеры. Однако, когда пользователь меняет местоположение, эти методы будут выполняться снова для обновления отображаемых мест. По этой причине первое, что мы должны сделать, это удалить все существующие маркеры с карты, чтобы подготовиться к созданию нового пакета.

Мы будем использовать ресурсы Java JSON для обработки полученных данных о месте. Поскольку эти классы выдают определенные исключения, нам необходимо встроить уровень обработки ошибок в этот раздел. Начните с добавления блоков try и catch :

1
2
3
4
5
6
try {
    //parse JSON
}
catch (Exception e) {
    e.printStackTrace();
}

Внутри блока try создайте новый JSONObject и передайте его в результирующую строку JSON, возвращаемую из doInBackground :

1
JSONObject resultObject = new JSONObject(result);

Если вы посмотрите на страницу поиска мест в документации по API Google Адресов, вы увидите пример того, что запрос фактически возвращает в JSON. Вы увидите, что места содержатся в массиве с именем «results». Давайте сначала извлечем этот массив из возвращенного объекта JSON:

1
JSONArray placesArray = resultObject.getJSONArray(«results»);

Вы должны ссылаться на пример результата JSON, когда мы завершаем каждый раздел этого процесса — оставляйте страницу открытой в браузере, пока вы завершаете оставшуюся часть руководства. Далее давайте создадим экземпляр массива MarkerOptions, который мы создали, с длиной возвращенного массива «results»:

1
places = new MarkerOptions[placesArray.length()];

Это должно дать нам объект MarkerOptions для каждого возвращаемого места. Добавьте цикл для перебора массива мест:

1
2
3
4
//loop through places
for (int p=0; p<placesArray.length(); p++) {
    //parse each place
}

Теперь мы можем проанализировать данные для каждого возвращенного места. Внутри цикла for мы создадим детали для передачи в объект MarkerOptions для текущего места. Это будет включать в себя широту и долготу, название места, тип и окрестности, что является отрывком из данных адреса для этого места. Мы получим все эти данные из JSON Google Адресов, передав их Маркеру для этого места через объект MarkerOptions . Если какое-либо из значений отсутствует в возвращенном фиде JSON, мы просто не будем отображать маркер для этого места в случае исключений. Чтобы отслеживать это, добавьте логический флаг:

1
boolean missingValue=false;

Теперь добавьте локальные переменные для каждого аспекта места, которое нам нужно найти и передать маркеру:

1
2
3
4
LatLng placeLL=null;
String placeName=»»;
String vicinity=»»;
int currIcon = otherIcon;

Мы создаем и инициализируем объект LatLng для широты и долготы, строки для названия места и окрестности и первоначально устанавливаем иконку для использования значка по умолчанию, который мы создали. Теперь нам нужен еще один блок try , чтобы мы могли определить, действительно ли отсутствуют какие-либо значения:

1
2
3
4
5
6
7
try{
    //attempt to retrieve place data values
}
catch(JSONException jse){
    missingValue=true;
    jse.printStackTrace();
}

Мы устанавливаем флаг отсутствующего значения в true для проверки позже. Внутри этого блока try теперь мы можем попытаться получить требуемые значения из данных места. Начните с инициализации логического флага значением false, предполагая, что пропущенных значений нет, пока мы не обнаружим иное:

1
missingValue=false;

Теперь получите текущий объект из массива place:

1
JSONObject placeObject = placesArray.getJSONObject(p);

Если вы посмотрите на пример данных поиска места, вы увидите, что каждый раздел места содержит раздел «геометрия», который, в свою очередь, содержит раздел «местоположение». Здесь находятся данные о широте и долготе этого места, поэтому получите их сейчас:

1
JSONObject loc = placeObject.getJSONObject(«geometry»).getJSONObject(«location»);

Попытайтесь прочитать данные широты и долготы из этого, обращаясь к значениям «lat» и «lng» в JSON:

1
2
3
placeLL = new LatLng(
    Double.valueOf(loc.getString(«lat»)),
    Double.valueOf(loc.getString(«lng»)));

Затем получите массив «types», который вы можете увидеть в примере JSON:

1
JSONArray types = placeObject.getJSONArray(«types»);

Подсказка. Мы знаем, что это массив в том виде, в котором он отображается в фиде JSON в окружении символов «[» и «]». Мы рассматриваем любые другие вложенные разделы как объекты JSON, а не как массивы.

Перебрать массив типов:

1
2
3
for(int t=0; t<types.length(); t++){
    //what type is it
}

Получите строку типа:

1
String thisType=types.get(t).toString();

Мы собираемся использовать определенные значки для определенных типов мест (еда, бар и магазин), поэтому добавьте условие:

01
02
03
04
05
06
07
08
09
10
11
12
if(thisType.contains(«food»)){
    currIcon = foodIcon;
    break;
}
else if(thisType.contains(«bar»)){
    currIcon = drinkIcon;
    break;
}
else if(thisType.contains(«store»)){
    currIcon = shopIcon;
    break;
}

Список типов для места может на самом деле содержать более одного из этих мест, но для удобства мы просто будем использовать первое из найденных. Если список типов для места не содержит ни одного из них, мы оставим его с отображением значка по умолчанию. Помните, что мы указали эти типы в строке запроса URL поиска по месту:

1
food|bar|store|museum|art_gallery

Это означает, что единственными типами мест, использующими значок по умолчанию, будут музеи или художественные галереи, поскольку это единственные другие типы, которые мы просили.

После цикла по массиву типов извлеките данные о окрестности:

1
vicinity = placeObject.getString(«vicinity»);

Наконец, найдите название места:

1
placeName = placeObject.getString(«name»);

После блока catch, в котором вы устанавливаете флаг missingValue в значение true, проверьте это значение и установите для объекта MarkerOptions значение null, чтобы мы не пытались создавать экземпляры объектов Marker с отсутствующими данными:

1
if(missingValue) places[p]=null;

В противном случае мы можем создать объект MarkerOptions в этой позиции в массиве:

1
2
3
4
5
6
else
    places[p]=new MarkerOptions()
    .position(placeLL)
    .title(placeName)
    .icon(BitmapDescriptorFactory.fromResource(currIcon))
    .snippet(vicinity);

Теперь, в конце onPostExecute после внешних блоков try и catch , переберите массив MarkerOptions , создайте для него экземпляр Marker, добавьте его на карту и сохраните ссылку на него в созданном нами массиве:

1
2
3
4
5
6
7
if(places!=null && placeMarkers!=null){
    for(int p=0; p<places.length && p<placeMarkers.length; p++){
        //will be null if a value was missing
        if(places[p]!=null)
            placeMarkers[p]=theMap.addMarker(places[p]);
    }
}

Хранение ссылки на Маркер позволяет нам легко удалить ее при обновлении мест, как мы реализовали в начале метода onPostExecute . Обратите внимание, что мы включаем два условных теста каждый раз, когда этот цикл повторяется, в случае, если Поиск по месту не вернул все 20 мест. Мы также проверяем, имеет ли значение MarkerOptions значение NULL, что указывает на отсутствие значения.

Наконец, мы можем создать экземпляр класса AsyncTask и выполнить его . В вашем методе updatePlaces после существующего кода, в котором мы создали строку поискового запроса, запустите эту фоновую обработку, чтобы получить данные о месте, используя эту строку:

1
new GetPlaces().execute(placesSearchStr);

Вы можете запустить свое приложение сейчас, чтобы увидеть его в действии. Он должен отображать ваше последнее записанное местоположение вместе с близлежащими достопримечательностями. Цвета, которые вы видите на маркерах, будут зависеть от мест возврата. Вот приложение, отображающее местоположение пользователя в центре города Глазго, Великобритания:

Приложение в Глазго

Возможно неудивительно, что много мест, перечисленных в Глазго, являются барами.

Когда пользователь нажимает на маркер, он увидит название места и информацию об фрагменте:

Маркер для постукивания в Глазго

Приложение в том виде, в каком оно есть, будет запущено один раз при запуске. Давайте встроим функциональность, необходимую для его обновления, чтобы отразить изменения в местоположении пользователя, одновременно обновляя маркеры соседних мест.

Измените начальную строку объявления класса Activity, чтобы он реализовал интерфейс LocationListener, чтобы мы могли обнаруживать изменения в местоположении пользователя:

1
public class MyMapActivity extends Activity implements LocationListener {

Прослушиватель местоположения может реагировать на различные изменения, каждое из которых использует отдельный метод. Внутри класса Activity реализуйте эти методы:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@Override
public void onLocationChanged(Location location) {
    Log.v(«MyMapActivity», «location changed»);
    updatePlaces();
}
@Override
public void onProviderDisabled(String provider){
    Log.v(«MyMapActivity», «provider disabled»);
}
@Override
public void onProviderEnabled(String provider) {
    Log.v(«MyMapActivity», «provider enabled»);
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
    Log.v(«MyMapActivity», «status changed»);
}

Единственное, что нас действительно интересует, это первое, которое указывает, что местоположение изменилось. В этом случае мы снова вызываем метод updatePlaces . В противном случае мы просто выписываем сообщение журнала.

В конце метода updatePlaces добавьте запрос в приложение для получения обновлений местоположения:

1
locMan.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 30000, 100, this);

Мы используем диспетчер местоположений, который мы создали ранее в этой серии, для запроса обновлений с использованием сетевого провайдера с задержкой в ​​30 секунд (указывается в миллисекундах) с минимальным изменением местоположения 100 метров и самого класса Activity для получения обновлений. Конечно, вы можете изменить некоторые параметры в соответствии со своими потребностями.

Совет. Хотя метод requestLocationUpdates указывает минимальное время и расстояние для обновлений, в действительности он может привести к тому, что метод onLocationChanged будет выполняться гораздо чаще, что имеет серьезные последствия для производительности. Поэтому в любых приложениях, которые вы планируете выпустить пользователям, вы должны ограничивать частоту, с которой ваш код реагирует на эти обновления местоположения. Возможно, стоит рассмотреть альтернативный метод requestSingleUpdate, используемый по времени.

И последнее, но не менее важное: нам нужно позаботиться о том, что происходит, когда приложение останавливается и возобновляет работу. Переопределите два метода следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@Override
protected void onResume() {
    super.onResume();
    if(theMap!=null){
        locMan.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 30000, 100, this);
    }
}
 
@Override
protected void onPause() {
    super.onPause();
    if(theMap!=null){
        locMan.removeUpdates(this);
    }
}

Мы проверяем объект GoogleMap перед попыткой какой-либо обработки, как в onCreate . Если приложение приостанавливается, мы запрещаем ему запрашивать обновления местоположения. Если приложение возобновляет работу, мы снова начинаем запрашивать обновления.

Совет: в этой серии мы несколько раз использовали LocationManager.NETWORK_PROVIDER . Если вы изучаете функциональность локализации в своих приложениях, воспользуйтесь альтернативным методом getBestProvider, с помощью которого вы можете указать критерии для Android для выбора поставщика на основе таких факторов, как точность и скорость.


Это в значительной степени завершает приложение! Однако есть много аспектов API Карт Google Android v2, которые мы даже не затронули. После запуска приложения вы можете экспериментировать с такими функциями, как поворот и наклон. Обновленный сервис карт отображает внутренние и трехмерные карты в определенных местах. На следующем рисунке показано 3D-приложение с приложением, если пользователь находился в Венеции, Италия:

3D карта

Это имеет тип карты, установленный на нормальный — вот другой вид Венеции с установленным типом гибридной карты:

Гибридная карта в Венеции

В этой серии руководств мы проработали процесс интеграции Google Maps и API Google Адресов в одном приложении Android. Мы обрабатывали доступ к API-ключам, настраивали среду разработки, рабочее пространство и приложение для использования Сервисов Google Play. Мы использовали данные о местоположении, показывая местоположение пользователя вместе с близлежащими достопримечательностями и отображая данные с элементами пользовательского интерфейса. Хотя то, что мы рассмотрели в этой серии, довольно обширно, на самом деле это только начало, когда речь идет о внедрении функций локализации в приложения для Android. С выпуском версии 2 API Карт приложения Android настроены на переход таких функций на новый уровень.