Pull to refresh

Geonames, Google Maps, Geocoding, часовые пояса и все, все, все

Reading time 11 min
Views 23K

Пользователям не хочется разбираться с особенностями координат, часовых поясов. Некоторые даже не знают, как эти координаты выражаются, и что такое часовые пояса.
Как сделать, чтобы было хорошо пользователю?

В данной статье будет разобрано, как работать с координатами и часовыми поясами:
А именно,
  1. Установка Google Maps, рассмотрен небольшой функционал с примером.
  2. Поиск часового пояса с по координатам (Geonames.org).
  3. Поиск координат и часового пояса по названию города (Geonames.org).
  4. Определение названия местности по координатам.




Итак,
Пользователь хочет:

  1. Мало думать.
  2. Мало кликать.


Поэтому мы просто обязаны научить телефон:
  1. Находить координаты и часовой пояс по названию города.
  2. Находить часовой пояс по координатам, если используются карты.
  3. Узнавать текущее положение пользователя.
  4. Узнавать точный адрес по координатам.


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

Введение


Для начала, что такое часовые пояса (часовые зоны). В принципе, это всё есть на Wikipedia.org, но сжато можно посмотреть и тут
Точку на глобусе можно определить при помощи двух координат — широты и долготы. И это вполне себе известный факт. Но с часовыми поясами возникает загвоздка: в любой точке Земли может быть абсолютно любой часовой пояс. Всё зависит от уровня фантазии управленца на той или иной территории.
Например, для ясности, сейчас на Европейской России истинный полдень, которому положено быть в 12:00, появляется почти на 2 часа позже. В день написания статьи истинный полдень — Солнце в зените в Москве — было в 13:38. На некоторых территориях России получается сдвиг прохождения Солнца по зениту аж на 3 часа.
Вернёмся к цифрам. Мировым правительством сообществом был принят за нулевой меридиан — гринвичский. Часовые пояса вычисляются относительно него и следующим образом. Часовой пояс — это сдвиг локального времени в некоторой точке планеты относительно гринвичского меридиана. Весь глобус делится на 24 частей по меридианам. То есть, берутся базовые меридианы 0°, 15°, 30°, и т.п., и от них отсчитываются часовые пояса ± 7,5°. Таким образом, +00:00 часовой пояс — а обозначается он по-разному: и UTC, и GMT, и GMT+00:00 (разница между UTC и GMT есть, но на практике в большинстве случаев это не применяется) — находится от 7,5° з.д до 7,5° в.д., GMT+01:00 — от 7,5° в.д. до 22,5° в.д. и так далее. Но, помимо этого, есть также границы государств, а также некоторые из государств находятся в так называемой средней полосе, где экономически целесообразно делать сдвиг летом на +01:00(обычно). В южном полушарии летний сдвиг происходит, когда в северном зима. В России также было применено декретное время, из-за которого часы были сдвинуты ещё на час вперёд. На морях и океанах используется часовой пояс, который вычисляется по положению обычным образом. А в Антарктиде, вообще, отдельная история, как это дело считать. Из-за всех этих социально-экономических и политических причин часовых поясов на планете накопилось несколько сотен. А если в программе нужно ещё и время использовать (а часовые пояса меняются и во времени), то со всеми этими сдвигами можно запутаться.

Кстати, бывает, что класс Calendar в Java выдаёт ошибку со сдвигом на час, если инициализировать, например, с текущем временем и часовым поясом, а затем установить время до того, как мы перешли исключительно на летнее время, например, 1988г., то календарь будет выводить часы со сдвигом на 1 час. Лечится это, если постоянно при установке новых миллисекунд заново инициализировать календарь. SimpleDateFormat всегда выдаёт подобную ошибку, потому не советую им пользоваться для старых дат.

Ссылка на статью «часовой пояс» в Википедии

Geonames.org — это великолепный ресурс. Там можно как скачать базы данных, так и использовать для этого ресурса родной API. В данной статье я буду использовать как раз API.

Для работы с Geonames нужно регистрировать пользователя, при помощи которого будет доступ к API. А также не забыть его активировать. Для этого зайдите в аккаунт и нажмите там на ссылку: Click here to enable.
Тут документация
Есть даже библиотека для доступа, но библиотеку лично я не использую, потому что иногда возникают ошибки. Напрямую с API через http-запрос работать понятнее.
Лицензия. Можно бесплатно пользоваться этим продуктом.

О Google Maps все знают.
Можно общую справку по использованию Google Maps можно посмотреть тут.

Также есть такая фишка, как GeoCoder. Можно узнавать адрес по координатам. Но часовых поясов, почему-то там нет.
Для поиска часовых поясов по клику я также буду использовать Geonames.org.

Настройка Google Maps


Для новичков кратко алгоритм редактирования AndroidManifest.xml, что откуда брать, чтобы работали карты Google Maps и определение положения телефона.
Кому это не интересно — кликайте сюда, чтобы пролистать сразу к описанию особенностей приложения.

1.
внутри
<manifest>
:


<permission
        android:name="com.example.android.permission.MAPS_RECEIVE"
        android:protectionLevel="signature" />
	<uses-permission android:name="android.permission.INTERNET"/>
	<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
	<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES"/>
	<!-- The following two permissions are not required to use
		 Google Maps Android API v2, but are recommended. -->
	<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
	<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
	
	<uses-feature
        android:glEsVersion="0x00020000"
        android:required="true"/>


2.
внутри
<application>
:

<meta-data
    android:name="com.google.android.gms.version"
    android:value="@integer/google_play_services_version" />
	<meta-data
    android:name="com.google.android.maps.v2.API_KEY"
    android:value="API_KEY"/>


Чтобы получить android:value=«API_KEY» (ссылка, где это в коде), нужно:
  • сюда
  • APIs & auth > Credentials > Create new KEY > Android key
  • в поле записываем следующее SHA-1-KEY;com.example.android.mapexample

Картинки для android:value=«API_KEY»
Картинка 1: Открыть консоль

Картинка 2: Ввести ключ

Картинка 3: получить API_KEY

особенности получения SHA-1-KEY
  • он бывает или debug или release. Получается из файла-сертификата.
  • файл-сертификат для debug — берётся из %USERPROFILE%\.android\debug.keystore. Он привязан к компьютеру и Eclipse.
  • файл-сертификат для release — my-release-key.keystore, который используется для цифровой подписи приложения. Об этом каждый разработчик знает. Получить его можно так: developer.android.com/tools/publishing/app-signing.html#cert
  • для того, чтобы получить debug-key. открываем документацию, а там раздел Displaying the debug certificate fingerprint и смотрим, как это сделать
  • чтобы получить release-key, нужно открыть документацию, там раздел Displaying the release certificate fingerprint
  • В текущей версии api-console можно сделать несколько сочетаний SHA-1-KEY;com.example.android.mapexample для одного выходного API_KEY. Удобно с одним ключом на нескольких компьютерах.


Для работы Google Maps добавить в проект Android-библиотеку. путь такой: <Android-SDK-Path>\extras\google\google_play_services\. Добавлять нужно так: File>New>Project>Android>Android Prject from Existing Code>Next
Не забудьте отметить в настройках основного проекта habrtimezone добавить проект google_play_services как библиотеку. Это надо сделать в разделе Project>Properties>Android>Library>Add...

Собирать проект нужно для Google APIs 4.4.2 (можно > 4.0). Для этого поставьте галочку напротив соответствующего пункта списка в Project>Properties>Android>Project Build Target. Если там такого пункта нет, то необходимо через Android SDK Manager установить Google APIs.

Картинка, которая показывает, где и что включать.


Что скачать в Android SDK Manager.



Код приложения



Ниже приведён пример приложения, которое выложено на Google Play, исходные коды есть на GitHub.
Будут приведены комментарии к особенностям работы приложения.
QR для приложения


Скриншот Eclipse с проектом.


Итак, что умеет приложение:
1. Определять текущие координаты
2. Работать с картой
3. Определять положение города и его часовой пояс по его названию.

Не буду объяснять все аспекты работы приложения, а лишь покажу основные детали и особенности. При наличии кода с мелочами разобраться не составит труда.

Класс AMain. Первый экран


Начинается работа приложения с AMain Activity
Класс AMain.java. Код класса.

public class AMain extends Activity implements OnClickListener

(ссылка)

OnClickListener — для обработки кликов. На мой взгляд, обрабатывать клики лучше таким образом, так как память будет потребляться в меньших количествах, если использовать
 button.setOnClickListener(this);
, а не
button.setOnClickListener(<новый Listener>);
Этот эффект особенно заметен, когда используется большое количество кликабельных объектов.
(ссылка для кнопки и ссылка для обработчика)

Что тут происходит:
Происходит поиск координат телефона в пространстве при помощи вышек GSM, а также GPS.
Через это окно можно открыть:
1. Карту. Для маркеров на этой карте будут переданы текущее положение телефона и положение (широта/долгота), которое записано в текстовых полях AGMap
2. Поиск городов через интернет. ACityListOnline

Поиск координат сделан при помощи следующих основных объектов:

private LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
private LocationListener gpsLocationListener;
private LocationListener networkLocationListener;

(ссылка)
Как только изменятся координаты и/или время, событие будет обработано при помощи LocListener implements LocationListener (ссылка) в

@Override
	public void onLocationChanged(Location location) {}

(ссылка)
Работа с картой

Класс AGMap. Экран для работы с картой


Для того, чтобы карта была в приложении, нужно в ресурсах в файле agmap.xml (ссылка) создать следующий блок:


<fragment 
	android:id="@+id/map"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	class="com.google.android.gms.maps.SupportMapFragment"/>

, который и является картой.(ссылка)

Класс Activity: AGMap.java Код класса.

Что тут происходит.
1. Открывается карта,
2. В неё передаются координаты с предыдущего экрана.
3. Есть 2 маркера — текущие координаты, и маркер по центру экрана.
4. Можно двигать карту, при нажатии на кнопку Сохранить будут применены координаты центра карты, будет определено название региона, будет определён часовой пояс при помощи Geonames.org.

Особенности настройки и использования карты.

public class AGMap extends FragmentActivity implements OnCameraChangeListener, OnClickListener
(ссылка)
FragmentActivity — так как включена карта в это Activity, от родительским нужно использовать именно этот класс.
OnCameraChangeListener — нужен для обработки передвижения карты

private GoogleMap mMap; //основной объект (карта), 

который инициализирован следующим образом:

mMap = ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map)).getMap();
(ссылка)

Положения объектов (указателей на карте) хранятся в

LatLng HEREIAM = null; // текущее положение
LatLng pos = null; // текущее положение центра карты
(ссылка)
Для того чтобы нарисовать маркер, нужно сделать объект с настройками маркера
mo = new markerOptions(... //настроить его текст, цвет, и т.п.
(ссылка)
и применить его к карте:
mar = mMap.addMarker(mo); 

на выходе получится Marker mar(ссылка)

Заметил, что на некоторых устройствах при инициализации карты возникает ошибка в onCreate(), поэтому приходится ошибку оборачивать в
try{}catch(NullPointerException e){}
(ссылка)

Все настройки карты находятся

GoogleMapOptions options = new GoogleMapOptions();// настройки карты

(ссылка)
Центрирование маркера по карте:

@Override
public void onCameraChange(CameraPosition cp) {
			// TODO Auto-generated method stub
			  pos = cp.target;
			  mar1.setPosition(pos);
		}

(ссылка)
Изначально в эту функцию были добавлены функции обработки результата

uniqueExec(); //поиск названия положения
uniqueExecTZ(pos); //поиск часового пояса

Но в случае плохого соединения будет всё притормаживать. Использовать это неудобно.

Поиск название положения происходит таким образом:


Geocoder geoCoder = new Geocoder(getBaseContext(), Locale.getDefault());
		try {
			List<Address> addresses = geoCoder.getFromLocation(point.latitude, point.longitude, 1);// тут и будет список адресов
		}
		catch (IOException e) {                
			e.printStackTrace();
		}
		return address;
	} 
(ссылка)
Справедливости ради стоит добавить, что иногда возникает ошибка «Service not Available». В этом случае лучше дополнить эту функцию проверкой вида

if (Geocoder.isPresent()){}
else{}

и в else сделать прямой запрос по URL:

String googleMapUrl = "http://maps.googleapis.com/maps/api/geocode/json?latlng=" + point.latitude + ","
        		+ point.longitude + "&sensor=false&language=" + Locale.getDefault().getLanguage();

В данном случае добавлять условие if-else надо будет тут.

На выходе будет JSON-объект, который также разбирается на кусочки как в случае с GeoNames.(см. чуть ниже)
К сожалению, есть ограничение на 2500 запросов в день в случае бесплатного варианта.

Подробней тут

При нажатии на кнопку сохранить (ссылка) происходит следующее:
1. поиск названия местности по координатам в центре экрана. Выполнено при помощи geocodertask extends AsyncTask<Void, Integer, Void> (ссылка) в функции uniqueExec();(ссылка)
2. поиск часового пояса по координатам в центре экрана. Выполнено при помощи TimeZoneTask extends AsyncTask<Double, Integer, Void> (ссылка) в функции uniqueExecTZ(pos); (ссылка)

Запросы по http для Geocoder и Time Zone обвёрнуты в AsyncTask (ссылка 1 и ссылка 2), потому что доступ в интернет должен происходить асинхронно, не забивая основной поток.

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


String[] urlString = {"http://api.geonames.org/findNearbyJSON?lat=","&lng=", "&radius=50&username=","&style=full&maxRows=1"};
outURL = urlString[0] +  f.format(lat).replace(",", ".") + urlString[1] + f.format(lon).replace(",", ".") + urlString[2] + _.names1[r.nextInt(_.names1.length)] + urlString[3];

(ссылка 1)
(ссылка 2)
В запросе происходит поиск любого одного объекта на расстоянии ближе 50 км., если этот объект имеет часовой пояс, то это является результатом. Если нет или такие объекты не обнаружены (например, расположение где-то в океане), то производятся вычисление часового пояса по текущей широте и долготе с обычным делением глобуса на 24 части (ссылка). Возможно объект выбирается не произвольным образом, а берётся ближайший, но я это детально не проверял.
Результат выводится в формате JSON. Поэтому параллельно можно вытянуть и другую информацию.

Массив names1 (ссылка) из класса _.java хранит в себе логины на Geonames.org. Можно хранить один логин, но лучше несколько, так как есть ограничение на количество запросов в час. В этом случае, увеличив количество пользователей и выбирая их случайным образом, можно будет таким искусственным способом пропорционально увеличить и количество запросов. Выбор пользователя, через которого происходит соединение, возникает случайным образом при помощи r.nextInt() (что такое r). В лицензии я не нашёл ограничений для подобной уловке.

Ещё раз напишу. Внимание, не забудьте активировать пользователя. Для этого зайдите в аккаунт и нажмите там на ссылку: Click here to enable.

Метод, конечно, не абсолютно точный, то зато рабочий. И обычно такого метода вполне достаточно. От конечных пользователей претензий пока не поступало.

Класс ACityListOnline. На этом экране происходит поиск городов по названию.



Activity ACityListOnline.java Код класса.

Также используется Geonames.org в

CityTask extends AsyncTask<String, Integer, Void>
(ссылка на класс)
(тут выполняется запуск объекта класса CityTask)
Здесь запросы аналогичны AGMap.java, но происходит поиск по названию.
Вот второй из тех запросов, которые работают с GeoNames:


String[] urlString = {"http://api.geonames.org/searchJSON?q=", "&username=","&style=full"};
outURL = urlString[0] +  URLEncoder.encode(str, "UTF-8") + urlString[1] + _.names1[r.nextInt(_.names1.length)] + urlString[2];

(ссылка 1)
(ссылка 2)
Если результат найден, то сохраняется в объект класса _Info
(ссылка на класс)
(ссылка на список объектов класса, которые хранят результаты поиска)
Далее детальную информацию можно просмотреть в другом Activity, кликнув на элемент ListView.
Сейчас в объекте класса _Info хранятся широта, долгота, часовой пояс и международное название региона. Если хотите вытащить другую информацию (она также доступна), можете её взять из JSON-объекта самостоятельно. Всё просто и нудно.

Результаты


По основному функционалу всё.
Ещё раз ссылка на Google Play.
QR для приложения

Перед тем как использовать код на GitHub обратите ВНИМАНИЕ:
В этом коде нет пользователей от Geonames.org
А именно, необходимо будет вставить имена пользователей в переменную

public static final String[] names1 = {"your_account_1","your_account_2"}; 
в классе _.java (ссылка), а также вставить свой API_KEY. в AndroidManifest.xml (ссылка)
Как получить эти значения, я объяснял выше.
Чтобы проект с GitHub собрался, нужно добавить в Project>Properties>Java Build Path>Libraries>Add External JARs вот этот файл: Android_SDK_Path\extras\android\support\v4\android-support-v4.jar. Затем перейти в Project>Properties>Java Build Path>Order and Export и включить этот файл.
Если есть интерес к подобной статье, могу в следующий раз могу рассказать, как это может быть сделано локально без подключения к интернету.
Tags:
Hubs:
+13
Comments 2
Comments Comments 2

Articles