Pull to refresh

OSM и карта лежачих полицейских в навигаторах

OpenStreetMap *
imageТак как начинать с начала неинтересно, начну с конца.
Мы ее-таки сделали. Достали из данных OpenStreetMap лежачих полицейских, скрестили их со страшной коммерческой программой Навител, сделали веб-просмотрщик этих самых лежачих полицейских, и интерфейс для их добавления для новичков на http://latlon.org/tc/. И даже написали небольшой пресс-релизик, ссылку на который можно разослать друзьям и знакомым-автомобилистам.
Но для хабра можно рассказать и кое-что особенное: как это всё устроено внутри, и как оно делалось.

Главное правило OpenStreetMap — Have fun. Всё описанное делалось не ради денег или ещё каких-то плюшек, и даже не для собственного удобства.

На одной из Минских мини-OSMовок (мини, потому что нигде, кроме IRC-канала они не объявляются) меня наградили заданием: «хочешь сделать полезное дело? достань из осма лежекопы в навител». Потом всё как-то забылось, и через пару месяцев таки вспомнилось, благодаря героическому труду товарища stas_symba c форума 4pda.ru. Он уже несколько месяцев в одиночку собирал и выкладывал обновления по лежачим полицейским по Беларуси. «Но ведь такие данные проще собирать сразу в OSM», — подумалось мне, и я засел за экспорт.

Формат


Так как ни навигатора, ни Навитела, ни автомобиля у меня нет, делать всё пришлось вслепую, читая форумы и думая мозгами. Для начала и понимания масштаба бедствия — вкратце про то, что удалось добыть про формат данных из форумов.

Файл cops.txt (и/или speedcam.txt) — обычный csv, в первой строчке — заголовок, во всех последующих — данные. Заголовок:

idx,x,y,type,speed,dirtype,direction
где:
  • idx — уникальный номер объекта;
  • x,y — координаты, в проекции долгота-широта EPSG:4326;
  • speed — разрешенная скорость для объекта;
  • dirtype — тип направления действия — в 1 или 2 стороны;
  • direction — нправление действия;


Выбор инструмента

Инструменты для решения задачи обычно выбираются из того, что есть под рукой. Под рукой было множество скриптов для поточной обработки OSM XML, и база PostGIS. Несмотря на то, что привычнее работать как раз с поточными скриптами, последнее поле файла спидкамов намекало, что придётся поработать с геометрией объектов, чем и славится PostGIS.

Для начала на домашнем сервере при помощи osm2pgsql была импортирована обычная база для Mapnik. Первый запрос написался быстро и просто:
select * from planet_osm_point where traffic_calming is not NULL;
В ответ на это мне вернулось множество полей, в том числе геометрия в формате WKB, закодированная в hex. Совсем не годится.

Пришлось залезть в мауал по PostGIS, в котором нашлись функции ST_X и ST_Y. Показалось, то, что надо. Переписываем:
gis=> select st_x(way), st_y(way) from planet_osm_point where traffic_calming is not NULL limit 1;
st_x | st_y
------------------+------------------
3085590.21426068 | 7159526.18035388
(1 запись)


Неожиданно? Ожиданно, но надо сделать пояснение.

В веб-картографии широко используются две проекции: для передачи и показа пользователю — широта-долгота, она же EPSG:4326, и «гугловская», она же «меркатор на сфере», она же EPSG:3857, она же EPSG:900913, она же EPSG:3785 (почему так много кодов? долгая история споров больших корпораций, любителей и регистраторов, достойная отдельного поста). Проекция хороша тем, что переход из нее в 4326 на любом языке занимает от силы две строчки математики. Именно она, метрическая, а не в угловых координатах, используется при выводе карт на экран. И для облегчения рассчётов, именно в ней хранится база Mapnik.

Хорошо, ладно. Перепроецируем.
gis=> select ST_X(transform(p.way,94326)) as X, ST_Y(transform(p.way,94326)) as Y from planet_osm_point p where traffic_calming is not NULL limit 1;
x | y
------------+------------
27.7183285 | 53.9438334
(1 запись)


Что там дальше? Скорость.

Никто в OSM на лежачие полицейские скорость не проставляет. Надо придумывать, откуда ее брать. Спрашиваем первого попавшегося человека (им оказался wildMan), получаем ответ — по ПДД разрешенная скорость 60 в городе, 90 вне города. А ещё может быть ограничение по скорости на самой дороге, где ее занесут в соответствющий тег — maxspeed. Ну, заодно можно из тега oneway достать признак односторонности — зря, что ли, в формате поле под него отведено?

Объединяем с таблицей полигонов — в ней хранятся административные границы, и таблицей линий — в ней хранятся линии дорог.

select ST_X(transform(p.way,94326)) as X,
ST_Y(transform(p.way,94326)) as Y, '102' as TYPE,
case when l.maxspeed is not NULL then l.maxspeed else case when t.admin_level = '8' then '60' else '90' end end as SPEED,
case when l.oneway = 'yes' then '1' else '2' end as DirType

from (planet_osm_point p
join planet_osm_line l on (l.highway is not NULL and ST_DWithin(p.way,l.way,.1)))
LEFT OUTER JOIN planet_osm_polygon t on (t.admin_level = '8' and ST_Within(p.way, t.way))
where p.traffic_calming is not NULL;


При помощи стандартного SQL join мы сопоставляем все три таблицы — каждому лежачему полицейскому находим в пару линию дороги в не дальше, чем десяти сантиметрах от нее, и полигон города, ее окружающий, если получится. Далее, поле speed мы пытаемся заполнить по тегу maxspeed линии, а если его не заполнили, то смотрим, попала ли точка в административную границу города: если да, ставим 60, если нет — 90.

idx — порядковый номер — был соблазн взять из osm_id. Но потом стало понятно, что полицейский, нахолящийся на стыке двух линий, будет встречаться в обоих линиях в обе стороны, и id у него получится один и тот же при разном направлении, в связи с чем лучшим вариантом показалось — пронумеровать их все по порядку.

Страх и отсутствие документации


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

Чтобы посчитать азимут, PostGIS требует две точки. Очевидно, они должны лежать на той же самой дороге, на которой установлен лежачий полицейский, на некотором расстоянии в обе стороны от него.
Метод ST_Line_Locate_Point позволил найти длину вдоль линии от ее начала до заданной точки, а ST_Line_Interpolate_Point — наоборот, по длине вдоль линии найти точку.

Но psql рисует в ответ на запросы мне табличку в псевдографике, когда мне нужен CSV! Неужели писать обёртку?
Оказалось, что всё намного проще. Функция COPY позволяет доставить результат запроса сразу в CSV — как раз то, что нужно. Вот такая маленькая кошачья радость.

Выгоняю первый результат, нахожу обладателя Навитела, заставляю проверить… и упс. Направления не параллельны дорогам. Возникает мысль «неужели...», и лапы сам пишут перепроецирование в 4326 при расчёте азимута.

Да, доблестные разработчики Навитела считают направление в проекции, не сохраняющей направления. Честь им, хвала и почёт.

Квазиитог


В итоге, запросом стал вот такой вот монстрик, добывающий из данных OpenStreetMap то, чего в них изначально как будто бы и нет — лежачих полицейских в формате Навител. Запрос, достающий из них камеры контроля скорости, можете написать в качестве домашнего задания. :)
CREATE TEMP SEQUENCE idx ;
COPY (

select nextval('idx') as idx,
ST_X(transform(p.way,94326)) as X,
ST_Y(transform(p.way,94326)) as Y, '102' as TYPE,
case when l.maxspeed is not NULL then l.maxspeed else case when t.admin_level = '8' then '60' else '90' end end as SPEED,
case when l.oneway = 'yes' then '1' else '2' end as DirType,
floor(ST_Azimuth(
transform(ST_Line_Interpolate_Point(l.way,
case when (ST_Line_Locate_Point(l.way,p.way)-0.01 < 0) then 0 else ST_Line_Locate_Point(l.way,p.way)-0.0000001 end),94326),
transform(ST_Line_Interpolate_Point(l.way,
case when (ST_Line_Locate_Point(l.way,p.way)+0.01 > 1) then 1 else ST_Line_Locate_Point(l.way,p.way)+0.0000001 end)
,94326))/(2*pi())*360) as Direction
from (planet_osm_point p
join planet_osm_line l on (l.highway is not NULL and ST_DWithin(p.way,l.way,.1)))
LEFT OUTER JOIN planet_osm_polygon t on (t.admin_level = '8' and ST_Within(p.way, t.way))
where p.traffic_calming is not NULL
) TO STDOUT WITH CSV HEADER;


Но Котяра не был бы Котярой, если бы отовсюду не торчали его кошачьи уши. Потому в качестве разминки кроме ежепятиминутной выгрузки обновлённых лежачих полицейских и камер для СНГ был организован рендеринг всего этого безобразия в тайлы, чтобы на него могли полюбоваться те, у кого нет навигатора с Навителом.

Ближе к выходным товарищ andrewsh (думаю, ему тоже есть о чём рассказать, если его попросить :) написал удобный интерфейс для сообщения о новых лежачих полицейских без регистрации — latlon.org/tc, и все сервисы было решено пустить в народ. Надеюсь, вам понравится.

Have fun! ;)
Tags:
Hubs:
Total votes 50: ↑45 and ↓5 +40
Views 5.6K
Comments 32
Comments Comments 32

Posts