Spaces:
Sleeping
Sleeping
File size: 5,667 Bytes
0ae639b |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
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;"
)
)
|