1C + офлайн карты на Android

в 11:31, , рубрики: 1c интеграция, Maps API, Разработка под android, метки: , ,

Здравствуйте!
Продолжим знакомство с фреймворком «FBA Toolkit» и рассмотрим еще один пример разработки андроид-клиента для 1С. Наше приложение «Аудит торговой точки» будет загружать маршрут посещения из 1С и отображать его на карте. Используются офлайн карты на базе картографического сервиса OpenStreetMap.

Предполагается, что у вас уже установлены и настроены все необходимые инструменты разработчика. Если это не так, см. предыдущую часть. Фреймворк «FBA Toolkit» должен быть не ниже версии 1.03, обновите конфигурацию 1С и Android-проект ru_profi1c_fba из SVN-репозитария.

Серверная часть (в базе 1С)

1. В информационной базе 1С создаем 2 справочника, которые будут выступать в роли источника данных для примера.

1C + офлайн карты на Android

Реквизиты справочника «Торговые точки»:

  • «Широта» и «Долгота» содержат координаты этой точки, число 10.6
  • «Фото» ссылка на справочник «Хранилище дополнительной информации» — ссылка на основное изображение;
  • все прочие реквизиты строкового типа.

Реквизиты справочника «Хранилище дополнительной информации»:

  • «Объект» составного типа в т.ч содержит тип «СправочникСсылка.ТорговаяТочка», определяет объект которому принадлежит дополнительная информация. В мобильном приложении не используется.
  • «Хранилище», тип «ХранилищеЗначения», содержит само изображение.

2. Так же добавлен план обмена «МобильноеПриложениеАудитТочки» в состав которого входят только эти два справочника, авто-регистрация изменений включена.

3. Справочники заполняем некоторой тестовой информацией, в этом примере это пиццерии в центре Москвы (данные взяты из открытых источников).

1C + офлайн карты на Android

Для некоторых точек загружены изображения. Код сохранения значения в хранилище стандартный для 1С:

ДвоичныеДанные = Новый ДвоичныеДанные(ИмяФайла);
Хранилище = Новый ХранилищеЗначения(ДвоичныеДанные, Новый СжатиеДанных(9));

Форма элемента справочника «Хранилище дополнительной информации», «ИмяФайла» содержит путь к загружаемому изображению.

4. Создаем мобильное приложение «Аудит торговой точки». В дерево метаданных добавляем наши справочники.

1C + офлайн карты на Android
Просто ради удобства, имена реквизитов и объектов из латиницы переведены на английский.

1C + офлайн карты на Android

Схема обмена настроена таким образом, чтобы выгружать только измененные в 1С объекты:

  • установлен флаг использования плана обмена «МобильноеПриложениеАудитТочки»
  • при изменении любой «Торговой точки» в 1С она будет передана мобильному приложению;
  • на справочник «Хранилище дополнительной информации» наложена дополнительная фильтрация: передавать измененную информацию только по торговым точкам. Это делается запросом:

ВЫБРАТЬ
            ХранилищеДополнительнойИнформацииИзменения.Ссылка КАК Ссылка
ИЗ
            Справочник.ХранилищеДополнительнойИнформации.Изменения КАК ХранилищеДополнительнойИнформацииИзменения
ГДЕ
            ХранилищеДополнительнойИнформацииИзменения.Узел = &УзелОбмена
            И ХранилищеДополнительнойИнформацииИзменения.Ссылка.Объект ССЫЛКА Справочник.ТорговыеТочки

где «УзелОбмена» это предопределенный параметр, в момент обмена будет содержать значение установленного узла для мобильного сотрудника (ниже покажем где это устанавливается).

5. Генерируем шаблон мобильного приложения Android. Стоимость и порядок приобретения лицензии описаны здесь. Как внести полученные лицензионные данные в 1С написано здесь.

1C + офлайн карты на Android

Можно так же воспользоваться тестовой лицензией:
client_id: TEST-CLIENT-000
client_name: test-client-name
client_pwd: 31qX9OqZ_V
lisence_id: f7a42162-e27f-4246-a89b-b69c02387740
Только в этом случае не рекомендуется изменять имя пакета мобильного приложения: ru.profi1c.samples.audit.salespoint.

6. В справочник «Мобильные сотрудники» добавляем наше приложение для сотрудника, который используется для тестирования обмена. Обратите внимание на колонку «Узел обмена», это новый элемент нашего плана обмена.

1C + офлайн карты на Android

Как правило, для каждого сотрудника создается свой узел обмена. В этом случае получается, что изменения данных в 1С будут автоматически регистрироваться для всех узлов (все мобильных сотрудников), а при обмене каждый мобильный клиент «заберет свою порцию данных».
Этот параметр может использоваться в запросах на выборку данных, как нашем случае со справочником «Хранилище дополнительной информации».

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

Подготовительные работы в 1С завершены, для тестирования правил обмена можете воспользоваться обработкой «Тест обмена с мобильным приложением», подробнее здесь. В 1С больше ничего делать не надо, всю рутинную работу по организации обмена FBA берет на себя. Программист может сконцентрироваться на разработке мобильного приложения.

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

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

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

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

Показать

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 размещаем элемент карты:

<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 это тип доп. данных хранимых вместе с элементом, в вашем случае это CatalogSalesPoint. Подготовим список наших торговых точек:

Показать

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:

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

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

Показать

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 (разметка приведена частично):

<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()

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

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

Показать

<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>

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

Показать

/*
 * Скрыть всплывающее окно
 */
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С) и выполните обмен с базой. Пример работы приложения:

1C + офлайн карты на Android

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

Полный код примера доступен в SVN хранилище. Дополнительно в нем показано как сохранить координаты у перемещенных точек и обновить маркер.

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

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

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

Автор: avesha

Источник

Поделиться

* - обязательные к заполнению поля