Spaces:
Sleeping
Sleeping
File size: 5,485 Bytes
f97f945 1685498 f97f945 |
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 |
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
# ---------------------------------------------------------------
@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 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;"
)
)
|