Как сделать интерактивную карту с маршрутами на Python

Распространённая задача программистов в работе с геопространственными данными — отобразить маршруты между различными точками.
Установка OSMnx
OSMnx — это пакет Python для загрузки геопространственных данных OpenStreetMap, моделирования, проектирования, визуализации и анализа реальных уличных сетей и другой геометрии геопространства.
Установка OSMnx немного хитрая: до обычных pip/conda install нужно выполнить следующие шаги:
$ conda config --prepend channels conda-forge $ conda install osmnx
Если pip/conda install не работает, выполните pip install osmnx.
Вы получите готовый к работе OSMnx.
Нахождение самого быстрого маршрута
Теперь, чтобы найти самый короткий маршрут между двумя точками, можно воспользоваться пакетом NetworkX вместе с OSMnx. NetworkX — пакет Python для создания, манипулирования, а также изучения структуры, динамики и функций сложных сетей.
Следующий фрагмент кода находит самый быстрый путь между двумя точками в Сан-Франциско:
import osmnx as ox
import networkx as nx
ox.config(log_console=True, use_cache=True)
# define the start and end locations in latlng
start_latlng = (37.78497,-122.43327)
end_latlng = (37.78071,-122.41445)
# location where you want to find your route
place = 'San Francisco, California, United States'
# find shortest route based on the mode of travel
mode = 'walk' # 'drive', 'bike', 'walk'
# find shortest path based on distance or time
optimizer = 'time' # 'length','time'
# create graph from OSM within the boundaries of some
# geocodable place(s)
graph = ox.graph_from_place(place, network_type = mode)
# find the nearest node to the start location
orig_node = ox.get_nearest_node(graph, start_latlng)
# find the nearest node to the end location
dest_node = ox.get_nearest_node(graph, end_latlng)
# find the shortest path
shortest_route = nx.shortest_path(graph, orig_node,dest_node,
weight=optimizer)
Метод нахождения кратчайшего пути по умолчанию — алгоритм Дейкстры, строка dijkstra. Изменить его на bellman-ford можно, изменив параметр method в shortest_path(). Сейчас переменная shortest_route сейчас содержит коллекцию пути, позволяющую за кратчайшее время пешком пройти из одной точки в другую:
[5287124093, 65314192, 258759765, 65314189, 5429032435, 65303568, 65292734, 65303566, 2220968863, 4014319583, 65303561, 65303560, 4759501665, 65303559, 258758548, 4759501667, 65303556, 65303554, 65281835, 65303553, 65303552, 65314163, 65334128, 65317951, 65333826, 65362158, 65362154, 5429620634, 65308268, 4064226224, 7240837048, 65352325, 7240837026, 7240837027]
Отрисовка путей
Очевидно, список путей нам не очень поможет. Более осмысленный способ интерпретации результата — отрисовка путей с помощью функции plot_route_folium():
shortest_route_map = ox.plot_route_folium(graph, shortest_route) shortest_route_map
Эта функция возвращает карту folium (folium.folium.Map). В Jupyter Notebook она выглядит так:

Набор тайлов по умолчанию — cartodbpositron. Если нужно изменить его, установите аргумент tiles в соответствующее значение. Следующий фрагмент кода покажет карту с набором тайлов openstreetmap:
shortest_route_map = ox.plot_route_folium(graph, shortest_route,
tiles='openstreetmap')
shortest_route_map
Ниже отображён набор openstreetmap:

Если нужно позволить пользователям выбирать предпочтения во время выполнения, воспользуйтесь этим фрагментом кода:
import folium
folium.TileLayer('openstreetmap').add_to(shortest_route_map)
folium.TileLayer('Stamen Terrain').add_to(shortest_route_map)
folium.TileLayer('Stamen Toner').add_to(shortest_route_map)
folium.TileLayer('Stamen Water Color').add_to(shortest_route_map)
folium.TileLayer('cartodbpositron').add_to(shortest_route_map)
folium.TileLayer('cartodbdark_matter').add_to(shortest_route_map)
folium.LayerControl().add_to(shortest_route_map)
shortest_route_map
Теперь пользователь может выбирать набор тайлов:

Изменения типа перемещения и оптимизатор
Помимо поиска кратчайшего пути пешехода можно найти кратчайший путь для водителя:
# find shortest route based on the mode of travel mode = 'drive' # 'drive', 'bike', 'walk' # find shortest path based on distance or time optimizer = 'time' # 'length','time'
Вот он:

А что с велосипедом?
# find shortest route based on the mode of travel mode = 'bike' # 'drive', 'bike', 'walk' # find shortest path based on distance or time optimizer = 'time' # 'length','time'
Вот самый быстрый путь на велосипеде:

Можно найти не только самый быстрый путь, но и самый короткий:
# find shortest route based on the mode of travel mode = 'bike' # 'drive', 'bike', 'walk' # find shortest path based on distance or time optimizer = 'time' # 'length','time'
Вот соответствующий код для велосипедов:

Другие варианты комбинирования оставляю вам.
Геокодирование местоположения
В поиске маршрута не очень удобно определять широту и долготу, если только их уже нет в вашем наборе данных. Проще может быть другой подход: дать расположениям удобные названия. Сделать это можно с помощью геокодирования, используя модуль geopy.
Геокодирование — это преобразование адреса в координаты этого адреса. Обратное геокодирование — это преобразование пары координат в удобный адрес.
Чтобы установить geopy, выполните в терминале эту команду:
$ pip install geopy
Следующий фрагмент кода создаёт экземпляр класса геокодера Nominatim для данных OpenStreetMap. В коде вызывается метод geocode(), чтобы закодировать расположение Golden Gate Bridge. При помощи геокодирования можно извлечь широту и долготу расположения:
from geopy.geocoders import Nominatim
locator = Nominatim(user_agent = "myapp")
location = locator.geocode("Golden Gate Bridge")
print(location.latitude, location.longitude)
# 37.8303213 -122.4797496
print(location.point)
# 37 49m 49.1567s N, 122 28m 47.0986s W
print(type(location.point))
# <class 'geopy.point.Point'>
Проверить результат можно здесь. В поисковую строку введите широту и долготу:

Изменим оригинальный код, чтобы геокодировать начальную и конечную точки:
import osmnx as ox import networkx as nx from geopy.geocoders import Nominatim ox.config(log_console=True, use_cache=True) locator = Nominatim(user_agent = "myapp") # define the start and end locations in latlng # start_latlng = (37.78497,-122.43327) # end_latlng = (37.78071,-122.41445) start_location = "Hilton San Francisco Union Square" end_location = "Golden Gateway Tennis & Swim Club" # stores the start and end points as geopy.point.Point objects start_latlng = locator.geocode(start_location).point end_latlng = locator.geocode(end_location).point # location where you want to find your route place = 'San Francisco, California, United States' # find shortest route based on the mode of travel mode = 'bike' # 'drive', 'bike', 'walk' # find shortest path based on distance or time optimizer = 'length' # 'length','time' # create graph from OSM within the boundaries of some # geocodable place(s) graph = ox.graph_from_place(place, network_type = mode) # find the nearest node to the start location orig_node = ox.get_nearest_node(graph, start_latlng) # find the nearest node to the end location dest_node = ox.get_nearest_node(graph, end_latlng) ...
Заметьте, что функция get_nearest_node() принимает и кортеж широта — долгота, и объект geopy.point.Point. Вот самая короткая дистанция на велосипеде от отеля Hilton и до Golden Gateway Tennis & Swim Club в Сан-Франциско:

Отображение маркеров начальной и конечной точек
Карта станет понятнее, если отображать маркеры, указывающие начальную и конечную точки маршрутов. Чтобы отобразить маркер со всплывающим окном, можно воспользоваться классом Marker, о котором я рассказывал в предыдущей статье. Код ниже отображает два маркера: зелёный означает начальную точку, красный — конечную:
import folium
# Marker class only accepts coordinates in tuple form
start_latlng = (start_latlng[0],start_latlng[1])
end_latlng = (end_latlng[0],end_latlng[1])
start_marker = folium.Marker(
location = start_latlng,
popup = start_location,
icon = folium.Icon(color='green'))
end_marker = folium.Marker(
location = end_latlng,
popup = end_location,
icon = folium.Icon(color='red'))
# add the circle marker to the map
start_marker.add_to(shortest_route_map)
end_marker.add_to(shortest_route_map)
shortest_route_map
Класс Marker принимает координаты только в форме кортежа. А значит, чтобы кортеж содержал широту и долготу, нужно изменить start_lating и end_lating. Вот два маркера, показывающих начало и конец пути:

Отрисовка статичного графа
Ранее мы воспользовались plot_route_folium(), чтобы показать кратчайший путь на карте folium:
shortest_route_map = ox.plot_route_folium(graph, shortest_route) shortest_route_map
Есть ещё одна интересная функция — plot_graph_route(), которая вместо вывода интерактивной карты чертит статический граф. Это полезно, когда вам нужно изображение с маршрутом между двумя точками. Следующий код выводит статичный граф для точек из предыдущего раздела статьи:
import osmnx as ox
import networkx as nx
ox.config(log_console=True, use_cache=True)
graph = ox.graph_from_place(place, network_type = mode)
orig_node = ox.get_nearest_node(graph, start_latlng)
dest_node = ox.get_nearest_node(graph, end_latlng)
shortest_route = nx.shortest_path(graph, orig_node, dest_node,
weight=optimizer)
fig, ax = ox.plot_graph_route(graph, shortest_route, save=True)
Вы увидите такой вывод:
