Initial checkin

This commit is contained in:
2023-05-18 18:21:00 -04:00
commit 1c64f16aef
21 changed files with 1020 additions and 0 deletions

37
app/__init__.py Normal file
View File

@@ -0,0 +1,37 @@
from flask import Flask
from test.vehicle_data import vehicle_data
from .extensions import db, ma
from .models.vehicle import Vehicle
def create_app():
app = Flask(__name__)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite3'
db.init_app(app)
ma.init_app(app)
@app.cli.command("reset-db")
def reset_db():
db.drop_all()
db.create_all()
@app.cli.command("create-vehicles")
def create_vehicles():
for vehicle in vehicle_data:
db.session.add(Vehicle(**vehicle))
db.session.commit()
from .routes.vehicle import vehicle_routes
app.register_blueprint(vehicle_routes)
from .routes.shift import shift_routes
app.register_blueprint(shift_routes)
from .routes.auto import auto_routes
app.register_blueprint(auto_routes)
return app

5
app/extensions.py Normal file
View File

@@ -0,0 +1,5 @@
from flask_sqlalchemy import SQLAlchemy
from flask_marshmallow import Marshmallow
db = SQLAlchemy()
ma = Marshmallow()

0
app/models/__init__.py Normal file
View File

View File

@@ -0,0 +1,16 @@
from app.extensions import db, ma
class BatteryChange(db.Model):
order = db.Column(db.Integer)
shift_id = db.Column(db.Integer, db.ForeignKey('shift.id'), primary_key=True, nullable=False)
vehicle_id = db.Column(db.Integer, db.ForeignKey('vehicle.id'), primary_key=True, nullable=False)
completed = db.Column(db.Boolean, nullable=False, default=False)
vehicle = db.relationship('Vehicle', backref='change_to_vehicle')
class BatteryChangeSchema(ma.Schema):
vehicle = ma.Nested('VehicleSchema', only=['license_plate', 'battery_level',
'in_use', 'model', 'location_lat',
'location_long', 'id'], many=False)
class Meta:
model=BatteryChange
fields = ('shift_id', 'vehicle_id', 'completed', 'vehicle')

19
app/models/shift.py Normal file
View File

@@ -0,0 +1,19 @@
from app.extensions import db, ma
from flask_marshmallow import fields
class Shift(db.Model):
id = db.Column(db.Integer, primary_key=True)
battery_changes = db.relationship('BatteryChange',
backref='battery_changes',
order_by='BatteryChange.order')
class ShiftSchema(ma.Schema):
battery_changes = ma.Nested('BatteryChangeSchema', many=True)
all_completed = fields.fields.Method('get_all_completed')
class Meta:
model=Shift
include_fk=True
fields = ("id", "battery_changes", "all_completed")
def get_all_completed(self, obj):
return all(change.completed == True for change in obj.battery_changes)

23
app/models/vehicle.py Normal file
View File

@@ -0,0 +1,23 @@
from app.extensions import db, ma
class Vehicle(db.Model):
__tablename__ = 'vehicle'
id = db.Column(db.Integer, primary_key=True)
license_plate = db.Column(db.String(80), unique=True, nullable=False)
battery_level = db.Column(db.Float, nullable=False)
in_use = db.Column(db.Boolean, nullable=False)
model = db.Column(db.String(10), nullable=False)
location_lat = db.Column(db.Float, nullable=False)
location_long = db.Column(db.Float, nullable=False)
battery_change_shifts = db.relationship('BatteryChange', backref='battery_change_shifts')
shifts = db.relationship('Shift', secondary="battery_change", backref='vehicles_to_shifts')
class VehicleSchema(ma.Schema):
battery_change_shifts = ma.Nested('BatteryChangeSchema', many=True)
shifts = ma.Nested('ShiftSchema', only=['id'], many=True)
class Meta:
# Fields to expose
model = Vehicle
fields = ("id", "license_plate", "battery_level", "model",
"in_use", "location_lat", "location_long", 'battery_change_shifts',
"shifts")

0
app/routes/__init__.py Normal file
View File

145
app/routes/auto.py Normal file
View File

@@ -0,0 +1,145 @@
from flask import Blueprint
from itertools import permutations
from app.extensions import db
from app.models.vehicle import Vehicle
from app.models.shift import Shift, ShiftSchema
from app.models.battery_change import BatteryChange
from math import sqrt, pow
auto_routes = Blueprint('auto_routes', __name__)
@auto_routes.route('/auto/<lat>/<long>')
def generate_auto_shift(lat, long):
lat = float(lat)
long = float(long)
closest_vehicles = get_closest_vehicles(lat, long, 20)
solution = kruskals(closest_vehicles)
order = get_final_order(solution, closest_vehicles)
new_shift = Shift()
for i, ind in enumerate(order):
id = closest_vehicles[ind].id
new_change = BatteryChange(**{
"order": i,
"shift_id": new_shift.id,
"vehicle_id": id
})
new_shift.battery_changes.append(new_change)
db.session.add(new_shift)
db.session.commit()
shift_schema = ShiftSchema(many=False)
return shift_schema.dumps(new_shift)
def get_closest_vehicles(lat, long, num):
vehicles = Vehicle.query.all()
distances = []
for vehicle in vehicles:
distance = sqrt(pow(abs(lat) - abs(vehicle.location_lat), 2) + pow(abs(long) - abs(vehicle.location_long), 2))
distances.append((vehicle, distance))
distances = sorted(distances, key=lambda x: x[1])[:num]
return [x[0] for x in distances]
def get_distance_between_vehicles(left, right):
# Assumes all lats/longs of cars to be consistently positive or negatively.
# Would most likely include data point for city/region and only query cars in that area
return sqrt(pow(abs(left.location_lat) - abs(right.location_lat), 2) + pow(abs(left.location_long) - abs(right.location_long), 2))
def get_sorted_edges(vehicles):
'''
Returns list of tuples representing edges (<source_node>, <other_node>, <distance>)
where node is index of vehicle in sorted vehicles by distance from starting point.
The return list is sorted by edge distance.
'''
if not vehicles or len(vehicles) == 1: return []
edge_list = []
for source_ind, vehicle in enumerate(vehicles):
for other_ind, other_vehicle in enumerate(vehicles):
# So we don't double count edges
if source_ind < other_ind:
distance = get_distance_between_vehicles(vehicle, other_vehicle)
edge_list.append((source_ind, other_ind, distance))
return sorted(edge_list, key = lambda edge: edge[2])
def kruskals(vehicles):
'''
Gets the optimal edges to reduce the distance travelled between nodes.
Returns list of lists where each index represents a vehicle in the closest
vehicle list (indexes match) and each sublist represents the edges
to other nodes
'''
edges = get_sorted_edges(vehicles)
parents = [i for i in range(len(vehicles))]
ranks = [0 for _ in range(len(vehicles))]
solution = [[] for _ in range(len(vehicles))]
def find(vertex, parents):
if vertex != parents[vertex]:
parents[vertex] = find(parents[vertex], parents)
return parents[vertex]
def union(root1, root2, parents, ranks):
if ranks[root1] < ranks[root2]:
parents[root1] = root2
elif ranks[root1] > ranks[root2]:
parents[root2] = root1
else:
parents[root2] = root1
ranks[root1] += 1
for edge in edges:
root1 = find(edge[0], parents)
root2 = find(edge[1], parents)
if root1 != root2:
solution[edge[0]].append((edge[1], edge[2]))
solution[edge[1]].append((edge[0], edge[2]))
union(root1, root2, parents, ranks)
return solution
def get_final_order(solution, closest_vehicles):
'''
Uses the solution from kruskals algorithm to determine the final route.
'''
# Find first vertex with only one edge, this will be out starting point
# i.e. Closest leaf node
ind = 0
for i in range(len(solution)):
if len(solution[i]) == 1:
ind = i
break
order = [ind]
# Keep a stack to go back to previous nodes if we hit a dead end
stack = []
while len(order) != len(closest_vehicles):
while len(solution[ind]) != 0:
# Iterate through edges of current node, removing once travelled
edge = solution[ind].pop(0)
# New node, add to order and check that node's edges
if edge[0] not in order:
if solution[ind]:
stack.append(ind)
ind = edge[0]
order.append(ind)
break
# Hit the end of edges for current node, go back
if len(solution[ind]) == 0 and stack:
ind = stack.pop()
# If we're out of edges in the current node and nowhere to go back to,
# we must have visited all nodes, return out
elif len(solution[ind]) == 0 and not stack:
break
return order

97
app/routes/shift.py Normal file
View File

@@ -0,0 +1,97 @@
from flask import Blueprint, request, render_template_string
from app.extensions import db
from app.models.shift import Shift, ShiftSchema
from app.models.battery_change import BatteryChange, BatteryChangeSchema
shift_routes = Blueprint('shift_routes', __name__)
@shift_routes.route('/shifts')
def list_shifts():
shift_schema = ShiftSchema(many=True)
shifts = Shift.query.all()
return shift_schema.dumps(shifts)
@shift_routes.route('/shifts/<id>')
def get_shift(id):
shift_schema = ShiftSchema(many=False)
shift = Shift.query.get_or_404(id)
return shift_schema.dumps(shift)
@shift_routes.route('/shifts', methods=['POST'])
def create_shift():
new_shift = Shift()
if request.form.get('vehicles'):
vehicle_ids = [int(i) for i in request.form.get('vehicles').split(',')]
for id in vehicle_ids:
new_change = BatteryChange(**{
"shift_id": new_shift.id,
"vehicle_id": id
})
new_shift.battery_changes.append(new_change)
db.session.add(new_shift)
db.session.commit()
shift_schema = ShiftSchema(many=False)
return shift_schema.dumps(new_shift)
@shift_routes.route('/shifts/<id>', methods=['DELETE'])
def delete_shift(id):
shift = Shift.query.get_or_404(id)
for change in shift.battery_changes:
db.session.delete(change)
db.session.commit()
db.session.delete(shift)
db.session.commit()
return id
@shift_routes.route('/shifts/<shift_id>/vehicles/<vehicle_id>', methods=['GET'])
def get_vehicle_in_shift(shift_id, vehicle_id):
shift = Shift.query.get_or_404(shift_id)
for change in shift.battery_changes:
if change.vehicle_id == int(vehicle_id):
change_schema = BatteryChangeSchema(many=False)
return change_schema.dumps(change)
return 'No vehicle found with id %s in shift %s' % (vehicle_id, shift_id), 404
@shift_routes.route('/shifts/<shift_id>/vehicles/<vehicle_id>', methods=['POST'])
def add_vehicle_to_shift(shift_id, vehicle_id):
shift_schema = ShiftSchema(many=False)
shift = Shift.query.get_or_404(shift_id)
shift.battery_changes.append(BatteryChange**{
'shift_id': shift.id,
'vehicle_id': vehicle_id
})
db.session.commit()
return shift_schema.dumps(shift)
@shift_routes.route('/shifts/<shift_id>/vehicles/<vehicle_id>/completed', methods=['POST'])
def complete_vehicle_in_shift(shift_id, vehicle_id):
shift_schema = ShiftSchema(many=False)
shift = Shift.query.get_or_404(shift_id)
for change in shift.battery_changes:
if int(change.vehicle_id) == int(vehicle_id):
change.completed = True
db.session.commit()
return shift_schema.dumps(shift)
return 'Vehicle not found', 404
@shift_routes.route('/shifts/<shift_id>/vehicles/<vehicle_id>', methods=['DELETE'])
def remove_vehicle_from_shift(shift_id, vehicle_id):
shift_schema = ShiftSchema(many=False)
shift = Shift.query.get_or_404(shift_id)
delete_me = []
keep_me = []
for change in shift.battery_changes:
if int(change.vehicle_id) == int(vehicle_id):
delete_me.append(change)
else:
keep_me.append(change)
if not delete_me: return 'No vehicles found with id %s' % vehicle_id, 404
shift.battery_changes = keep_me
for change in delete_me:
db.session.delete(change)
db.session.commit()
return shift_schema.dumps(shift)

54
app/routes/vehicle.py Normal file
View File

@@ -0,0 +1,54 @@
from flask import Blueprint, request
from app.extensions import db
from app.models.vehicle import Vehicle, VehicleSchema
vehicle_routes = Blueprint('vehicle_routes', __name__)
@vehicle_routes.route('/vehicles')
def list_vehicles():
vehicles_schema = VehicleSchema(many=True)
vehicles = Vehicle.query.all()
return vehicles_schema.dumps(vehicles)
@vehicle_routes.route('/vehicles/<id>')
def get_vehicle(id):
vehicles_schema = VehicleSchema(many=False)
vehicle = Vehicle.query.get_or_404(id)
return vehicles_schema.dumps(vehicle)
@vehicle_routes.route('/vehicles', methods=['POST'])
def create_vehicle():
new_vehicle = Vehicle(**{
"license_plate": request.form.get('license_plate'),
"battery_level": request.form.get('battery_level', 100),
"in_use": bool(request.form.get('in_use', True)),
"model": request.form.get('model'),
"location_lat": request.form.get('location_lat'),
"location_long": request.form.get('location_long'),
})
db.session.add(new_vehicle)
db.session.commit()
vehicles_schema = VehicleSchema(many=False)
return vehicles_schema.dumps(new_vehicle)
@vehicle_routes.route('/vehicles/<id>', methods=['POST'])
def update_vehicle(id):
vehicles_schema = VehicleSchema(many=False)
vehicle = Vehicle.query.get_or_404(id)
vehicle.license_plate = request.form.get('license_plate', vehicle.license_plate)
vehicle.battery_level = request.form.get('battery_level', vehicle.battery_level)
vehicle.in_use = request.form.get('in_use', vehicle.in_use)
vehicle.model = request.form.get('model', vehicle.model)
vehicle.location_lat = request.form.get('location_lat', vehicle.location_lat)
vehicle.location_long = request.form.get('location_long', vehicle.location_long)
db.session.commit()
return vehicles_schema.dumps(vehicle)
@vehicle_routes.route('/vehicles/<id>', methods=['DELETE'])
def delete_vehicle(id):
vehicle = Vehicle.query.get_or_404(id)
db.session.delete(vehicle)
db.session.commit()
return id