Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add automatic retrieval of an osm map and simplify user flow #85

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion dkroutingtool/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,7 @@ olddevelop:
dashboard:latest

develop:
docker compose up
docker compose up &

cleanup:
docker compose down
9 changes: 8 additions & 1 deletion dkroutingtool/src/py/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@

app = fastapi.FastAPI()

stateful_info = dict()

def find_most_recent_output(session_id):
most_recent = sorted(glob.glob(f'/WORKING_DATA_DIR/data{session_id}/output_data/*'))[-1]
return most_recent
Expand Down Expand Up @@ -65,6 +67,9 @@ def get_solution(session_id: str=''):
main_application.main(user_directory=f'data{session_id}')
return {'message': 'Done'}

@app.get('/get_map_info')
def get_map_info():
return {'message': stateful_info.get('bounding_box')}

@app.post('/adjust_solution')
def get_solution(files: List[UploadFile] = File(...), session_id: str=''):
Expand Down Expand Up @@ -96,7 +101,9 @@ def download(session_id: str=''):


@app.get('/request_map/')
def request_map(minlat, minlon, maxlat, maxlon):
def request_map(minlat, minlon, maxlat, maxlon):
stateful_info['bounding_box'] = [minlat, minlon, maxlat, maxlon]

request_template = f'''
[out:xml]
[bbox:{minlon},{minlat}, {maxlon}, {maxlat}];
Expand Down
145 changes: 107 additions & 38 deletions dkroutingtool/src/py/ui/dashboard.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import base64
import requests
import datetime
Expand All @@ -11,13 +12,19 @@
from streamlit.runtime import get_instance
from streamlit.runtime.scriptrunner import get_script_run_ctx

import numpy as np
import pandas as pd
import yaml

st.set_page_config(page_title='Container-based Action Routing Tool (CART)', layout="wide")

runtime = get_instance()
session_id = ''

host_url = 'http://{}:5001'.format(os.environ['SERVER_HOST'])

recalculate_map = True

def download_solution(solution_path, map_path):
timestamp = datetime.datetime.now().strftime(format='%Y%m%d-%H-%M-%S')
response = requests.get(f'{host_url}/download/?session_id={session_id}')
Expand All @@ -34,15 +41,15 @@ def download_solution(solution_path, map_path):
solution = solution_txt.read().replace('\n', ' \n')

with open(f'solution_files_{timestamp}/{map_path}', 'r') as map_html:
map = map_html.read()
solutionmap = map_html.read()

return solution, map, solution_zip
return solution, solutionmap, solution_zip

def request_solution():
response = requests.get(f'{host_url}/get_solution/?session_id={session_id}')
solution, map, solution_zip = download_solution(solution_path='solution.txt', map_path='/maps/route_map.html')
solution, solutionmap, solution_zip = download_solution(solution_path='solution.txt', map_path='/maps/route_map.html')

return solution, map, solution_zip
return solution, solutionmap, solution_zip

def request_map(bounding_box):
response = requests.get(f'{host_url}/request_map/?minlat={bounding_box[0]}&minlon={bounding_box[1]}&maxlat={bounding_box[2]}&maxlon={bounding_box[3]}')
Expand All @@ -52,6 +59,9 @@ def request_map(bounding_box):
def update_vehicle_or_map():
response = requests.post(f'{host_url}/update_vehicle_or_map/?session_id={session_id}')

def calculate_bounding_box():
pass

def adjust(adjusted_file):
headers = {
'accept': 'application/json'
Expand All @@ -65,8 +75,8 @@ def adjust(adjusted_file):
else:
message = 'Error, verify the adjusted routes file or raise an issue'

solution, map, solution_zip = download_solution(solution_path='manual_edits/manual_solution.txt', map_path='maps/trip_data.html')
return message, solution, map, solution_zip
solution, solutionmap, solution_zip = download_solution(solution_path='manual_edits/manual_solution.txt', map_path='maps/trip_data.html')
return message, solution, solutionmap, solution_zip

def upload_data(files_from_streamlit):
global session_id
Expand Down Expand Up @@ -96,61 +106,120 @@ def main():

vehicles_text = st.empty()
vehicles_text.text('Available vehicle profiles: '+ requests.get(f'{host_url}/available_vehicles').json()['message'])
recalculate_map = st.toggle(label='Calculate area to download from OpenStreetMaps automatically based on the locations to visit', value=True)
if not recalculate_map:
st.write('If required, draw a rectangle over the area you want to use for routing. Download it again only if you updated the OpenStreetMap data. Please select an area as small as possible.')
m = folium.Map(location=[-11.9858, -77.019], zoom_start=5)
Draw(export=False).add_to(m)
map_output = st_folium(m, width=700, height=500)
if map_output['last_active_drawing'] is not None:
coords = map_output['last_active_drawing']['geometry']['coordinates']
lats = [coords[0][i][0] for i in range(5)] # 5 because we expect a rectangle including its center point
lons = [coords[0][i][1] for i in range(5)]
bounding_box = [min(lats), min(lons), max(lats), max(lons)] # did I invert lats and lons here?
area = abs(bounding_box[2] - bounding_box[0]) * abs(bounding_box[3] - bounding_box[1])
st.write(f"Bounding box: {bounding_box}, area: {round(area,5)} Cartesian square units")
if area > 0.05:
st.write(f'Please choose a smaller area. We typically allow areas below 0.05')
else:
map_requested = st.button('Click here to download the area. You do not need to download it again if you try out multiple solutions below')
if map_requested:
with st.spinner('Downloading the road network. This may take a few minutes, please wait...'):
request_map(bounding_box)
st.write('Road network ready for routing')

uploaded_files = st.file_uploader('Upload all required files (config.json, customer_data.xlsx, extra_points.csv, custom_header.yaml)', accept_multiple_files=True)

st.write('If required, draw a rectangle over the area you want to use for routing. Download it again only if you updated the OpenStreetMap data. Please select an area as small as possible.')
m = folium.Map(location=[-11.9858, -77.019], zoom_start=5)
Draw(export=False).add_to(m)
map_output = st_folium(m, width=700, height=500)
if map_output['last_active_drawing'] is not None:
coords = map_output['last_active_drawing']['geometry']['coordinates']
lats = [coords[0][i][0] for i in range(5)] # 5 because we expect a rectangle including its center point
lons = [coords[0][i][1] for i in range(5)]
bounding_box = [min(lats), min(lons), max(lats), max(lons)]
area = abs(bounding_box[2] - bounding_box[0]) * abs(bounding_box[3] - bounding_box[1])
st.write(f"Bounding box: {bounding_box}, area: {round(area,5)} square units")
if area > 0.04:
st.write(f'Please choose a smaller area. We allow areas below 0.04')
else:
map_requested = st.button('Click here to download the area. You do not need to download it again if you try out multiple solutions below')
if map_requested:
with st.spinner('Downloading the road network. This may take a few minutes, please wait...'):
request_map(bounding_box)
st.write('Road network ready for routing')
lat_lon_columns = []

extra_configuration = False

solution_requested = False

uploaded_files = st.file_uploader('Upload all required files (config, locations, extra points)', accept_multiple_files=True)
if len(uploaded_files) > 0:

for uploaded in uploaded_files:
if uploaded.name == 'config.json':
try:
loaded = json.load(uploaded)
uploaded.seek(0)
except json.JSONDecodeError:
st.error('The file config.json is not valid JSON. Please validate the syntax in a text editor.')
if uploaded.name.endswith('lua') or uploaded.name.endswith('osm.pbf') or uploaded.name.endswith('build_parameters.yml'):
extra_configuration = True

if recalculate_map:
for uploaded in uploaded_files:
if uploaded.name == 'customer_data.xlsx':
customers = pd.read_excel(uploaded)
uploaded.seek(0)
#st.write(customers)
if uploaded.name == 'custom_header.yaml':
headers = yaml.load(uploaded, Loader=yaml.CLoader)
uploaded.seek(0)
lat_lon_columns.append(headers['lat_orig'])
lat_lon_columns.append(headers['long_orig'])
#st.write(headers)
if uploaded.name == 'extra_points.csv':
extra = pd.read_csv(uploaded)
uploaded.seek(0)
extra_coordinates = extra[['GPS (Latitude)','GPS (Longitude)']]
#st.write(extra)
all_coords = np.concatenate([customers[lat_lon_columns].values, extra_coordinates.values])
minima = all_coords.min(axis=0)-0.07 # 0.1 being a buffer for the road network
maxima = all_coords.max(axis=0)+0.07
bounding_box = [minima[1], minima[0], maxima[1], maxima[0]]
area = abs(bounding_box[2] - bounding_box[0]) * abs(bounding_box[3] - bounding_box[1])

response = requests.get(f'{host_url}/get_map_info/')
old_bounding_box = [0,0,0,0]
if response.json()["message"] is not None:
old_bounding_box = tuple(map(np.float64, response.json()['message']))

response = upload_data(uploaded_files)
st.write(response)
vehicle_or_map_update_requested = st.button('If you uploaded modified *.lua, build_parameters.yml, or *.osm.pbf files, click here to update the network')
if vehicle_or_map_update_requested:
with st.spinner('Rebuilding based on updated vehicles/maps. This may take a few minutes, please wait...'):
update_vehicle_or_map()
vehicles_text.text('Available vehicle profiles: '+ requests.get(f'{host_url}/available_vehicles').json()['message'])
if extra_configuration:
vehicle_or_map_update_requested = st.button('If you uploaded modified *.lua, build_parameters.yml, or *.osm.pbf files, click here to update the network')
if vehicle_or_map_update_requested:
with st.spinner('Rebuilding based on updated vehicles/maps. This may take a few minutes, please wait...'):
update_vehicle_or_map()
vehicles_text.text('Available vehicle profiles: '+ requests.get(f'{host_url}/available_vehicles').json()['message'])

if recalculate_map:
if tuple(bounding_box) == old_bounding_box:
st.write('The currently available map covers the same area, no need to redownload it unless you edited OSM since the last download')
else:
st.error("It would be recommended to download the area as you have locations in your input data outside the currently downloaded area")

map_requested_auto = st.button(f'Click here to download the area ({round(area,2)} in Cartesian square units). You do not need to download it again if you try out multiple scenarios with the same customer_data.xlsx file')
if map_requested_auto:
with st.spinner('Downloading the road network. Please wait...'):
request_map(bounding_box)
st.write('Road network ready for routing')

st.write('Calculating a solution will take up to twice the amount of time specified by the config file')

solution_requested = st.button('Click here to calculate routes')
st.write('Calculating a solution will take up to twice the amount of time specified by the config file')
solution_requested = st.button('Click here to calculate routes')

if solution_requested:
with st.spinner('Computing routes, please wait...'):
solution, map, solution_zip = request_solution()
solution, solutionmap, solution_zip = request_solution()
#this button reloads the page, let's avoid it
#st.download_button('Download solution files', solution_zip, file_name='solution.zip',
# mime='application/octet-stream', help='Downloads all the files generated by the tool')
b64 = base64.b64encode(solution_zip).decode()
st.markdown(f'<a href="data:application/octet-stream;base64,{b64}" download="solution.zip">Download solution files</a>', unsafe_allow_html=True)
components.html(map, height = 800)
components.html(solutionmap, height = 800)
st.write(solution)

st.subheader('Optional route adjustments')
uploaded_files = st.file_uploader('If adjustments are made in the manual_edits spreadsheet, upload it here to get adjusted solutions', accept_multiple_files=True)
if len(uploaded_files) > 0:
with st.spinner('Adjusting routes, please wait...'):
response, solution, map, solution_zip = adjust(uploaded_files)
response, solution, solutionmap, solution_zip = adjust(uploaded_files)
st.write(response)
b64 = base64.b64encode(solution_zip).decode()
st.markdown(f'<a href="data:application/octet-stream;base64,{b64}" download="solution.zip">Download solution files</a>', unsafe_allow_html=True)
components.html(map, height = 800)
components.html(solutionmap, height = 800)
st.write(solution)

if __name__ == '__main__':
Expand Down
6 changes: 4 additions & 2 deletions dkroutingtool/src/py/ui/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
streamlit==1.23.1
streamlit==1.41.0
streamlit-folium==0.18.0
folium==0.16.0
folium==0.16.0
openpyxl==3.1.5
pyyaml==6.0.2
Loading