Привет Хабр, сегодня я расскажу немного про использование 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