[Из песочницы] Создаём интерактивную векторную схему московского метро
Московское метро меняется. Желающий представить себе схему, скажем, 1945 года без проблем соберёт данные из открытых источников; остаётся вопрос с представлением результата, — не на круговой диаграмме же его показывать. В статье я расскажу об основных шагах в создании proof-of-conceptсервиса, позволяющего показать схему метро, например, на 1 мая 74 года (слева) или станции с глубиной заложения больше 30 метров (справа).
Формируем требования
- Показ станций и линий для выбранного диапазона дат
- Красота — «Студия Лебедева» разработала замечательную схему; её и возьмём за основу.
- Удобство использования.
- Для ввода значений я решил использовать ползунки (range slider). С ними мы добьёмся интерактивности и изменений «на лету».
- Я привык к функционалу картографических сервисов, позволяющих менять масштаб и двигать карту, — реализуем его; тем более что наша схема, по факту, и есть карта.
- Добавим возможность поделиться ссылкой на выборку.
Предполагается, что читатель хотя бы поверхностно знаком с javascript, имеет представление о векторной графике и вообще молодец (заметит ошибку или неоптимальное решение и не поленится предложить лучший вариант комментариях).
Отображаем схему
Чтобы сделать схему интерактивной нам, для начала, нужна сама схема. Взглянем на то, что мы хотим получить и разберём картинку на простые элементы.
Станции
Самое главное на схеме, — станции, поэтому начнём с них. Всего 3 типа: конечные, пересадочные и обычные. 2 прямоугольника и круг. Что может быть проще? Кропотливо расставим их на полотне, а некоторые ещё и повернём.
Сохраните схему как картинку, задайте невысокую прозрачность и расположите её под нашим svg-холстом, которому задайте такие же размеры. Это упростит расстановку станций.
Чтобы с комфортом двигаться дальше, я так же добавил в свойства станций их индекс (числовой) и линию, — чуть ниже вы поймёте, для чего это сделано.
Если вы делаете нечто большее, нежели proof of concept, не пожалейте нескольких часов на написание редактора с возможностью перетаскивания станций и указания их свойств. Обслуживать вашу карту станет на порядок проще. Целесообразность прямо пропорциональна количеству станций и их свойств.
Линии
Что же такое линия? Это набор отрезков между станциями. У нас есть координаты, индексы и линии станций — а значит мы легко нарисуем отрезки. Пройдёмся по станциям и будем руководствоваться следующей логикой для каждой из них: если есть станция той же линии, но с индексом старше на единицу, то рисуем между ними сегмент.
Взгляните на схему и попробуйте ответить, какие 2 сегмента не будут нарисованы?
1. отрезок на кольцевой, соединяющий «младшую» и «старшую» по индексу станции
2. отрезок между «Выставочной» и «Киевской»
Как мы увидим, начало и конец сегмента идут не совсем так, как нам надо. Добавим для каждого из 3 типов станций смещение, высчитанное опытным путём. Так же наши сегменты заданы прямыми, но сами линии много где причудливо изогнуты. Пожалуй, это самая кропотливая часть работы над проектом, для ряда сегментов добавим исключение в поведении. Благо, синтаксис несложный.
Переходы
Svg графика использует модель слоёв, а значит чтобы получить нужный результат, каждый переход сделаем с помощью двух кривых: цветная с толстой обводкой пониже и белая с тонкой повыше. Итогое расположение элементов следующее, от нижних к верхним: линии, отрезки переходов с цветной толстой обводкой, станции, отрезки переходов с тонкой белой обводкой, подложка названия станций, название станций.
Названия станций и подложка
Отобразим названия и выровняем текст с помощью свойства text-anchor. Способа осуществить перенос строки, кроме как созданием дополнительного элемента, к сожалению, нет.
Подложку текста сделаем обычным полупрозрачным прямоугольком. Размеры и координаты текста мы получим с помощью getBBox.
Потратив некоторое время на поиски более изящного решения подложки текста, на stackoverflow вы найдёте заплюсованное предложение использовать фильтр. Я советую использовать это решение только в случае единичного отображения графики. Если предполагаются дальнейшие манипуляции с графикой, фильтр поведёт себя некорректно.
Перекрашиваем
Я выбрал цвета по умолчанию для станций как цвета их линий. Но что, если мы хотим перекрасить всё по произвольному градиенту? Это не так сложно как может показаться.
Мы имеем произвольный градиент от одного цвета к другому, который для удобства берём в каналах, значение станции (например, глубину заложения 23 метра) и максимальное и минимальное возможые значения (0 для наземных станций и 80 для самой глубокой «Парк Победы»). Посчитаем процентное соотношение значения станции к разности крайних значений и полученное отношение применим к разности значений по кажому каналу. Вот он наш цвет.
Остальные элементы на странице покрасим градиентом, у нас есть координаты и цвета станций — а это всё что нужно.
Фильтруем
Здесь тоже нестрашно. Проверим, находится ли значение станции в выбранном диапозоне и спрячем станцию, если нет. Когда разберёмся со станциями, поймём, что делать с остальными элементами: если станция спрятана, ни название, ни переход к ней, ни отрезок ветки не должны быть отображены.
Используйте свойство visibility, так как opacity хоть и спрячет элементы, но оставит их выделяемыми и кликабельными.
Так же мы видим, что некоторые станции имеют одно название. Соответственно, прятать название нужно только когда спрятана последняя из станций, связанных с этим названием.
Остаётся настроить понравившийся вам range slider. По определенным причинам я написал свой. Ленивым адекватным людям посоветую, например, этот.
Масштабируем и двигаем
Ответственность за это возьмёт на себя свойство viewBox. Ребята из «Микрософт» написали отличную статью с примерами. Для перехвата скрола я использовал jQuery Mousewheel. Признаться, не самая тривиальная задача, так как при изменении масштаба нужно учитывать сдвиг относительно изначального положения с соответсвующим зуму коэффициентом.
Почти всё
Я использовал модульную архитектуру (вышло где-то полтора десятка модулей), в помощь взял Snap.svg. Данные подгружались динамично, просчитывались и, так как часть вещей была сделана с помощью promises (я использовал jquery, так что их взял оттуда же), я даже смог добавить нехитрый прогресс бар пока всё загружается.
Радость длилась, пока я не решил зайти с телефона… На весьма неглупой lumia 1020 сервис грузился дольше, чем пол минуты. Я же абсолютно забыли о цене манипуляций с dom. У нас, на секунду, больше тысячи элементов! Да и манипуляции с viewBox на мобильном устройстве не работали нормально.
Исправляем недочёты
С клиента на сервер
Волевым решением переносим логику на сервер и кешируем выдачу, чтобы не считать каждый раз. Теперь можно отказаться от Snap.svg на клиенте!
Самописный генератор градиентов для svg весил всего ничего и был изящен, но иногда не срабатывал: к некоторым элементам не применялся ни один из градиентов, только цвет. В то же время с другими элементами градиенты работали абсолютно нормально. Устав от «чёрной магии» я вернул библиотеку в строй. Всё же я пишу доказательство работоспособности, а не «продукт».
Нужно закрыть вопрос с масштабированием и возможностью двигать на мобильных. Подумайте, как бы вы справились с этим.
У нас на носимых устройствах уже есть замечательная поддержка масштабирования и сдвига. А значит… Да, я просто вырубил для телефонов и планшетов манипуляцию с viewBox-свойством.
Что принёс перенос в цифрах?
Выводы
Проблемы с «Mozilla Firefox»
В процессе работы выяснилось, что «Mozilla Firefox» переоптимизированна. В то время как другие ребята исправно отображают всю графику, «Mozilla» допускает пропуск рендера элементов, если элемент не виден пользователю — закрыт div'ом сверху или просто вне области видимости монитора. Как сознательный гражданин я добавил баг, который до сих пор неподтверждён, так как удовлетворить просьбу предоставить the simplest possible testcase у меня не получается, на малом количестве элементов всё работает нормально, а ссылки на проект, видимо, не достаточно.
Если это читает представитель «Mozilla»: Ребят, работает криво независимо от платформы (windows/mac) и версии (наблюдается и в старых версиях), чесслово.
В качестве заключения
Несмотря на то, что в html5 осталась некоторая сырость, пришло время его использовать. Не нужно нагружать процессоры пользователей, добавляя на каждый второй сайт «модные» эффекты, которые ещё вчера были в рассылке для frontend-разработчиков.
Но пришло время делать удобные транспортные схемы и интерактивные продукты, доступные с любого устройства.
Я не понимаю, почему тот же «Яндекс» не переведёт Я.Метро на новую официальную схему, которая будет работать везде; пользователям мобильных устройств ребята предпочитают показывать ссылки на приложения. Магазины (приложений) полны клиентов для соц.сетей и крупных сайтов и, на мой взгляд, есть в этом что-то неправильное.