import pandas as pd from itertools import permutations, combinations from fastapi import FastAPI, File, UploadFile, Form, HTTPException, Request, Response from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel from typing import List, Dict, Any import uuid import requests import json app = FastAPI() stored_combinations = {} processed_data_store: Dict[str, Dict[str, Any]] = {} # Store processed data with session_id app.add_middleware( CORSMiddleware, allow_origins=["*"], # Adjust as needed for security allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) @app.options("/{rest_of_path:path}") async def preflight_handler(request: Request, rest_of_path: str) -> Response: return Response(headers={ "Access-Control-Allow-Origin": "*", # Adjust as needed for security "Access-Control-Allow-Methods": "POST, GET, OPTIONS, PUT, DELETE", "Access-Control-Allow-Headers": "*", }) # Box and Truck classes class Box: def __init__(self, length, width, height, quantity, box_type): self.length = length self.width = width self.height = height self.quantity = quantity if quantity > 0 else 1 self.type = box_type def rotated_dimensions(self): possible_rotations = [ (self.length, self.width, self.height), (self.length, self.height, self.width), (self.width, self.length, self.height), (self.width, self.height, self.length), (self.height, self.length, self.width), (self.height, self.width, self.length) ] return sorted(possible_rotations, key=lambda x: (x[0] * x[1], x[2]), reverse=True) def volume(self): return self.length * self.width * self.height class Truck: def __init__(self, length, width, height): self.length = length self.width = width self.height = height self.volume = length * width * height self.placed_boxes = [] self.available_space = [(0, 0, 0, length, width, height)] def is_space_supported(self, position, box): x, y, z = position bl, bw, bh = box if z == 0: return True for placed_box, placed_position in self.placed_boxes: px, py, pz = placed_position pl, pw, ph = placed_box if (x >= px and x + bl <= px + pl) and (y >= py and y + bw <= py + pw) and (z == pz + ph): return True return False def add_box_ffd(self, box): best_fit = None best_fit_space_index = -1 best_fit_volume_waste = float('inf') for rotation in box.rotated_dimensions(): for i, space in enumerate(self.available_space): x, y, z, l, w, h = space if rotation[0] <= l and rotation[1] <= w and rotation[2] <= h: if not self.is_space_supported((x, y, z), rotation): continue leftover_volume = (l * w * h) - (rotation[0] * rotation[1] * rotation[2]) if leftover_volume < best_fit_volume_waste: best_fit = rotation best_fit_space_index = i best_fit_volume_waste = leftover_volume if best_fit: x, y, z, l, w, h = self.available_space[best_fit_space_index] box_position = (x, y, z) self.placed_boxes.append((best_fit, box_position)) self.available_space.pop(best_fit_space_index) self.update_space(best_fit, box_position, l, w, h) return box_position else: return None def update_space(self, box, position, l, w, h): x, y, z = position bl, bw, bh = box new_spaces = [ (x + bl, y, z, l - bl, w, h), (x, y + bw, z, bl, w - bw, h), (x, y, z + bh, bl, bw, h - bh), ] new_spaces = [space for space in new_spaces if space[3] > 0 and space[4] > 0 and space[5] > 0] self.available_space.extend(new_spaces) self.available_space.sort(key=lambda space: (space[2], space[1], space[0])) # Helper functions for box packing def pack_boxes_ffd(truck, consignments): packed_positions = [] consignments = sorted(consignments, key=lambda c: (not c[1], -sum(box.volume() * box.quantity for box in c[0]))) for consignment in consignments: consignment_packed = [] for box in consignment[0]: for _ in range(box.quantity): position = truck.add_box_ffd(box) if position is None: break consignment_packed.append((box, position)) if len(consignment_packed) == sum(box.quantity for box in consignment[0]): packed_positions.extend(consignment_packed) else: break return packed_positions def find_max_consignments_combinations(truck, consignments, combination_limit=100): combinations_that_fit = [] consignments_sorted = sorted(consignments, key=lambda c: (not c[1], -sum(box.volume() * box.quantity for box in c[0]))) combo_count = 0 max_consignments_fitted = 0 for r in range(1, len(consignments_sorted) + 1): for combo in combinations(consignments_sorted, r): combo_count += 1 print(f"Checking combination {combo_count}: {[f'Consignment {boxes[0].type}' for boxes, _ in combo]}") if combo_count > combination_limit: print(f"Reached combination limit of {combination_limit}. Stopping further checks.") break temp_truck = Truck(truck.length, truck.width, truck.height) total_volume = sum(sum(box.volume() * box.quantity for box in consignment[0]) for consignment in combo) if total_volume > temp_truck.volume: continue try: packed_positions = pack_boxes_ffd(temp_truck, combo) if packed_positions: print(f"Combination {combo_count} fits with {len(combo)} consignments") combinations_that_fit.append(combo) max_consignments_fitted = max(max_consignments_fitted, len(combo)) if len(combinations_that_fit) >= 5: break except HTTPException: print(f"Combination {combo_count} does not fit") continue if combo_count > combination_limit: break return combinations_that_fit[-5:] def suggest_truck(consignments): trucks = { "TATA ACE": {"length": 7, "width": 4.8, "height": 4.8}, "ASHOK LEYLAND DOST": {"length": 7, "width": 4.8, "height": 4.8}, "MAHINDRA BOLERO PICK UP": {"length": 8, "width": 5, "height": 4.8}, "ASHOK LEYLAND BADA DOST": {"length": 9.5, "width": 5.5, "height": 5}, "TATA 407": {"length": 9, "width": 5.5, "height": 5}, "EICHER 14 FEET": {"length": 14, "width": 6, "height": 6.5}, "EICHER 17 FEET": {"length": 17, "width": 6, "height": 7}, "EICHER 19 FEET": {"length": 19, "width": 7, "height": 7}, "TATA 22 FEET": {"length": 22, "width": 7.5, "height": 7}, "TATA TRUCK (6 TYRE)": {"length": 17.5, "width": 7, "height": 7}, "TAURUS 16 T (10 TYRE)": {"length": 21, "width": 7.2, "height": 7}, "TAURUS 21 T (12 TYRE)": {"length": 24, "width": 7.3, "height": 7}, "TAURUS 25 T (14 TYRE)": {"length": 28, "width": 7.8, "height": 7}, "CONTAINER 20 FT": {"length": 20, "width": 8, "height": 8}, "CONTAINER 32 FT SXL": {"length": 32, "width": 8, "height": 8}, "CONTAINER 32 FT MXL": {"length": 32, "width": 8, "height": 8}, "CONTAINER 32 FT SXL / MXL HQ": {"length": 32, "width": 8, "height": 10}, "20 FEET OPEN ALL SIDE (ODC)": {"length": 20, "width": 8, "height": 8}, "28-32 FEET OPEN-TRAILOR JCB ODC": {"length": 28, "width": 8, "height": 8}, "32 FEET OPEN-TRAILOR ODC": {"length": 32, "width": 8, "height": 8}, "40 FEET OPEN-TRAILOR ODC": {"length": 40, "width": 8, "height": 8}, "SCV": {"length": 5, "width": 12, "height": 6}, "LCV": {"length": 11, "width": 5, "height": 5}, "ICV": {"length": 16, "width": 6.5, "height": 6.5}, "MCV": {"length": 19, "width": 7, "height": 7} } sorted_trucks = sorted(trucks.items(), key=lambda t: t[1]['length'] * t[1]['width'] * t[1]['height']) for truck_name, dimensions in sorted_trucks: truck = Truck(dimensions['length'] * 12, dimensions['width'] * 12, dimensions['height'] * 12) packed_positions = pack_boxes_ffd(truck, consignments) total_boxes = sum(box.quantity for consignment in consignments for box in consignment[0]) if len(packed_positions) == total_boxes: return {"name": truck_name, "dimensions": dimensions} return None # Models class BoxData(BaseModel): PieceLength: float PieceBreadth: float PieceHeight: float Priority: int class ConsignmentData(BaseModel): ConsignmentNo: str Priority: int boxes: List[BoxData] class SubmitDataRequest(BaseModel): truck_name: str autoSuggest: bool consignments_data: List[ConsignmentData] from fastapi.responses import RedirectResponse @app.post("/submit_data/") async def submit_data(request: SubmitDataRequest): """ Endpoint to receive data from the client via API. """ truck_name = request.truck_name autoSuggest = request.autoSuggest consignments_data = request.consignments_data # This is already parsed from JSON global stored_combinations, processed_data_store # Parse consignments and calculate total quantity based on box dimensions consignments = [] for consignment_data in consignments_data: consignment_no = consignment_data.ConsignmentNo # ConsignmentNo is now accessible as a string is_priority = consignment_data.Priority # Aggregate the boxes with same dimensions box_aggregation = {} for box_data in consignment_data.boxes: dimensions = (box_data.PieceLength, box_data.PieceBreadth, box_data.PieceHeight) if dimensions in box_aggregation: box_aggregation[dimensions]['Quantity'] += 1 # Increment the count if dimensions are the same else: box_aggregation[dimensions] = { 'PieceLength': box_data.PieceLength, 'PieceBreadth': box_data.PieceBreadth, 'PieceHeight': box_data.PieceHeight, 'Quantity': 1 # Initialize count to 1 } # Convert aggregated boxes back to the expected list format consignment_boxes = [ Box( box['PieceLength'] / 2.54, # Convert cm to inches box['PieceBreadth'] / 2.54, box['PieceHeight'] / 2.54, box['Quantity'], f"Consignment {consignment_no} ({'Priority' if is_priority else 'Non-Priority'})" ) for box in box_aggregation.values() ] consignments.append((consignment_boxes, is_priority)) # Handle autoSuggest logic and other functionality here if autoSuggest: suggested_truck = suggest_truck(consignments) if suggested_truck: truck_name = suggested_truck["name"] length = suggested_truck['dimensions']['length'] width = suggested_truck['dimensions']['width'] height = suggested_truck['dimensions']['height'] else: raise HTTPException(status_code=400, detail="No suitable truck found") else: # Assuming truck dimensions are known based on truck_name trucks = { "TATA ACE": {"length": 7, "width": 4.8, "height": 4.8}, "ASHOK LEYLAND DOST": {"length": 7, "width": 4.8, "height": 4.8}, "MAHINDRA BOLERO PICK UP": {"length": 8, "width": 5, "height": 4.8}, "ASHOK LEYLAND BADA DOST": {"length": 9.5, "width": 5.5, "height": 5}, "TATA 407": {"length": 9, "width": 5.5, "height": 5}, "EICHER 14 FEET": {"length": 14, "width": 6, "height": 6.5}, "EICHER 17 FEET": {"length": 17, "width": 6, "height": 7}, "EICHER 19 FEET": {"length": 19, "width": 7, "height": 7}, "TATA 22 FEET": {"length": 22, "width": 7.5, "height": 7}, "TATA TRUCK (6 TYRE)": {"length": 17.5, "width": 7, "height": 7}, "TAURUS 16 T (10 TYRE)": {"length": 21, "width": 7.2, "height": 7}, "TAURUS 21 T (12 TYRE)": {"length": 24, "width": 7.3, "height": 7}, "TAURUS 25 T (14 TYRE)": {"length": 28, "width": 7.8, "height": 7}, "CONTAINER 20 FT": {"length": 20, "width": 8, "height": 8}, "CONTAINER 32 FT SXL": {"length": 32, "width": 8, "height": 8}, "CONTAINER 32 FT MXL": {"length": 32, "width": 8, "height": 8}, "CONTAINER 32 FT SXL / MXL HQ": {"length": 32, "width": 8, "height": 10}, "20 FEET OPEN ALL SIDE (ODC)": {"length": 20, "width": 8, "height": 8}, "28-32 FEET OPEN-TRAILOR JCB ODC": {"length": 28, "width": 8, "height": 8}, "32 FEET OPEN-TRAILOR ODC": {"length": 32, "width": 8, "height": 8}, "40 FEET OPEN-TRAILOR ODC": {"length": 40, "width": 8, "height": 8}, "SCV": {"length": 5, "width": 12, "height": 6}, "LCV": {"length": 11, "width": 5, "height": 5}, "ICV": {"length": 16, "width": 6.5, "height": 6.5}, "MCV": {"length": 19, "width": 7, "height": 7} } if truck_name not in trucks: raise HTTPException(status_code=400, detail="Invalid truck name provided") truck_dimensions = trucks[truck_name] length = truck_dimensions['length'] width = truck_dimensions['width'] height = truck_dimensions['height'] # Initialize the Truck object and pack boxes truck = Truck(length * 12, width * 12, height * 12) # Convert feet to inches packed_positions = pack_boxes_ffd(truck, consignments) total_boxes = sum(box.quantity for consignment in consignments for box in consignment[0]) if len(packed_positions) < total_boxes: stored_combinations = find_max_consignments_combinations(truck, consignments) if stored_combinations: combinations_info = [] for index, combo in enumerate(stored_combinations): total_quantity = sum(box.quantity for boxes, _ in combo for box in boxes) combination_info = { "combination_number": index + 1, "consignments": [ { "consignment_number": f"{boxes[0].type.split(' ')[1]}", "priority_status": boxes[0].type.split('(')[1].strip(')') } for boxes, _ in combo ], "total_quantity": total_quantity # Add total quantity for this combination } combinations_info.append(combination_info) # Generate a unique session_id session_id = str(uuid.uuid4()) processed_data_store[session_id] = { "error": "Not all consignments fit.", "combinations_that_fit": combinations_info, "truck": { "name": truck_name, "dimensions": { "length": length, "width": width, "height": height } } } # Redirect to the visualization page return RedirectResponse(url=f"https://66e6a59e6eaa79674a3c7c47--stellar-jelly-eac527.netlify.app?session_id={session_id}", status_code=302) # Prepare box data for visualization box_data = [ { 'length': box.length, 'width': box.width, 'height': box.height, 'type': box.type, 'quantity': box.quantity, 'position': {'x': pos[0], 'y': pos[1], 'z': pos[2]} } for box, pos in packed_positions ] # Generate a unique session_id session_id = str(uuid.uuid4()) processed_data_store[session_id] = { "boxes": box_data, "truck": { "name": truck_name, "dimensions": { "length": length, "width": width, "height": height } } } # Redirect to the visualization page return RedirectResponse(url=f"https://66e6a59e6eaa79674a3c7c47--stellar-jelly-eac527.netlify.app?session_id={session_id}", status_code=302) @app.post("/load_combination/") async def load_combination( index: int = Form(...), truck_length: float = Form(...), truck_width: float = Form(...), truck_height: float = Form(...), truck_name: str = Form(...) ): """ Endpoint to visualize the selected combination. """ print(f"Received index: {index}") print(f"Received truck details: {truck_length}L x {truck_width}W x {truck_height}H, Name: {truck_name}") # Ensure the index is within the valid range if index >= len(stored_combinations): raise HTTPException(status_code=400, detail="Invalid combination index") selected_combination = stored_combinations[index] # Create a Truck object using the received truck dimensions truck = Truck(truck_length * 12, truck_width * 12, truck_height * 12) # Convert feet to inches # Unpack and structure the combination data for the pack_boxes_ffd function consignments = [ (boxes, priority) for boxes, priority in selected_combination # Unpack the combination as expected ] # Generate the box data for this combination packed_positions = pack_boxes_ffd(truck, consignments) # Prepare the box data box_data = [ { 'length': box.length, 'width': box.width, 'height': box.height, 'type': box.type, 'quantity': box.quantity, 'position': {'x': pos[0], 'y': pos[1], 'z': pos[2]} } for box, pos in packed_positions ] # Return the box data and truck information to the frontend return { "boxes": box_data, "truck": { "name": truck_name, "dimensions": { "length": truck_length, "width": truck_width, "height": truck_height } } } @app.get("/get_visualization/{session_id}/") async def get_visualization(session_id: str): """ Endpoint for the frontend to fetch processed data using session_id. """ if session_id not in processed_data_store: raise HTTPException(status_code=404, detail="Session ID not found") return processed_data_store[session_id]