Офлайн карты

Мобильное приложение Android

1. Создаем новый проект на основании сгенерированого шаблона. Для этого в Eclipse необходимо выполнить импорт, как это сделать подробно описано здесь.

2. Займемся картой. В состав FBA включены вспомогательные классы, облегчающие новичку работу с офлайн картами Mapsforge.  Один из таких классов – BaseRouteMapActivity, активити предназначенная для отображения точек маршрута и навигации по ним. Его и будем использовать.

2.1 Создаем свой класс, наследник от BaseRouteMapActivity:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class MapsforgeRouteMap extends BaseRouteMapActivity {
        /*
         * На сколько должна изменится текущая позиция (в метрах) прежде чем будет
         * получено новое значение координат
         */
        public static int DEF_LOCATION_MIN_DISTANCE = 500;
 
        /*
         * Максимальное время, которое должно пройти, прежде чем пользователь
         * получает обновление местоположения.
         */
        public static long DEF_LOCATION_MIN_TIME = 5000 * 60;
 
        //Москва, нулевой километр
        private static final double DEFAULT_GEOPOINT_LAT = 55.755831;
        private static final double DEFAULT_GEOPOINT_LNG = 37.617673;
 
        //Файл карты
        private static final String MAP_FILE = "ru_moscow.map";
 
        private MapView mMapView;
 
        @Override
        protected void onCreate(Bundle savedInstanceState) {
                super.onCreate(savedInstanceState);
                setContentView(R.layout.activity_route_map);
                init();
        }
 
        private void init() {
                mMapView = (MapView) findViewById(R.id.mapView);
                //Инициализация карты
                onCreateMapView(mMapView);
        }
 
        @Override
        public File getMapFile() {
                return new File(getAppSettings().getBuckupDir(),MAP_FILE);
        }
 
        @Override
        public GeoPoint getMapCenterPoint() {
                return MapHelper.toGeoPoint(DEFAULT_GEOPOINT_LAT, DEFAULT_GEOPOINT_LNG);
        }
 
        @Override
        public int getLocationMinDistance() {
                return DEF_LOCATION_MIN_DISTANCE;
        }
 
        @Override
        public long getLocationMinTime() {
                return DEF_LOCATION_MIN_TIME;
        }
 
        @Override
        public boolean showCurrentPosition() {
                return true;
        }
 
        @Override
        public boolean requestLocationUpdates() {
                return true;
        }
 
        @Override
        protected void onRouteItemSelect(RouteOverlayItem<?> item) {
        }
 
        @Override
        public void onLocationChanged(Location location) {
        }
}
public class MapsforgeRouteMap extends BaseRouteMapActivity {
    	/*
    	 * На сколько должна изменится текущая позиция (в метрах) прежде чем будет
    	 * получено новое значение координат
    	 */
    	public static int DEF_LOCATION_MIN_DISTANCE = 500;

    	/*
    	 * Максимальное время, которое должно пройти, прежде чем пользователь
    	 * получает обновление местоположения.
    	 */
    	public static long DEF_LOCATION_MIN_TIME = 5000 * 60;

    	//Москва, нулевой километр
    	private static final double DEFAULT_GEOPOINT_LAT = 55.755831;
    	private static final double DEFAULT_GEOPOINT_LNG = 37.617673;

    	//Файл карты
    	private static final String MAP_FILE = "ru_moscow.map";

    	private MapView mMapView;

    	@Override
    	protected void onCreate(Bundle savedInstanceState) {
            	super.onCreate(savedInstanceState);
            	setContentView(R.layout.activity_route_map);
            	init();
    	}

    	private void init() {
            	mMapView = (MapView) findViewById(R.id.mapView);
            	//Инициализация карты
            	onCreateMapView(mMapView);
     	}

    	@Override
    	public File getMapFile() {
            	return new File(getAppSettings().getBuckupDir(),MAP_FILE);
    	}

    	@Override
    	public GeoPoint getMapCenterPoint() {
            	return MapHelper.toGeoPoint(DEFAULT_GEOPOINT_LAT, DEFAULT_GEOPOINT_LNG);
    	}

    	@Override
    	public int getLocationMinDistance() {
            	return DEF_LOCATION_MIN_DISTANCE;
    	}

    	@Override
    	public long getLocationMinTime() {
            	return DEF_LOCATION_MIN_TIME;
    	}

    	@Override
    	public boolean showCurrentPosition() {
            	return true;
    	}

    	@Override
    	public boolean requestLocationUpdates() {
            	return true;
    	}

    	@Override
    	protected void onRouteItemSelect(RouteOverlayItem<?> item) {
    	}

    	@Override
    	public void onLocationChanged(Location location) {
        }
}

Отображать будем карту Москвы, которую необходимо поместить на эмулятор в каталог /mnt/sdcard/backup/<имя пакета>. Этот каталог будет создан автоматически при первом запуска программы. Где взять файл карты или как создать самому, смотрите в конце статьи.

При запуске наша карта будет отцентрирована по координатам переданным в методе getMapCenterPoint().  Функция showCurrentPosition() должна возвращать true, если вы ходите отображать маркер с вашей текущей позицией.

Если методом requestLocationUpdates() включено отслеживание координат, то при изменении будет вызываться обработчик onLocationChanged. Настройки частоты обновления координат задаются через getLocationMinDistance() и getLocationMinTime(). По умолчанию используются оба провайдера (GPS и NETWORK) для получения координат, изменить можно в BaseMapActivity т.к исходный код открыт.

В макете activity_route_map.xml размещаем элемент карты:

1
2
3
4
5
<org.mapsforge.android.maps.MapView
        android:id="@+id/mapView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >
    </org.mapsforge.android.maps.MapView>
<org.mapsforge.android.maps.MapView
        android:id="@+id/mapView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >
    </org.mapsforge.android.maps.MapView>

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

2.2  Добавление слоя с точками маршрута выполняется с помощью метода addRouteOverlay, в который передается список элементов List<RouteOverlayItem<T>>. T это тип доп. данных хранимых вместе с элементом, в вашем случае это CatalogSalesPoint. Подготовим список наших торговых точек:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
private void initDummyRoute() throws SQLException {
 
        //DAO для отображения картинки и для записи координат
        mStorageDao = new CatalogAddInfoStorageDao(getConnectionSource());
        mSalesPointDao = new CatalogSalesPointDao(getConnectionSource());
 
        // Выбрать все торговые точки
        CatalogSalesPointDao salePointDao = new CatalogSalesPointDao(getConnectionSource());
        List<CatalogSalesPoint> lst = salePointDao.select();
 
        // Список точек маршрута отображаемый на карте
        routeItems = new ArrayList<RouteOverlayItem<CatalogSalesPoint>>();
 
        // Для точек, у которых не установлены координаты
        final double lng = DEFAULT_GEOPOINT_LNG;
        double lat = DEFAULT_GEOPOINT_LAT;
 
        int count = lst.size();
        for (int i = 0; i < count; i++) {
 
                CatalogSalesPoint salesPoint = lst.get(i);
 
                boolean movable = (salesPoint.lat == 0 || salesPoint.lng == 0);
                int resIdDrawable = R.drawable.fba_map_maker_green;
                GeoPoint geoPoint = new GeoPoint(salesPoint.lat, salesPoint.lng);
 
                if (movable) {
                        resIdDrawable = R.drawable.fba_map_marker_red;
                        lat -= 0.003f;
                        geoPoint = new GeoPoint(lat, lng);
                }
 
                //На маркере вывести порядковый  номер
                Drawable marker = MapHelper.makeNumberedMarker(this, resIdDrawable,i + 1);
 
                RouteOverlayItem<CatalogSalesPoint> routePoint = new RouteOverlayItem<CatalogSalesPoint>(
                                geoPoint, marker, salesPoint);
                routePoint.setMovable(movable);
                routePoint.setOrdinal(i + 1);
                routeItems.add(routePoint);
        }
        Drawable defaultMarker = getResources().getDrawable(R.drawable.fba_map_marker_orange);
        addRouteOverlay(mMapView, routeItems, defaultMarker);
 
}
private void initDummyRoute() throws SQLException {

    	//DAO для отображения картинки и для записи координат
    	mStorageDao = new CatalogAddInfoStorageDao(getConnectionSource());
    	mSalesPointDao = new CatalogSalesPointDao(getConnectionSource());

    	// Выбрать все торговые точки
    	CatalogSalesPointDao salePointDao = new CatalogSalesPointDao(getConnectionSource());
    	List<CatalogSalesPoint> lst = salePointDao.select();

    	// Список точек маршрута отображаемый на карте
    	routeItems = new ArrayList<RouteOverlayItem<CatalogSalesPoint>>();

    	// Для точек, у которых не установлены координаты
    	final double lng = DEFAULT_GEOPOINT_LNG;
    	double lat = DEFAULT_GEOPOINT_LAT;

    	int count = lst.size();
    	for (int i = 0; i < count; i++) {

    	    	CatalogSalesPoint salesPoint = lst.get(i);

            	boolean movable = (salesPoint.lat == 0 || salesPoint.lng == 0);
            	int resIdDrawable = R.drawable.fba_map_maker_green;
            	GeoPoint geoPoint = new GeoPoint(salesPoint.lat, salesPoint.lng);

            	if (movable) {
                    	resIdDrawable = R.drawable.fba_map_marker_red;
                    	lat -= 0.003f;
                    	geoPoint = new GeoPoint(lat, lng);
            	}

            	//На маркере вывести порядковый  номер
            	Drawable marker = MapHelper.makeNumberedMarker(this, resIdDrawable,i + 1);

            	RouteOverlayItem<CatalogSalesPoint> routePoint = new RouteOverlayItem<CatalogSalesPoint>(
                            	geoPoint, marker, salesPoint);
            	routePoint.setMovable(movable);
            	routePoint.setOrdinal(i + 1);
            	routeItems.add(routePoint);
    	}
    	Drawable defaultMarker = getResources().getDrawable(R.drawable.fba_map_marker_orange);
    	addRouteOverlay(mMapView, routeItems, defaultMarker);

}

и добавим вызов этого метода в init().

Здесь из локальной базы выбираются все элементы справочника “Торговые точки”  и добавляются в коллекцию отображаемых элементов. Причем для точек, у которых установлены координаты в 1С,  маркер будет зеленого цвета. Точки без координат размещаются по центру карты с маркером красного цвета. Пользователь может перемещать их по карте с целью уточнения координат.

2.3 Давайте добавим реакцию на клик по маркеру в обработчик onRouteItemSelect:

1
2
CatalogSalesPoint salesPoint = (CatalogSalesPoint) item.getData();
inflatePopup(salesPoint);
CatalogSalesPoint salesPoint = (CatalogSalesPoint) item.getData();
inflatePopup(salesPoint);

Поля класса и метод отображающий дополнительную информацию о торговой точке:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private ObjectView mSalesPointView, mFotoStorageView;
 
/*
 * Имена реквизитов отображаемых в всплывающем окне  (имена полей класса)
 */
private static String[] fields = new String[] {
    CatalogSalesPoint.FIELD_NAME_DESCRIPTION,
    CatalogSalesPoint.FIELD_NAME_ADRESS,
    CatalogSalesPoint.FIELD_NAME_PHONE,
    CatalogSalesPoint.FIELD_NAME_SITE
};
 
/*
 * Идентификаторы view-элементов для отображения реквизитов
 */
private static int[] ids = new int[] { R.id.tvDescription, R.id.tvAdress,
            R.id.tvPhone, R.id.tvSite};
 
/*
 * Заполнить данные по торговой точке на всплывающем окне и отобразить его
 */
private void inflatePopup(CatalogSalesPoint salesPoint) throws SQLException {
 
        if (CatalogSalesPoint.isEmpty(salesPoint.foto)) {
                mFotoStorageView.setVisibility(View.GONE);
        } else {
                //Прочитать ссылку на фото
                mStorageDao.refresh(salesPoint.foto);
                mFotoStorageView.build(salesPoint.foto, getHelper(),
                                new String[] { CatalogAddInfoStorage.FIELD_NAME_STORAGE },
                                new int[] { R.id.ivFoto });
                mFotoStorageView.setVisibility(View.VISIBLE);
        }
        mSalesPointView.build(salesPoint, getHelper(), fields, ids);
}
private ObjectView mSalesPointView, mFotoStorageView;

/*
 * Имена реквизитов отображаемых в всплывающем окне  (имена полей класса)
 */
private static String[] fields = new String[] {
   	CatalogSalesPoint.FIELD_NAME_DESCRIPTION,
   	CatalogSalesPoint.FIELD_NAME_ADRESS,
   	CatalogSalesPoint.FIELD_NAME_PHONE,
   	CatalogSalesPoint.FIELD_NAME_SITE
};

/*
 * Идентификаторы view-элементов для отображения реквизитов
 */
private static int[] ids = new int[] { R.id.tvDescription, R.id.tvAdress,
          	R.id.tvPhone, R.id.tvSite};

/*
 * Заполнить данные по торговой точке на всплывающем окне и отобразить его
 */
private void inflatePopup(CatalogSalesPoint salesPoint) throws SQLException {

    	if (CatalogSalesPoint.isEmpty(salesPoint.foto)) {
            	mFotoStorageView.setVisibility(View.GONE);
    	} else {
            	//Прочитать ссылку на фото
            	mStorageDao.refresh(salesPoint.foto);
            	mFotoStorageView.build(salesPoint.foto, getHelper(),
                            	new String[] { CatalogAddInfoStorage.FIELD_NAME_STORAGE },
                            	new int[] { R.id.ivFoto });
            	mFotoStorageView.setVisibility(View.VISIBLE);
    	}
    	mSalesPointView.build(salesPoint, getHelper(), fields, ids);
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<ru.profi1c.engine.widget.ObjectView
    android:id="@+id/ovSalesPoint">
 
    <ru.profi1c.engine.widget.ObjectView
        android:id="@+id/ovFotoStorage" >
         <ImageView
                android:id="@+id/ivFoto"/>
    </ru.profi1c.engine.widget.ObjectView>
 
    <TextView android:id="@+id/tvDescription"/>
    <TextView android:id="@+id/tvAdress"/>
    <TextView android:id="@+id/tvSite" android:autoLink="web"/>
    <TextView  android:id="@+id/tvPhone"    android:autoLink="phone"/>
</ru.profi1c.engine.widget.ObjectView>
<ru.profi1c.engine.widget.ObjectView
    android:id="@+id/ovSalesPoint">

    <ru.profi1c.engine.widget.ObjectView
        android:id="@+id/ovFotoStorage" >
         <ImageView
            	android:id="@+id/ivFoto"/>
    </ru.profi1c.engine.widget.ObjectView>

    <TextView android:id="@+id/tvDescription"/>
    <TextView android:id="@+id/tvAdress"/>
    <TextView android:id="@+id/tvSite" android:autoLink="web"/>
    <TextView  android:id="@+id/tvPhone" 	android:autoLink="phone"/>
</ru.profi1c.engine.widget.ObjectView>

На макете отображается информация по одной торговой точке, реквизиты “Наименование”, “Адрес”, “Сайт” и “Телефон” автоматически проецируются на TextView-элементы в mSalesPointView.build(…).
Для отображения фото используется подчиненный объект ovFotoStorage с дочерним ImageView. Фото считывается из базы и проецируется на ImаgeView в методе mFotoStorageView.build(…).
А так как в реквизите “Фото” хранится только ссылка на справочник “Хранилище дополнительной информации”, ее необходимо предварительно считать методом refresh.

Инициализацию добавляем в метод в init()

1
2
mSalesPointView = (ObjectView) findViewById(R.id.ovSalesPoint);
mFotoStorageView = (ObjectView) findViewById(R.id.ovFotoStorage);
mSalesPointView = (ObjectView) findViewById(R.id.ovSalesPoint);
mFotoStorageView = (ObjectView) findViewById(R.id.ovFotoStorage);

Макет map_popup.xml можно установить как кастомный для Toast, но в этом примере он встроен в основной макет activity_route_map.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >
 
    <org.mapsforge.android.maps.MapView
        android:id="@+id/mapView"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent" >
    </org.mapsforge.android.maps.MapView>
 
    <include
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:layout_marginTop="10dp"
        layout="@layout/map_popup"
        android:visibility="gone" />
 
</FrameLayout>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent" >

	<org.mapsforge.android.maps.MapView
    	android:id="@+id/mapView"
    	android:layout_width="fill_parent"
    	android:layout_height="fill_parent" >
	</org.mapsforge.android.maps.MapView>

	<include
    	android:layout_width="wrap_content"
    	android:layout_height="wrap_content"
    	android:layout_marginLeft="20dp"
    	android:layout_marginRight="20dp"
    	android:layout_marginTop="10dp"
    	layout="@layout/map_popup"
    	android:visibility="gone" />

</FrameLayout>

Для управления видимостью используются методы:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
 * Скрыть всплывающее окно
 */
private void closePopup(){
    if(mSalesPointView.getVisibility() ==  View.VISIBLE){
            mSalesPointView.setVisibility(View.GONE);
    }
}
 
/*
 * Показать всплывающее окно
 */
private void showPopup(){
    if(mSalesPointView.getVisibility() !=  View.VISIBLE){
            mSalesPointView.setVisibility(View.VISIBLE);
    }
}
/*
 * Скрыть всплывающее окно
 */
private void closePopup(){
   	if(mSalesPointView.getVisibility() ==  View.VISIBLE){
          	mSalesPointView.setVisibility(View.GONE);
   	}
}

/*
 * Показать всплывающее окно
 */
private void showPopup(){
   	if(mSalesPointView.getVisibility() !=  View.VISIBLE){
          	mSalesPointView.setVisibility(View.VISIBLE);
   	}
}

Скрывается всплывающее сообщение по клику на нем, а отображается при нажатии на маркер точки.

Приложение готово, запустите его на эмуляторе, настройте параметры авторизации (как в 1С) и выполните обмен с базой. Пример работы приложения:

Клик по номеру телефона откроет стандартную “звонилку”, а по клику на ссылку будет запущен браузер. Такое поведение достигается простой установкой свойства android:autoLink для TextView.

Полный код примера доступен в SVN хранилище: https://xp-dev.com/svn/fba_toolkit_public/samples/fbaSampleAuditPoint/
Дополнительно в нем показано как сохранить координаты у перемещенных точек и обновить маркер.

Где взять карты

Некоторые карты планируем размещать на нашем сайте, приоритет у городов (областей) непокрытых с достаточной детализацией на Google Maps и карт, подготовленных для клиентов FBA. Краткая инструкция по созданию файла карты приведена там же.

Надеюсь, мне удалось хоть чуть-чуть развеять миф о том, что разработка  мобильных бизнес-приложений это «сложно, дорого и долго».

Похожие записи: