By Ryan Freed, Bobby Basnet, Milad Korde
Background:
In the previous three posts on Distance Calculation Series, we tried a few approaches to deal with the straight-line distance between two points. Here are the links to the articles in order of appearance:
We invite you to read and try the previous programs we wrote to understand better this part of the project.
This post discovers the network distance between two points instead of the direct line. It is unlikely to search for the distance between two points and stick to a straight line to get from point A to point B unless you are in the middle of a vast area with no network of streets that can be considered for transportation. However, our attempt uses an external API instead of a network of roads created on our side.
Needed library:
There are two newly used libraries at the beginning of the Python script you need to call in. We briefly introduced Pyodide previously. Next, we want to catch your attention to the pyfetch class to call external APIs and asyncio to write asynchronous code using the async/await syntax. The response is contained in the response variable, where we implement the API key.
API Call:
The API we are calling is coming from Openrouteservice.org. Openrouteservice’s directions can be used all around the globe. It includes route instructions for cars, trucks, different bike profiles, walking, hiking, or wheelchair, and many more transportation-related services.
Configuring the output:
The form to read Pyscript content is hidden from the previous code version. The placeholders are also removed from all four styles for pairs of lat and long.
We also removed the variable to add the polyline because drawing a straight line between two points while we are trying to calculate the network distance based on the streets and roads does not make sense. However, we are planning to implement that too.
Event Listener:
Regarding the Event Listener, we have changed the approach. If there is a marker, which means that the user has already started the process, we show a message on the lower left corner of the map guiding the user to choose the second point. In the other part, we push the origin point if not selected.
The first API call attempt returns a dictionary with all the attributes. The information, however, is cut to the points along the route. It is also possible to see all the points (coordinates) along the way by uncommenting pyscript.write(‘storage’, route). The result of the route containing all the points along the way is stored in the storage div. right before the section where we start Python and PyScript.
Then we needed to pass all of this into a button click event to run the API call and store the response, done by the setup function in PyScript that executes after a PyScript asynchronous button click function.
JavaScript:
<script>
//variable to store clicked coordinates
let lat1, lng1, lat2, lng2;
//Flag to check if origin or destination
let origin = 0;
//Array for points along the way
let latlngs = Array();
//Add the basemap
let map = L.map('map').setView([42.282280316412546, -71.80865838018761], 13);
//Points along the route
let routePoint = L.featureGroup().addTo(map);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
}).addTo(map);
// function to draw marker on map
function drawToMap(ev, what) {
let marker = L.marker([ev.latlng.lat, ev.latlng.lng]).addTo(routePoint);
}
// function to clear the map
function clearMap() {
latlngs = Array()
routePoint.clearLayers();
}
map.addEventListener('click', function(ev) {
if (origin == 0) {
clearMap()
// if there is already a marker on the map, do the following
document.getElementById('status').textContent= "Select Destination on The Map"
document.getElementById('FirstLat').value = ev.latlng.lat
document.getElementById('FirstLong').value = ev.latlng.lng
latlngs.push([ev.latlng.lat,ev.latlng.lng]);
drawToMap(ev, "Starting Position");
origin = 1;
}
else {
// if there is no marker, push these elements
document.getElementById('status').textContent= "Select Origin on The Map"
document.getElementById('SecLat').value = ev.latlng.lat
document.getElementById('SecLong').value = ev.latlng.lng
latlngs.push([ev.latlng.lat,ev.latlng.lng]);
// manually force button click event to activate PyScript on-click listener
let manual_event = document.getElementById('button');
manual_event.click();
drawToMap(ev, "Destination Point");
latlngs.push([ev.latlng.lat, ev.latlng.lng]);
let marker = L.marker([ev.latlng.lat, ev.latlng.lng]).addTo(route_polyline);
origin = 0
}
});
</script>
The georoute calculation:
After the API grabbed the points along the route, a Haversine formula on each lat-long pair in the 2D route array converts the degree values to radians to return a georouted distance in kilometers. Then the Haversine results and the sum of the differences produced the final calculation printed on the screen.
The PyScript section will be as follows:
<py-script output = 'storage'>
from pyodide.http import pyfetch
from js import document, alert
from pyodide import create_proxy
import asyncio
from math import radians, cos, sin, asin, sqrt
def haversine(lon1, lat1, lon2, lat2):
"""
Calculate the great circle distance in kilometers between two points
on the earth (specified in decimal degrees)
"""
# convert decimal degrees to radians
lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
# haversine formula
dlon = lon2 - lon1
dlat = lat2 - lat1
a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
c = 2 * asin(sqrt(a))
r = 6371 # Radius of earth in kilometers
return c * r
async def button_click(event):
FirstLat = document.getElementById("FirstLat").value
FirstLong = document.getElementById('FirstLong').value
SecLat = document.getElementById('SecLat').value
SecLong = document.getElementById('SecLong').value
route = []
headers = {
'Accept': 'application/json, application/geo+json, application/gpx+xml, img/png; charset=utf-8',
}
response = await pyfetch(url=f"https://api.openrouteservice.org/v2/directions/driving-car?api_key=5b3ce3597851110001cf62487621a9df4b6a49cdbc6f26847723515b&start={FirstLong},{FirstLat}&end={SecLong},{SecLat}", method="GET", headers=headers)
response_dict = await response.json()
# create coordinates object, append it to the coords list for export to javascript
coordinates = response_dict['features'][0]['geometry']['coordinates']
route.extend(coordinates)
route.insert(0, [float(FirstLong), float(FirstLat)])
route.append([float(SecLong), float(SecLat)])
#lon1, lat1, lon2, lat2
route_diffs = []
for i in route: # switch from LongLat to LatLong
i[0], i[1] = i[1], i[0]
for i in range(len(route)): # need to use range len idiom (bad practice) to grab next index value without using itertools
try: # pairwise calculation of lat longs in georoute
lat1 = route[i][0]
lon1 = route[i][1]
lat2 = route[i + 1][0]
lon2 = route[i + 1][1]
difference = haversine(lon1 = lon1, lat1 = lat1, lon2 = lon2, lat2= lat2)
route_diffs.append(difference)
except IndexError:
break
finaldist = sum(route_diffs)
# Uncomment this if you would like to see the full route lat long array printed
#pyscript.write('storage', route)
pyscript.write('distance', f'<b>Distance from A to B:</b> {finaldist:.2f} km')
def setup():
"""
Initializes site content and HTML listeners that are typically handled by JavaScript
"""
click_proxy = create_proxy(button_click)
e = document.getElementById("button")
e.addEventListener("click", click_proxy)
setup()
</py-script>
Conclusion:
The main takeaway from this project is that PyScript is great for performing computations that are otherwise tedious in JavaScript. Still, the functionality is not yet there to do full visualizations inside of PyScript. The cartography itself is best done inside the JS environment as of right now.
We used API calls in the PyScript environment, then passed that information to the JavaScript environment without messing with the global namespace (which is, unfortunately, really buggy right now in PyScript).
GitHub Repo: LINK