gabcares's picture
Upload 4 files
0ae639b verified
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 MAPS_API_KEY, BRANDCOLORS, BASEMAPS, ONE_WEEK_SEC, LOCATIONS, TRIP_DISTANCE, ETA
# Cache expires after 1 week
# R.install_cache('yassir_requests_cache', expire_after=ONE_WEEK_SEC) # Sqlite
# ---------------------------------------------------------------
# Helper functions for map and location inputs on predict page
# ---------------------------------------------------------------
# @cached(cache=TTLCache(maxsize=300, ttl=ONE_WEEK_SEC)) # 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
# @cached(cache=TTLCache(maxsize=3000, ttl=ONE_WEEK_SEC)) # Memory
def google_maps_trip_distance_eta(origin: Tuple[float], destination: Tuple[float]) -> Tuple[float]:
"""
The road distance calculated using Google Maps distance matrix api 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
"""
# Google Maps API URL
url = f"https://maps.googleapis.com/maps/api/distancematrix/json?origins={origin[0]},{origin[1]}&destinations={destination[0]},{destination[1]}&key={MAPS_API_KEY}"
# Send request
response = requests.get(url)
if response.status_code == 200:
# Decode the response
data = response.json()
# Extract distance information
if "rows" in data and len(data["rows"]) > 0:
distance_info = data["rows"][0]["elements"][0]["distance"]
eta_info = data["rows"][0]["elements"][0]["duration"]
distance = float(distance_info['value'])
eta = float(eta_info['value'])
else:
distance = TRIP_DISTANCE # Default
back_to_nairobi()
else:
distance = TRIP_DISTANCE # Default
eta = ETA # Default
back_to_nairobi()
return distance, eta
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;"
)
)