Пробую вышедший недавно четвёртый pgAdmin — а это (внезапно!) веб-приложение, как сейчас модно. Ладно хоть, не тащит за собой ни хром, ни мозиллу, а запускается в новой вкладке существующего браузера. Написано на питоне, весит 22 МБ, из коих половина — картинки, клиентские скрипты, шрифты да переводы.
Архив рубрики: PostgreSQL
Karta ne po-russki
Недавно появилась задача — перевести карту студенческого городка на английский. Или хотя бы транслитерировать её, избавившись от кириллицы. Карта состоит из двух групп слоёв, в одной из них содержатся слои с маркерами, задаными яваскриптовым кодом — перевести их не составит труда, а вот растровый слой, подложку, поверх которой отображаются маркеры, перевести чуть сложнее — об этом сегодняшняя история.
Для того, чтоб иметь контроль над внешним видом подложки, не зависеть от размещающих тайлы (квадратные растровые фрагменты карт) сторонних сервисов, и не платить им денег в конце концов, тайлы генерируются из геоданных OpenStreetMap самостоятельно. OpenStreetMap для любого объекта может содержать множество имён — это и то, что хранится с ключом name
— имя вообще, и int_name
— международное имя, и куча имён с ключами вида name:ru
, name:en
, name:что_попало
. Если делать тайлы с помощью TileMill, а свой стиль создавать на основе OSM Bright, то доступно только одно имя — name
, однако в настройках сопоставления для imposm можно выбрать нужный язык — по умолчанию эта строка в файле imposm-mapping.py закомментирована:
set_default_name_type(LocalizedName(['name:en', 'int_name', 'name']))
Запускаем импорт, английские имена попадают в базу… Однако английских имён мало, сильно меньше, чем объектов с именами, записанными кириллицей.
Выхода из этой ситуации два — правильный и быстрый.
Правильный заключается в аккуратном переводе имён в OSM — слишком долго, да и неохота руками ковыряться.
Быстрый способ — не трогать OSM, а имена транслитерировать локально, в своём экземпляре базы данных. Так и поступим: создадим функцию транслитерации (прообраз подсмотрел на sql.ru) и выполним кучу UPDATE, вызывающих эту функцию. Мне, как перловому программисту, больше был симпатичен вариант с написанием перловой функции внутри PostgreSQL, но сразу такой вариант у меня не заработал, а разбираться было лень.
Итак, скармливаем постгресу такой код:
CREATE OR REPLACE FUNCTION ru_translit(p_string character varying)
RETURNS character varying AS
$BODY$
-- Transliteration of Cyrillic letters
select
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
replace(
translate(
$1,
'АБВГДЕЗИЙКЛМНОПРСТУФЫЭабвгдезийклмнопрстуфыэ',
'ABVGDEZIYKLMNOPRSTUFYEabvgdeziyklmnoprstufye'
),
'ё', 'yo'),
'ж', 'zh'),
'х', 'kh'),
'ц', 'ts'),
'ч', 'ch'),
'ш', 'sh'),
'щ', 'shch'),
'ъ', ''),
'ь', ''),
'э', 'e'),
'ю', 'yu'),
'я', 'ya'),
'Ё', 'Yo'),
'Ж', 'Zh'),
'Х', 'Kh'),
'Ц', 'Ts'),
'Ч', 'Ch'),
'Ш', 'Sh'),
'Щ', 'Shch'),
'Ъ', ''),
'Ь', ''),
'Э', 'E'),
'Ю', 'Yu'),
'Я', 'Ya');
$BODY$
LANGUAGE sql IMMUTABLE
COST 100;
UPDATE osm_admin SET name=ru_translit(name);
UPDATE osm_aeroways SET name=ru_translit(name);
UPDATE osm_amenities SET name=ru_translit(name);
UPDATE osm_barrierpoints SET name=ru_translit(name);
UPDATE osm_barrierways SET name=ru_translit(name);
UPDATE osm_buildings SET name=ru_translit(name);
UPDATE osm_landusages SET name=ru_translit(name);
UPDATE osm_landusages_gen0 SET name=ru_translit(name);
UPDATE osm_landusages_gen1 SET name=ru_translit(name);
UPDATE osm_mainroads SET name=ru_translit(name);
UPDATE osm_mainroads_gen0 SET name=ru_translit(name);
UPDATE osm_mainroads_gen1 SET name=ru_translit(name);
UPDATE osm_minorroads SET name=ru_translit(name);
UPDATE osm_motorways SET name=ru_translit(name);
UPDATE osm_motorways_gen0 SET name=ru_translit(name);
UPDATE osm_motorways_gen1 SET name=ru_translit(name);
UPDATE osm_places SET name=ru_translit(name);
UPDATE osm_railways SET name=ru_translit(name);
UPDATE osm_railways_gen0 SET name=ru_translit(name);
UPDATE osm_railways_gen1 SET name=ru_translit(name);
UPDATE osm_transport_points SET name=ru_translit(name);
UPDATE osm_waterareas SET name=ru_translit(name);
UPDATE osm_waterareas_gen0 SET name=ru_translit(name);
UPDATE osm_waterareas_gen1 SET name=ru_translit(name);
UPDATE osm_waterways SET name=ru_translit(name);
Если мы добавляли номера домов на карту — транслитерируем и их заодно:
UPDATE osm_buildings SET "addr:housenumber"=ru_translit("addr:housenumber");
После чего можно запускать отрисовку своих тайлов — кириллицы там уже не будет.
Трамвайная схема — можно и в TileMill
В марте я рисовал для википедии челябинскую трамвайную схему при помощи TileMill, однако как выделить трамвайные пути самим тайлмиллом, я не знал. Пришлось тогда экспортировать схему в SVG и затем редактировать полученный SVG-файл в Inkscape, вручную выделяя нужные рельсы и меняя им внешний вид.
На самом деле в том, чтоб TileMill нарисовал трамвайные пути, нет особо хитрой магии. Более того, даже не надо, пугаясь питона, ковырять imposm-mapping.py — трамвайные пути можно выделить при помощи CartoCSS, потому что для железных дорог сохраняется их тип:
SELECT DISTINCT type FROM osm_railways;
type -------------- preserved narrow_gauge light_rail subway tram rail
Стиль трамвайных путей можно задать в файле roads.mss. В OSM Bright все доро́ги: и железные, и обычные — слиты в один слой. Правила рисования дорог весьма витиеваты, поэтому, чтоб не тратить на эксперименты лишние усилия, мы не будем пытаться переопределить стиль трамвайных путей — вместо этого добавим новое правило, рисующее широкие красные линии поверх трамвайных путей, не трогая более ничего. Правило это надо поместить после правил, описывающих слой #roads_high
:
#roads_high::tram_highlight [type='tram'] {
line-width: 5;
line-color: @tram_line;
line-comp-op: darken;
}
Результат:
Для того, чтоб убедиться в работоспособности метода, этого вполне достаточно.
Бросается в глаза отсутствие подсветки на мостах — связано это с тем, что слой #bridge
находится выше, чем #roads_high
. Возможный способ решения — завести свой слой железных дорог (а для этого надо уже́ лезть в imposm-mapping.py, но теперь это не страшно), поместить его выше слоя с мостами и написать для железнодорожного слоя свои стилевые правила.
Раскрасим домики
Итак, после предыдущих упражнений здания на нашей карте обзавелись номерами и высотой. Что дальше? Дальше можно, например, раскрасить здания в разные цвета. Можно раскрасить дома в зависимости от основного их предназначения (то есть, в зависимости от содержимого тэгов amenity
и building
). Можно — в зависимости от принадлежности здания.
Когда-то на сайте нашего университета располагалась схема студенческого городка, на которой были здания четырёх разных цветов: общежития, учебные корпуса, прочие здания студгородка, а также посторонние здания, в студгородок не входящие. То есть, на той схеме здания выделялись и по предназначению, и по принадлежности.
Достичь подобного результат с картами OpenStreetMap, рисуемыми через библиотеку Leaflet, можно разными путями: можно, взяв какую-либо карту, не трогать её растровый слой, оставить его без изменений, добавив свой векторный слой, куда нанести контуры зданий. Пример — usjeans.ru/map. Другой подход — сделать собственный растровый слой, на котором сразу и отметить необходимое. Так как предыдущие эксперименты касались как раз собственного растрового слоя — продолжим опыты в том же направлении.
Отметить принадлежность зданий в OpenStreetMap можно разными путями: первый, очевидный — задать у каждого нужного нам здания какой-нибудь новый тэг. Тэг может быть практически любым — даже при существующих ограничениях на имена можно придумать что-нибудь. То множество тэгов, что применяется в OSM — результат не жёсткого диктата разработчиков, а договорённости сообщества участников и множество это легко расширяемо. Другой путь, который мне представляется более верным — создать отношение. Для группировки объектов, представляющих собой что-то общее, предназначено отношение с типом site. Как выяснилось, нужное мне отношение уже́ существовало — надо было лишь проверить его корректность и внести необходимые изменения (например, добавить здания, в отношение не попавшие).
На следующем этапе, импорте данных, выяснилось, что в imposm не предусмотрен способ сохранить в базе данных информацию о принадлежности к отношению. Во всяком случае, в документации он не описан. Не беда — эту информацию можно внести и после импорта, напрямую обратившись к базе данных. Для хранения информации о принадлежности нужно создать дополнительное поле. Так как нам нужен только признак принадлежности здания конкретному отношению, достаточно будет типа Bool
. После всех изменений описание сопоставления данных для зданий в файле imposm-mapping.py стало выглядеть так:
buildings = Polygons(
name = 'buildings',
fields = (
('area', PseudoArea()),
('addr:housenumber',String()),
('building:levels',Integer()),
('height',Integer()),
('belongs_to_susu',Bool()),
),
mapping = {
'building': (
'__any__',
),
'railway': (
'station',
),
'aeroway': (
'terminal',
),
}
)
Выставить флаг принадлежности можно выполнением SQL-запроса
UPDATE osm_buildings
SET belongs_to_susu=1
WHERE osm_id IN (список_id, через запятую);
Чтоб не перечислять объекты вручную, был написан перловый скрипт. Попутно выяснилось, что, несмотря на то, что отношение может в себе содержать другие отношения, нет смысла устраивать рекурсивный обход: в поле osm_buildings.osm_id
хранится первый попавшийся номер: он может быть как номером линии (для большинства зданий), так и номером отношения.
Задача определения роли здания после всех предыдущих манипуляций кажется не такой уж и сложной: во-первых, тип здания (точнее, его предназначение) может хранится в тэге building
— его содержимое при импорте попадает в поле osm_buildings.type
. Среди допустимых типов есть и dormitory
— общежития (кстати, не надо путать с tourism:hostel
— это совсем разные тэги). Тип «учебные корпуса» (например, academic_building) среди часто применяемых не встречается — есть лишь university, который не совсем подходит: университет — это не только учебные корпуса. Можно, конечно, прямо у самих зданий указывать building=academic
, но мне более правильным показался вариант указания роли здания внутри отношения — там и поле для роли есть, и оно не используется совсем. Больше шансов, что данные спокойно сохранятся именно тогда, когда они лежат внутри отношения. А вот в базу данных роль вносится в поле osm_buildings.type
:
UPDATE osm_buildings
SET type='academic'
WHERE osm_id IN (список_id, через, запятую);
Как и прежде, для упрощения назначения ролей, написан ещё один перловый скрипт.
Стилевые правила для зданий (в OSM Bright он лежат в файле base.mss) начинаются так:
#buildings[zoom>=12] {
polygon-fill:@building;
// Our buildings
[belongs_to_susu=1] {
polygon-fill: @susu_building;
[type='dormitory'] {
polygon-fill: @susu_dormitory;
}
[type='academic'] {
polygon-fill: @susu_academic;
}
}
Определение цветов @susu_building
, @susu_dormitory
, @susu_academic
добавлено в файл palette.mss — там для них самое подходящее место.
@susu_building: saturate(darken(@building, 15%), 10);
@susu_dormitory: mix(@susu_building, #f60, 95%);
@susu_academic: mix(@susu_building, #06f, 93%);