Spaces:
Sleeping
Sleeping
import json | |
import requests | |
import requests_cache as R | |
from cachetools import TTLCache, cached | |
from typing import Literal, Tuple, Optional | |
import ipyleaflet as L | |
from ipyleaflet import AwesomeIcon | |
from shiny.express import ui | |
from utils.config import ors_api_key, BRANDCOLORS, BASEMAPS, ONE_WEEK_SEC, LOCATIONS, TRIP_DISTANCE | |
# Cache expires after 1 week | |
# R.install_cache('/client/requests_cache/yassir_requests_cache', expire_after=ONE_WEEK_SEC) # Sqlite | |
R.install_cache(backend='memory', expire_after=ONE_WEEK_SEC) # Sqlite | |
# --------------------------------------------------------------- | |
# Helper functions for map and location inputs on predict page | |
# --------------------------------------------------------------- | |
# Memory | |
def get_bounds(country: str) -> Tuple[float]: | |
headers = { | |
'User-Agent': 'Yassir ETA Shiny App/1.0 ([email protected])' | |
} | |
response = requests.get( | |
f"http://nominatim.openstreetmap.org/search?q={country}&format=json", headers=headers) | |
boundingbox = json.loads(response.text)[0]["boundingbox"] | |
# Extract the bounds as float datatype | |
lat_min, lat_max, lon_min, lon_max = (float(b) for b in boundingbox) | |
return lat_min, lat_max, lon_min, lon_max | |
# Memory | |
def ops_trip_distance(origin: tuple, destination: tuple) -> float: | |
""" | |
The road distance calculated using openrouteservice with the driving car is the shortest | |
or optimal road distance based on the available road data and routing algorithm. | |
origin is a tuple of lat, lon | |
destination is a tuple of lat, lon | |
Returns: the calculiated trip distance or a default value | |
""" | |
# OpenRouteService API URL | |
# https://giscience.github.io/openrouteservice/api-reference/endpoints/directions/extra-info/ | |
url = 'https://api.openrouteservice.org/v2/directions/driving-car' | |
# Request parameters | |
params = { | |
'api_key': ors_api_key, | |
'start': f'{origin[1]},{origin[0]}', # lon, lat | |
'end': f'{destination[1]},{destination[0]}' # lon, lat | |
} | |
# Send request | |
# https://openrouteservice.org/dev/#/api-docs/v2/directions/{profile}/get | |
response = requests.get(url, params=params) | |
data = response.json() | |
# Extract distance | |
if response.status_code == 200 and data.get('features'): | |
# Distance in meters | |
distance = data['features'][0]['properties']['summary']['distance'] | |
else: | |
distance = TRIP_DISTANCE # Default | |
back_to_nairobi() | |
return distance | |
def update_marker(map: L.Map, loc: tuple, on_move: object, name: str, icon: AwesomeIcon): | |
remove_layer(map, name) | |
m = L.Marker(location=loc, draggable=True, name=name, icon=icon) | |
m.on_move(on_move) | |
map.add_layer(m) | |
def update_line(map: L.Map, loc1: tuple, loc2: tuple): | |
remove_layer(map, "line") | |
map.add_layer( | |
L.Polyline(locations=[loc1, loc2], | |
color=BRANDCOLORS['red'], weight=3, name="line") | |
) | |
def update_basemap(map: L.Map, basemap: str): | |
for layer in map.layers: | |
if isinstance(layer, L.TileLayer): | |
map.remove_layer(layer) | |
map.add_layer(L.basemap_to_tiles(BASEMAPS[basemap])) | |
def remove_layer(map: L.Map, name: str): | |
for layer in map.layers: | |
if layer.name == name: | |
map.remove_layer(layer) | |
def on_move1(**kwargs): | |
return on_move("origin", **kwargs) | |
def on_move2(**kwargs): | |
return on_move("destination", **kwargs) | |
# When the markers are moved, update the numeric location inputs to include the new | |
# location (which results in the locations() reactive value getting updated, | |
# which invalidates any downstream reactivity that depends on it) | |
def on_move(loc_type: Literal['origin', 'destination'], **kwargs): | |
location = kwargs["location"] | |
loc_lat, loc_lon = location | |
ui.update_numeric(f"{loc_type}_lat", value=loc_lat) | |
ui.update_numeric(f"{loc_type}_lon", value=loc_lon) | |
# origin_lat | |
# origin_lon | |
# destination_lat | |
# destination_lon | |
# Re-center to Kenya region | |
def back_to_nairobi(): | |
ui.update_numeric("origin_lat", value=LOCATIONS["Nairobi"]['latitude']) | |
ui.update_numeric( | |
"origin_lon", value=LOCATIONS["Nairobi"]['longitude']) | |
ui.update_numeric( | |
"destination_lat", value=LOCATIONS["National Museum of Kenya"]['latitude']) | |
ui.update_numeric( | |
"destination_lon", value=LOCATIONS["National Museum of Kenya"]['longitude']) | |
def validate_inputs(origin_lat: float = None, origin_lon: float = None, destination_lat: float = None, destination_lon: float = None) -> bool: | |
lat_min, lat_max, lon_min, lon_max = get_bounds(country='Kenya') | |
valid = True | |
for lat, lon in [(origin_lat, origin_lon), (destination_lat, destination_lon)]: | |
if lat is not None and lon is not None: | |
if (lat < lat_min or lat > lat_max) or (lon < lon_min or lon > lon_max): | |
ui.notification_show( | |
"๐ฎ Location is outside Kenya, taking you back to Nairobi", type="error") | |
valid = False | |
back_to_nairobi() | |
break | |
return valid | |
# Footer | |
footer = ui.tags.footer( | |
ui.tags.div( | |
"ยฉ 2024. Made with ๐", | |
style=f"text-align: center; padding: 10px; color: #fff; background-color: {BRANDCOLORS['purple-dark']}; margin-top: 50px;" | |
) | |
) | |