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

Nathaniel - ETA API #1

Merged
merged 31 commits into from
Mar 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
92682bd
README
Li-Nathaniel Jul 11, 2023
fcf17f0
eta integration
Li-Nathaniel Jul 11, 2023
907e0c7
Modified to work with postgresql
Li-Nathaniel Sep 14, 2023
f265bac
Modified README.md
Li-Nathaniel Sep 16, 2023
ea01352
.env file
Li-Nathaniel Sep 16, 2023
6835d21
.env file
Li-Nathaniel Sep 16, 2023
92f8f45
Modify .env file
Li-Nathaniel Sep 16, 2023
855c9de
Modified for use with PostgreSQL
Li-Nathaniel Sep 17, 2023
a4c456d
Modified to test eta.py
Li-Nathaniel Oct 11, 2023
5d1e31f
Getting rid of old venv and .DS_store
Li-Nathaniel Oct 11, 2023
f46b162
Removed old venv and .DS_Store
Li-Nathaniel Oct 11, 2023
a78021b
Remove .DS_Store
Li-Nathaniel Oct 11, 2023
6f0bf49
Remove .DS_Store
Li-Nathaniel Oct 11, 2023
511eba0
Remove .DS_Store
Li-Nathaniel Oct 11, 2023
38fb3a6
Remove .DS_Store
Li-Nathaniel Oct 11, 2023
f23720e
Fixed list appending issues in PSQL and handle NaN
Li-Nathaniel Oct 31, 2023
83c0d85
Merge branch 'eta_api' of https://github.com/uw-midsun/victini-micros…
Li-Nathaniel Oct 31, 2023
1a7a400
Merge branch 'eta_api' of https://github.com/uw-midsun/victini-micros…
Li-Nathaniel Oct 31, 2023
63b8609
Delete eta_api directory
Li-Nathaniel Oct 31, 2023
c153a37
Update gitignore
Li-Nathaniel Oct 31, 2023
11f29df
Removed old .DS_Store
Li-Nathaniel Oct 31, 2023
8e6e390
Update gitignore
Li-Nathaniel Oct 31, 2023
0d14a17
Delete .DS_Store
Li-Nathaniel Oct 31, 2023
7aa115f
Updated comments for clarity and Schemas folder
Li-Nathaniel Oct 31, 2023
bdffbe3
Updated DB credentials
Li-Nathaniel Nov 26, 2023
cab6df6
Fixed the issue where the eta to future checkpoints were not being ca…
Li-Nathaniel Dec 1, 2023
4590616
Discarded testing code
Li-Nathaniel Dec 2, 2023
c7ed123
Create README.md
Li-Nathaniel Dec 2, 2023
687bd85
Update eta.py
kasethi23 Feb 11, 2024
8f26425
Merge pull request #2 from kasethi23/eta_api
rodrigotiscareno Mar 16, 2024
d6ec178
Delete .vscode/settings.json
rodrigotiscareno Mar 16, 2024
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.DS_Store
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down Expand Up @@ -126,4 +127,4 @@ venv.bak/
dmypy.json

# Pyre type checker
.pyre/
.pyre/
49 changes: 49 additions & 0 deletions eta_microservice/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# ETA Microservice

A Flask microservice that updates the PostgreSQL database with the ETA (in mins) from the inputted point to the checkpoints in the checkpoint_model_db.csv file.

## Setup

### Python install

Install Python 3 on your system. To test, open a command prompt or a terminal and write `python3` or `py`.

### Project setup

Once Python is installed, clone this repo into your editor of choice.

Cloning the repository will create a new folder named `victini-microservices` with all the code -- you will have 'cloned' the repository!

Once cloned, run `cd victini-microservices/eta_microservice` to go into the new folder.

First, set up a virtual environment using `python3 -m venv`. This will create a `venv/` folder in your directory and isolate all of the porpject dependencies from other projects.

To source this environment run `source venv/bin/activate`.

Then, to install the project dependencies, run `pip install -r requirements.txt` -- this will install all the libraries listed in the `requirements.txt` file.

### Database Setup

To setup the database, run the SQL script found in the schemas folder to create the table. The table should have columns: id, lat, long and eta.

### Connection Setup

Once the DB is setup, navigate to the `.env` file and ensure that the DB connection details are correctly populated.

### Running the Microservice

After ensuring that you're in the `eta_microservice` folder and running your **virtual environment**, run `python main.py` to start the microservice.

The terminal should show that the Flask app is running on localhost on port 6000.

### Calculating ETA

With the microservice running, you can send a POST request for the `calculate_eta` function to run.

Using tool like Postman, send a POST request like `http://localhost:6000/calculate_eta` and ensure that the **lat** and **long** and included in the **JSON** file:

eg. `{"lat": 43.46786317655638, "lon": -80.56637564010215}`

Update the checkpoints table to see the new point and the list of ETAs to each checkpoint.

If a checkpoint has already been passed or an error occurred while calculating the ETA, -1.0 will be the ETA.
6 changes: 6 additions & 0 deletions eta_microservice/Schemas/checkpoints_DDL.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE public.checkpoints (
id int4 NULL,
lat float8 NULL,
lon float8 NULL,
eta _float8 NULL
);
3 changes: 3 additions & 0 deletions eta_microservice/current_location.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
43.46786317655638, -80.56637564010215

This location can be used for testing purposes. Alternatively, any point from the route_model_db.csv in data folder can be used.
8 changes: 8 additions & 0 deletions eta_microservice/data/checkpoint_model_db.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
latitude,longitude,route_model_index,last_updated,forecast_time
43.46786317655638,-80.56637564010215,0,,
43.47181929178542,-80.55498944295489,41,,
43.47567782098289,-80.54387952656383,81,,
43.47953526800491,-80.53276819650009,121,,
43.48532332236007,-80.52733698515767,162,,
43.49079136987552,-80.53590572263501,205,,
43.49414882508247,-80.54735874761738,246,,
10 changes: 10 additions & 0 deletions eta_microservice/data/route.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
43.46786317655638, -80.56637564010215
43.48175280991097, -80.52637854159468
43.48536483714299, -80.5270651870626
43.48511573875007, -80.52869597004897
43.48536483714299, -80.52989759961787
43.48604985242712, -80.5304984144023
43.48816712335336, -80.5320433668198
43.489910702452285, -80.53461828732458
43.49196557034531, -80.53762236124682
43.494347261633344, -80.5482439084337
251 changes: 251 additions & 0 deletions eta_microservice/data/route_model_db.csv

Large diffs are not rendered by default.

98 changes: 98 additions & 0 deletions eta_microservice/eta.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import sys
import os.path
sys.path.append(os.path.dirname(sys.path[0]))

import pandas as pd
from geopy.distance import distance as geodist
import math

class ETAQuery():
def __init__(self, lat, lon):
self.lat = lat
self.lon = lon
route_model_path = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'eta_microservice', 'data', 'route_model_db.csv')) #changed it for interoperability between unix and windows
checkpoint_model_path = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'eta_microservice', 'data', 'checkpoint_model_db.csv'))
# Read CSV files
self.route_model = pd.read_csv(route_model_path)
self.checkpoint_model = pd.read_csv(checkpoint_model_path)

self.checkpoint_coords = self.checkpoint_model.loc[:, ['latitude', 'longitude']].values.tolist()
self.ckpt_to_route_model_index = self.checkpoint_model.loc[:, ['route_model_index']].values.tolist()
self.eta_to_checkpoint = []
self.speed = self.get_speed_at_point(lat, lon) # Speed in km/h

self.generate_eta()

def get_speed_at_point(self, lat, lon):
return 5 # TODO: Take speed from route model after it is precomputed

def find_closest_point(self, lat, lon):
'''
The function takes in a coordinate point (lat/lon) and finds the closest point to it inside the route model
@param lat: float representing the latitude
@param lon: float representing the longitude
@return: a row of the route_model dataframe containing information about the closest point on the race route
'''

distances = self.route_model.apply(lambda row: geodist((lat, lon), (row['latitude'], row['longitude'])).meters, axis=1)
return self.route_model.iloc[distances.idxmin()]

def generate_eta(self):
"""
@return: a list of float values representing the time it takes to reach the checkpoints
"""
closest_point = self.find_closest_point(self.lat, self.lon)
current_checkpoint = closest_point['checkpoint']
next_closest_checkpoint = current_checkpoint + 1

n = len(self.checkpoint_coords)

# Modified to instantiate eta list with -1.0 instead of -1 since returned eta will be a float
eta = [-1.0 for _ in range(n)]

if next_closest_checkpoint >= n:
self.eta = eta
return self.eta

slat = self.lat
slon = self.lon
elat = self.checkpoint_coords[next_closest_checkpoint][0]
elon = self.checkpoint_coords[next_closest_checkpoint][1]

dist_to_next_checkpoint = geodist((slat, slon), (elat, elon)).meters
# Unit conversion is to put meters into kilometers and time into minutes
eta[next_closest_checkpoint] = (dist_to_next_checkpoint / 1000) / self.speed * 60

for future_checkpoint in range(next_closest_checkpoint + 1, n):
a = self.ckpt_to_route_model_index[next_closest_checkpoint][0]
b = self.ckpt_to_route_model_index[future_checkpoint]

print(self.route_model.iloc[b]['trip(m)'])

# Unit conversion is to put meters into kilometers and time into minutes
eta_next_to_future = ((self.route_model.iloc[b]['trip(m)'] - self.route_model.iloc[a]['trip(m)']) / 1000) / self.speed * 60
eta_value = eta[next_closest_checkpoint] + float(eta_next_to_future.iloc[0])

# Additional check for NaN -> leave it as -1.0 in the list
if not math.isnan(eta_value):
eta[future_checkpoint] = eta_value

self.eta = eta
return eta

def get_times(self):
"""
@return: a list of float values representing the time it takes to reach the checkpoints
"""
return self.eta


def get_time_to_point(self, lat, lon):
"""
@return: float representing the time it takes to reach a given point
"""
closest_point = self.find_closest_point(lat, lon)
return self.eta[closest_point['checkpoint']]



68 changes: 68 additions & 0 deletions eta_microservice/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import sys
import os.path
sys.path.append(os.path.dirname(sys.path[0]))

import psycopg2
import pandas as pd
from geopy.distance import distance as geodist
from flask import Flask, request, jsonify
from dotenv import load_dotenv
from eta import ETAQuery

load_dotenv()

app = Flask(__name__)

# Retrieve environment variables from .env file
DB_HOST = os.getenv("DATABASE_HOST")
DB_PORT = int(os.getenv("DATABASE_PORT"))
DB_NAME = os.getenv("DATABASE_NAME")
DB_USER = os.getenv("DATABASE_USER")
DB_PASSWORD = os.getenv("DATABASE_PASSWORD")

# DB connection
connection = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD
)

# Calculate_eta method requires that the new (lat, lon) position be inputted as json data -> eta appended to DB will be for all points in route.csv
@app.route('/calculate_eta', methods=['POST'])
def calculate_eta():
try:
data = request.get_json()
lat = data.get('lat')
lon = data.get('lon')

# Invalid lat or lon
if lat is None or lon is None:
return jsonify({'error': 'Invalid request data'}), 400

eta_query = ETAQuery(lat, lon)
eta_values = eta_query.get_times()

# Insert a new row with lat, lon, and calculated eta (in mins)
with connection.cursor() as cursor:
# Find the maximum id in the table (ie. newest row) -> if no checkpoints, instantiate with 0
cursor.execute("SELECT MAX(id) FROM public.checkpoints")
max_id = cursor.fetchone()[0] or 0

# Insert the new row with an incremented id
cursor.execute(
"INSERT INTO public.checkpoints (id, lat, lon, eta) VALUES (%s, %s, %s, %s)",
(max_id + 1, lat, lon, eta_values)
)
connection.commit()

# Successful calculation and DB change
return jsonify({'message': 'New row created with calculated ETA.', 'id': max_id + 1}), 200

# Error message
except Exception as e:
return jsonify({'error': str(e)}), 500

if __name__ == '__main__':
app.run(host='0.0.0.0', port=6000)
6 changes: 6 additions & 0 deletions eta_microservice/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Flask==3.0.0
psycopg2==2.9.9
numpy==1.26.1
pandas==2.1.2
geopy==2.4.0
python-dotenv==1.0.0
Loading