Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
15 мая 1935 была открыта первая очередь Московского метрополитена. С этого момента началась новая эра в истории города, в котором подземка, безусловно, играет важную роль.
Любопытная деталь, что в течение следующих десятилетий схема метро изображалась с привязкой к реальному расположению на карте города. И только в семидесятых годах прошлого века, когда общее количество станций приблизилось к сотне, начали использовать схематичное изображение линий метро, которое оказалось удобнее и проще для восприятия пассажирами.
Упрощенная схема читается быстрее. Но, в то же время, не дает представления о реальном расположении линий, скрывает детали. Длинные перегоны метро ничем не отличаются от коротких. Нет привязки к географии.
Давайте создадим интерактивную карту линий Московского метрополитена и посмотрим на его историю — как развивалась одна из самых больших сетей мира.
Для создания такой карты нам потребуются:
Механизм для отображения карты
Удобная модель хранения данных
Сами данные
Механизм
Для начала, определяемся с требования к механизму. Он должен уметь:
отображать базовый слой с «обычной» картой — дорогами, водоемами, улицами, зданиями и т.д.;
отображать поверх базового слоя круги и линии, которые будут изображать станции и линии метро соответственно;
обновлять отрисованные станции и линии метро в зависимости от выбранной даты;
переключать текущую дату в интервале от 15 мая 1935 года (запуск первой линии метро) до настоящего времени.
Первые три пункта из коробки есть в open source библиотеке Leaflet. Четвертый будем реализовывать самостоятельно.
Базовый слой карты
Пример 1: Создание базового слоя карты
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script>
</head>
<body>
<div id="map" style="width: 720px; height: 400px;"></div>
<script>
// Создаем карту
const mymap = L.map('map').setView([55.754181, 37.622821], 12);
// Создаем tile layer и добавляем его на карту
const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
minZoom: 1,
maxZoom: 18,
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(mymap);
</script>
</body>
</html>
Что такое tile server?
Небольшое пояснение для тех, кто никогда не сталкивался с понятиями тайлов (tiles) и тайлового сервера (tile server).
Онлайн карты, к которым привыкло большинство пользователей, — OpenStreetMap, Яндекс.Карты, 2ГИС и т.п. — состоят из тайлов (tile): квадратов с длиной стороны в 256 пикселей.
Каждый тайл имеет координаты x, y и z:
z — масштаб, при котором он отображается (чем больше значение z, тем больше увеличение);
x — порядковый номер тайла по горизонтали при выбранном масштабе z;
y — порядковый номер тайла по вертикали при выбранном масштабе z.
Например, тайл с координатами x=2476, y=1280 и z=12 выглядит вот так:
Эти “квадраты” хранятся на тайловом сервере (tile server) в определенной структуре. Например, тайл выше доступен по адресу https://tile.openstreetmap.org/12/2476/1280.png
Полное изображение, которое мы, в конечном счете, видим, склеивается из множества тайлов в единую картину:
Когда мы работаем с картой в браузере — изменяем масштаб или прокручиваем в стороны — нужные тайлы, по мере необходимости, загружаются с сервера.
В нашем случае в качестве тайлового сервера используется сервер tile.openstreetmap.org.
Станции и линии метро
Для отображения станций метро используем circleMarker: круг с заданными в пикселях радиусом, цветом обводки и заливки, прозрачностью и прочими свойствами.
Пример 2: Добавляем станцию метро на карту
Создаем красную окружность с координатами 55.7578487° северной широты и 37.6163659° восточной долготы (станция «Охотный ряд»), с белой заливкой, радиусом 6 пикселей и добавляем его на карту:
L.circleMarker([55.7578487, 37.6163659], {
radius: 6,
width: 2.5,
color: "red",
fillColor: "white",
fillOpacity: 1
}).bindPopup("Охотный Ряд")
.addTo(mymap);
Для отображения линий метро будем использовать Polyline: ломаную с заданной шириной, цветом, прозрачностью и другими параметрами.
Пример: Добавляем линию метро на карту
Добавим первую очередь московского метро, открытую 15 мая 1935 года. Это будет ломаная красного цвета, шириной 4, проходящая через соответствующие станции метро.
const plln = [
[
[55.7888499, 37.6801924], // Сокольники
[55.7798626, 37.6666841], // Красносельская
[55.7754719, 37.6566036], // Комсомольская
[55.7691137, 37.64887], // Красные Ворота
[55.7658915, 37.6388871], // Кировская
[55.7598058, 37.6264977], // Дзержинская
[55.7578487, 37.6163659], // Охотный Ряд
[55.7515247, 37.6102524], // Библиотека имени Ленина
[55.7454796, 37.6036755], // Дворец Советов
[55.7352856, 37.5940414] // Парк культуры имени Горького
],
[
[55.7578487, 37.6163659], // Охотный Ряд
[55.7524242, 37.6086732], // Улица Коминтерна
[55.7519189, 37.6007512], // Арбатская
[55.7488633, 37.5826955] // Смоленская
]
];
L.polyline(plln, {
color: "red",
weight: 6,
}).bindPopup("Кировско-Фрунзенская линия")
.addTo(mymap);
Обратим внимание, что при движении от Сокольников, после Охотного ряда, была развилка. Поезда следовали в двух направлениях: в сторону Смоленской и в сторону Парка культуры имени Горького.
Удаление и добавление новых кругов (станций) и ломаных (линий метро) также является встроенными функциями Leaflet — это потребуется для перерисовки схемы метро в зависимости от выбранной даты.
Переключение текущей даты
Следующим шагом нужно будет прикрутить слайдер с диапазоном от «15 мая 1935» и до сего дня. С помощью него мы будем задавать «текущую» дату для отображения схемы метро, какой она была в этот день.
Алгоритм прост: при изменении значения слайдера выполняем по порядку:
очищаем текущую схему,
проходим по всем объектам (станциям и линиям) в “базе”,
выбираем те объекты, которые существовали в новую выбранную дату,
отображаем их на карте.
Чтобы определить, какие объекты существовали в указанный день, необходимы правильным образом подготовленные данные. Например, нужно сохранить в “базе” детали о том, что с момента открытия в 1935 году и до 1990 года станция метро «Чистые пруды» называлась «Кировской». Или, например, что первая версия станции «Первомайская» полностью закрылась в 1961 году.
Чем проще и удобнее будет модель хранения данных, тем удобнее будет работа с самими данными.
Модель
За основу возьмем готовое решение, прошедшим проверку временем — модель данных OpenStreetMap. И доработаем ее до наших целей там, где её будет не хватать.
В OSM все данные состоят из трех видов элементов (elements): точки (node), линии (way) и отношения (relation).
Точка (Node)
Node — простейший элемент, который хранит в себе координаты широты и долготы.
Пример Node
Nodes, с координатами станций первой линии метро на момент её открытия 15 мая 1935 года в нотации OSM XML формате (без служебных данных):
<!-- Сокольники -->
<node id="1" lon="55.7888499" lat="37.6801924"/>
<!-- Красносельская -->
<node id="2" lon="55.7798626" lat="37.6666841"/>
<!-- Комсомольская -->
<node id="3" lon="55.7754719" lat="37.6566036"/>
<!-- Красные Ворота -->
<node id="4" lon="55.7691137" lat="37.64887"/>
<!-- Кировская -->
<node id="5" lon="55.7658915" lat="37.6388871"/>
<!-- Дзержинская -->
<node id="6" lon="55.7598058" lat="37.6264977"/>
<!-- Охотный Ряд -->
<node id="7" lon="55.7578487" lat="37.6163659"/>
<!-- Библиотека имени Ленина -->
<node id="8" lon="55.7515247" lat="37.6102524"/>
<!-- Дворец Советов -->
<node id="9" lon="55.7454796" lat="37.6036755"/>
<!-- Парк культуры имени Горького -->
<node id="10" lon="55.7352856" lat="37.5940414"/>
<!-- Смоленская -->
<node id="11" lon="55.7488633" lat="37.5826955"/>
<!-- Арбатская -->
<node id="12" lon="55.7519189" lat="37.6007512"/>
<!-- Улица Коминтерна -->
<node id="13" lon="55.7524242" lat="37.6086732"/>
Линия (Way)
Way — упорядоченная ломаная линия, состоящая из последовательно заданных точек (nodes).
Пример Way
<!-- Участок «Сокольники — Парк Культуры имени Горького» -->
<way id="1">
<nd rf="1"/>
<nd rf="2"/>
<nd rf="3"/>
<nd rf="4"/>
<nd rf="5"/>
<nd rf="6"/>
<nd rf="7"/>
<nd rf="8"/>
<nd rf="9"/>
<nd rf="10"/>
</way>
<!-- Участок «Охотный ряд — Смоленская» -->
<way id=”2”>
<nd rf="7"/>
<nd rf="13"/>
<nd rf="12"/>
<nd rf="11"/>
</way>
Отношение (Relation)
Отношение (relation) — это группировка элементов (elements) по определенному признаку. В отношение могут входит точки (nodes), линии (ways), а также, другие отношения (relations).
Пример Relation
Например, объединение всех станций и участков метро могут объединяться под одним отношением (relation) — «Московский метрополитен».
<!-- Московский метрополитен -->
<relation id="1">
<member type="node" ref="1" role=""/>
<member type="node" ref="2" role=""/>
…
<member type="node" ref="13" role=""/>
<member type="way" ref="1 role=""/>
<member type="way" ref="2" role=""/>
</relation>
Тег (Tag)
Также, у каждого из элементов (elements) могут быть заданы теги: пара "ключ=значение". Теги определяют свойства объекта, и используются для описания того, как объекты будут отображаться на карте.
Пример Tags
Например, станция метро может иметь такой набор тегов
<!-- Сокольники -->
<node id="1" lon="55.7888499" lat="37.6801924">
<tag k=”name” v=”Сокольники”/>
<tag k=”type” v=”subway_station”/>
<tag k=”color” v=”red”/>
</node>
Доработка модели данных OSM
OpenStreetMap создана и развивается для отображения актуальных деталей — того, что существует в данный момент. Для некоторых типов объектов есть возможность указать год их возникновения (например, год постройки здания). Но про объекты, которых уже не существует, или которые изменялись в прошлом, данных нет.
Чтобы это исправить, к каждому объекту, который может отображаться на карте, добавляем две характеристики:
from_date — дата, с которой объект появляется на карте
to_date — дата, с которой объект более не должен отображаться на карте (например, временная станция Калужская: from_date = 1964-04-15, to_date = 1974-08-12).
С помощью этих характеристик можно будет управлять отображаемыми на карте данными, в зависимости от выбранной даты.
Собираем всё вместе
Точки (nodes) и линии (ways) будут использоваться для определения геометрии — точек и ломаных линий, из которых состоят все объекты. У них есть только уникальные идентификаторы (id) и их координаты: широта и долгота.
Отношения (relations) будут использоваться для определения самих объектов (станция или линия) и их характеристик. Relations состоят из комбинации nodes или ways, и дополняются необходимыми тегами. Теги from_date и to_date являются обязательными.
Такой подход позволяет использовать одни и те же nodes и ways повторно: вся информация об объекте хранится на уровне relation.
Например, чтобы отобразить, что станция метро Кировская была переименована в Чистые пруды, будет использовано два relation. Один: с момента открытия до момента переименования. Второй: с момента переименования и до настоящего времени.
Пример relations для переименованной станции метро
<relation id=”2”>
<member type=”node” ref=”5”/>
<tag k=”name” value=”Кировская”/>
<tag k=”type” value=”subway_station”/>
<tag k=”color” value=”red”/>
<tag k=”from_date” value=”1935-05-15”/>
<tag k=”to_date” value=”1990-11-05”/>
</relation>
<relation id=”3”>
<member type=”node” ref=”5”/>
<tag k=”name” value=”Чистые пруды”/>
<tag k=”type” value=”subway_station”/>
<tag k=”color” value=”red”/>
<tag k=”from_date” value=”1990-11-05”/>
<tag k=”to_date” value=”now”/>
</relation>
Аналогичная история с участками линий. Например, если с 1935 года участок путей принадлежал Сокольнической линии, а с 1938 года стал участком Арбатской линии, то нам потребуется три relation.
Пример relations для изменения линий
<relation id=”247”>
<member type=”way” ref=”1”/>
<member type=”way” ref=”102”/>
<member type=”way” ref=”2”/>
<member type=”way” ref=”3”/>
<member type=”way” ref=”4”/>
<tag k=”name” value=”Кировско-Фрунзенская линия”/>
<tag k=”type” value=”subway_line”/>
<tag k=”color” value=”red”/>
<tag k=”from_date” value=”1937-03-20”/>
<tag k=”to_date” value=”1938-03-13”/>
</relation>
<relation id=”248”>
<member type=”way” ref=”1”/>
<member type=”way” ref=”102”/>
<tag k=”name” value=”Кировско-Фрунзенская линия”/>
<tag k=”type” value=”subway_line”/>
<tag k=”color” value=”red”/>
<tag k=”from_date” value=”1938-03-13”/>
<tag k=”to_date” value=”1957-05-01”/>
</relation>
<relation id=”249”>
<member type=”way” ref=”3”/>
<member type=”way” ref=”4”/>
<member type=”way” ref=”5”/>
<member type=”way” ref=”6”/>
<tag k=”name” value=”Арбатско-покровская линия”/>
<tag k=”type” value=”subway_line”/>
<tag k=”color” value=”blue”/>
<tag k=”from_date” value=”1938-03-13”/>
<tag k=”to_date” value=”1944-01-18”/>
</relation>
Данные
Подготовка данных состояла из трех шагов.
Первым шагом была начальная загрузка данных из википедии со страницы список станций Московского метрополитена, которая содержит всю нужную информацию — список станций, их координаты, дату открытия. Все детали были загружены в электронные таблицы Google документов для дальнейшей обработки и доработки.
Второй шаг — уточнение и добавление деталей по тем станциям и линиям, «свойства» которых изменялись.
Лирическое отступление
Переименование станций метро — история известная.
Переход участка линии из-под одной линии к другой — тоже не что-то из ряда вон выходящее (например, Каховская линия (ныне закрытая) до 20 ноября 1995 была частью Замоскворецкой; участок от Кунцевской до Крылатского принадлежал Филевской линии, а с 7 января 2008 стал частью Арбатско-Покровской линии).
Но некоторые изменения оказались довольно неожиданными. Например, что в Москве было две станции метро, которые изначально создавались как временные: Калужская и Первомайская.
Впрочем, мой победитель: переименование в 1963 году станций «Измайловская» и «Измайловский парк» в.... «Измайловский парк» и «Измайловскую». Две подряд идущие станции «поменяли местами». Остается только догадываться, как должно было рвать шаблоны у москвичей и гостей столицы, которых это изменение непосредственно коснулось.
И, заключительный третий шаг: выгрузка таблиц в CSV и конвертация её в json структуру данных для использования через JavaScript.
Немного косметики
Карта-подложка, которую предоставляет OSM, — достаточно яркая и шумная. На таком фоне наша схема метро теряется. Чтобы сделать её более наглядной, добавим второй базовый слой, с менее выраженным акцентом. Здесь очень хорошо подойдет светлый слой от Mapbox — Mapbox Light.
Стоит заметить, что в отличии от тайлов OSM, бесплатное использование тайлов Mapbox ограничено, и при превышении определенного количества в месяц тайлы перестанут подгружаться.
Пример: Добавляем альтернативный базовый слой — Mapbox Light
// Создаем новый слой
const grayscale = L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoidGltZWxhcHNlbWFwIiwiYSI6ImNrcWZuemE3dzBxbjIyb3Njazk4ODd4M3oifQ._f0k3m2zgmxrdleisUaymQ', {
minZoom: 1,
maxZoom: 18,
id: 'mapbox/light-v10',
tileSize: 512,
zoomOffset: -1,
attribution: '© <a href="https://www.mapbox.com/about/maps/">Mapbox</a> | © <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> | <strong><a href="https://www.mapbox.com/map-feedback/" target="_blank">Improve this map</a></strong>',
});
// Создаем и добавляем на карту контрол для переключениям между слоями
const mapLayers = {
"OpenStreetMap": osm,
"Grayscale": grayscale,
};
L.control.layers(mapLayers).addTo(mymap);
Ещё одно визуальное улучшение, которое напрашивается сразу, — изменение размеров станций и линий метро в зависимости от масштаба.
Для каждого масштаба (zoom) определяем стили окружностей и линий — толщину, прозрачность, радиус, толщину обводки. При изменении масштаба (zoom) вызываем принудительную перерисовку всех объектов с использованием соответствующих стилей.
Вот теперь — красота:
Итоги
Получилась интерактивная карта московского метро, на которой можно проследить, как развивалась сеть. Например, в каких направлениях она росла, с какой скоростью шел запуск новых станций, да и просто понастольгировать.
Итоговый результат можно посмотреть здесь: https://mm.timelapsemap.com.
Карта по ссылке выше будет развиваться дальше. Для истории здесь https://timelapsemap.com/h/bc4fc2 лежит сохраненная копия, соответствующая этой статье.
При подготовке использованы материалы OpenStreetMap и Wikipedia. КДПВ — с сайта http://n-metro.ru.
PS
Несмотря на большое количество данных в Википедии, не про все изменения удалось найти точную информации.
Поделитесь, пожалуйста, в комментариях если знаете:
С какой даты «Парк Культуры имени Горького» стал просто «Парком культуры»?
С какой даты «Измайловский парк культуры и отдыха имени Сталина» был переименован в «Партизанскую»?
С какой даты станция «Имени Кагановича» была переименована в «Охотный ряд»?
Детали о закрытии станции «Чистые пруды» во время Великой Отечественной войны
Детали о переименовании станции «Калининская» в станцию «Воздвиженка» (ныне «Александровский сад»)