Spaces:
Sleeping
Sleeping
File size: 71,053 Bytes
b42db06 | 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 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 | import sqlite3
from datetime import datetime
import joblib
import pandas as pd
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, EmailStr, constr
from paddleocr import PaddleOCR
import shutil
import os
import uuid
import cv2
import numpy as np
from PIL import Image
import logging
from typing import List, Optional, Tuple, Dict, Any
import hashlib
import secrets
import requests
import json
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import base64
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError
# Set up logging first
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Load environment variables from .env file
def load_env_file():
"""Load environment variables from .env file if it exists"""
# Try multiple possible locations for .env file
possible_paths = [
'.env', # Current directory
os.path.join(os.path.dirname(__file__), '.env'), # Same directory as main.py
os.path.join(os.getcwd(), '.env'), # Current working directory
]
for env_file in possible_paths:
if os.path.exists(env_file):
logger.info(f"Loading environment variables from: {env_file}")
with open(env_file, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
os.environ[key] = value
logger.info(f"Loaded env var: {key}")
return True
logger.warning("No .env file found in any of the expected locations")
return False
load_env_file()
# --- Initialize Models ---
try:
# Use relative paths for better portability
current_dir = os.path.dirname(__file__)
eta_model_path = os.path.join(current_dir, 'eta_prediction_model.pkl')
model_columns_path = os.path.join(current_dir, 'model_columns.pkl')
eta_model = joblib.load(eta_model_path)
model_columns = joblib.load(model_columns_path)
logger.info("ML models loaded successfully")
except Exception as e:
logger.warning(f"Could not load ML models: {e}")
eta_model = None
model_columns = None
# --- Initialize Training Data Database ---
def init_training_db():
# Ensure db directory exists
os.makedirs('db', exist_ok=True)
conn = sqlite3.connect('db/training_data.db')
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS training_data (
id INTEGER PRIMARY KEY AUTOINCREMENT,
route_id TEXT UNIQUE NOT NULL,
addresses TEXT NOT NULL,
coordinates TEXT NOT NULL,
predicted_eta_minutes REAL NOT NULL,
actual_eta_minutes REAL,
start_time TEXT NOT NULL,
end_time TEXT,
sensor_data TEXT NOT NULL,
user_id TEXT NOT NULL,
route_metadata TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
logger.info("Training data database initialized")
init_training_db()
# -------------------- Auth DB Init --------------------
def init_auth_db():
# Ensure db directory exists
os.makedirs('db', exist_ok=True)
conn = sqlite3.connect('db/auth.db')
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
name TEXT,
is_verified INTEGER DEFAULT 0,
otp_code TEXT,
otp_expires_at TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
# Staging table: accounts that registered but have not verified OTP yet
cursor.execute('''
CREATE TABLE IF NOT EXISTS pending_users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
name TEXT,
otp_code TEXT,
otp_expires_at TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
conn.commit()
conn.close()
logger.info("Auth database initialized")
init_auth_db()
# --- Initialize PaddleOCR ---
try:
ocr_model = PaddleOCR(use_angle_cls=True, lang='en')
logger.info("OCR model initialized successfully")
except Exception as e:
logger.error(f"Failed to initialize OCR model: {e}")
ocr_model = None
# --- Initialize FastAPI App ---
app = FastAPI(title="Delivery Logistics API")
# Enable permissive CORS so frontend apps can call the API over any WiFi / network
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# -------------------- Auth Utilities --------------------
def hash_password(password: str) -> str:
return hashlib.sha256(password.encode('utf-8')).hexdigest()
def generate_otp(length: int = 6) -> str:
return ''.join(str(secrets.randbelow(10)) for _ in range(length))
# Gmail API Configuration
SCOPES = ['https://www.googleapis.com/auth/gmail.send']
CREDENTIALS_FILE = 'credentials.json'
TOKEN_FILE = 'token.json'
def get_gmail_service():
"""Get authenticated Gmail service using OAuth2"""
creds = None
# Load existing token
if os.path.exists(TOKEN_FILE):
creds = Credentials.from_authorized_user_file(TOKEN_FILE, SCOPES)
# If no valid credentials, run OAuth flow
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
if not os.path.exists(CREDENTIALS_FILE):
logger.error(f"Gmail credentials file not found: {CREDENTIALS_FILE}")
return None
flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_FILE, SCOPES)
creds = flow.run_local_server(port=0)
# Save credentials for next run
with open(TOKEN_FILE, 'w') as token:
token.write(creds.to_json())
try:
service = build('gmail', 'v1', credentials=creds)
return service
except Exception as e:
logger.error(f"Failed to build Gmail service: {e}")
return None
def send_verification_email(email: str, otp: str) -> bool:
"""Send verification email via Gmail SMTP (app password). Falls back to OAuth if configured."""
# Build HTML body once
body_html = f"""
<html>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="text-align: center; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 10px 10px 0 0;">
<h1 style="margin: 0; font-size: 28px;">π Delivery Route Optimizer</h1>
</div>
<div style="background: #f8f9fa; padding: 30px; border-radius: 0 0 10px 10px; border: 1px solid #e9ecef;">
<h2 style="color: #333; text-align: center; margin-bottom: 20px;">Email Verification</h2>
<p style="color: #666; font-size: 16px; text-align: center; margin-bottom: 30px;">
Your verification code is:
</p>
<div style="text-align: center; margin: 30px 0;">
<span style="background: #1976d2; color: white; font-size: 36px; font-weight: bold; padding: 20px 40px; border-radius: 8px; letter-spacing: 8px; display: inline-block; box-shadow: 0 4px 8px rgba(0,0,0,0.1);">
{otp}
</span>
</div>
<p style="color: #666; font-size: 14px; text-align: center; margin: 20px 0;">
β° This code will expire in 10 minutes
</p>
<p style="color: #999; font-size: 12px; text-align: center; margin-top: 30px;">
If you didn't request this code, please ignore this email.
</p>
<div style="text-align: center; margin-top: 30px; padding-top: 20px; border-top: 1px solid #e9ecef;">
<p style="color: #666; font-size: 14px; margin: 0;">
Best regards,<br>
<strong>Delivery Route Optimizer Team</strong>
</p>
</div>
</div>
</body>
</html>
"""
# Use SMTP with environment variables
gmail_user = os.environ.get("GMAIL_USER")
gmail_app_password = os.environ.get("GMAIL_APP_PASSWORD")
if not gmail_user or not gmail_app_password:
logger.warning("Gmail credentials not found in environment variables. Logging OTP to console.")
logger.info(f"[EMAIL] To: {email} | Subject: Your verification code | Body: Your OTP is: {otp}")
return False
try:
msg = MIMEMultipart('alternative')
msg['Subject'] = "Your Delivery Route Optimizer Verification Code"
msg['From'] = gmail_user
msg['To'] = email
msg.attach(MIMEText(body_html, 'html'))
with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
smtp.login(gmail_user, gmail_app_password)
smtp.sendmail(gmail_user, [email], msg.as_string())
logger.info(f"β
Verification email sent via SMTP to {email}")
return True
except Exception as e:
logger.error(f"β SMTP send failed: {e}")
logger.warning("Logging OTP to console as fallback.")
logger.info(f"[EMAIL] To: {email} | Subject: Your verification code | Body: Your OTP is: {otp}")
return False
def send_email_placeholder(email: str, subject: str, body: str) -> None:
"""Legacy function - now calls send_verification_email for OTP emails"""
if "verification code" in subject.lower() and "otp is:" in body.lower():
# Extract OTP from body
otp = body.split("OTP is: ")[1] if "OTP is: " in body else "123456"
send_verification_email(email, otp)
else:
logger.info(f"[EMAIL] To: {email} | Subject: {subject} | Body: {body}")
# --- Define Data Models ---
class RouteData(BaseModel):
ors_duration_minutes: float
total_distance_km: float
num_stops: int
start_time: str
class CompletedRoute(BaseModel):
start_time: str
end_time: str
ors_duration_minutes: float
total_distance_km: float
num_stops: int
class PlanRouteRequest(BaseModel):
addresses: List[str]
start_time: Optional[str] = None # ISO8601 string; if omitted, uses now
vehicle_start_address: Optional[str] = None # if omitted, uses first address as start
class TrainingDataRequest(BaseModel):
route_id: str
addresses: List[str]
coordinates: List[List[float]]
predicted_eta_minutes: float
actual_eta_minutes: Optional[float] = None
start_time: str
end_time: Optional[str] = None
sensor_data: List[Dict[str, Any]]
user_id: str
route_metadata: Dict[str, Any]
class UpdateEtaRequest(BaseModel):
route_id: str
actual_eta_minutes: float
# -------------------- Auth Models --------------------
class RegisterRequest(BaseModel):
email: EmailStr
password: constr(min_length=6)
name: Optional[str] = None
class VerifyEmailRequest(BaseModel):
email: EmailStr
otp: constr(min_length=4, max_length=8)
class LoginRequest(BaseModel):
email: EmailStr
password: str
class PlannedRouteResponse(BaseModel):
ordered_addresses: List[str]
ordered_coordinates: List[Tuple[float, float]] # (lat, lon)
ors_duration_minutes: float
total_distance_km: float
num_stops: int
predicted_eta_minutes: Optional[float] = None
route_geometry_geojson: Optional[Dict[str, Any]] = None
# --- Geocoding and ORS Utilities ---
# Load API keys from environment variables
ORS_API_KEY: str = os.environ.get("ORS_API_KEY", "eyJvcmciOiI1YjNjZTM1OTc4NTExMTAwMDFjZjYyNDgiLCJpZCI6IjE0MWRkZGMxZWNlMDFjZWZiNzMwNzNmNjZkZjIxN2JkMWZlZDM5OGI3NDc0YjMwNDc5YmNlMzljIiwiaCI6Im11cm11cjY0In0=")
# Traffic data API keys (optional - for real-time traffic)
MAPMYINDIA_API_KEY: str = os.environ.get("MAPMYINDIA_API_KEY", "")
TRAFFIC_API_KEY: str = os.environ.get("TRAFFIC_API_KEY", "")
# OpenRouteService provides both routing AND geocoding services
# FREE: 1,000 geocoding requests/day, no credit card required
NOMINATIM_URL = "https://nominatim.openstreetmap.org/search"
ORS_DIRECTIONS_URL = "https://api.openrouteservice.org/v2/directions/driving-car/geojson"
ORS_MATRIX_URL = "https://api.openrouteservice.org/v2/matrix/driving-car"
def geocode_address(address: str) -> Optional[Tuple[float, float]]:
"""Geocode address using OpenRouteService first (free), then Nominatim as fallback."""
# Check if address is already coordinates (lat,lon format)
if ',' in address and address.count(',') == 1:
try:
parts = address.strip().split(',')
lat = float(parts[0].strip())
lon = float(parts[1].strip())
# Validate coordinate ranges
if -90 <= lat <= 90 and -180 <= lon <= 180:
logger.info(f"β
Using provided coordinates '{address}' -> [{lat}, {lon}]")
return (lat, lon)
except ValueError:
pass # Not valid coordinates, continue with geocoding
# Try OpenRouteService Geocoding API first (FREE: 1,000 requests/day)
if ORS_API_KEY:
try:
url = "https://api.openrouteservice.org/geocode/search"
# Try different header formats for ORS API
headers = {"Authorization": ORS_API_KEY}
params = {
"text": address,
"boundary.country": "IN", # Focus on India
"size": 5, # Reduce size to avoid potential issues
# "layers": "address,poi,street", # Simplified layers
"sources": "openstreetmap" # Single source to avoid conflicts
}
logger.info(f"π ORS geocoding request for '{address}' with key: {ORS_API_KEY[:20]}...")
resp = requests.get(url, params=params, headers=headers, timeout=10)
# If Bearer format fails, try without Bearer
if resp.status_code == 401 or resp.status_code == 400:
logger.warning(f"π Retrying ORS with different auth format...")
headers = {"Authorization": ORS_API_KEY}
resp = requests.get(url, params=params, headers=headers, timeout=10)
if resp.status_code == 200:
data = resp.json()
if data.get("features"):
# Try to find the most specific match with better scoring
best_feature = None
best_score = 0
for feature in data["features"]:
props = feature.get("properties", {})
confidence = props.get("confidence", 0)
layer = props.get("layer", "")
name = props.get("name", "").lower()
label = props.get("label", "").lower()
# Calculate score based on multiple factors
score = confidence
# Boost score for exact name matches
if address.lower() in name or address.lower() in label:
score += 0.3
# Boost score for specific layers
if layer == "address":
score += 0.2
elif layer == "poi":
score += 0.15
elif layer == "street":
score += 0.1
# Boost score for higher confidence
if confidence > 0.8:
score += 0.2
elif confidence > 0.6:
score += 0.1
if score > best_score:
best_score = score
best_feature = feature
if not best_feature:
best_feature = data["features"][0] # Fallback to first result
coordinates = best_feature["geometry"]["coordinates"]
lon, lat = coordinates[0], coordinates[1]
props = best_feature.get("properties", {})
logger.info(f"β
ORS geocoded '{address}' -> [{lat}, {lon}] (confidence: {props.get('confidence', 'N/A')}, layer: {props.get('layer', 'N/A')})")
return (lat, lon)
else:
logger.warning(f"β ORS geocoding failed for '{address}': No features found")
else:
logger.warning(f"β ORS API error for '{address}': HTTP {resp.status_code}")
logger.warning(f" Response: {resp.text[:200]}...")
# Don't fail completely, continue to Nominatim fallback
except Exception as e:
logger.error(f"β ORS geocoding error for '{address}': {e}")
# Continue to Nominatim fallback
# Fallback to Nominatim with better parameters
logger.info(f"π Trying Nominatim fallback for '{address}'")
headers = {"User-Agent": "delivery-route-app/1.0 (contact: tejas12gowda.com)"}
params = {
"q": address,
"format": "json",
"limit": 5, # Get more results
"addressdetails": 1, # Get detailed address info
"countrycodes": "in", # Focus on India
"extratags": 1 # Get extra tags for better matching
}
try:
resp = requests.get(NOMINATIM_URL, params=params, headers=headers, timeout=15)
if resp.status_code != 200:
logger.warning(f"β Nominatim non-200 for '{address}': {resp.status_code}")
return None
data = resp.json()
if not data:
logger.warning(f"β No geocoding results for '{address}' from any service")
# Try a simple fallback for common Indian cities
address_lower = address.lower()
if 'bangalore' in address_lower or 'bengaluru' in address_lower:
logger.info(f"π Using Bangalore fallback coordinates")
return (12.9716, 77.5946) # Bangalore center
elif 'mumbai' in address_lower or 'bombay' in address_lower:
logger.info(f"π Using Mumbai fallback coordinates")
return (19.0760, 72.8777) # Mumbai center
elif 'delhi' in address_lower or 'new delhi' in address_lower:
logger.info(f"π Using Delhi fallback coordinates")
return (28.6139, 77.2090) # Delhi center
return None
# Try to find the best match from Nominatim results
best_result = None
for result in data:
importance = result.get("importance", 0)
osm_type = result.get("osm_type", "")
# Prefer house/building results for specific addresses
if osm_type in ["way", "node"] and importance > 0.5:
best_result = result
break
elif not best_result and importance > 0.3:
best_result = result
if not best_result:
best_result = data[0] # Fallback to first result
lat = float(best_result["lat"]) # type: ignore
lon = float(best_result["lon"]) # type: ignore
display_name = best_result.get("display_name", "Unknown")
logger.info(f"β
Nominatim geocoded '{address}' -> [{lat}, {lon}] (importance: {best_result.get('importance', 'N/A')}, type: {best_result.get('osm_type', 'N/A')})")
logger.info(f" Found: {display_name[:100]}...")
return (lat, lon)
except Exception as e:
logger.error(f"β Nominatim error for '{address}': {e}")
return None
def ors_matrix(api_key: str, coords_latlon: List[Tuple[float, float]]) -> Optional[Dict[str, Any]]:
# ORS expects [lon, lat]
locations = [[lon, lat] for (lat, lon) in coords_latlon]
headers = {"Authorization": api_key, "Content-Type": "application/json"}
body = {"locations": locations, "metrics": ["distance", "duration"], "units": "km"}
try:
resp = requests.post(ORS_MATRIX_URL, json=body, headers=headers, timeout=30)
if resp.status_code != 200:
logger.warning(f"ORS matrix non-200: {resp.status_code} {resp.text[:200]}")
return None
return resp.json()
except Exception as e:
logger.error(f"ORS matrix error: {e}")
return None
def nearest_neighbor_order(matrix_dist: List[List[float]], start_index: int = 0) -> List[int]:
n = len(matrix_dist)
visited = [False] * n
order = [start_index]
visited[start_index] = True
current = start_index
for _ in range(n - 1):
next_idx = None
next_cost = float("inf")
for j in range(n):
if not visited[j] and matrix_dist[current][j] is not None:
if matrix_dist[current][j] < next_cost:
next_cost = matrix_dist[current][j]
next_idx = j
if next_idx is None:
# Fallback: pick any unvisited
for j in range(n):
if not visited[j]:
next_idx = j
break
order.append(next_idx) # type: ignore
visited[next_idx] = True # type: ignore
current = next_idx # type: ignore
return order
def _route_cost(matrix_dist: List[List[float]], order: List[int]) -> float:
total = 0.0
for i in range(len(order) - 1):
total += float(matrix_dist[order[i]][order[i + 1]])
return total
def two_opt_improvement(matrix_dist: List[List[float]], order: List[int], max_iterations: int = 200) -> List[int]:
"""Simple 2-opt local search to improve a given route order.
Keeps first and last nodes fixed; improves internal sequence for lower total distance.
"""
if len(order) <= 3:
return order
best = order[:]
best_cost = _route_cost(matrix_dist, best)
n = len(order)
iterations = 0
improved = True
while improved and iterations < max_iterations:
improved = False
iterations += 1
# Do not swap the very first index to preserve start
for i in range(1, n - 2):
for k in range(i + 1, n - 1):
# Create new order by reversing the segment [i:k]
new_order = best[:i] + list(reversed(best[i:k + 1])) + best[k + 1:]
new_cost = _route_cost(matrix_dist, new_order)
if new_cost + 1e-9 < best_cost:
best = new_order
best_cost = new_cost
improved = True
# loop again if improved
return best
def _nearest_neighbor_with_start(matrix: List[List[float]], start_index: int) -> List[int]:
return nearest_neighbor_order(matrix, start_index)
def build_best_order_multistart(matrix: List[List[float]], prefer_start: int = 0) -> List[int]:
"""Always start from the preferred start point and optimize from there."""
n = len(matrix)
if n <= 1:
return list(range(n))
# For delivery routes, use simple sequential order starting from current location
# This ensures we visit all stops in the order they were added
if prefer_start == 0:
# If starting from current location (index 0), use sequential order
order = list(range(n))
logger.info(f"π Using sequential delivery order: {order}")
else:
# For other cases, use nearest neighbor with 2-opt
order = _nearest_neighbor_with_start(matrix, prefer_start)
order = two_opt_improvement(matrix, order)
logger.info(f"π Using optimized order: {order}")
return order
def get_traffic_multiplier(lat: float, lon: float, time_of_day: str = None) -> float:
"""Get traffic multiplier based on location and time."""
try:
# For now, use time-based multipliers until we integrate real traffic APIs
current_hour = datetime.now().hour
# Peak hours in India (adjust based on your city)
if 7 <= current_hour <= 10 or 17 <= current_hour <= 20: # Rush hours
base_multiplier = 2.2
elif 10 <= current_hour <= 17: # Daytime
base_multiplier = 1.8
elif 20 <= current_hour <= 23: # Evening
base_multiplier = 1.5
else: # Night/Early morning
base_multiplier = 1.2
# Location-based adjustments (you can expand this)
# Bangalore traffic is generally heavier
if 12.5 <= lat <= 13.5 and 77.0 <= lon <= 78.0: # Bangalore area
base_multiplier *= 1.1
# Mumbai traffic
elif 18.5 <= lat <= 19.5 and 72.5 <= lon <= 73.5: # Mumbai area
base_multiplier *= 1.2
# Delhi traffic
elif 28.0 <= lat <= 29.0 and 76.5 <= lon <= 77.5: # Delhi area
base_multiplier *= 1.15
logger.info(f"π¦ Traffic multiplier: {base_multiplier:.2f} (hour: {current_hour}, location: {lat:.3f}, {lon:.3f})")
return base_multiplier
except Exception as e:
logger.warning(f"β Error calculating traffic multiplier: {e}")
return 1.8 # Default fallback
def get_real_time_traffic_data(lat: float, lon: float) -> Optional[Dict[str, Any]]:
"""Get real-time traffic data from external APIs."""
try:
# MapmyIndia Traffic API (if key is available)
if MAPMYINDIA_API_KEY:
# Implementation for MapmyIndia traffic API
# This would require their specific API endpoints
pass
# Alternative traffic API (if key is available)
if TRAFFIC_API_KEY:
# Implementation for other traffic APIs
pass
# For now, return None to use time-based multipliers
return None
except Exception as e:
logger.warning(f"β Error getting real-time traffic data: {e}")
return None
def ors_directions(api_key: str, coords_latlon_ordered: List[Tuple[float, float]]) -> Optional[Dict[str, Any]]:
coordinates = [[lon, lat] for (lat, lon) in coords_latlon_ordered]
headers = {"Authorization": api_key, "Content-Type": "application/json"}
body = {"coordinates": coordinates, "units": "km"}
try:
resp = requests.post(ORS_DIRECTIONS_URL, json=body, headers=headers, timeout=45)
if resp.status_code != 200:
logger.warning(f"ORS directions non-200: {resp.status_code} {resp.text[:200]}")
return None
return resp.json()
except Exception as e:
logger.error(f"ORS directions error: {e}")
return None
# --- OCR Parsing Functions ---
def parse_ocr_original(result):
"""Your current parsing method"""
extracted_lines = []
if result:
for line in result:
if line: # Check if line is not empty
words = []
for word in line:
if len(word) >= 2 and len(word[1]) >= 1:
words.append(word[1][0])
if words:
extracted_lines.append(" ".join(words))
return "\n".join(extracted_lines)
def parse_ocr_alternative(result):
"""Alternative parsing approach"""
all_text = []
if result and isinstance(result, list):
for page_result in result:
if isinstance(page_result, list):
for detection in page_result:
if (isinstance(detection, list) and
len(detection) >= 2 and
isinstance(detection[1], list) and
len(detection[1]) >= 1):
text = detection[1][0]
confidence = detection[1][1] if len(detection[1]) >= 2 else 0
# Only include text with reasonable confidence
if isinstance(text, str) and text.strip() and confidence > 0.5:
all_text.append(text.strip())
return " ".join(all_text)
def parse_ocr_defensive(result):
"""Most defensive parsing - extract any strings found"""
def find_strings(obj, strings_found=None):
if strings_found is None:
strings_found = []
if isinstance(obj, str) and len(obj.strip()) > 0:
# Only add meaningful text (not just single chars unless it's a letter/number)
text = obj.strip()
if len(text) > 1 or text.isalnum():
strings_found.append(text)
elif isinstance(obj, (list, tuple)):
for item in obj:
find_strings(item, strings_found)
elif isinstance(obj, dict):
for value in obj.values():
find_strings(value, strings_found)
return strings_found
found_strings = find_strings(result)
return " ".join(found_strings)
# --- API Endpoints ---
@app.get("/")
def read_root():
return {"message": "Welcome to the AI Route Optimization API! It is running correctly."}
@app.get("/health")
def health_check():
"""Health check endpoint"""
return {
"status": "healthy",
"ocr_model_loaded": ocr_model is not None,
"ml_models_loaded": eta_model is not None and model_columns is not None
}
@app.post("/ocr/extract-text")
async def extract_text_from_image(image: UploadFile = File(...)):
"""Fixed OCR endpoint with correct parsing for new PaddleOCR format"""
if ocr_model is None:
return {"error": "OCR model not initialized", "extracted_text": ""}
file_extension = os.path.splitext(image.filename)[1]
temp_filename = f"{uuid.uuid4()}{file_extension}"
temp_file_path = f"./{temp_filename}"
try:
# Save uploaded file
with open(temp_file_path, "wb") as buffer:
shutil.copyfileobj(image.file, buffer)
# Run OCR
result = ocr_model.ocr(temp_file_path)
# Fixed parsing for new PaddleOCR format
all_texts = []
if result and len(result) > 0 and isinstance(result[0], dict):
# New format: extract from 'rec_texts' key
for page_result in result:
if 'rec_texts' in page_result:
texts = page_result['rec_texts']
scores = page_result.get('rec_scores', [])
for i, text in enumerate(texts):
if isinstance(text, str) and text.strip():
# Check confidence if available
confidence = scores[i] if i < len(scores) else 1.0
if confidence > 0.5: # Only include high-confidence text
all_texts.append(text.strip())
else:
# Fallback to old format parsing
full_text = parse_ocr_original(result)
return {"extracted_text": full_text}
# Join with spaces instead of newlines and clean up
full_text = " ".join(all_texts)
# Additional cleanup options:
# Remove extra whitespaces
full_text = " ".join(full_text.split())
except Exception as e:
logger.error(f"OCR error: {e}")
return {"error": str(e), "extracted_text": ""}
finally:
# Cleanup
if os.path.exists(temp_file_path):
os.remove(temp_file_path)
return {"extracted_text": full_text}
@app.post("/ocr/diagnose")
async def diagnose_ocr(image: UploadFile = File(...)):
"""Diagnostic endpoint to troubleshoot OCR issues"""
if ocr_model is None:
return {"error": "OCR model not initialized"}
file_extension = os.path.splitext(image.filename)[1].lower()
temp_filename = f"diagnose_{uuid.uuid4()}{file_extension}"
temp_file_path = f"./{temp_filename}"
try:
# Save uploaded file
with open(temp_file_path, "wb") as buffer:
shutil.copyfileobj(image.file, buffer)
diagnosis_results = {}
# Test 1: Get raw OCR result
try:
result = ocr_model.ocr(temp_file_path)
diagnosis_results["raw_ocr_result"] = {
"type": str(type(result)),
"length": len(result) if result else 0,
"structure": str(result)[:500] + "..." if result and len(str(result)) > 500 else str(result)
}
# Test different parsing methods
parsing_results = {}
try:
parsing_results["original_method"] = parse_ocr_original(result)
except Exception as e:
parsing_results["original_method"] = f"Error: {str(e)}"
try:
parsing_results["alternative_method"] = parse_ocr_alternative(result)
except Exception as e:
parsing_results["alternative_method"] = f"Error: {str(e)}"
try:
parsing_results["defensive_method"] = parse_ocr_defensive(result)
except Exception as e:
parsing_results["defensive_method"] = f"Error: {str(e)}"
diagnosis_results["parsing_methods"] = parsing_results
except Exception as e:
diagnosis_results["ocr_error"] = str(e)
# Test 2: Image info
try:
img = cv2.imread(temp_file_path)
if img is not None:
diagnosis_results["image_info"] = {
"opencv_shape": img.shape,
"opencv_dtype": str(img.dtype),
"file_size_bytes": os.path.getsize(temp_file_path),
"file_extension": file_extension
}
else:
diagnosis_results["image_info"] = "Could not read with OpenCV"
# Try PIL as well
pil_img = Image.open(temp_file_path)
diagnosis_results["pil_info"] = {
"size": pil_img.size,
"mode": pil_img.mode,
"format": pil_img.format
}
except Exception as e:
diagnosis_results["image_info"] = f"Error reading image: {str(e)}"
return diagnosis_results
finally:
# Cleanup
if os.path.exists(temp_file_path):
os.remove(temp_file_path)
@app.post("/ocr/minimal-test")
async def minimal_ocr_test(image: UploadFile = File(...)):
"""Minimal OCR test to isolate the issue"""
temp_file = f"minimal_test_{uuid.uuid4().hex[:8]}.png"
try:
with open(temp_file, "wb") as buffer:
shutil.copyfileobj(image.file, buffer)
# Create a fresh OCR instance for this test
test_ocr = PaddleOCR(use_angle_cls=True, lang='en')
raw_result = test_ocr.ocr(temp_file)
return {
"raw_result_str": str(raw_result),
"raw_result_type": str(type(raw_result)),
"raw_result_len": len(raw_result) if raw_result else 0,
"first_element": str(raw_result[0]) if raw_result and len(raw_result) > 0 else "None"
}
except Exception as e:
return {"error": str(e)}
finally:
if os.path.exists(temp_file):
os.remove(temp_file)
@app.post("/predict-eta")
def predict_eta(data: RouteData):
if eta_model is None or model_columns is None:
return {"error": "ML models not loaded"}
input_df = pd.DataFrame([data.dict()])
input_df['start_time'] = pd.to_datetime(input_df['start_time'])
hour = input_df['start_time'].dt.hour
def get_time_of_day(hour):
if 6 <= hour < 11: return 'Morning'
elif 11 <= hour < 17: return 'Afternoon'
elif 17 <= hour < 21: return 'Evening_Rush'
else: return 'Night'
input_df['time_of_day'] = hour.apply(get_time_of_day)
input_df['day_of_week'] = input_df['start_time'].dt.dayofweek.apply(lambda x: 'Weekday' if x < 5 else 'Weekend')
input_df = input_df.drop('start_time', axis=1)
input_df = pd.get_dummies(input_df)
input_df = input_df.reindex(columns=model_columns, fill_value=0)
prediction = eta_model.predict(input_df)
output = float(prediction[0])
return {"predicted_eta_minutes": round(output, 2)}
@app.post("/log-completed-route")
def log_route(route: CompletedRoute):
actual_duration = (datetime.fromisoformat(route.end_time) - datetime.fromisoformat(route.start_time)).total_seconds() / 60
try:
os.makedirs('db', exist_ok=True)
conn = sqlite3.connect('db/routes.db')
cursor = conn.cursor()
sql = """
INSERT INTO completed_routes
(start_time, end_time, ors_duration_minutes, total_distance_km, num_stops, actual_duration_minutes)
VALUES (?, ?, ?, ?, ?, ?);
"""
cursor.execute(sql, (route.start_time, route.end_time, route.ors_duration_minutes, route.total_distance_km, route.num_stops, actual_duration))
conn.commit()
conn.close()
return {"status": "success", "message": "Route logged successfully."}
except Exception as e:
return {"status": "error", "message": str(e)}
@app.get("/nearby-places")
def get_nearby_places(lat: float, lon: float, radius: int = 5000):
"""Get nearby famous places and landmarks."""
try:
# Use Overpass API (OpenStreetMap) to get nearby places
overpass_url = "http://overpass-api.de/api/interpreter"
# Query for nearby places of interest
query = f"""
[out:json][timeout:25];
(
node["amenity"~"^(restaurant|hospital|school|university|bank|fuel|parking|pharmacy|post_office|police|fire_station)$"](around:{radius},{lat},{lon});
way["amenity"~"^(restaurant|hospital|school|university|bank|fuel|parking|pharmacy|post_office|police|fire_station)$"](around:{radius},{lat},{lon});
relation["amenity"~"^(restaurant|hospital|school|university|bank|fuel|parking|pharmacy|post_office|police|fire_station)$"](around:{radius},{lat},{lon});
);
out center;
"""
response = requests.post(overpass_url, data=query, timeout=30)
if response.status_code == 200:
data = response.json()
places = []
for element in data.get("elements", []):
tags = element.get("tags", {})
name = tags.get("name", "Unknown")
amenity = tags.get("amenity", "")
# Get coordinates
if element["type"] == "node":
place_lat = element["lat"]
place_lon = element["lon"]
else:
# For ways and relations, use center
center = element.get("center", {})
place_lat = center.get("lat")
place_lon = center.get("lon")
if place_lat and place_lon and name != "Unknown":
places.append({
"name": name,
"amenity": amenity,
"lat": place_lat,
"lon": place_lon,
"display_name": f"{name} ({amenity.replace('_', ' ').title()})"
})
# Sort by distance and limit results
places = sorted(places, key=lambda x: ((x["lat"] - lat)**2 + (x["lon"] - lon)**2)**0.5)[:10]
logger.info(f"Found {len(places)} nearby places")
return {"places": places}
else:
logger.warning(f"Overpass API error: {response.status_code}")
return {"places": []}
except Exception as e:
logger.error(f"β Error getting nearby places: {e}")
return {"places": []}
@app.get("/search-suggestions")
def search_suggestions(q: str):
"""Get search suggestions using ORS geocoding API."""
if not q or len(q.strip()) < 2:
return {"suggestions": []}
suggestions = []
# Try ORS Geocoding API first
if ORS_API_KEY:
try:
url = "https://api.openrouteservice.org/geocode/search"
headers = {"Authorization": ORS_API_KEY}
params = {
"api_key": ORS_API_KEY,
"text": q.strip(),
"boundary.country": "IN", # Focus on India
"size": 5
}
resp = requests.get(url, params=params, headers=headers, timeout=10)
if resp.status_code == 200:
data = resp.json()
if data.get("features"):
for feature in data["features"]:
props = feature.get("properties", {})
coords = feature.get("geometry", {}).get("coordinates", [0, 0])
suggestions.append({
"display_name": props.get("label", props.get("name", "Unknown location")),
"lat": coords[1],
"lon": coords[0]
})
logger.info(f"β
ORS found {len(suggestions)} suggestions for '{q}'")
return {"suggestions": suggestions}
except Exception as e:
logger.warning(f"β ORS search error for '{q}': {e}")
# Fallback to Nominatim
try:
headers = {"User-Agent": "delivery-route-app/1.0 (contact: dev@example.com)"}
params = {
"q": q.strip(),
"format": "json",
"addressdetails": "1",
"limit": 5
}
resp = requests.get(NOMINATIM_URL, params=params, headers=headers, timeout=10)
if resp.status_code == 200:
data = resp.json()
for item in data:
suggestions.append({
"display_name": item.get("display_name", "Unknown location"),
"lat": float(item.get("lat", 0)),
"lon": float(item.get("lon", 0))
})
logger.info(f"β
Nominatim found {len(suggestions)} suggestions for '{q}'")
except Exception as e:
logger.error(f"β Nominatim search error for '{q}': {e}")
return {"suggestions": suggestions}
@app.post("/plan-full-route", response_model=PlannedRouteResponse)
def plan_full_route(req: PlanRouteRequest):
# 1) Geocode all addresses
addresses = req.addresses
if not addresses or len(addresses) < 1:
return {
"ordered_addresses": [],
"ordered_coordinates": [],
"ors_duration_minutes": 0.0,
"total_distance_km": 0.0,
"num_stops": 0,
"predicted_eta_minutes": None,
"route_geometry_geojson": None
}
coords: List[Tuple[float, float]] = []
for addr in addresses:
c = geocode_address(addr)
if c is None:
return {
"ordered_addresses": [],
"ordered_coordinates": [],
"ors_duration_minutes": 0.0,
"total_distance_km": 0.0,
"num_stops": 0,
"predicted_eta_minutes": None,
"route_geometry_geojson": None
}
coords.append(c)
# Find the start index (current location should be first)
start_index = 0
if req.vehicle_start_address and req.vehicle_start_address in addresses:
start_index = addresses.index(req.vehicle_start_address)
logger.info(f"Found vehicle start address at index {start_index}")
else:
logger.info("Using first address as start point")
# 2) Build ordering using ORS matrix (nearest neighbor heuristic)
# Prefer in-code key; fallback to environment
ors_key = ORS_API_KEY or os.environ.get("ORS_API_KEY", "")
if not ors_key:
logger.warning("ORS_API_KEY not set; route planning will fail")
return {
"ordered_addresses": [],
"ordered_coordinates": [],
"ors_duration_minutes": 0.0,
"total_distance_km": 0.0,
"num_stops": 0,
"predicted_eta_minutes": None,
"route_geometry_geojson": None
}
matrix = ors_matrix(ors_key, coords)
if matrix is None or "distances" not in matrix:
return {
"ordered_addresses": [],
"ordered_coordinates": [],
"ors_duration_minutes": 0.0,
"total_distance_km": 0.0,
"num_stops": 0,
"predicted_eta_minutes": None,
"route_geometry_geojson": None
}
# For delivery routes, use original order (no optimization)
# This ensures we visit stops in the order they were added
if start_index == 0:
# Sequential delivery order: Current β Stop 1 β Stop 2 β Stop 3
order_idx = list(range(len(addresses)))
ordered_addresses = addresses.copy()
ordered_coords = coords.copy()
logger.info(f"π Using sequential delivery order (no optimization):")
logger.info(f" Order: {order_idx}")
logger.info(f" Addresses: {ordered_addresses}")
logger.info(f" Coordinates: {ordered_coords}")
else:
# Use matrix-based optimization for other cases
matrix_dist = matrix.get("durations") or matrix.get("distances")
order_idx = build_best_order_multistart(matrix_dist, start_index)
ordered_addresses = [addresses[i] for i in order_idx]
ordered_coords = [coords[i] for i in order_idx]
logger.info(f"π Using matrix-based optimization:")
logger.info(f" Original order: {list(range(len(addresses)))}")
logger.info(f" Optimized order: {order_idx}")
logger.info(f" Addresses: {ordered_addresses}")
logger.info(f" Coordinates: {ordered_coords}")
# 3) Calculate proper multi-stop route duration
num_stops = len(ordered_addresses)
total_distance_km = 0.0
ors_duration_minutes = 0.0
route_geojson = None
# Calculate total duration by summing individual segments
logger.info(f"π Calculating linear delivery route:")
logger.info(f" Total stops: {num_stops}")
logger.info(f" Route: Current β {ordered_addresses[1:] if len(ordered_addresses) > 1 else 'No stops'}")
logger.info(f" Final destination: {ordered_addresses[-1] if ordered_addresses else 'None'}")
if num_stops >= 2:
# Calculate duration for each segment in the optimized route
total_segment_duration = 0.0
total_segment_distance = 0.0
for i in range(num_stops - 1):
start_coord = ordered_coords[i]
end_coord = ordered_coords[i + 1]
# Get directions for this segment
segment_directions = ors_directions(ors_key, [start_coord, end_coord])
if segment_directions and "features" in segment_directions and len(segment_directions["features"]) > 0:
feat = segment_directions["features"][0]
summary = feat.get("properties", {}).get("summary", {})
segment_distance = float(summary.get("distance", 0.0))
segment_duration = float(summary.get("duration", 0.0)) / 60.0 # Convert to minutes
# Apply more accurate traffic multiplier based on location and time
# Get the midpoint of the segment for traffic calculation
mid_lat = (start_coord[0] + end_coord[0]) / 2
mid_lon = (start_coord[1] + end_coord[1]) / 2
# Try to get real-time traffic data first
real_time_traffic = get_real_time_traffic_data(mid_lat, mid_lon)
if real_time_traffic:
# Use real-time traffic data if available
traffic_multiplier = real_time_traffic.get('multiplier', 1.8)
logger.info(f" Using real-time traffic multiplier: {traffic_multiplier:.2f}")
else:
# Use improved traffic calculation
traffic_multiplier = get_traffic_multiplier(mid_lat, mid_lon)
# Additional adjustment based on distance (longer routes have more variability)
if segment_distance > 10: # Long distance
traffic_multiplier *= 1.1
elif segment_distance < 2: # Short distance
traffic_multiplier *= 0.9
# Apply traffic multiplier with some smoothing
segment_duration *= traffic_multiplier
# Add small buffer for real-world conditions (parking, traffic lights, etc.)
segment_duration += 1.0 # 1 minute buffer per segment
total_segment_duration += segment_duration
total_segment_distance += segment_distance
raw_duration = segment_duration / traffic_multiplier
logger.info(f" Segment {i+1}: {ordered_addresses[i]} β {ordered_addresses[i+1]}")
logger.info(f" Coordinates: {start_coord} β {end_coord}")
logger.info(f" Distance: {segment_distance:.2f} km")
logger.info(f" Raw ORS duration: {raw_duration:.2f} min")
logger.info(f" Adjusted duration: {segment_duration:.2f} min (Γ{traffic_multiplier})")
# Debug: Also test the reverse direction to see if there's a difference
reverse_directions = ors_directions(ors_key, [end_coord, start_coord])
if reverse_directions and "features" in reverse_directions and len(reverse_directions["features"]) > 0:
reverse_feat = reverse_directions["features"][0]
reverse_summary = reverse_feat.get("properties", {}).get("summary", {})
reverse_duration = float(reverse_summary.get("duration", 0.0)) / 60.0
logger.info(f" Reverse duration: {reverse_duration:.2f} min (difference: {abs(segment_duration - reverse_duration):.2f} min)")
else:
logger.warning(f" Failed to get directions for segment {i+1}")
# Add delivery time at each stop (except the last one)
delivery_time_per_stop = 3.0 # 3 minutes per stop for delivery (more realistic)
total_delivery_time = (num_stops - 1) * delivery_time_per_stop
# This is a linear delivery route (not round trip)
# Route: Current β Stop A β Stop B β Stop C (final destination)
# No return journey calculation needed
ors_duration_minutes = total_segment_duration + total_delivery_time
logger.info(f" Total driving time: {total_segment_duration:.2f} min")
logger.info(f" Total delivery time: {total_delivery_time:.2f} min")
logger.info(f" Total route time: {ors_duration_minutes:.2f} min")
logger.info(f" Total distance: {total_distance_km:.2f} km")
# Debug: Compare with Google Maps expectations
logger.info(f"π Route Analysis:")
logger.info(f" Expected Google Maps: CurrentβGAT (8min) + GATβBMS (24min) = 32min")
logger.info(f" Our calculation: {ors_duration_minutes:.2f} min")
logger.info(f" Difference: {ors_duration_minutes - 32:.2f} min")
# Get the full route geometry for display (from start to end)
directions = ors_directions(ors_key, ordered_coords)
if directions and "features" in directions and len(directions["features"]) > 0:
route_geojson = directions["features"][0]
else:
logger.warning("Need at least 2 stops for route calculation")
# 4) Predict ETA using our ML model (if loaded)
predicted_eta = None
try:
use_start_time = req.start_time or datetime.now().isoformat()
if eta_model is not None and model_columns is not None:
input_df = pd.DataFrame([{
"ors_duration_minutes": ors_duration_minutes,
"total_distance_km": total_distance_km,
"num_stops": num_stops,
"start_time": use_start_time,
}])
input_df['start_time'] = pd.to_datetime(input_df['start_time'])
hour = input_df['start_time'].dt.hour
def get_time_of_day(hour):
if 6 <= hour < 11: return 'Morning'
elif 11 <= hour < 17: return 'Afternoon'
elif 17 <= hour < 21: return 'Evening_Rush'
else: return 'Night'
input_df['time_of_day'] = hour.apply(get_time_of_day)
input_df['day_of_week'] = input_df['start_time'].dt.dayofweek.apply(lambda x: 'Weekday' if x < 5 else 'Weekend')
input_df = input_df.drop('start_time', axis=1)
input_df = pd.get_dummies(input_df)
input_df = input_df.reindex(columns=model_columns, fill_value=0)
pred = eta_model.predict(input_df)
predicted_eta = float(pred[0])
# Check if ML prediction is reasonable (not more than 2x ORS duration)
if predicted_eta > ors_duration_minutes * 2:
logger.warning(f"ML prediction seems too high: {predicted_eta:.1f} min vs ORS: {ors_duration_minutes:.1f} min")
# Use ORS duration with a small buffer for traffic
predicted_eta = ors_duration_minutes * 1.2 # 20% buffer for traffic
logger.info(f"Using ORS-based prediction: {predicted_eta:.1f} min")
else:
logger.info(f"ML prediction: {predicted_eta:.1f} min, ORS duration: {ors_duration_minutes:.1f} min")
else:
# Fallback to ORS duration with traffic buffer
predicted_eta = ors_duration_minutes * 1.2 # 20% buffer for traffic
logger.info(f"Using ORS-based fallback prediction: {predicted_eta:.1f} min")
except Exception as e:
logger.warning(f"ETA prediction failed: {e}")
# Fallback to ORS duration with traffic buffer
predicted_eta = ors_duration_minutes * 1.2 # 20% buffer for traffic
logger.info(f"Using ORS-based fallback after error: {predicted_eta:.1f} min")
return {
"ordered_addresses": ordered_addresses,
"ordered_coordinates": ordered_coords,
"ors_duration_minutes": round(ors_duration_minutes, 2),
"total_distance_km": round(total_distance_km, 3),
"num_stops": num_stops,
"predicted_eta_minutes": round(predicted_eta, 2) if predicted_eta is not None else None,
"route_geometry_geojson": route_geojson,
}
# Run the app
@app.post("/submit-training-data")
def submit_training_data(data: TrainingDataRequest):
"""Submit training data for model improvement."""
try:
os.makedirs('db', exist_ok=True)
conn = sqlite3.connect('db/training_data.db')
cursor = conn.cursor()
cursor.execute('''
INSERT OR REPLACE INTO training_data
(route_id, addresses, coordinates, predicted_eta_minutes, actual_eta_minutes,
start_time, end_time, sensor_data, user_id, route_metadata)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
data.route_id,
json.dumps(data.addresses),
json.dumps(data.coordinates),
data.predicted_eta_minutes,
data.actual_eta_minutes,
data.start_time,
data.end_time,
json.dumps(data.sensor_data),
data.user_id,
json.dumps(data.route_metadata)
))
conn.commit()
conn.close()
logger.info(f"β
Training data submitted for route {data.route_id}")
return {"status": "success", "message": "Training data submitted successfully"}
except Exception as e:
logger.error(f"β Error submitting training data: {e}")
return {"status": "error", "message": str(e)}
@app.patch("/update-actual-eta")
def update_actual_eta(data: UpdateEtaRequest):
"""Update actual ETA for a completed route."""
try:
os.makedirs('db', exist_ok=True)
conn = sqlite3.connect('db/training_data.db')
cursor = conn.cursor()
cursor.execute('''
UPDATE training_data
SET actual_eta_minutes = ?, end_time = ?
WHERE route_id = ?
''', (data.actual_eta_minutes, datetime.now().isoformat(), data.route_id))
if cursor.rowcount == 0:
conn.close()
return {"status": "error", "message": "Route not found"}
conn.commit()
conn.close()
logger.info(f"β
Actual ETA updated for route {data.route_id}: {data.actual_eta_minutes} minutes")
return {"status": "success", "message": "Actual ETA updated successfully"}
except Exception as e:
logger.error(f"β Error updating actual ETA: {e}")
return {"status": "error", "message": str(e)}
@app.get("/training-data-stats")
def get_training_data_stats():
"""Get statistics about collected training data."""
try:
os.makedirs('db', exist_ok=True)
conn = sqlite3.connect('db/training_data.db')
cursor = conn.cursor()
# Get total routes
cursor.execute("SELECT COUNT(*) FROM training_data")
total_routes = cursor.fetchone()[0]
# Get routes with actual ETA
cursor.execute("SELECT COUNT(*) FROM training_data WHERE actual_eta_minutes IS NOT NULL")
completed_routes = cursor.fetchone()[0]
# Get average prediction accuracy
cursor.execute('''
SELECT AVG(ABS(predicted_eta_minutes - actual_eta_minutes))
FROM training_data
WHERE actual_eta_minutes IS NOT NULL
''')
avg_error = cursor.fetchone()[0]
conn.close()
return {
"total_routes": total_routes,
"completed_routes": completed_routes,
"average_prediction_error_minutes": round(avg_error, 2) if avg_error else None
}
except Exception as e:
logger.error(f"β Error getting training data stats: {e}")
return {"status": "error", "message": str(e)}
@app.get("/test-segment")
def test_segment(from_addr: str, to_addr: str):
"""Test a single segment to verify ORS calculation."""
try:
logger.info(f"π Testing segment: {from_addr} β {to_addr}")
# Geocode both addresses
from_coord = geocode_address(from_addr.strip())
to_coord = geocode_address(to_addr.strip())
if not from_coord or not to_coord:
return {"error": "Failed to geocode addresses"}
logger.info(f" From: {from_coord}")
logger.info(f" To: {to_coord}")
# Get ORS directions
ors_key = ORS_API_KEY or os.environ.get("ORS_API_KEY", "")
if not ors_key:
return {"error": "ORS API key not set"}
directions = ors_directions(ors_key, [from_coord, to_coord])
if not directions or "features" not in directions or len(directions["features"]) == 0:
return {"error": "Failed to get ORS directions"}
feat = directions["features"][0]
summary = feat.get("properties", {}).get("summary", {})
distance = float(summary.get("distance", 0.0))
duration = float(summary.get("duration", 0.0)) / 60.0
logger.info(f" ORS Result: {distance:.2f} km, {duration:.2f} min")
return {
"from": from_addr,
"to": to_addr,
"from_coordinates": from_coord,
"to_coordinates": to_coord,
"distance_km": round(distance, 2),
"duration_minutes": round(duration, 2),
"google_maps_estimate": "Check manually in Google Maps"
}
except Exception as e:
logger.error(f"β Test segment error: {e}")
return {"error": str(e)}
@app.get("/traffic-config")
def get_traffic_config():
"""Get current traffic configuration and multipliers."""
current_hour = datetime.now().hour
# Determine current time period
if 7 <= current_hour <= 10 or 17 <= current_hour <= 20:
time_period = "Rush Hours"
base_multiplier = 2.2
elif 10 <= current_hour <= 17:
time_period = "Daytime"
base_multiplier = 1.8
elif 20 <= current_hour <= 23:
time_period = "Evening"
base_multiplier = 1.5
else:
time_period = "Night/Early Morning"
base_multiplier = 1.2
return {
"current_time": datetime.now().isoformat(),
"current_hour": current_hour,
"time_period": time_period,
"base_multiplier": base_multiplier,
"location_adjustments": {
"bangalore": 1.1,
"mumbai": 1.2,
"delhi": 1.15
},
"real_time_traffic_available": bool(MAPMYINDIA_API_KEY or TRAFFIC_API_KEY),
"apis_configured": {
"mapmyindia": bool(MAPMYINDIA_API_KEY),
"traffic_api": bool(TRAFFIC_API_KEY)
}
}
# -------------------- Auth Endpoints --------------------
@app.post("/auth/register")
def register(req: RegisterRequest):
os.makedirs('db', exist_ok=True)
conn = sqlite3.connect('db/auth.db')
cursor = conn.cursor()
try:
email = req.email.lower()
# If already verified user exists, do not error, just respond idempotently
cursor.execute("SELECT id FROM users WHERE email = ? AND is_verified = 1", (email,))
row = cursor.fetchone()
if row:
return {"message": "User already exists and is verified"}
# If pending exists, update OTP and password_hash; else insert new pending
otp = generate_otp()
expires = (datetime.utcnow()).isoformat()
cursor.execute("SELECT id FROM pending_users WHERE email = ?", (email,))
pending = cursor.fetchone()
if pending:
cursor.execute(
"UPDATE pending_users SET password_hash = ?, name = ?, otp_code = ?, otp_expires_at = ? WHERE email = ?",
(hash_password(req.password), req.name, otp, expires, email)
)
else:
cursor.execute(
"INSERT INTO pending_users (email, password_hash, name, otp_code, otp_expires_at) VALUES (?, ?, ?, ?, ?)",
(email, hash_password(req.password), req.name, otp, expires)
)
conn.commit()
email_sent = send_verification_email(email, otp)
if email_sent:
return {"message": "Verification OTP sent to email"}
else:
return {"message": "Verification OTP sent to email (check console for OTP)"}
finally:
conn.close()
@app.post("/auth/verify-email")
def verify_email(req: VerifyEmailRequest):
os.makedirs('db', exist_ok=True)
conn = sqlite3.connect('db/auth.db')
cursor = conn.cursor()
try:
email = req.email.lower()
# First, try pending_users
cursor.execute("SELECT id, password_hash, name, otp_code FROM pending_users WHERE email = ?", (email,))
prow = cursor.fetchone()
if prow:
pid, pwd_hash, name, otp_code = prow
if otp_code != req.otp:
raise HTTPException(status_code=400, detail="Invalid OTP")
# Move to users as verified
try:
cursor.execute(
"INSERT INTO users (email, password_hash, name, is_verified) VALUES (?, ?, ?, 1)",
(email, pwd_hash, name)
)
cursor.execute("DELETE FROM pending_users WHERE id = ?", (pid,))
conn.commit()
return {"message": "Email verified"}
except sqlite3.IntegrityError:
# If user already exists (rare race), just mark verified
cursor.execute("UPDATE users SET is_verified = 1 WHERE email = ?", (email,))
cursor.execute("DELETE FROM pending_users WHERE id = ?", (pid,))
conn.commit()
return {"message": "Email verified"}
# If not in pending, check if already verified user exists
cursor.execute("SELECT id FROM users WHERE email = ?", (email,))
urow = cursor.fetchone()
if urow:
return {"message": "Email already verified"}
raise HTTPException(status_code=404, detail="User not found")
finally:
conn.close()
@app.post("/auth/login")
def login(req: LoginRequest):
os.makedirs('db', exist_ok=True)
conn = sqlite3.connect('db/auth.db')
cursor = conn.cursor()
try:
cursor.execute("SELECT id, password_hash, is_verified, name FROM users WHERE email = ?", (req.email.lower(),))
row = cursor.fetchone()
if not row:
raise HTTPException(status_code=401, detail="Invalid credentials")
uid, pwd_hash, is_verified, name = row
if hash_password(req.password) != pwd_hash:
raise HTTPException(status_code=401, detail="Invalid credentials")
if not is_verified:
# Do not reveal existence; treat as invalid credentials
raise HTTPException(status_code=401, detail="Invalid credentials")
# Simple session placeholder: return user info
return {"user_id": uid, "email": req.email, "name": name}
finally:
conn.close()
@app.get("/auth/dev-otp")
def get_dev_otp(email: str):
"""Dev helper: returns the current OTP for an email for local testing.
Requires environment variable DEV_MODE=1 to be set.
"""
if os.environ.get('DEV_MODE') != '1':
raise HTTPException(status_code=404, detail="Not found")
em = email.lower()
os.makedirs('db', exist_ok=True)
conn = sqlite3.connect('db/auth.db')
cursor = conn.cursor()
try:
cursor.execute("SELECT otp_code FROM pending_users WHERE email = ?", (em,))
row = cursor.fetchone()
if row and row[0]:
return {"email": em, "otp": row[0]}
# Legacy/pathological state: user row with stored OTP
cursor.execute("SELECT otp_code FROM users WHERE email = ? AND (is_verified = 0 OR otp_code IS NOT NULL)", (em,))
row = cursor.fetchone()
if row and row[0]:
return {"email": em, "otp": row[0]}
raise HTTPException(status_code=404, detail="No OTP found")
finally:
conn.close()
@app.post("/auth/reset-users")
def reset_users():
"""Debug-only: clears users and pending_users tables."""
os.makedirs('db', exist_ok=True)
conn = sqlite3.connect('db/auth.db')
cursor = conn.cursor()
try:
cursor.execute("DELETE FROM users")
cursor.execute("DELETE FROM pending_users")
conn.commit()
return {"message": "All users cleared"}
finally:
conn.close()
@app.get("/debug-route")
def debug_route(addresses: str):
"""Debug endpoint to check route calculation details."""
try:
address_list = addresses.split(',')
logger.info(f"π Debug route for: {address_list}")
# Geocode addresses
coords = []
for addr in address_list:
coord = geocode_address(addr.strip())
if coord:
coords.append(coord)
logger.info(f" {addr.strip()} -> {coord}")
else:
logger.warning(f" Failed to geocode: {addr.strip()}")
if len(coords) < 2:
return {"error": "Need at least 2 valid addresses"}
# Get ORS matrix
ors_key = ORS_API_KEY or os.environ.get("ORS_API_KEY", "")
if not ors_key:
return {"error": "ORS API key not set"}
matrix = ors_matrix(ors_key, coords)
if not matrix:
return {"error": "Failed to get ORS matrix"}
# Test individual segments
segment_results = []
for i in range(len(coords) - 1):
start_coord = coords[i]
end_coord = coords[i + 1]
segment_directions = ors_directions(ors_key, [start_coord, end_coord])
if segment_directions and "features" in segment_directions and len(segment_directions["features"]) > 0:
feat = segment_directions["features"][0]
summary = feat.get("properties", {}).get("summary", {})
segment_distance = float(summary.get("distance", 0.0))
segment_duration = float(summary.get("duration", 0.0)) / 60.0
segment_results.append({
"from": address_list[i],
"to": address_list[i + 1],
"distance_km": round(segment_distance, 2),
"duration_minutes": round(segment_duration, 2)
})
# Get full route directions
directions = ors_directions(ors_key, coords)
ors_duration = 0.0
total_distance = 0.0
if directions and "features" in directions:
feat = directions["features"][0]
summary = feat.get("properties", {}).get("summary", {})
total_distance = float(summary.get("distance", 0.0))
ors_duration = float(summary.get("duration", 0.0)) / 60.0
return {
"addresses": address_list,
"coordinates": coords,
"individual_segments": segment_results,
"full_route_duration_minutes": round(ors_duration, 2),
"full_route_distance_km": round(total_distance, 3),
"matrix_available": "durations" in matrix or "distances" in matrix,
"google_maps_estimate": "Check manually in Google Maps"
}
except Exception as e:
logger.error(f"β Debug route error: {e}")
return {"error": str(e)}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000) |