Python Django и OSRM: маршрут на интерактивной онлайн-карте

Написание небольших, но функциональных приложений  —  это интересный и полезный способ научиться чему-то новому в области разработки программного обеспечения. 

В руководстве рассмотрим веб-приложение на Django, объединяющее картографию с маршрутизацией: создадим веб-сайт с отображением кратчайшего маршрута между двумя точками, выбираемыми пользователем через щелчки мышью или тапы по интерактивной онлайн-карте. 

Содержание:

  1. Схема веб-приложения.
  2. Пакет Folium и библиотека Leaflet.
  3. Технология OSRM и основы OSRM API.
  4. Конфигурация Django и веб-приложения.
  5. Выводы.

1. Схема веб-приложения

Приблизительная схема работы веб-приложения:

  1. Сначала посетителю веб-страницы показывается карта.
  2. Каждый раз, когда пользователь выбирает два места на карте через щелчок мышью, координаты отправляются на сервер.
  3. Затем координаты передаются по API для построения маршрутов. Ответ ожидается сервером в закодированном виде.
  4. После получения сервером ответ декодируется и отображается на веб-странице.

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

  1. Django, Folium, Requests, Polyline.
  2. Leaflet: допустимо подключение через CDN, устанавливать JavaScript-библиотеку не нужно.

Конечно, для решения задачи потребуется немного JavaScript, ведь Django  —  это серверная технология. Нужно запустить программу на стороне клиента, чтобы сначала получить координаты через пользовательское нажатие по карте, а затем  —  отправить эти координаты на Python-сервер.


2. Пакет Folium и библиотека Leaflet

Рассмотрим вышеперечисленные требования подробнее.

Folium

Folium весьма полезен в построении географических карт на Python, ведь он опирается одновременно и на сильные стороны экосистемы Python в работе с данными, и на сильные стороны библиотеки Leaflet в создании интерфейсов с интерактивными картами. Пакет работает на основе концепции первоначального манипулирования данными в Python, с последующей их визуализацией на карте Leaflet через Folium.

Folium позволяет легко визуализировать на интерактивной карте Leaflet предварительно обработанные на Python-сервере данные. Пакет содержит ряд встроенных наборов тайлов от провайдеров OpenStreetMap, Mapbox и Stamen, а также поддерживает пользовательские наборы тайлов с ключами API Mapbox или Cloudmade.

Параметры интерактивной карты поддаются тонкой настройке, такие как масштаб, слой, маркер и подобные.

За подробностями проследуйте по ссылке на документацию Folium.

Folium запущен в Jupyter Notebook

Leaflet

Leaflet  —  это лидирующая библиотека для интерактивных карт с открытым исходным кодом, написанная на JavaScript и удобная даже на мобильных устройствах. Она весит всего 39 КБ JS, но обладает всеми возможностями картографии, которые только могут понадобиться в разработке.

Leaflet создан с учетом простоты, производительности и удобства в применении. Он эффективно работает на всех основных настольных и мобильных платформах, расширяется плагинами, располагает красивым, простым в использовании, хорошо документированным API и читабельным исходным кодом.


3. Технология OSRM и основы OSRM API

Open Source Routing Machine или OSRM — высокопроизводительный механизм маршрутизации для поиска кратчайшего пути в дорожных сетях, реализованный на C++.

OSRM сочетает сложные алгоритмы маршрутизации с открытыми и бесплатными данными дорожных сетей проекта OpenStreetMap (OSM). Вычисление кратчайшего пути в сети континентального размера может занимать до нескольких секунд, если оно выполняется без так называемой техники ускорения. 

Благодаря алгоритму contraction hierarchies OSRM способен вычислить и вывести кратчайший путь между любыми двумя местами за несколько миллисекунд, при этом чистое вычисление маршрута занимает гораздо меньше времени. Большая часть усилий тратится на аннотирование маршрута и передачу геометрии по сети.

Для получения маршрутов воспользуемся OSRM API. Конечно, маршрутизацию можно выполнить на локальной машине без внешнего API, но такая реализация крайне требовательна к вычислительной мощности и памяти, поэтому выбираем стороннее API для построения маршрутов.

Давайте запросим маршрутизацию OSRM через API, перейдя по соответствующему URL: передайте координаты отправной точки и места назначения как дополнительные параметры.

'http://router.project-osrm.org/route/v1/driving/79.81039,12.00679;80.28150,13.08195?alternatives=true&geometries=polyline'

При разработке на Python для взаимодействия с API через URL доступна библиотека requests:

import requests
import json

route_url='http://router.project-osrm.org/route/v1/driving/79.81039,12.00679;80.28150,13.08195?alternatives=true&geometries=polyline'
r=requests.get(route_url)
res=r.json()
print(res)

Полученный через запрос по API ответ выглядит следующим образом:

{'code': 'Ok',
 'routes': [{'distance': 163945.4,
   'duration': 7840.1,
   'geometry': '}ihhAoxbfNmUb|Bdv@dTewCftBm~Hb~AkjBneBc{BgLiaSpsN_hCq`@ktE{wDodI{~N{lI_{A}sD}fEceHseCs_LiiIqdYcvGkQa{BcgDzDccHauC{wa@o_VscDnIigB_wB{lJsw@koB_}Bk~B_Oe}CarHib@e_O',
   'legs': [{'distance': 163945.4,
     'duration': 7840.1,
     'steps': [],
     'summary': '',
     'weight': 7915.3}],
   'weight': 7915.3,
   'weight_name': 'routability'},
  {'distance': 149325.7,
   'duration': 8733.4,
   'geometry': '}ihhAoxbfN{vAbcAmlF}pAhWaeKy_XkxKmzDufEmaKmbAsnDkyC|LmjAc`NyzKi~HwrC_zGoh@qpD_~EeiAl}@ciKwAs_FaoD_mV{{B{pNkvFkjUaLa_PynDy_BspByzHqW',
   'legs': [{'distance': 149325.7,
     'duration': 8733.4,
     'steps': [],
     'summary': '',
     'weight': 8733.4}],
   'weight': 8733.4,
   'weight_name': 'routability'}],
 'waypoints': [{'distance': 177.818519,
   'hint': 'sHXvg_p174MAAAAASgAAAAAAAACYAAAAAAAAAMQbo0EAAAAATAUpQgAAAABKAAAAAAAAAJgAAAAE5QAA8cvBBNc6twBWz8EEhjW3AAAATxZtVXiL',
   'location': [79.809521, 12.008151],
   'name': ''},
  {'distance': 0.774596,
   'hint': 'x11XgP___38IAAAADQAAAAoAAAAXAAAAw4KaQaeYIkHumMJBQlhaQggAAAANAAAACgAAABcAAAAE5QAAof_IBFmdxwCc_8gEXp3HAAEA7w1tVXiL',
   'location': [80.281505, 13.081945],
   'name': 'Muthuswamy Road'}]}

В ответе содержатся расстояние, геометрия, местоположение и другая информация. Что нам с этого?

Давайте посмотрим на геометрию, она определена примерно так:

‘}ihhAoxbfNmUb|Bdv@dTewCftBm~Hb~AkjBneBc{BgLiaSpsN_hCq`@ktE{wDodI{~N{lI_{A}sD}fEceHseCs_LiiIqdYcvGkQa{BcgDzDccHauC{wa@o_VscDnIigB_wB{lJsw@koB_}Bk~B_Oe}CarHib@e_O’

Сразу видно, что геометрия закодирована в специальном картографическом формате polylines. Чтобы получить координаты в обычном формате широты и долготы, нужно декодировать ответ.

Примените метод polyline.decode() следующим образом:

routes = polyline.decode(res[‘routes’][0][‘geometry’])

Метод возвращает список из двух координат, lat и lng. Теперь довольно легко нарисовать маршрут на карте через метод folium.polyline().

Благодаря детальной официальной документации ознакомиться детальнее с возможностями OSRM API можно в любой момент.


4. Конфигурация Django и веб-приложения

Напишем серверное приложение для управления запросами на веб-фреймворке Python Django, ссылка на полный код указана в самом конце руководства

  • Создайте новый Django-проект и новое Django-приложение:
django-admin startproject show_route
python manage.py startapp route
  • Следом отредактируйте файл settings.py: добавьте в него путь к директориям приложений и шаблонов.
  • Затем создайте новый файл под названием getroute.py в директории приложения route. В этом файле напишите функцию вызова OSRM API, принимающую в качестве параметров ширину и долготу. Помимо прочего, функция должна декодировать ответ для его возврата в виде списка.
import requests
import json
import polyline
import folium

def get_route(pickup_lon, pickup_lat, dropoff_lon, dropoff_lat):
    loc = "{},{};{},{}".format(pickup_lon, pickup_lat, dropoff_lon, dropoff_lat)
    url = "http://router.project-osrm.org/route/v1/driving/"
    r = requests.get(url + loc) 
    if r.status_code!= 200:
        return {}
    res = r.json()   
    routes = polyline.decode(res['routes'][0]['geometry'])
    start_point = [res['waypoints'][0]['location'][1], res['waypoints'][0]['location'][0]]
    end_point = [res['waypoints'][1]['location'][1], res['waypoints'][1]['location'][0]]
    distance = res['routes'][0]['distance']
    
    out = {'route':routes,
           'start_point':start_point,
           'end_point':end_point,
           'distance':distance
          }

    return out

Настал этап определения контроллера согласно паттерну Model-View-Controller, Модель-Представление-Контроллер. В Django паттерн MVC реализован через сопоставление Model-Template-View, Модель-Шаблон-Представление

  • Создайте файл представлений views.py, в нем определите функцию-представление с идентификатором showmap. Данное представление непосредственно передает браузеру пользователя файл showmap.html.
import folium
from django.shortcuts import render,redirect
from . import getroute


def showmap(request):
    return render(request,'showmap.html')

def showroute(request,lat1,long1,lat2,long2):
    figure = folium.Figure()
    lat1,long1,lat2,long2=float(lat1),float(long1),float(lat2),float(long2)
    route=getroute.get_route(long1,lat1,long2,lat2)
    m = folium.Map(location=[(route['start_point'][0]),
                                 (route['start_point'][1])], 
                       zoom_start=10)
    m.add_to(figure)
    folium.PolyLine(route['route'],weight=8,color='blue',opacity=0.6).add_to(m)
    folium.Marker(location=route['start_point'],icon=folium.Icon(icon='play', color='green')).add_to(m)
    folium.Marker(location=route['end_point'],icon=folium.Icon(icon='stop', color='red')).add_to(m)
    figure.render()
    context={'map':figure}
    return render(request,'showroute.html',context)
  • В директории шаблонов Django напишите HTML-шаблон showmap.html, не забудьте указать в нем CDN для подключения JavaScript-библиотеки Leaflet:
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>Map to find route</title>
  <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />

  <!-- Load Leaflet from CDN -->
  <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css"
    integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
    crossorigin=""/>
  <script src="https://unpkg.com/[email protected]/dist/leaflet.js"
    integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
    crossorigin=""></script>

  <!-- Load Esri Leaflet from CDN -->
  <script src="https://unpkg.com/[email protected]/dist/esri-leaflet.js"
    integrity="sha512-ucw7Grpc+iEQZa711gcjgMBnmd9qju1CICsRaryvX7HJklK0pGl/prxKvtHwpgm5ZHdvAil7YPxI1oWPOWK3UQ=="
    crossorigin=""></script>

  <!-- Load Esri Leaflet Geocoder from CDN -->
  <link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/esri-leaflet-geocoder.css"
    integrity="sha512-IM3Hs+feyi40yZhDH6kV8vQMg4Fh20s9OzInIIAc4nx7aMYMfo+IenRUekoYsHZqGkREUgx0VvlEsgm7nCDW9g=="
    crossorigin="">
  <script src="https://unpkg.com/[email protected]/dist/esri-leaflet-geocoder.js"
    integrity="sha512-HrFUyCEtIpxZloTgEKKMq4RFYhxjJkCiF5sDxuAokklOeZ68U2NPfh4MFtyIVWlsKtVbK5GD2/JzFyAfvT5ejA=="
    crossorigin=""></script>
    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
  <style>
    body { margin:0; padding:0; }
    #map { position: absolute; top:0; bottom:0; right:0; left:0; }
  </style>
</head>
<body>
<div id="map"></div>
<script>
  var map = L.map('map').setView([11,79], 10);
  data={};
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: '&copy; <a href="https://osm.org/copyright">OpenStreetMap</a> contributors'
  }).addTo(map);
  var gcs = L.esri.Geocoding.geocodeService();
  var count=0;
  map.on('click', (e)=>{
    count+=1;
    gcs.reverse().latlng(e.latlng).run((err, res)=>{
      if(err) return;
      L.marker(res.latlng).addTo(map).bindPopup(res.address.Match_addr).openPopup();
      k=count.toString()
      data[k+'lat']=res.latlng['lat'];
      data[k+'lon']=res.latlng['lng'];
      if(count==2){
        const route_url='http://localhost:8000/'+data['1lat']+','+data['1lon']+','+data['2lat']+','+data['2lon'];
        count=0;
        window.location.replace(route_url);
      }
   });
});
</script>
</body>
</html>

Для просчета маршрута необходимо знать две точки местоположения, поэтому JavaScript-переменная count настроена для ожидания двух событий щелчка мыши. Если у вас есть предложения по альтернативной реализации JavaScript части приложения, то не стесняйтесь упоминать их в комментариях!

  • Файл шаблона showroute.html просто отобразит контекст, предоставленный бэкендом Django.
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Route between the locations</title>
    {{map.header.render|safe}}
</head>
<body>
	{% if map %}

  	{{map.html.render|safe}}
  	<script>
    	{{map.script.render|safe}}
  	</script>
	{% endif %}
</body>
</html>
  • Наконец, нужно сопоставить функции представления с URL. Маршрутизация запросов веб-приложения (не имеет отношения к построению маршрутов) происходит в специальном файле urls.py,
    в приведенном ниже примере <str:val> предназначен для передачи строковых значений через URL.
from django.contrib import admin
from django.urls import path

from route.views import showroute,showmap

urlpatterns = [
    path('admin/', admin.site.urls),
    path('<str:lat1>,<str:long1>,<str:lat2>,<str:long2>',showroute,name='showroute'),
    path('',showmap,name='showmap'),
    ]

Выводы

Оцените результат ваших трудов после выполнения всех шагов руководства!

Располагаясь в директории проекта Django, введите в командную строку следующую команду:

python manage.py runserver
htttp://127.0.0.1/<ширина><долгота><ширина><долгота>/
  • Для ознакомления с полным кодом веб-приложения перейдите по ссылке на репозиторий GitHub.

Поздравляем, вы успешно выполнили руководство! Сегодня вы реализовали только часть полноценного проекта, поэтому пока что в нем только одна функция. Как приложение  —  отлично подойдет для любого проекта Django с функционалом маршрутизации. 


Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Rajavel M: Django Web App for Plotting the Route between Two Points in a Map

Предыдущая статьяПортрет плохого программиста
Следующая статьяКак ИИ влияет на развитие NFT