gabcares commited on
Commit
f97f945
·
verified ·
1 Parent(s): b38e594

Update utils/utils.py

Browse files

Request cache on the same directory as app ./

Files changed (1) hide show
  1. utils/utils.py +169 -169
utils/utils.py CHANGED
@@ -1,169 +1,169 @@
1
- import json
2
- import requests
3
- import requests_cache as R
4
- from cachetools import TTLCache, cached
5
-
6
- from typing import Literal, Tuple, Optional
7
-
8
- import ipyleaflet as L
9
- from ipyleaflet import AwesomeIcon
10
-
11
- from shiny.express import ui
12
-
13
- from utils.config import ors_api_key, BRANDCOLORS, BASEMAPS, ONE_WEEK_SEC, LOCATIONS, TRIP_DISTANCE
14
-
15
-
16
- # Cache expires after 1 week
17
- R.install_cache('yassir_requests_cache', expire_after=ONE_WEEK_SEC) # Sqlite
18
-
19
-
20
- # ---------------------------------------------------------------
21
- # Helper functions for map and location inputs on predict page
22
- # ---------------------------------------------------------------
23
-
24
- @cached(cache=TTLCache(maxsize=300, ttl=ONE_WEEK_SEC)) # Memory
25
- def get_bounds(country: str) -> Tuple[float]:
26
- headers = {
27
- 'User-Agent': 'Yassir ETA Shiny App/1.0 ([email protected])'
28
- }
29
-
30
- response = requests.get(
31
- f"http://nominatim.openstreetmap.org/search?q={country}&format=json", headers=headers)
32
-
33
- boundingbox = json.loads(response.text)[0]["boundingbox"]
34
-
35
- # Extract the bounds as float datatype
36
- lat_min, lat_max, lon_min, lon_max = (float(b) for b in boundingbox)
37
-
38
- return lat_min, lat_max, lon_min, lon_max
39
-
40
-
41
- @cached(cache=TTLCache(maxsize=3000, ttl=ONE_WEEK_SEC)) # Memory
42
- def ops_trip_distance(origin: tuple, destination: tuple) -> float:
43
- """
44
- The road distance calculated using openrouteservice with the driving car is the shortest
45
- or optimal road distance based on the available road data and routing algorithm.
46
-
47
- origin is a tuple of lat, lon
48
- destination is a tuple of lat, lon
49
-
50
- Returns: the calculiated trip distance or a default value
51
- """
52
-
53
- # OpenRouteService API URL
54
- # https://giscience.github.io/openrouteservice/api-reference/endpoints/directions/extra-info/
55
- url = 'https://api.openrouteservice.org/v2/directions/driving-car'
56
-
57
- # Request parameters
58
- params = {
59
- 'api_key': ors_api_key,
60
- 'start': f'{origin[1]},{origin[0]}', # lon, lat
61
- 'end': f'{destination[1]},{destination[0]}' # lon, lat
62
- }
63
-
64
- # Send request
65
- # https://openrouteservice.org/dev/#/api-docs/v2/directions/{profile}/get
66
- response = requests.get(url, params=params)
67
- data = response.json()
68
-
69
- # Extract distance
70
- if response.status_code == 200 and data.get('features'):
71
- # Distance in meters
72
- distance = data['features'][0]['properties']['summary']['distance']
73
- else:
74
- distance = TRIP_DISTANCE # Default
75
- back_to_nairobi()
76
-
77
- return distance
78
-
79
-
80
- def update_marker(map: L.Map, loc: tuple, on_move: object, name: str, icon: AwesomeIcon):
81
- remove_layer(map, name)
82
- m = L.Marker(location=loc, draggable=True, name=name, icon=icon)
83
- m.on_move(on_move)
84
- map.add_layer(m)
85
-
86
-
87
- def update_line(map: L.Map, loc1: tuple, loc2: tuple):
88
- remove_layer(map, "line")
89
- map.add_layer(
90
- L.Polyline(locations=[loc1, loc2],
91
- color=BRANDCOLORS['red'], weight=3, name="line")
92
- )
93
-
94
-
95
- def update_basemap(map: L.Map, basemap: str):
96
- for layer in map.layers:
97
- if isinstance(layer, L.TileLayer):
98
- map.remove_layer(layer)
99
- map.add_layer(L.basemap_to_tiles(BASEMAPS[basemap]))
100
-
101
-
102
- def remove_layer(map: L.Map, name: str):
103
- for layer in map.layers:
104
- if layer.name == name:
105
- map.remove_layer(layer)
106
-
107
-
108
- def on_move1(**kwargs):
109
- return on_move("origin", **kwargs)
110
-
111
-
112
- def on_move2(**kwargs):
113
- return on_move("destination", **kwargs)
114
-
115
- # When the markers are moved, update the numeric location inputs to include the new
116
- # location (which results in the locations() reactive value getting updated,
117
- # which invalidates any downstream reactivity that depends on it)
118
-
119
-
120
- def on_move(loc_type: Literal['origin', 'destination'], **kwargs):
121
- location = kwargs["location"]
122
- loc_lat, loc_lon = location
123
-
124
- ui.update_numeric(f"{loc_type}_lat", value=loc_lat)
125
- ui.update_numeric(f"{loc_type}_lon", value=loc_lon)
126
-
127
- # origin_lat
128
- # origin_lon
129
- # destination_lat
130
- # destination_lon
131
-
132
- # Re-center to Kenya region
133
-
134
-
135
- def back_to_nairobi():
136
- ui.update_numeric("origin_lat", value=LOCATIONS["Nairobi"]['latitude'])
137
- ui.update_numeric(
138
- "origin_lon", value=LOCATIONS["Nairobi"]['longitude'])
139
- ui.update_numeric(
140
- "destination_lat", value=LOCATIONS["National Museum of Kenya"]['latitude'])
141
- ui.update_numeric(
142
- "destination_lon", value=LOCATIONS["National Museum of Kenya"]['longitude'])
143
-
144
-
145
- def validate_inputs(origin_lat: float = None, origin_lon: float = None, destination_lat: float = None, destination_lon: float = None) -> bool:
146
- lat_min, lat_max, lon_min, lon_max = get_bounds(country='Kenya')
147
-
148
- valid = True
149
-
150
- for lat, lon in [(origin_lat, origin_lon), (destination_lat, destination_lon)]:
151
- if lat is not None and lon is not None:
152
- if (lat < lat_min or lat > lat_max) or (lon < lon_min or lon > lon_max):
153
- ui.notification_show(
154
- "😮 Location is outside Kenya, taking you back to Nairobi", type="error")
155
- valid = False
156
- back_to_nairobi()
157
- break
158
-
159
-
160
- return valid
161
-
162
-
163
- # Footer
164
- footer = ui.tags.footer(
165
- ui.tags.div(
166
- "© 2024. Made with 💖",
167
- style=f"text-align: center; padding: 10px; color: #fff; background-color: {BRANDCOLORS['purple-dark']}; margin-top: 50px;"
168
- )
169
- )
 
1
+ import json
2
+ import requests
3
+ import requests_cache as R
4
+ from cachetools import TTLCache, cached
5
+
6
+ from typing import Literal, Tuple, Optional
7
+
8
+ import ipyleaflet as L
9
+ from ipyleaflet import AwesomeIcon
10
+
11
+ from shiny.express import ui
12
+
13
+ from utils.config import ors_api_key, BRANDCOLORS, BASEMAPS, ONE_WEEK_SEC, LOCATIONS, TRIP_DISTANCE
14
+
15
+
16
+ # Cache expires after 1 week
17
+ R.install_cache('./yassir_requests_cache', expire_after=ONE_WEEK_SEC) # Sqlite
18
+
19
+
20
+ # ---------------------------------------------------------------
21
+ # Helper functions for map and location inputs on predict page
22
+ # ---------------------------------------------------------------
23
+
24
+ @cached(cache=TTLCache(maxsize=300, ttl=ONE_WEEK_SEC)) # Memory
25
+ def get_bounds(country: str) -> Tuple[float]:
26
+ headers = {
27
+ 'User-Agent': 'Yassir ETA Shiny App/1.0 ([email protected])'
28
+ }
29
+
30
+ response = requests.get(
31
+ f"http://nominatim.openstreetmap.org/search?q={country}&format=json", headers=headers)
32
+
33
+ boundingbox = json.loads(response.text)[0]["boundingbox"]
34
+
35
+ # Extract the bounds as float datatype
36
+ lat_min, lat_max, lon_min, lon_max = (float(b) for b in boundingbox)
37
+
38
+ return lat_min, lat_max, lon_min, lon_max
39
+
40
+
41
+ @cached(cache=TTLCache(maxsize=3000, ttl=ONE_WEEK_SEC)) # Memory
42
+ def ops_trip_distance(origin: tuple, destination: tuple) -> float:
43
+ """
44
+ The road distance calculated using openrouteservice with the driving car is the shortest
45
+ or optimal road distance based on the available road data and routing algorithm.
46
+
47
+ origin is a tuple of lat, lon
48
+ destination is a tuple of lat, lon
49
+
50
+ Returns: the calculiated trip distance or a default value
51
+ """
52
+
53
+ # OpenRouteService API URL
54
+ # https://giscience.github.io/openrouteservice/api-reference/endpoints/directions/extra-info/
55
+ url = 'https://api.openrouteservice.org/v2/directions/driving-car'
56
+
57
+ # Request parameters
58
+ params = {
59
+ 'api_key': ors_api_key,
60
+ 'start': f'{origin[1]},{origin[0]}', # lon, lat
61
+ 'end': f'{destination[1]},{destination[0]}' # lon, lat
62
+ }
63
+
64
+ # Send request
65
+ # https://openrouteservice.org/dev/#/api-docs/v2/directions/{profile}/get
66
+ response = requests.get(url, params=params)
67
+ data = response.json()
68
+
69
+ # Extract distance
70
+ if response.status_code == 200 and data.get('features'):
71
+ # Distance in meters
72
+ distance = data['features'][0]['properties']['summary']['distance']
73
+ else:
74
+ distance = TRIP_DISTANCE # Default
75
+ back_to_nairobi()
76
+
77
+ return distance
78
+
79
+
80
+ def update_marker(map: L.Map, loc: tuple, on_move: object, name: str, icon: AwesomeIcon):
81
+ remove_layer(map, name)
82
+ m = L.Marker(location=loc, draggable=True, name=name, icon=icon)
83
+ m.on_move(on_move)
84
+ map.add_layer(m)
85
+
86
+
87
+ def update_line(map: L.Map, loc1: tuple, loc2: tuple):
88
+ remove_layer(map, "line")
89
+ map.add_layer(
90
+ L.Polyline(locations=[loc1, loc2],
91
+ color=BRANDCOLORS['red'], weight=3, name="line")
92
+ )
93
+
94
+
95
+ def update_basemap(map: L.Map, basemap: str):
96
+ for layer in map.layers:
97
+ if isinstance(layer, L.TileLayer):
98
+ map.remove_layer(layer)
99
+ map.add_layer(L.basemap_to_tiles(BASEMAPS[basemap]))
100
+
101
+
102
+ def remove_layer(map: L.Map, name: str):
103
+ for layer in map.layers:
104
+ if layer.name == name:
105
+ map.remove_layer(layer)
106
+
107
+
108
+ def on_move1(**kwargs):
109
+ return on_move("origin", **kwargs)
110
+
111
+
112
+ def on_move2(**kwargs):
113
+ return on_move("destination", **kwargs)
114
+
115
+ # When the markers are moved, update the numeric location inputs to include the new
116
+ # location (which results in the locations() reactive value getting updated,
117
+ # which invalidates any downstream reactivity that depends on it)
118
+
119
+
120
+ def on_move(loc_type: Literal['origin', 'destination'], **kwargs):
121
+ location = kwargs["location"]
122
+ loc_lat, loc_lon = location
123
+
124
+ ui.update_numeric(f"{loc_type}_lat", value=loc_lat)
125
+ ui.update_numeric(f"{loc_type}_lon", value=loc_lon)
126
+
127
+ # origin_lat
128
+ # origin_lon
129
+ # destination_lat
130
+ # destination_lon
131
+
132
+ # Re-center to Kenya region
133
+
134
+
135
+ def back_to_nairobi():
136
+ ui.update_numeric("origin_lat", value=LOCATIONS["Nairobi"]['latitude'])
137
+ ui.update_numeric(
138
+ "origin_lon", value=LOCATIONS["Nairobi"]['longitude'])
139
+ ui.update_numeric(
140
+ "destination_lat", value=LOCATIONS["National Museum of Kenya"]['latitude'])
141
+ ui.update_numeric(
142
+ "destination_lon", value=LOCATIONS["National Museum of Kenya"]['longitude'])
143
+
144
+
145
+ def validate_inputs(origin_lat: float = None, origin_lon: float = None, destination_lat: float = None, destination_lon: float = None) -> bool:
146
+ lat_min, lat_max, lon_min, lon_max = get_bounds(country='Kenya')
147
+
148
+ valid = True
149
+
150
+ for lat, lon in [(origin_lat, origin_lon), (destination_lat, destination_lon)]:
151
+ if lat is not None and lon is not None:
152
+ if (lat < lat_min or lat > lat_max) or (lon < lon_min or lon > lon_max):
153
+ ui.notification_show(
154
+ "😮 Location is outside Kenya, taking you back to Nairobi", type="error")
155
+ valid = False
156
+ back_to_nairobi()
157
+ break
158
+
159
+
160
+ return valid
161
+
162
+
163
+ # Footer
164
+ footer = ui.tags.footer(
165
+ ui.tags.div(
166
+ "© 2024. Made with 💖",
167
+ style=f"text-align: center; padding: 10px; color: #fff; background-color: {BRANDCOLORS['purple-dark']}; margin-top: 50px;"
168
+ )
169
+ )