Распространённая задача программистов в работе с геопространственными данными — отобразить маршруты между различными точками. Решением, которое может понадобиться в разработке веб-сайта, делимся к старту курса по Fullstack-разработке на 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)

Вы увидите такой вывод:

Резюме

Надеюсь, эта статья вдохновит вас и вы используете её, чтобы создавать маршруты по достопримечательностям. Возможно, вам захочется позволить пользователям вводить их текущие местоположения и прокладывать маршруты до места назначения.

Ссылки

[1] Boeing, G. 2017. OSMnx: New Methods for Acquiring, Constructing, Analyzing, and Visualizing Complex Street Networks. Computers, Environment and Urban Systems 65, 126–139. doi:10.1016/j.compenvurbsys.2017.05.004

А мы поможем вам освоить надёжную профессию в IT с самого начала или развить ваши навыки:

Выбрать другую востребованную профессию.

Краткий каталог курсов и профессий

Комментарии (5)


  1. gnome2_terminal_is_best
    05.03.2022 09:11
    +1

    Думал, увижу алгоритм расчёта путей, на основе массива данных, а тут просто про использование очередной библиотеки. Куда делось настоящее программирование?


    1. AigizK
      05.03.2022 09:30

      Да давно уже все алгоритмы реализованы. Используйте готовые библиотеки /sarkazm_off


      1. gnome2_terminal_is_best
        05.03.2022 20:45

        Согласен, но на собеседованиях, очень любят спрашивать про настоящее программирование, когда на практике, придётся только с фреймворками работать, где всё уже есть в коробке.


    1. RandomProgrammer
      05.03.2022 10:47

      "Python" в тегах явно намекает на использование библиотеки)


      1. gnome2_terminal_is_best
        05.03.2022 20:47

        Ну, не только же с библиотеками работать? Хотя это тренд для 90% случаев.