Немного osm и openlayers для корпоративных систем

в 21:22, , рубрики: openlayers, OpenStreetMap, Песочница, метки: , ,

Привет Хабр, сегодня я расскажу немного про использование osm для предприятий и b2b.
А именно, как и зачем перейти от google maps api к osm, openlayers и счастью.

Первый вопрос, который непременно возникнет: зачем?

Начнем с того, что использование google maps api для непубличных сервисов ограничено условиями предоставления сервиса. Второе: апи карт гугл — это именно апи карт гугл, а не апи для отображения всего, что напоминает карту. Т.е. если вам захотелось отображать другую подложку или добавить растровый слой, отрисовываемый сервером в локальной сети предприятия, готовтесь лепить костыли. Ну и просто будьте готовы, что если чего-то в апи нет, добавлять будет мучительно. Третье: вы не можете получить данные карты, соответсвенно, вы не можете создать локальный картографический сервис для сотрудников предприятия. То есть с каждой клиентской машины должны быть доступны карты гугл. Звучит диковато, но далеко не всегда сотрудникам открыт доступ во внешние сети.

Предположим, я вас убедил. С чего начать?

Первое, подключаем карту в openlayers. Тут все просто и мало чем отличается от google, yandex, leaflet.

// создаем карту, использовав в качестве контейнера элемент с id='map'
var map = new OpenLayers.Map('map'); 

// создаем слой с типом OSM и именем "OSM mapnik"
var layer = new OpenLayers.Layer.OSM('OSM mapnik'); 

//добавляем слой на карту	
map.addLayer(layer); 

//выставляем зум и центр карты, таким образом, чтобы поместился весь мир.
map.zoomToMaxExtent();

Крупные компании зачастую могут себе позволить купить картографический сервер и карты и раздавать их в локальной сети через wms или tms. WMS и TMS — это стандарты, по которым вы можете получить кусочки изображения карт. Основное отличие: по wms вы можете запросить произвольный кусочек карты произвольного масштаба, по tms — набор масштабов и деление карты на квадратики, которые могут быть получены фиксированно.

Соответсвенно, меняем строку создания слоя для wms на

//Создаем новый слой типа WMS
var wms = new OpenLayers.Layer.WMS( 	
     //имя слоя для контрола выбора слоев
     "NASA Global Mosaic",		
     
     //адрес wms сервера		
     "http://wms.jpl.nasa.gov/wms.cgi", 

      //параметры специфичные для wms: 
      //набор слоев, которые wms сервер склеит в картинку перед тем, как вернуть вам результат
     {layers: "modis,global_mosaic"}
); 

для tms:

//Создаем новый слой типа TMS
var layer = new OpenLayers.Layer.TMS(					
    //Имя для контролла выбора слоев
    "My TMS Layer", 										
    
    //путь до TMS сервера
    "http://tilecache.osgeo.org/wms-c/Basic.py/", 		

    //параметры специфичные для TMS: 
    //набор слоев и тип картинки, которую вы желаете получить (png/jpeg/gif) 
    {layername: "basic", type: "png"} 					
);

Также никто не мешает вам добавить все 3 слоя на карту и переключаться между ними, или добавить один из слоев поверх других.

Проекции

Теперь пару слов о граблях, на которые вы скорее всего наступите.

Допустим вы добавили один слой с мапником с osm.org (OpenLayers.Layer.OSM), но хотите, чтобы карта открывалась отзуммированной на Московскую область, а не на весь мир. Смотрим широту и долготу Москвы и вместо

map.zoomToMaxExtent

пишем

map.moveTo(
        //Примерные координаты центра Москвы
	new OpenLayers.LonLat(37.16, 55.604),
        //зум
	9									  
);

И попадаем в океан. Все дело в системе координат. Для слоя мапника родная система координат EPSG:900913. Точкой отсчета координат в ней является пересечение гринвичского меридиана и экватора, а единицами измерения являются метры. Соответсвенно, мы попали на 37 метров восточнее и на 55 метров севернее точки отсчета.

Привычные всем долгота и широта подразумевают, что заданы они в EPSG:4326. Соответсвенно, надо пересчитать координаты.

map.moveTo(
         //пересчитать координаты точки
	new OpenLayers.LonLat(37.16, 55.604).transform(
                //из системы координат EPSG:4326
		new OpenLayers.Projection('EPSG:4326'), 
                //в систему координат карты (EPSG:900913)
		map.getProjectionObject()					
	), 
	9
);

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

Для этого подключаем proj4js ( trac.osgeo.org/proj4js/wiki/Download ) и добавляем строчку

Proj4js.defs['EPSG:28403'] = '+proj=tmerc +lat_0=0 +lon_0=39 +k=1 ' + 
	'+x_0=500000 +y_0=0 +no_defs +a=6378140 +rf=298,257223563 +units=m ' + 
	'+towgs84=28.000,-130.000,-95.000 +to_meter=1';

Тепперь вы можете при пересчете координат указывать новую проекцию.

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

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

Маркеры

В леерсах их 2 типа:
HTML (OpenLayers.Marker) и векторные (OpenLayers.Geometry.Point). Чуть позже я объясню, что здесь имеется ввиду под маркером и почему point.

HTML маркер создает один или несколько дивов, в который помещает картинку и располагает над картой в соответсвии с координатами. Если вы когда-нибудь смотрели фаербагом или другим отладчиком как устроен маркер на карте гугл, вам все будет знакомо. К ним относительно легко подключаются попапы, с ними легко (т.к. есть html объект) работать из jQuerry, и тем не менее создатели библиотеки не рекомендуют ими пользоваться. Почему? Они несколько тяжеловесны: когда у вас 1 маркер — все хорошо, когда тысяча — плохо. Они выбиваются из общей концепции хранения и отображения данных принятой в ol. Ну и из практических соображений: когда я начинал работать с леерсами, для таких маркеров не было drag контрола. Впрочем, его и сейчас нет.

Тем не менее, немного кода для добавления маркеров на карту:

//создаем слой с маркерами
var markers = new OpenLayers.Layer.Markers( "Markers" ); 	

//добавляем его на карту
map.addLayer(markers);		

//координаты, куда добавляем маркер							
var lonLat = new OpenLayers.LonLat( 0, 0 );	

//создаем маркер с дефолтной картинкой с координатами 0, 0 
//идобавляем его в слой				
markers.addMarker(new OpenLayers.Marker(lonLat));			

Да, слоев с маркерами может быть несколько. Да, вы можете управлять видимостью слоев и прятать группы маркеров по своему усмотрению.

Теперь о том, кто такие векторные маркеры и о «концепции хранения и отображения данных».
Мало кого сейчас устроит возможность просто показывать разные растровые слои с картами. Основная прелесть в отображении своих уникальных данных поверх них. Итак, предположим, мы хотим отображать поверх карты оптические кабели, медные кабели и колодцы/опоры, через которые это добро проходит. Каждый объект будет содержать геометрическую информацию (как собственно проходит кабель / где располагается опора или колодец) и атрибутивную (тип кабеля, количество жил кабеля, затухание сигнала, высота опоры… список можно пополнять до бесконечности). Собственно эта идея напрямую реализуется в леерсах:

Объект — «OpenLayers.Feature.Vector», хранится вместе со своими атрибутами, геометрией и, опционально, стилем отображения.
Для точечных объектов:

new OpenLayers.Feature.Vector(
        //Геометрия - точка с координатами x, y (x -longitude, y - latitude)
	new OpenLayers.Geometry.Point(x, y), 	
        
        //Атрибуты: тип, высота	
	{'type':'pillon', 'height':100}, 

        //Стиль, в соответсвие с правилами которого мы хотим отображать объект.
        //В данном случае будет использован тиль по умолчанию		
	null									
);

Пару слов про стили.
В стилях можно задать, как именно отображать геометрию, используя атрибуты объекта. В зависимости от типа можно использовать различные иконки, например, колодцы рисовать кружками, а опоры — столбиками. Можно определить цвет заливки, толщину и цвет обводки. На основе атрибутов можно выводить текстовые подписи к объектам и т.д. Можно задать стиль конкретному объекту, можно, например, назначить стиль для слоя, чтобы все объекты слоя отображались в соответсвии с ним.

И все же к маркерам:

//Создаем слой с маркерами. 
//Markers - имя для отображения в списке слоев. 
//Передав в конструктор вторым атрибутом {showInLayerSwitcher: false} 
//можно спрятать слой из контролла выбора слоев.
var markers = new OpenLayers.Layer.Vector('Markers'); 						

//добавляем его на карту
map.addLayer(markers);	
													
//объект, для которого рисуем маркер
var marker = new OpenLayers.Feature.Vector(									
        //долгота/широта
	new OpenLayers.Geometry.Point(0, 0),

        //данные по вкусу
	{},				

	//Стиль, как отрисовывать
	{
                //рисуем картинку
                externalGraphic:'http://someware.com/my_favorite_marker_icon.png',
                //вот такой ширины
                graphicWidth:16,
                //вот такой вышины *
                graphicHeight:16,
                //сместив картинку на 8 пикселей влево 
                //относительно координат геометрии
                graphicXOffset:-8,
                //и на 16 пикселей вверх
                graphicYOffset:-16,
                //с милой подписью (подпись будет выводиться прямо на карту)
                label:'Мой самый любимый маркер',
                //с базовой точкой текста подписи посередине-сверху текста
                labelAlign: 'ct',
                //сдвинув текст на 5 пикселей вниз
                labelYOffset: '5'
	}
);

markers.addFeatures([marker]);

*Дорогой grammar-nazi, это была аллюзия к детской песенке.

Если вам надо добавить несколько маркеров с одним стилем, достаточно использовать один объект стиля, но важно помнить при этом что изменения в инстансе стиля отразятся на всех маркерах.

Итак, маркер мы добавили. Теперь давайте добавим возможность его перемещать, кликать по нему, и реагировать на прочие события. Собственно обработкой событий леерсы сильнее всего отличаются от остальных библиотек, с которыми довелось поработать (внимательный читатель, который прочтет всю статью, не пропуская абзацы, узнает, что, в первую очередь, это google и чуток leaflet с яндексом).

Добавляем драг:

//Создаем контрол для слоя с маркерами, который позволяет перемещать объекты по карте.
var drag = OpenLayers.Control.DragFeature(markers); 

//Если у вас много слоев с объектами, можно добавить их внутрь 
//OpenLayers.Layer.Vector.RootContainer и передать его
//контролу	

//добавляем контрол на карту	
map.addControl(drag);								

//включить контрол 
//(при добавлении на карту должен включиться автоматом, но вдруг...)
drag.activate();									

Если память меня не подводит, вы, наконец, добъетесь перемещения маркеров по карте.

Или добавим нашему маркеру попап по клику:

selectControl = new OpenLayers.Control.SelectFeature(markers, {
        //колбэк на клик по маркеру		
	onSelect: onFeatureSelect, 	
	
	//колбэк на клик вне маркера
	onUnselect: onFeatureUnselect
});

function onFeatureSelect(feature) {

    popup = new OpenLayers.Popup.FramedCloud("chicken", 
         feature.geometry.getBounds().getCenterLonLat(),
         null,
         "<div style='font-size:.8em'>Привет Habr!</div>",
         null, true, onPopupClose
	);

	feature.popup = popup;
	map.addPopup(popup);
}

function onFeatureUnselect(feature) {
    map.removePopup(feature.popup);
    feature.popup.destroy();
    feature.popup = null;
} 

Можно посмотреть пример вот тут: openlayers.org/dev/examples/select-feature-openpopup.html

Большие проблемы начинаются, когда вам одновременно нужно:

  • иметь возможность двигать маркеры
  • отображать hover (т.е. обрабатывать onmousein и onmouseout)
  • обрабатывать клик, даблклик по объекту
  • обрабатывать клик, даблклик вне объекта

Это решаемая беда, но решение, пожалуй, заслуживает отдельной статьи.

Геокодирование

Фуф, мы добавили OSM на сайт и научились отображать свои данные поверх. Но все же значительная часть api гугла (яндекса) осталась за бортом. А именно геокодирование (получение координат по адресу и адреса по координатам) и прокладка маршрутов.

Про маршрутизацию по картам в осм я расскажу в другой раз, сейчас пару слов о геокодировании. Я не делал обратный геокодинг (адрес по координатам), поэтому остановлюсь пока на поиске координат по адресу.

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

1. К примеру, вас интересует только Москва, соответсвенно, данные будет импортировать проще, и поисковые запросы будут простыми, без указания города.
2. По каким-то причинам вы не можете дать доступ во внешку ни с клиентских машин, ни с сервера.
3. Вам нужен геокодер по собственной базе адресов клиента.

Что же, никакой магии нет: самый простой вариант — использовать Solr или Sphynx. По сути я просто сохраняю в solr документы с полным адресом и координатами объекта.

Чтобы получить список адресов, можно к примеру взять интересующий вас регион в shp формате, загрузить в postgis, после чего достать адреса запросом вида:

select bldng.osm_id, bldng."A_STRT", bldng."A_SBRB", bldng."A_HSNMBR", 
settle."NAME", ST_AsText(ST_Centroid(bldng.geom)) 
	from building-polygon bldng 
	join settlement-polygon settle on ST_Within(bldng.geom, settle.geom)

На сегодня все. В следующий раз постараюсь подробнее рассказать про систему событий в openlayers.

Ссылки

Документация к openlayers — dev.openlayers.org/docs/files/OpenLayers/Map-js.html

Там же песочница с примерами — openlayers.org/dev/examples/

Proj4js — trac.osgeo.org/proj4js/

Сайт с описанием различных систем координат в разных форматах,
в том числе в формате proj4 — spatialreference.org/

Автор: kiselev_dv

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


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js