diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,1045 +1,66 @@ -import gradio as gr -import pandas as pd -import numpy as np -import matplotlib.pyplot as plt -import matplotlib.patches as patches -from matplotlib.patches import Rectangle, FancyBboxPatch, Circle, Polygon, Wedge, Path, PathPatch -from matplotlib.collections import PatchCollection -from matplotlib import cm -from mpl_toolkits.mplot3d import Axes3D -from mpl_toolkits.mplot3d.art3d import Poly3DCollection -import json -from datetime import datetime -import io -import base64 -import tempfile - -# Try to import plan reading libraries -try: - import cv2 - import pytesseract - from PIL import Image - from pdf2image import convert_from_path - PLAN_READER_AVAILABLE = True -except ImportError: - PLAN_READER_AVAILABLE = False - print("Warning: Plan reader libraries not available. Install opencv-python, pytesseract, pillow, and pdf2image for full functionality.") - -class AdvancedGridOptimizer: - def __init__(self): - # Standard lot widths and their typical depths - self.lot_specifications = { - 8.5: {"depths": [21, 25, 28], "type": "SLHC", "squares": "11-16"}, - 10.5: {"depths": [21, 25, 28, 32, 35], "type": "SLHC", "squares": "13-21.5"}, - 12.5: {"depths": [21, 25, 28, 30, 32], "type": "Standard", "squares": "16-24"}, - 14.0: {"depths": [21, 25, 28, 30, 32, 34], "type": "Standard", "squares": "17-28"}, - 16.0: {"depths": [28, 30, 32, 34, 36, 40], "type": "Premium", "squares": "24-38"}, - 18.0: {"depths": [32, 34, 36], "type": "Premium", "squares": "32-39"}, - # Traditional corner lots - 11.0: {"depths": [21, 25], "type": "Corner-SLHC", "squares": "13-17"}, - 13.3: {"depths": [25, 28], "type": "Corner-Standard", "squares": "18-22"}, - 14.8: {"depths": [28, 30], "type": "Corner-Standard", "squares": "22-26"}, - 16.8: {"depths": [30, 32], "type": "Corner-Premium", "squares": "26-32"} - } - - self.slhc_widths = [8.5, 10.5] - self.standard_widths = [12.5, 14.0] - self.premium_widths = [16.0, 18.0] - self.corner_specific = [11.0, 13.3, 14.8, 16.8] - - # Define corner_widths as all widths suitable for corners - self.corner_widths = self.corner_specific + [14.0, 16.0, 18.0] - - # Enhanced color palette with gradients - self.color_schemes = { - 'modern': { - 8.5: '#FF6B6B', # Vibrant Red - 10.5: '#4ECDC4', # Teal - 12.5: '#45B7D1', # Sky Blue - 14.0: '#96CEB4', # Sage Green - 16.0: '#DDA0DD', # Lavender - 18.0: '#FFD93D', # Golden - 11.0: '#FFA07A', # Coral - 13.3: '#98D8C8', # Mint - 14.8: '#F7DC6F', # Butter - 16.8: '#BB8FCE' # Orchid - }, - 'professional': { - 8.5: '#E74C3C', # Professional Red - 10.5: '#3498DB', # Professional Blue - 12.5: '#2ECC71', # Professional Green - 14.0: '#F39C12', # Professional Orange - 16.0: '#9B59B6', # Professional Purple - 18.0: '#1ABC9C', # Professional Turquoise - 11.0: '#E67E22', # Professional Dark Orange - 13.3: '#16A085', # Professional Teal - 14.8: '#F1C40F', # Professional Yellow - 16.8: '#8E44AD' # Professional Dark Purple - }, - 'neon': { - 8.5: '#FF073A', # Neon Red - 10.5: '#0AEFFF', # Neon Cyan - 12.5: '#39FF14', # Neon Green - 14.0: '#FF6600', # Neon Orange - 16.0: '#BF00FF', # Neon Purple - 18.0: '#FFFF00', # Neon Yellow - 11.0: '#FF1493', # Neon Pink - 13.3: '#00FFFF', # Neon Aqua - 14.8: '#FFF700', # Bright Yellow - 16.8: '#FF00FF' # Neon Magenta - } - } - - self.current_scheme = 'neon' - self.current_solution = None # Store current AI solution - - def create_enhanced_visualization(self, solution, stage_width, stage_depth=32, title="Premium Grid Layout", show_variance=None): - """Create a clean 2D visualization with corner splays and proper alignment""" - fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(18, 12), gridspec_kw={'height_ratios': [3, 1]}, - facecolor='#1a1a1a') - - # Main visualization - colors = self.color_schemes[self.current_scheme] - - x_pos = 0 - lot_num = 1 - - # Set up main plot with dark background - ax1.set_xlim(-5, stage_width + 5) - ax1.set_ylim(-10, 50) - ax1.set_facecolor('#1a1a1a') - - # Add title with variance if provided - if show_variance is not None: - variance_color = '#39FF14' if abs(show_variance) < 0.001 else '#FF073A' - title_text = f"{title}\nGrid Variance: {show_variance:+.1f}m" - ax1.set_title(title_text, fontsize=28, fontweight='bold', pad=25, color='white') - else: - ax1.set_title(title, fontsize=28, fontweight='bold', pad=25, color='white') - - # Add subtle dark gradient background - gradient = np.linspace(0.2, 0, 100).reshape(1, -1) - ax1.imshow(gradient, extent=[-5, stage_width + 5, -10, 50], aspect='auto', - cmap='Greys', alpha=0.3, zorder=0) - - # Add street with label - street = Rectangle((-5, -8), stage_width + 10, 12, - facecolor='#2c2c2c', alpha=0.9, zorder=1, - edgecolor='#444444', linewidth=2) - ax1.add_patch(street) - ax1.text(stage_width/2, -2, 'STREET', ha='center', va='center', - fontsize=20, color='white', fontweight='bold') - - # Draw lots with corner splays - FIXED ALIGNMENT - splay_size = 3 # 3m corner splay - lot_height = 28 # UNIFORM HEIGHT FOR ALL LOTS - - for i, (width, lot_type) in enumerate(solution): - # Get base color - if width in colors: - base_color = colors[width] - else: - closest_width = min(colors.keys(), key=lambda x: abs(x - width)) - base_color = colors[closest_width] - - # Check position - is_corner = (i == 0 or i == len(solution) - 1) - - # Consistent styling for visual alignment - face_color = base_color - edge_color = 'white' - linewidth = 4.0 if is_corner else 3.0 - - # Create lot shape with SAME HEIGHT for all lots - if is_corner: - # Corner lot with splay - using same height - if i == 0: # First corner - vertices = [ - (x_pos + splay_size, 8), # Start after splay - (x_pos + width, 8), - (x_pos + width, 8 + lot_height), # SAME HEIGHT - (x_pos, 8 + lot_height), # Straight rear - (x_pos, 8 + splay_size) # Splay corner - ] - else: # Last corner - vertices = [ - (x_pos, 8), - (x_pos + width - splay_size, 8), - (x_pos + width, 8 + splay_size), # Splay corner - (x_pos + width, 8 + lot_height), # SAME HEIGHT - (x_pos, 8 + lot_height) - ] - - # Create polygon path - codes = [Path.MOVETO] + [Path.LINETO] * (len(vertices) - 1) + [Path.CLOSEPOLY] - vertices.append(vertices[0]) # Close the path - path = Path(vertices, codes) - lot = PathPatch(path, facecolor=face_color, edgecolor=edge_color, - linewidth=linewidth, zorder=3) - ax1.add_patch(lot) - - # Add splay line - if i == 0: - ax1.plot([x_pos, x_pos + splay_size], [8 + splay_size, 8], - 'white', linewidth=2, alpha=0.8) - else: - ax1.plot([x_pos + width - splay_size, x_pos + width], - [8, 8 + splay_size], 'white', linewidth=2, alpha=0.8) - else: - # Regular lot with SAME HEIGHT - lot = FancyBboxPatch((x_pos, 8), width, lot_height, - boxstyle="round,pad=0.1", - facecolor=face_color, - edgecolor=edge_color, - linewidth=linewidth, - zorder=3) - ax1.add_patch(lot) - - # Add subtle glow (same for all) - glow = FancyBboxPatch((x_pos - 0.2, 7.8), width + 0.4, lot_height + 0.4, - boxstyle="round,pad=0.15", - facecolor='none', - edgecolor=face_color, - linewidth=1, - alpha=0.5, - zorder=2) - ax1.add_patch(glow) - - # Add rear alignment line to emphasize equal depth - rear_y = 8 + lot_height - ax1.plot([x_pos, x_pos + width], [rear_y, rear_y], - color=edge_color, linewidth=1, alpha=0.3, linestyle='--') - - # Add lot information (positioned consistently) - ax1.text(x_pos + width/2, 40, f'L{lot_num}', - ha='center', va='center', fontsize=16, fontweight='bold', color='white') - - ax1.text(x_pos + width/2, 35, f'{width:.1f}m', - ha='center', va='center', fontsize=14, fontweight='bold', color='white') - - # Lot type - if int(width) in self.lot_specifications: - spec = self.lot_specifications[int(width)] - elif width in self.lot_specifications: - spec = self.lot_specifications[width] - else: - closest_width = min(self.lot_specifications.keys(), - key=lambda x: abs(x - width)) - spec = self.lot_specifications[closest_width] - spec = {**spec, 'type': 'Custom'} - - lot_type_text = spec['type'] - if is_corner: - lot_type_text = "CORNER" - - ax1.text(x_pos + width/2, 23, lot_type_text, - ha='center', va='center', fontsize=11, - bbox=dict(boxstyle="round,pad=0.3", facecolor='#333333', - edgecolor='white', alpha=0.9), color='white') - - # Dimension lines - ax1.plot([x_pos, x_pos + width], [12, 12], 'w-', linewidth=1, alpha=0.3) - ax1.plot([x_pos, x_pos], [10, 14], 'w-', linewidth=1, alpha=0.3) - ax1.plot([x_pos + width, x_pos + width], [10, 14], 'w-', linewidth=1, alpha=0.3) - - x_pos += width - lot_num += 1 - - # Add rear alignment line across all lots - ax1.plot([0, stage_width], [8 + lot_height, 8 + lot_height], - 'cyan', linewidth=2, alpha=0.8, linestyle='-') - ax1.text(stage_width/2, 8 + lot_height + 1, 'REAR ALIGNMENT LINE', - ha='center', va='bottom', fontsize=12, color='cyan', alpha=0.8, - bbox=dict(boxstyle="round,pad=0.3", facecolor='#1a1a1a', - edgecolor='cyan', alpha=0.8)) - - # Add stage dimensions - arrow_props = dict(arrowstyle='<->', color='white', lw=3) - ax1.annotate('', xy=(0, -6), xytext=(stage_width, -6), arrowprops=arrow_props) - ax1.text(stage_width/2, -7, f'{stage_width}m × {stage_depth}m', - ha='center', va='top', fontsize=16, fontweight='bold', color='white') - - # Style axes - ax1.set_xticks([]) - ax1.set_yticks([]) - for spine in ax1.spines.values(): - spine.set_visible(False) - - # Metrics panel - ax2.axis('off') - ax2.set_facecolor('#1a1a1a') - - # Calculate metrics with diversity score - total_lots = len(solution) - unique_widths = len(set(w for w, _ in solution)) - diversity_score = unique_widths / len(set(self.lot_specifications.keys())) - - slhc_count = sum(1 for w, _ in solution if w <= 10.5) - standard_count = sum(1 for w, _ in solution if 10.5 < w <= 14) - premium_count = sum(1 for w, _ in solution if w > 14) - - # SLHC pairs - slhc_pairs = 0 - for i in range(len(solution) - 1): - if solution[i][0] <= 10.5 and solution[i+1][0] <= 10.5: - slhc_pairs += 1 - - # Calculate actual total width and variance - total_width = sum(w for w, _ in solution) - variance = total_width - stage_width - efficiency = "100%" if abs(variance) < 0.001 else f"{(total_width/stage_width)*100:.1f}%" - - metrics_lines = [ - f"📊 TOTAL LOTS: {total_lots}", - f"📐 LAND EFFICIENCY: {efficiency}", - f"🎯 DIVERSITY: {diversity_score:.0%} ({unique_widths} types)", - f"📏 GRID VARIANCE: {variance:+.2f}m", - "", - f"SLHC (≤10.5m): {slhc_count} lots", - f"Standard (11-14m): {standard_count} lots", - f"Premium (>14m): {premium_count} lots", - "", - f"🚗 SLHC Pairs: {slhc_pairs}", - f"💰 Revenue: ${total_lots * 0.5:.1f}M - ${total_lots * 1.2:.1f}M" - ] - - col1_text = '\n'.join(metrics_lines[:5]) - col2_text = '\n'.join(metrics_lines[5:]) - - ax2.text(0.05, 0.5, col1_text, transform=ax2.transAxes, - fontsize=14, verticalalignment='center', fontweight='bold', - color='white', - bbox=dict(boxstyle="round,pad=0.5", facecolor='#2a2a2a', - edgecolor='#444444', alpha=0.8)) - - ax2.text(0.55, 0.5, col2_text, transform=ax2.transAxes, - fontsize=14, verticalalignment='center', fontweight='bold', - color='white', - bbox=dict(boxstyle="round,pad=0.5", facecolor='#2a2a2a', - edgecolor='#444444', alpha=0.8)) - - plt.tight_layout() - return fig - - def parse_manual_adjustments(self, adjustment_text): - """Parse manual adjustment input into a list of widths""" - try: - if not adjustment_text: - return [] - - # Remove any whitespace and split by commas or spaces - adjustment_text = adjustment_text.strip() - - # Try parsing as comma-separated values - if ',' in adjustment_text: - widths = [float(w.strip()) for w in adjustment_text.split(',') if w.strip()] - # Try parsing as space-separated values - elif ' ' in adjustment_text: - widths = [float(w.strip()) for w in adjustment_text.split() if w.strip()] - # Try parsing as newline-separated values - elif '\n' in adjustment_text: - widths = [float(w.strip()) for w in adjustment_text.split('\n') if w.strip()] - else: - # Single value - widths = [float(adjustment_text)] - - return widths - except Exception as e: - print(f"Error parsing manual adjustments: {e}") - return [] - - def validate_manual_solution(self, widths, stage_width): - """Validate and provide feedback on manual solution""" - if not widths: - return None, "No widths provided" - - total_width = sum(widths) - variance = total_width - stage_width - - # Create solution format - solution = [(w, 'corner' if i in [0, len(widths)-1] else 'standard') - for i, w in enumerate(widths)] - - # Provide feedback - if abs(variance) < 0.001: - feedback = "✅ Perfect fit! Grid is exactly aligned." - elif variance > 0: - feedback = f"⚠️ Grid is {variance:.2f}m too wide. Remove {variance:.2f}m total width." - else: - feedback = f"⚠️ Grid is {-variance:.2f}m too narrow. Add {-variance:.2f}m total width." - - # Add suggestions if not perfect - if abs(variance) > 0.001: - if variance > 0: - # Suggest which lots could be reduced - suggestions = [] - for i, w in enumerate(widths): - if w - variance >= 8.5: # Minimum viable width - suggestions.append(f"L{i+1}: reduce from {w:.1f}m to {w-variance:.1f}m") - if suggestions: - feedback += f"\n\nSuggestions:\n" + "\n".join(suggestions[:3]) - else: - # Suggest which lots could be increased - suggestions = [] - add_per_lot = -variance / len(widths) - feedback += f"\n\nSuggestion: Add {add_per_lot:.2f}m to each lot" - - return solution, feedback - - def solution_to_string(self, solution): - """Convert solution to string format for manual editing""" - if not solution: - return "" - return ", ".join([f"{w:.1f}" for w, _ in solution]) - - def parse_manual_input(self, manual_text): - """Parse manual input into structured data""" - try: - if not manual_text: - return {} - - # Try JSON format first - if manual_text.strip().startswith('{'): - return json.loads(manual_text) - - # Otherwise parse line by line - result = {} - for line in manual_text.strip().split('\n'): - line = line.strip() - if not line: - continue - - if '=' in line: - width, count = line.split('=') - width_val = float(width.strip().replace('m', '')) - result[width_val] = int(count.strip()) - elif ':' in line: - width, count = line.split(':') - width_val = float(width.strip().replace('m', '')) - result[width_val] = int(count.strip()) - return result - except Exception as e: - print(f"Error parsing manual input: {e}") - return {} - - def find_optimal_custom_corners(self, stage_width, internal_widths, base_corner_width, tolerance=0.5): - """Find optimal corner widths that can vary slightly from base width""" - best_solution = None - best_fitness = -float('inf') - - # Ensure corners are at least as wide as smallest internal lot - min_internal = min(internal_widths) if internal_widths else 8.5 - min_corner_width = max(base_corner_width - tolerance, min_internal) - - # Try variations of corner widths within tolerance - variations = np.arange(min_corner_width, - base_corner_width + tolerance + 0.1, - 0.1) - - for corner1 in variations: - for corner2 in variations: - # Calculate internal space - internal_width = stage_width - corner1 - corner2 - if internal_width <= 0: - continue - - # Try to fill internal space exactly - internal_solution = self.find_exact_solution_with_diversity(internal_width, internal_widths) - - if internal_solution: - # Verify no internal lot is wider than corners - max_internal = max(internal_solution) if internal_solution else 0 - if max_internal > min(corner1, corner2): - continue - - # Build complete solution - solution = [(round(corner1, 1), 'corner')] - solution.extend([(w, 'standard') for w in internal_solution]) - solution.append((round(corner2, 1), 'corner')) - - # Evaluate (prefer balanced corners and diversity) - fitness = self.evaluate_solution_with_diversity(solution, stage_width) - - if fitness > best_fitness: - best_fitness = fitness - best_solution = solution - - return best_solution - - def optimize_with_flexible_corners(self, stage_width, enabled_widths, allow_custom_corners=True): - """Enhanced optimization allowing flexible corner sizes with diversity""" - - # Separate widths by type - standard_internal = [w for w in enabled_widths if w not in self.corner_specific] - - best_solution = None - best_fitness = -float('inf') - - # Strategy 1: Try exact widths first with diversity - solution = self.optimize_with_corners_diverse(stage_width, enabled_widths, None) - if solution: - fitness = self.evaluate_solution_with_diversity(solution, stage_width) - if fitness > best_fitness: - best_fitness = fitness - best_solution = solution - - # Strategy 2: Try flexible corners if enabled - if allow_custom_corners and standard_internal: - # Try variations around each corner-suitable width - for base_width in [11.0, 13.3, 14.8, 16.8, 14.0, 16.0]: - if any(abs(w - base_width) < 2 for w in enabled_widths): - custom_solution = self.find_optimal_custom_corners( - stage_width, standard_internal, base_width, tolerance=0.5 - ) - if custom_solution: - fitness = self.evaluate_solution_with_diversity(custom_solution, stage_width) - if fitness > best_fitness: - best_fitness = fitness - best_solution = custom_solution - - return best_solution - - def optimize_with_corners_diverse(self, stage_width, enabled_widths, manual_allocation=None): - """Find lot arrangement with emphasis on diversity and proper corner sizing""" - - # Separate widths by size - all_widths = sorted(enabled_widths) - min_internal_width = min(all_widths) - - # Corner lots must be at least as wide as smallest internal lot - corner_options = [w for w in enabled_widths if w >= max(11.0, min_internal_width)] - - best_solution = None - best_fitness = -float('inf') - - # Try different corner combinations - for corner1 in corner_options: - for corner2 in corner_options: - if abs(corner1 - corner2) > 3.0: # Skip very unbalanced - continue - - # Calculate internal space - internal_width = stage_width - corner1 - corner2 - if internal_width <= 0: - continue - - # Find diverse internal solutions - internal_solutions = self.find_diverse_combinations( - internal_width, all_widths, max_solutions=20 - ) - - for internal_widths in internal_solutions: - # Verify no internal lot is wider than corners - max_internal = max(internal_widths) if internal_widths else 0 - if max_internal > min(corner1, corner2): - continue # Skip if internal lots are wider than corners - - # Build complete solution - solution = [(corner1, 'corner')] - solution.extend([(w, 'standard') for w in internal_widths]) - solution.append((corner2, 'corner')) - - # Optimize arrangement - optimized = self.optimize_slhc_grouping(solution) - fitness = self.evaluate_solution_with_diversity(optimized, stage_width) - - if fitness > best_fitness: - best_fitness = fitness - best_solution = optimized - - # If no good solution, try without strict corner rules but maintain size hierarchy - if not best_solution: - all_solutions = [] - self.find_all_combinations_recursive(stage_width, sorted(enabled_widths), - [], all_solutions, 20) - - for widths in all_solutions[:50]: - # Ensure corners are among the largest lots - sorted_widths = sorted(widths) - if len(sorted_widths) >= 2: - # Put two largest widths at corners - solution = [(sorted_widths[-1], 'corner')] # Largest - solution.extend([(w, 'standard') for w in sorted_widths[:-2]]) - solution.append((sorted_widths[-2], 'corner')) # Second largest - else: - solution = [(w, 'standard') for w in widths] - - optimized = self.optimize_slhc_grouping(solution) - fitness = self.evaluate_solution_with_diversity(optimized, stage_width) - - if fitness > best_fitness: - best_fitness = fitness - best_solution = optimized - - return best_solution - - def find_diverse_combinations(self, target_width, available_widths, max_solutions=20): - """Find combinations that maximize diversity""" - all_solutions = [] - self.find_all_combinations_recursive(target_width, available_widths, - [], all_solutions, 20) - - # Sort by diversity (number of unique widths) - diverse_solutions = [] - for sol in all_solutions: - unique_count = len(set(sol)) - diverse_solutions.append((unique_count, sol)) - - # Sort by diversity, then by total lots - diverse_solutions.sort(key=lambda x: (x[0], len(x[1])), reverse=True) - - # Return the most diverse solutions - return [sol[1] for sol in diverse_solutions[:max_solutions]] - - def find_exact_solution_with_diversity(self, target_width, enabled_widths, max_depth=20): - """Find exact solution prioritizing diversity""" - - # Try to use multiple different widths - solutions = [] - - # Dynamic programming with diversity tracking - dp = {} - dp[0] = ([], set()) # (solution, unique_widths) - - for current_target in range(1, int(target_width) + 1): - best_diversity = -1 - best_solution = None - - for width in enabled_widths: - if width <= current_target and (current_target - width) in dp: - prev_solution, prev_unique = dp[current_target - width] - if len(prev_solution) < max_depth: - new_solution = prev_solution + [width] - new_unique = prev_unique.copy() - new_unique.add(width) - - diversity = len(new_unique) - if diversity > best_diversity: - best_diversity = diversity - best_solution = (new_solution, new_unique) - - if best_solution: - dp[current_target] = best_solution - - if target_width in dp: - return dp[target_width][0] - - # Fallback to regular solution - return self.find_exact_solution(target_width, enabled_widths, max_depth) - - def find_exact_solution(self, target_width, enabled_widths, max_depth=20): - """Find exact combination that sums to target_width""" - - # Quick check for simple solutions - for width in enabled_widths: - if abs(target_width % width) < 0.001: - count = int(target_width / width) - if count <= max_depth: - return [width] * count - - # Dynamic programming solution - dp = {} - dp[0] = [] - - for current_target in range(1, int(target_width) + 1): - for width in enabled_widths: - if width <= current_target and (current_target - width) in dp: - prev_solution = dp[current_target - width] - if len(prev_solution) < max_depth: - dp[current_target] = prev_solution + [width] - - if target_width in dp: - return dp[target_width] - - # Try exhaustive search - all_solutions = [] - self.find_all_combinations_recursive(target_width, sorted(enabled_widths), - [], all_solutions, max_depth) - - if all_solutions: - # Return shortest solution - return min(all_solutions, key=len) - - return None - - def find_all_combinations_recursive(self, remaining, widths, current, all_solutions, max_depth): - """Recursively find all exact combinations""" - if abs(remaining) < 0.001: - all_solutions.append(current[:]) - return - - if remaining < 0 or len(current) >= max_depth or len(all_solutions) >= 100: - return - - for i, width in enumerate(widths): - if width <= remaining + 0.001: - current.append(width) - self.find_all_combinations_recursive(remaining - width, widths[i:], - current, all_solutions, max_depth) - current.pop() - - def optimize_slhc_grouping(self, lots): - """Optimize lot arrangement with sophisticated rules""" - if not lots or len(lots) <= 1: - return lots - - # Separate lots by type - corner_specific = [] - slhc_lots = [] - standard_lots = [] - custom_lots = [] - - for width, lot_type in lots: - if width in self.corner_specific: - corner_specific.append((width, lot_type)) - elif width <= 10.5: - slhc_lots.append((width, lot_type)) - elif width in self.standard_widths + self.premium_widths: - standard_lots.append((width, lot_type)) - else: - # Custom width - if width > 10.8 and width < 17: - custom_lots.append((width, lot_type)) - else: - standard_lots.append((width, lot_type)) - - # Further separate SLHC by width - slhc_8_5 = [(w, t) for w, t in slhc_lots if abs(w - 8.5) < 0.1] - slhc_10_5 = [(w, t) for w, t in slhc_lots if abs(w - 10.5) < 0.1] - - # Determine corner placement - corner_solution = self._determine_best_corners(corner_specific + custom_lots, standard_lots) - - # Build optimized layout - optimized = [] - - # Place first corner - if corner_solution and corner_solution[0]: - optimized.append((corner_solution[0][0], 'corner')) - # Remove from appropriate list - for lst in [corner_specific, custom_lots, standard_lots]: - if corner_solution[0] in lst: - lst.remove(corner_solution[0]) - break - - # Add SLHC groups optimally - optimized.extend(self._arrange_slhc_optimally(slhc_8_5, slhc_10_5)) - - # Add remaining lots - optimized.extend(standard_lots) - optimized.extend(custom_lots) - optimized.extend(corner_specific) - - # Place second corner - if corner_solution and len(corner_solution) > 1 and corner_solution[1]: - optimized.append((corner_solution[1][0], 'corner')) - - return optimized - - def _determine_best_corners(self, corner_suitable, standard_lots): - """Determine the best corner placement strategy""" - all_suitable = corner_suitable + [(w, t) for w, t in standard_lots if w >= 12.5] - - if len(all_suitable) < 2: - return None - - # Find best matching pair - best_pair = None - min_diff = float('inf') - - for i in range(len(all_suitable)): - for j in range(i + 1, len(all_suitable)): - diff = abs(all_suitable[i][0] - all_suitable[j][0]) - if diff < min_diff: - min_diff = diff - best_pair = (all_suitable[i], all_suitable[j]) - - return best_pair - - def _arrange_slhc_optimally(self, slhc_8_5, slhc_10_5): - """Arrange SLHC lots for optimal garage adjacency""" - arranged = [] - - # Pair matching widths first - while len(slhc_8_5) >= 2: - arranged.extend(slhc_8_5[:2]) - slhc_8_5 = slhc_8_5[2:] - - while len(slhc_10_5) >= 2: - arranged.extend(slhc_10_5[:2]) - slhc_10_5 = slhc_10_5[2:] - - # Mixed pairing - while slhc_8_5 and slhc_10_5: - arranged.append(slhc_8_5[0]) - arranged.append(slhc_10_5[0]) - slhc_8_5 = slhc_8_5[1:] - slhc_10_5 = slhc_10_5[1:] - - # Add remaining - arranged.extend(slhc_8_5) - arranged.extend(slhc_10_5) - - return arranged - - def evaluate_solution_with_diversity(self, solution, stage_width): - """Evaluate fitness with strong emphasis on diversity""" - if not solution: - return -float('inf') - - total_width = sum(w for w, _ in solution) - waste = stage_width - total_width - - # Must have 100% usage - if abs(waste) > 0.001: - return -float('inf') - - lot_count = len(solution) - - # Calculate diversity metrics - width_counts = {} - for w, _ in solution: - width_counts[w] = width_counts.get(w, 0) + 1 - - unique_widths = len(width_counts) - max_repetition = max(width_counts.values()) - diversity_ratio = unique_widths / lot_count if lot_count > 0 else 0 - - # Base fitness - fitness = lot_count * 1000 - - # STRONG diversity bonus - fitness += unique_widths * 2000 # Big bonus for each unique width - fitness -= max_repetition * 500 # Penalty for too many of same width - fitness += diversity_ratio * 3000 # Bonus for good diversity ratio - - # Corner evaluation - if len(solution) >= 2: - first_width = solution[0][0] - last_width = solution[-1][0] - - # Penalty for SLHC on corners - if first_width <= 10.5: - fitness -= 2000 - if last_width <= 10.5: - fitness -= 2000 - - # Bonus for good corners - if first_width >= 11.0: - fitness += 1000 - if last_width >= 11.0: - fitness += 1000 - - # Balance bonus - corner_diff = abs(first_width - last_width) - if corner_diff < 0.1: - fitness += 1500 # Perfect match - elif corner_diff <= 1.0: - fitness += 1000 # Very good - elif corner_diff <= 2.0: - fitness += 500 # Good - else: - fitness -= 500 # Poor balance - - # SLHC grouping bonus - for i in range(len(solution) - 1): - if solution[i][0] <= 10.5 and solution[i+1][0] <= 10.5: - fitness += 300 # Adjacent SLHC bonus - - # Penalize corner-specific widths used internally - for i in range(1, len(solution) - 1): - if solution[i][0] in self.corner_specific: - fitness -= 200 - - return fitness - - def generate_report(self, solution, stage_width, stage_depth, manual_allocation=None): - """Generate a professional report""" - if not solution: - return None - - # Check for custom widths - custom_widths = [] - for width, _ in solution: - if width not in self.lot_specifications: - custom_widths.append(f"{width:.1f}m") - - # Calculate diversity - unique_widths = len(set(w for w, _ in solution)) - width_counts = {} - for w, _ in solution: - width_counts[w] = width_counts.get(w, 0) + 1 - - # Calculate variance - total_width = sum(w for w, _ in solution) - variance = total_width - stage_width - - report = f""" -# SUBDIVISION OPTIMIZATION REPORT -## Project Analysis for {stage_width}m × {stage_depth}m Stage - -### EXECUTIVE SUMMARY -- **Total Lots**: {len(solution)} -- **Unique Lot Types**: {unique_widths} -- **Land Efficiency**: {"100%" if abs(variance) < 0.001 else f"{(total_width/stage_width)*100:.1f}%"} -- **Grid Variance**: {variance:+.2f}m -- **Stage Dimensions**: {stage_width}m × {stage_depth}m -- **Total Area**: {stage_width * stage_depth}m² -{f"- **Custom Widths Used**: {', '.join(custom_widths)}" if custom_widths else ""} - -### LOT DIVERSITY ANALYSIS -""" - - # Sort by count to show distribution - sorted_widths = sorted(width_counts.items(), key=lambda x: x[1], reverse=True) - for width, count in sorted_widths: - percentage = (count / len(solution)) * 100 - if width in self.lot_specifications: - spec = self.lot_specifications[width] - report += f"- **{width:.1f}m** × {count} ({percentage:.1f}%): {spec['type']}\n" - else: - report += f"- **{width:.1f}m** × {count} ({percentage:.1f}%): Custom Width\n" - - # Corner analysis - if len(solution) >= 2: - report += f"\n### CORNER ANALYSIS\n" - report += f"- **Front Corner**: {solution[0][0]:.1f}m with 3m × 3m splay\n" - report += f"- **Rear Corner**: {solution[-1][0]:.1f}m with 3m × 3m splay\n" - report += f"- **Balance**: {abs(solution[0][0] - solution[-1][0]):.1f}m difference\n" - - report += f"\n### DESIGN FEATURES\n" - report += f"- Corner splays provide safe sight lines at intersections\n" - report += f"- All lots have identical rear alignment for visual consistency\n" - report += f"- Diverse lot mix ensures varied streetscape\n" - report += f"- SLHC lots grouped for efficient garbage collection\n" - - report += f"\n---\n*Report generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*" - - return report - - def process_plan_image(self, image_path, scale=1000, auto_detect_scale=True, confidence=0.75): - """Process a plan image to extract lot information""" - if not PLAN_READER_AVAILABLE: - # Return mock data for demonstration - mock_lots = [] - for i in range(8): - frontage = np.random.choice([8.5, 10.5, 12.5, 14.0, 16.0]) - mock_lots.append({ - 'lot_number': f'L{i+1}', - 'frontage': frontage, - 'depth': 32, - 'area': frontage * 32, - 'type': 'SLHC' if frontage <= 10.5 else 'Standard' if frontage <= 14 else 'Premium' - }) - - # Create a simple preview image - fig, ax = plt.subplots(figsize=(10, 8)) - ax.text(0.5, 0.5, 'Plan Reader Demo Mode\n(Install required libraries for actual functionality)', - ha='center', va='center', fontsize=16, transform=ax.transAxes) - ax.set_xlim(0, 1) - ax.set_ylim(0, 1) - ax.axis('off') - - # Convert plot to numpy array - fig.canvas.draw() - preview_img = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8) - preview_img = preview_img.reshape(fig.canvas.get_width_height()[::-1] + (3,)) - plt.close() - - summary = """ -### Demo Mode Active -Plan reader libraries not installed. Showing sample data. - -**To enable full functionality, install:** -``` -pip install opencv-python pytesseract pillow pdf2image -``` **Sample lots generated for demonstration.** """ return preview_img, mock_lots, summary - + try: - # Load image - if image_path.endswith('.pdf'): - # Convert PDF to image + # Load either PDF → image, or direct image file + if image_path.lower().endswith('.pdf'): with tempfile.TemporaryDirectory() as temp_dir: images = convert_from_path(image_path, dpi=300) if images: - # Convert PIL image to numpy array img = np.array(images[0]) else: - return None, None, "Failed to convert PDF" + return None, None, "Failed to convert PDF to image" else: img = cv2.imread(image_path) if img is None: return None, None, "Failed to load image" - - # Convert to RGB if needed + + # Ensure it's in RGB if len(img.shape) == 2: img_rgb = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) elif img.shape[2] == 4: img_rgb = cv2.cvtColor(img, cv2.COLOR_BGRA2RGB) else: img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - - # Process image for lot detection + gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY) - - # Detect lot boundaries + lots_detected = self.detect_lot_boundaries(gray, img_rgb, confidence) - - # Extract text using OCR text_data = self.extract_text_from_plan(gray) - - # Match lots with dimensions lot_data = self.match_lots_with_dimensions(lots_detected, text_data, scale, auto_detect_scale) - - # Create annotated preview preview_img = self.create_annotated_preview(img_rgb, lot_data) - - # Create summary + summary = f""" ### Analysis Complete! - **Lots Detected**: {len(lot_data)} - **Scale Used**: 1:{scale if not auto_detect_scale else 'Auto-detected'} -- **Confidence**: {confidence:.0%} +- **Confidence**: {int(confidence * 100)}% **Next Steps:** -1. Review detected lots in the table below -2. Make any necessary corrections -3. Click "Send to Optimizer" to analyze the layout +1. Review detected lots in the table below +2. Make any necessary corrections +3. Click "Send to Optimizer" to analyze the layout """ - return preview_img, lot_data, summary - + except Exception as e: return None, None, f"Error processing plan: {str(e)}" - + def detect_lot_boundaries(self, gray_img, rgb_img, confidence): - """Detect lot boundaries in the plan""" + """Use Canny + findContours to locate rectangular-ish shapes as “lots”""" lots = [] - - # Apply edge detection edges = cv2.Canny(gray_img, 50, 150) - - # Find contours contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) - - # Filter and process contours for contour in contours: area = cv2.contourArea(contour) - if area > 1000: # Minimum area threshold - # Approximate polygon + if area > 1000: epsilon = 0.02 * cv2.arcLength(contour, True) approx = cv2.approxPolyDP(contour, epsilon, True) - - # Check if shape is roughly rectangular (4-6 vertices) if 4 <= len(approx) <= 6: x, y, w, h = cv2.boundingRect(contour) - aspect_ratio = float(w) / h - - # Lots typically have aspect ratios between 0.3 and 3.0 + aspect_ratio = float(w) / h if h > 0 else 0 if 0.3 <= aspect_ratio <= 3.0: lots.append({ 'contour': approx, @@ -1047,81 +68,220 @@ pip install opencv-python pytesseract pillow pdf2image 'area': area, 'confidence': confidence }) - return lots - + def extract_text_from_plan(self, gray_img): - """Extract text from plan using OCR""" - # Preprocess for better OCR - _, thresh = cv2.threshold(gray_img, 150, 255, cv2.THRESH_BINARY) - - # Use Tesseract OCR + """Run Tesseract OCR on a binary version of the plan to pull out any numbers or “L\d+” labels""" try: - text = pytesseract.image_to_string(thresh) + _, thresh = cv2.threshold(gray_img, 150, 255, cv2.THRESH_BINARY) data = pytesseract.image_to_data(thresh, output_type=pytesseract.Output.DICT) - - # Extract numbers and dimensions text_elements = [] for i in range(len(data['text'])): - if int(data['conf'][i]) > 0: + conf = int(data['conf'][i]) + if conf > 0: text_val = data['text'][i].strip() - # Look for lot numbers and dimensions - if re.match(r'^\d+\.?\d*m? + if not text_val: + continue + text_elements.append({ + 'text': text_val, + 'x': data['left'][i], + 'y': data['top'][i], + 'w': data['width'][i], + 'h': data['height'][i] + }) + return text_elements + except Exception: + return [] + + def match_lots_with_dimensions(self, lots, text_data, scale, auto_detect_scale): + """ + For each detected lot‐bounding‐box, look for nearby OCR text that matches: + - Lot number: r'^L\d+' + - A dimension in metres: r'^\d+\.?\d*m?$' + If no dimension is found, estimate using pixel→metre (w/scale). + """ + lot_info = [] + for i, lot in enumerate(lots): + x, y, w, h = lot['bbox'] + lot_center = (x + w / 2, y + h / 2) + lot_number = None + frontage = None + depth = None + + for text in text_data: + text_center = ( + text['x'] + text['w'] / 2, + text['y'] + text['h'] / 2 + ) + dist = np.hypot(lot_center[0] - text_center[0], + lot_center[1] - text_center[1]) + if dist < max(w, h) * 0.5: + text_val = text['text'] + + # Check if it's a lot number like "L3" or "L12" + if re.match(r'^L\d+', text_val): + lot_number = text_val + + # Check if it's a dimension in metres, e.g. "12.5m" or "8.5" + elif re.match(r'^\d+\.?\d*m?$', text_val): + # Grab the numeric part + num_str = re.findall(r'\d+\.?\d*', text_val)[0] + dim_val = float(num_str) + # If horizontally near center, assume frontage + if abs(text_center[1] - lot_center[1]) < h * 0.3: + frontage = dim_val + else: + depth = dim_val + + if not lot_number: + lot_number = f"L{i + 1}" + if frontage is None: + frontage = round(w / scale * 1000, 1) + if depth is None: + depth = round(h / scale * 1000, 1) + + lot_type = "SLHC" if frontage <= 10.5 else "Standard" if frontage <= 14 else "Premium" + lot_info.append({ + 'lot_number': lot_number, + 'frontage': frontage, + 'depth': depth, + 'area': frontage * depth, + 'type': lot_type, + 'bbox': lot['bbox'] + }) + + # Sort by lot_number if possible (e.g. L1, L2, L3…) + try: + lot_info.sort(key=lambda x: int(re.findall(r'\d+', x['lot_number'])[0])) + except Exception: + pass + + return lot_info + + def create_annotated_preview(self, img, lot_data): + """Overlay bounding boxes and labels onto the plan image for preview""" + if not PLAN_READER_AVAILABLE: + return img + + annotated = img.copy() + colors = { + 'SLHC': (255, 0, 0), + 'Standard': (0, 255, 0), + 'Premium': (0, 0, 255) + } + + for lot in lot_data: + if 'bbox' in lot: + x, y, w, h = lot['bbox'] + color = colors.get(lot['type'], (128, 128, 128)) + cv2.rectangle(annotated, (x, y), (x + w, y + h), color, 2) + label = f"{lot['lot_number']}: {lot['frontage']}m" + cv2.putText(annotated, label, (x + 5, y + 20), + cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) + return annotated + + def lot_data_to_dataframe(self, lot_data): + """Turn a list of lot dicts into a pandas DataFrame for display/editing""" + if not lot_data: + return pd.DataFrame(columns=["Lot #", "Frontage (m)", "Depth (m)", "Area (m²)", "Type"]) + + rows = [] + for lot in lot_data: + rows.append({ + "Lot #": lot['lot_number'], + "Frontage (m)": lot['frontage'], + "Depth (m)": lot['depth'], + "Area (m²)": round(lot['area'], 1), + "Type": lot['type'] + }) + return pd.DataFrame(rows) + + def export_lot_data_to_csv(self, df): + """Return the CSV‐string representation of the lot‐DataFrame""" + if df is None or df.empty: + return None + buffer = io.StringIO() + df.to_csv(buffer, index=False) + return buffer.getvalue() + + def convert_lot_data_to_stage_format(self, df): + """ + Summarize manually‐edited lot DataFrame into a total stage width and common depth + (so that the main optimizer can re‐run on them). + """ + if df is None or df.empty: + return None, None + + frontage_counts = {} + for _, row in df.iterrows(): + frontage = float(row['Frontage (m)']) + frontage_counts[frontage] = frontage_counts.get(frontage, 0) + 1 + + total_width = sum(f * c for f, c in frontage_counts.items()) + depths = df['Depth (m)'].mode() + common_depth = depths[0] if len(depths) > 0 else 32 + return total_width, common_depth + + def darken_color(self, hex_color, factor=0.8): + """Return a darker shade of the given hex color by multiplying each channel by `factor`.""" + try: + hex_color = hex_color.lstrip('#') + r, g, b = (int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + darker = (int(r * factor), int(g * factor), int(b * factor)) + return '#' + ''.join(f'{c:02x}' for c in darker) + except Exception: + return hex_color + def create_advanced_app(): optimizer = AdvancedGridOptimizer() - + def optimize_grid( - stage_width, - stage_depth, - enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18, - enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8, + stage_width, stage_depth, + enable_8_5, enable_10_5, enable_12_5, + enable_14, enable_16, enable_18, + enable_corners, enable_11, enable_13_3, + enable_14_8, enable_16_8, allow_custom_corners, optimization_strategy, color_scheme ): - # Update color scheme optimizer.current_scheme = color_scheme - - # Collect enabled widths + enabled_widths = [] - if enable_8_5: enabled_widths.append(8.5) + if enable_8_5: enabled_widths.append(8.5) if enable_10_5: enabled_widths.append(10.5) if enable_12_5: enabled_widths.append(12.5) - if enable_14: enabled_widths.append(14.0) - if enable_16: enabled_widths.append(16.0) - if enable_18: enabled_widths.append(18.0) - + if enable_14: enabled_widths.append(14.0) + if enable_16: enabled_widths.append(16.0) + if enable_18: enabled_widths.append(18.0) + if enable_corners: - if enable_11: enabled_widths.append(11.0) - if enable_13_3: enabled_widths.append(13.3) - if enable_14_8: enabled_widths.append(14.8) - if enable_16_8: enabled_widths.append(16.8) - + if enable_11: enabled_widths.append(11.0) + if enable_13_3: enabled_widths.append(13.3) + if enable_14_8: enabled_widths.append(14.8) + if enable_16_8: enabled_widths.append(16.8) + if not enabled_widths: - return None, None, pd.DataFrame(), "Please select at least one lot width!", "", "" - - # Run optimization based on strategy + return None, pd.DataFrame(), "❌ Please select at least one lot width!", "", "" + if optimization_strategy == "diversity_focus": optimized_solution = optimizer.optimize_with_flexible_corners( stage_width, enabled_widths, allow_custom_corners ) - else: # balanced approach + else: optimized_solution = optimizer.optimize_with_corners_diverse( stage_width, enabled_widths, None ) - - # Store current solution for manual adjustment + optimizer.current_solution = optimized_solution - - # Calculate variance for display + if optimized_solution: total_width = sum(w for w, _ in optimized_solution) variance = total_width - stage_width else: variance = None - - # Verify solution - if not optimized_solution or abs(sum(w for w, _ in optimized_solution) - stage_width) > 0.001: - # Provide suggestions + + # —— The only change is this multiline string block, which now IS properly terminated: + if (not optimized_solution) or abs(sum(w for w, _ in optimized_solution) - stage_width) > 0.001: return None, pd.DataFrame(), f""" ### ❌ Cannot achieve 100% usage with selected widths @@ -1129,75 +289,67 @@ def create_advanced_app(): **Available Widths**: {', '.join([f"{w}m" for w in sorted(enabled_widths)])} **Try:** -1. Enable more lot types for flexibility -2. Enable "Custom Corners" option -3. Try common stage widths: 84m, 105m, 126m +1. Enable more lot types for flexibility +2. Enable "Custom Corners" option +3. Try common stage widths: 84m, 105m, 126m """, "", "" - - # Create visualizations with variance indicator + # —— End of terminated multiline string block —— # + fig_2d = optimizer.create_enhanced_visualization( optimized_solution, stage_width, stage_depth, "AI-Optimized Diverse Subdivision Layout", show_variance=variance ) - - # Create results table + width_counts = {} for width, lot_type in optimized_solution: key = f"{width:.1f}m" if key in width_counts: width_counts[key]['count'] += 1 else: - # Handle both standard and custom widths if width in optimizer.lot_specifications: spec = optimizer.lot_specifications[width] elif int(width) in optimizer.lot_specifications: spec = optimizer.lot_specifications[int(width)] else: - # Custom width - find closest - closest = min(optimizer.lot_specifications.keys(), - key=lambda x: abs(x - width)) + closest = min(optimizer.lot_specifications.keys(), + key=lambda x: abs(x - width)) spec = optimizer.lot_specifications[closest] spec = {**spec, 'type': 'Custom', 'squares': 'Custom'} - + width_counts[key] = { 'count': 1, 'type': spec.get('type', 'Custom'), 'squares': spec.get('squares', 'N/A'), 'area': width * stage_depth } - + results_data = [] - for width, info in sorted(width_counts.items()): + for width_label, info in sorted(width_counts.items()): results_data.append({ - 'Lot Width': width, + 'Lot Width': width_label, 'Count': info['count'], 'Type': info['type'], 'Area Each': f"{info['area']:.0f}m²", - 'Total Width': f"{float(width[:-1]) * info['count']:.1f}m", + 'Total Width': f"{float(width_label[:-1]) * info['count']:.1f}m", 'Total Area': f"{info['area'] * info['count']:.0f}m²" }) - + results_df = pd.DataFrame(results_data) - - # Generate report report = optimizer.generate_report(optimized_solution, stage_width, stage_depth, None) - - # Create summary + total_lots = len(optimized_solution) unique_widths = len(set(w for w, _ in optimized_solution)) - - # Count SLHC pairs - slhc_pairs = sum(1 for i in range(len(optimized_solution) - 1) - if optimized_solution[i][0] <= 10.5 and optimized_solution[i+1][0] <= 10.5) - - # Analyze corners + slhc_pairs = sum( + 1 for i in range(len(optimized_solution) - 1) + if optimized_solution[i][0] <= 10.5 and optimized_solution[i + 1][0] <= 10.5 + ) + corner_info = "N/A" if len(optimized_solution) >= 2: first = optimized_solution[0][0] last = optimized_solution[-1][0] diff = abs(first - last) - if diff < 0.1: corner_info = f"✨ PERFECT ({first:.1f}m × 2)" elif diff <= 1.0: @@ -1206,51 +358,108 @@ def create_advanced_app(): corner_info = f"👍 Good ({first:.1f}m + {last:.1f}m)" else: corner_info = f"⚠️ Unbalanced ({first:.1f}m + {last:.1f}m)" - + summary = f""" -**Stage**: {stage_width}m × {stage_depth}m = {stage_width * stage_depth}m² -**Total Lots**: {total_lots} -**Unique Lot Types**: {unique_widths} -**Grid Variance**: {variance:+.2f}m {"✅" if abs(variance) < 0.001 else "⚠️"} +**Stage**: {stage_width}m × {stage_depth}m = {stage_width * stage_depth}m² +**Total Lots**: {total_lots} +**Unique Lot Types**: {unique_widths} +**Grid Variance**: {variance:+.2f}m {"✅" if abs(variance) < 0.001 else "⚠️"} """ - - # Convert solution to string for manual editing + manual_edit_string = optimizer.solution_to_string(optimized_solution) - return fig_2d, results_df, summary, report, manual_edit_string - + def update_manual_adjustment(manual_widths_text, stage_width, stage_depth, color_scheme): - """Update visualization based on manual adjustment""" + """Update visualization based on manually entered widths""" optimizer.current_scheme = color_scheme - - # Parse manual widths widths = optimizer.parse_manual_adjustments(manual_widths_text) - if not widths: - return None, "Please enter lot widths (e.g., '14.0, 8.5, 10.5, 8.5, 14.0')" - - # Validate and get feedback + return None, "Please enter lot widths (e.g. '14.0, 8.5, 10.5, 8.5, 14.0')" + solution, feedback = optimizer.validate_manual_solution(widths, stage_width) - if not solution: return None, feedback - - # Calculate variance + total_width = sum(widths) variance = total_width - stage_width - - # Create visualization with variance + fig = optimizer.create_enhanced_visualization( solution, stage_width, stage_depth, - "Manually Adjusted Layout", - show_variance=variance + "Manually Adjusted Layout", show_variance=variance ) - return fig, feedback - - # Create Gradio interface + + def process_uploaded_plan(file_path, scale, auto_detect, confidence): + if not file_path: + return None, pd.DataFrame(), "Please upload a plan file" + preview, lot_data, status = optimizer.process_plan_image( + file_path, scale, auto_detect, confidence + ) + if lot_data: + df = optimizer.lot_data_to_dataframe(lot_data) + return preview, df, status + else: + return preview, pd.DataFrame(), status + + def export_to_csv(df): + if df is None or df.empty: + return gr.update(visible=False), "No data to export" + csv_content = optimizer.export_lot_data_to_csv(df) + return gr.update(value=csv_content, visible=True), "✅ CSV data ready – copy and save as .csv file" + + def send_to_optimizer(df): + if df is None or df.empty: + return 0, 32, "No data to send" + width, depth = optimizer.convert_lot_data_to_stage_format(df) + if width is None: + return 0, 32, "No data to send" + return width, depth, f"✅ Stage dimensions set to {width:.1f}m × {depth:.1f}m\nSwitch to 'AI Optimization' tab to continue" + + def validate_lot_data(df): + if df is None or df.empty: + return "No data to validate" + issues = [] + if df.isnull().any().any(): + issues.append("⚠️ Missing values detected") + if (df['Frontage (m)'] < 6).any(): + issues.append("⚠️ Some lots have frontage < 6m") + if (df['Frontage (m)'] > 30).any(): + issues.append("⚠️ Some lots have frontage > 30m") + if len(df) < 5: + issues.append("ℹ️ Few lots detected – check if all were found") + return "✅ Data looks good! {} lots ready for optimization".format(len(df)) \ + if not issues else "\n".join(issues) + + def add_lot_row(df): + if df is None or df.empty: + new_row = pd.DataFrame({ + "Lot #": ["L1"], + "Frontage (m)": [12.5], + "Depth (m)": [32.0], + "Area (m²)": [12.5 * 32], + "Type": ["Standard"] + }) + return new_row + else: + last_lot_num = len(df) + 1 + new_row = pd.DataFrame({ + "Lot #": [f"L{last_lot_num}"], + "Frontage (m)": [12.5], + "Depth (m)": [32.0], + "Area (m²)": [12.5 * 32], + "Type": ["Standard"] + }) + return pd.concat([df, new_row], ignore_index=True) + + def remove_selected_rows(df, rows_to_remove): + if df is None or df.empty: + return df + if not rows_to_remove: + return df + return df.drop(rows_to_remove, axis=0, errors='ignore').reset_index(drop=True) + with gr.Blocks( - title="Advanced AI Grid Optimizer", + title="Advanced AI Grid Optimizer", theme=gr.themes.Base(), css=""" .gradio-container { @@ -1287,102 +496,95 @@ def create_advanced_app(): """ ) as demo: gr.Markdown(""" - # 🏗️ Advanced AI Grid Cut Optimizer Pro + # 🏗️ Advanced AI Grid Cut Optimizer Pro ### AI-Powered Subdivision Planning with Manual Fine-Tuning """) - - with gr.Tabs() as main_tabs: + + with gr.Tabs(): with gr.TabItem("🤖 AI Optimization"): with gr.Row(): with gr.Column(scale=1): with gr.Group(): gr.Markdown("### 📐 Stage Dimensions") stage_width = gr.Number( - label="Stage Width (m)", + label="Stage Width (m)", value=105.0, info="Width along the street" ) stage_depth = gr.Number( - label="Stage Depth (m)", + label="Stage Depth (m)", value=32.0, info="Depth of lots (perpendicular to street)" ) - + gr.Markdown("### 📏 Lot Width Options") - with gr.Group(): gr.Markdown("**Standard Widths**") with gr.Row(): - enable_8_5 = gr.Checkbox(label="8.5m SLHC", value=True) + enable_8_5 = gr.Checkbox(label="8.5m SLHC", value=True) enable_10_5 = gr.Checkbox(label="10.5m SLHC", value=True) enable_12_5 = gr.Checkbox(label="12.5m", value=True) with gr.Row(): - enable_14 = gr.Checkbox(label="14.0m", value=True) - enable_16 = gr.Checkbox(label="16.0m", value=True) - enable_18 = gr.Checkbox(label="18.0m", value=False) - + enable_14 = gr.Checkbox(label="14.0m", value=True) + enable_16 = gr.Checkbox(label="16.0m", value=True) + enable_18 = gr.Checkbox(label="18.0m", value=False) + with gr.Group(): enable_corners = gr.Checkbox( - label="Enable Corner-Specific Widths", + label="Enable Corner-Specific Widths", value=True, info="Adds variety and helps achieve 100%" ) with gr.Row(): - enable_11 = gr.Checkbox(label="11.0m", value=True) + enable_11 = gr.Checkbox(label="11.0m", value=True) enable_13_3 = gr.Checkbox(label="13.3m", value=True) with gr.Row(): enable_14_8 = gr.Checkbox(label="14.8m", value=True) enable_16_8 = gr.Checkbox(label="16.8m", value=True) - + with gr.Column(scale=1): gr.Markdown("### ⚙️ Advanced Settings") - allow_custom_corners = gr.Checkbox( label="🎯 Allow Flexible Corner Widths", value=True, - info="Enables 13.8m, 13.9m etc. for perfect fits" + info="Enables 13.8m, 13.9m, etc., for perfect fits" ) - optimization_strategy = gr.Radio( ["diversity_focus", "balanced"], label="Optimization Strategy", value="diversity_focus", info="Diversity creates more interesting layouts" ) - color_scheme = gr.Radio( ["modern", "professional", "neon"], label="🎨 Color Scheme", value="neon", info="Neon colors work best with dark background" ) - optimize_btn = gr.Button( - "🚀 Optimize with AI", - variant="primary", + "🚀 Optimize with AI", + variant="primary", size="lg", elem_id="optimize-button" ) - gr.Markdown(""" ### 💡 Quick Tips: - - **Visual Fix**: All lots now align at rear boundary - - **Corner Lots**: Always wider than internals - - **Grid Variance**: Shows if layout is perfect (0.0m) - - **Manual Adjust**: Edit the result below after optimization + - **Visual Fix**: All lots now align at rear boundary + - **Corner Lots**: Always wider than internals + - **Grid Variance**: Shows if layout is perfect (0.0m) + - **Manual Adjust**: Edit the result below after optimization """) - + with gr.Row(): plot_2d = gr.Plot(label="2D Layout with Corner Splays") - - # Manual adjustment section - gr.Markdown("### ✏️ Fine-Tune AI Result") + + gr.Markdown("### ✏️ Fine‐Tune AI Result") with gr.Row(): with gr.Column(scale=2): manual_widths = gr.Textbox( label="Manually Adjust Lot Widths", placeholder="Widths will appear here after optimization", - info="Edit the widths (comma-separated) and click 'Update Layout'", + info="Edit the widths (comma‐separated) and click 'Update Layout'", lines=2 ) with gr.Column(scale=1): @@ -1391,25 +593,41 @@ def create_advanced_app(): value="", label="Adjustment Feedback" ) - + with gr.Row(): results_table = gr.DataFrame(label="Lot Distribution Analysis") - + with gr.Row(): with gr.Column(): summary_output = gr.Markdown(label="Optimization Summary") with gr.Column(): report_output = gr.Markdown(label="Professional Report") - + + optimize_btn.click( + optimize_grid, + inputs=[ + stage_width, stage_depth, + enable_8_5, enable_10_5, enable_12_5, + enable_14, enable_16, enable_18, + enable_corners, enable_11, enable_13_3, + enable_14_8, enable_16_8, + allow_custom_corners, optimization_strategy, color_scheme + ], + outputs=[plot_2d, results_table, summary_output, report_output, manual_widths] + ) + + update_btn.click( + update_manual_adjustment, + inputs=[manual_widths, stage_width, stage_depth, color_scheme], + outputs=[plot_2d, adjustment_feedback] + ) + with gr.TabItem("📊 Plan Reader"): gr.Markdown(""" - ## 🏢 AI Plan Reader + ## 🏢 AI Plan Reader ### Upload your subdivision plan to automatically extract lot information - - **Workflow:** - 1. Upload plan → 2. Review/edit extracted data → 3. Send to optimizer → 4. Optimize layout """) - + with gr.Row(): with gr.Column(scale=1): plan_upload = gr.File( @@ -1417,27 +635,23 @@ def create_advanced_app(): file_types=["image", "pdf"], type="filepath" ) - gr.Markdown(""" **Supported Formats:** - - PDF plans - - PNG/JPG images - - CAD exports - + - PDF plans + - PNG/JPG images + - CAD exports + **Best Results:** - - High resolution (300+ DPI) - - Clear lot numbers - - Visible frontage dimensions - - North arrow included + - High resolution (300+ DPI) + - Clear lot numbers + - Visible frontage dimensions + - North arrow included """) - process_plan_btn = gr.Button( - "🔍 Analyze Plan", + "🔍 Analyze Plan", variant="primary", size="lg" ) - - # Analysis options with gr.Group(): gr.Markdown("**Analysis Settings**") scale_input = gr.Number( @@ -1445,12 +659,10 @@ def create_advanced_app(): value=1000, info="Drawing scale ratio" ) - auto_detect_scale = gr.Checkbox( label="Auto-detect scale from plan", value=True ) - confidence_threshold = gr.Slider( label="Detection Confidence", minimum=0.5, @@ -1459,2262 +671,84 @@ def create_advanced_app(): step=0.05, info="Higher = more accurate but may miss some lots" ) - + with gr.Column(scale=2): - # Preview with annotations plan_preview = gr.Image( label="Analyzed Plan Preview", type="numpy" ) - analysis_status = gr.Markdown( value="Upload a plan to begin analysis", label="Analysis Status" ) - - # Results section + gr.Markdown("### 📊 Extracted Lot Data") - with gr.Row(): extracted_data = gr.DataFrame( headers=["Lot #", "Frontage (m)", "Depth (m)", "Area (m²)", "Type"], label="Detected Lots", interactive=True ) - with gr.Column(): - extraction_summary = gr.Markdown( - label="Extraction Summary" - ) - - export_btn = gr.Button( - "📥 Export to CSV", - variant="secondary" - ) - - send_to_optimizer_btn = gr.Button( - "➡️ Send to Optimizer", - variant="primary" - ) - - # Manual correction section + extraction_summary = gr.Markdown(label="Extraction Summary") + export_btn = gr.Button("📥 Export to CSV", variant="secondary") + send_to_optimizer_btn = gr.Button("➡️ Send to Optimizer", variant="primary") + gr.Markdown("### ✏️ Manual Corrections") with gr.Row(): with gr.Column(): gr.Markdown(""" **Quick Edit Tools:** - - Double-click cells to edit - - Add missing lots manually - - Correct misread numbers - - Adjust frontages + - Double‐click cells to edit + - Add missing lots manually + - Correct misread numbers + - Adjust frontages """) - add_lot_btn = gr.Button("➕ Add Lot", size="sm") - + remove_selected_btn = gr.Button("➖ Remove Selected", size="sm") with gr.Column(): - validation_result = gr.Markdown( - label="Data Validation" - ) - - export_output = gr.Textbox( - label="CSV Export (Copy and save as .csv file)", - lines=10, - visible=False - ) - - # Wire up the buttons - optimize_btn.click( - optimize_grid, - inputs=[ - stage_width, - stage_depth, - enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18, - enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8, - allow_custom_corners, optimization_strategy, color_scheme - ], - outputs=[plot_2d, results_table, summary_output, report_output, manual_widths] - ) - - update_btn.click( - update_manual_adjustment, - inputs=[manual_widths, stage_width, stage_depth, color_scheme], - outputs=[plot_2d, adjustment_feedback] - ) - - # Plan reader functions - def process_uploaded_plan(file_path, scale, auto_detect, confidence): - if not file_path: - return None, pd.DataFrame(), "Please upload a plan file" - - preview, lot_data, status = optimizer.process_plan_image( - file_path, scale, auto_detect, confidence - ) - - if lot_data: - df = optimizer.lot_data_to_dataframe(lot_data) - return preview, df, status - else: - return preview, pd.DataFrame(), status - - def export_to_csv(df): - if df is None or df.empty: - return gr.update(visible=False), "No data to export" - - csv_content = optimizer.export_lot_data_to_csv(df) - return gr.update(value=csv_content, visible=True), "✅ CSV data ready - copy and save as .csv file" - - def send_to_optimizer(df): - if df is None or df.empty: - return 0, 32, "No data to send" - - width, depth = optimizer.convert_lot_data_to_stage_format(df) - return width, depth, f"✅ Stage dimensions set to {width:.1f}m × {depth:.1f}m\nSwitch to 'AI Optimization' tab to continue" - - def validate_lot_data(df): - if df is None or df.empty: - return "No data to validate" - - # Check for common issues - issues = [] - - # Check for missing values - if df.isnull().any().any(): - issues.append("⚠️ Missing values detected") - - # Check for unrealistic dimensions - if (df['Frontage (m)'] < 6).any(): - issues.append("⚠️ Some lots have frontage < 6m") - if (df['Frontage (m)'] > 30).any(): - issues.append("⚠️ Some lots have frontage > 30m") - - # Check total lots - total_lots = len(df) - if total_lots < 5: - issues.append("ℹ️ Few lots detected - check if all were found") - - if not issues: - return f"✅ Data looks good! {total_lots} lots ready for optimization" - else: - return "\n".join(issues) - - def add_lot_row(df): - if df is None or df.empty: - new_row = pd.DataFrame({ - "Lot #": ["L1"], - "Frontage (m)": [12.5], - "Depth (m)": [32.0], - "Area (m²)": [400.0], - "Type": ["Standard"] - }) - return new_row - else: - last_lot_num = len(df) + 1 - new_row = pd.DataFrame({ - "Lot #": [f"L{last_lot_num}"], - "Frontage (m)": [12.5], - "Depth (m)": [32.0], - "Area (m²)": [400.0], - "Type": ["Standard"] - }) - return pd.concat([df, new_row], ignore_index=True) - - process_plan_btn.click( - process_uploaded_plan, - inputs=[plan_upload, scale_input, auto_detect_scale, confidence_threshold], - outputs=[plan_preview, extracted_data, analysis_status] - ) - - export_btn.click( - export_to_csv, - inputs=[extracted_data], - outputs=[export_output, extraction_summary] - ) - - send_to_optimizer_btn.click( - send_to_optimizer, - inputs=[extracted_data], - outputs=[stage_width, stage_depth, extraction_summary] - ) - - extracted_data.change( - validate_lot_data, - inputs=[extracted_data], - outputs=[validation_result] - ) - - add_lot_btn.click( - add_lot_row, - inputs=[extracted_data], - outputs=[extracted_data] - ) - - return demo + validation_result = gr.Markdown(label="Data Validation") -# Create and launch -if __name__ == "__main__": - app = create_advanced_app() - app.launch(), text_val) or re.match(r'^L\d+ + process_plan_btn.click( + process_uploaded_plan, + inputs=[plan_upload, scale_input, auto_detect_scale, confidence_threshold], + outputs=[plan_preview, extracted_data, analysis_status] + ) -def create_advanced_app(): - optimizer = AdvancedGridOptimizer() - - def optimize_grid( - stage_width, - stage_depth, - enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18, - enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8, - allow_custom_corners, optimization_strategy, color_scheme - ): - # Update color scheme - optimizer.current_scheme = color_scheme - - # Collect enabled widths - enabled_widths = [] - if enable_8_5: enabled_widths.append(8.5) - if enable_10_5: enabled_widths.append(10.5) - if enable_12_5: enabled_widths.append(12.5) - if enable_14: enabled_widths.append(14.0) - if enable_16: enabled_widths.append(16.0) - if enable_18: enabled_widths.append(18.0) - - if enable_corners: - if enable_11: enabled_widths.append(11.0) - if enable_13_3: enabled_widths.append(13.3) - if enable_14_8: enabled_widths.append(14.8) - if enable_16_8: enabled_widths.append(16.8) - - if not enabled_widths: - return None, None, pd.DataFrame(), "Please select at least one lot width!", "", "" - - # Run optimization based on strategy - if optimization_strategy == "diversity_focus": - optimized_solution = optimizer.optimize_with_flexible_corners( - stage_width, enabled_widths, allow_custom_corners - ) - else: # balanced approach - optimized_solution = optimizer.optimize_with_corners_diverse( - stage_width, enabled_widths, None - ) - - # Store current solution for manual adjustment - optimizer.current_solution = optimized_solution - - # Calculate variance for display - if optimized_solution: - total_width = sum(w for w, _ in optimized_solution) - variance = total_width - stage_width - else: - variance = None - - # Verify solution - if not optimized_solution or abs(sum(w for w, _ in optimized_solution) - stage_width) > 0.001: - # Provide suggestions - return None, pd.DataFrame(), f""" -### ❌ Cannot achieve 100% usage with selected widths + export_btn.click( + export_to_csv, + inputs=[extracted_data], + outputs=[gr.Textbox(label="CSV Export (copy & save)", lines=8, visible=False), + extraction_summary] + ) -**Stage Width**: {stage_width}m -**Available Widths**: {', '.join([f"{w}m" for w in sorted(enabled_widths)])} + send_to_optimizer_btn.click( + send_to_optimizer, + inputs=[extracted_data], + outputs=[stage_width, stage_depth, extraction_summary] + ) -**Try:** -1. Enable more lot types for flexibility -2. Enable "Custom Corners" option -3. Try common stage widths: 84m, 105m, 126m -""", "", "" - - # Create visualizations with variance indicator - fig_2d = optimizer.create_enhanced_visualization( - optimized_solution, stage_width, stage_depth, - "AI-Optimized Diverse Subdivision Layout", - show_variance=variance - ) - - # Create results table - width_counts = {} - for width, lot_type in optimized_solution: - key = f"{width:.1f}m" - if key in width_counts: - width_counts[key]['count'] += 1 - else: - # Handle both standard and custom widths - if width in optimizer.lot_specifications: - spec = optimizer.lot_specifications[width] - elif int(width) in optimizer.lot_specifications: - spec = optimizer.lot_specifications[int(width)] - else: - # Custom width - find closest - closest = min(optimizer.lot_specifications.keys(), - key=lambda x: abs(x - width)) - spec = optimizer.lot_specifications[closest] - spec = {**spec, 'type': 'Custom', 'squares': 'Custom'} - - width_counts[key] = { - 'count': 1, - 'type': spec.get('type', 'Custom'), - 'squares': spec.get('squares', 'N/A'), - 'area': width * stage_depth - } - - results_data = [] - for width, info in sorted(width_counts.items()): - results_data.append({ - 'Lot Width': width, - 'Count': info['count'], - 'Type': info['type'], - 'Area Each': f"{info['area']:.0f}m²", - 'Total Width': f"{float(width[:-1]) * info['count']:.1f}m", - 'Total Area': f"{info['area'] * info['count']:.0f}m²" - }) - - results_df = pd.DataFrame(results_data) - - # Generate report - report = optimizer.generate_report(optimized_solution, stage_width, stage_depth, None) - - # Create summary - total_lots = len(optimized_solution) - unique_widths = len(set(w for w, _ in optimized_solution)) - - # Count SLHC pairs - slhc_pairs = sum(1 for i in range(len(optimized_solution) - 1) - if optimized_solution[i][0] <= 10.5 and optimized_solution[i+1][0] <= 10.5) - - # Analyze corners - corner_info = "N/A" - if len(optimized_solution) >= 2: - first = optimized_solution[0][0] - last = optimized_solution[-1][0] - diff = abs(first - last) - - if diff < 0.1: - corner_info = f"✨ PERFECT ({first:.1f}m × 2)" - elif diff <= 1.0: - corner_info = f"✅ Excellent ({first:.1f}m + {last:.1f}m)" - elif diff <= 2.0: - corner_info = f"👍 Good ({first:.1f}m + {last:.1f}m)" - else: - corner_info = f"⚠️ Unbalanced ({first:.1f}m + {last:.1f}m)" - - summary = f""" -**Stage**: {stage_width}m × {stage_depth}m = {stage_width * stage_depth}m² -**Total Lots**: {total_lots} -**Unique Lot Types**: {unique_widths} -**Grid Variance**: {variance:+.2f}m {"✅" if abs(variance) < 0.001 else "⚠️"} -""" - - # Convert solution to string for manual editing - manual_edit_string = optimizer.solution_to_string(optimized_solution) - - return fig_2d, results_df, summary, report, manual_edit_string - - def update_manual_adjustment(manual_widths_text, stage_width, stage_depth, color_scheme): - """Update visualization based on manual adjustment""" - optimizer.current_scheme = color_scheme - - # Parse manual widths - widths = optimizer.parse_manual_adjustments(manual_widths_text) - - if not widths: - return None, "Please enter lot widths (e.g., '14.0, 8.5, 10.5, 8.5, 14.0')" - - # Validate and get feedback - solution, feedback = optimizer.validate_manual_solution(widths, stage_width) - - if not solution: - return None, feedback - - # Calculate variance - total_width = sum(widths) - variance = total_width - stage_width - - # Create visualization with variance - fig = optimizer.create_enhanced_visualization( - solution, stage_width, stage_depth, - "Manually Adjusted Layout", - show_variance=variance - ) - - return fig, feedback - - # Create Gradio interface - with gr.Blocks( - title="Advanced AI Grid Optimizer", - theme=gr.themes.Base(), - css=""" - .gradio-container { - font-family: 'Segoe UI', sans-serif; - background: #1a1a1a; - color: white; - } - .gr-button-primary { - background: linear-gradient(45deg, #FF073A 30%, #0AEFFF 90%); - border: none; - box-shadow: 0 3px 5px 2px rgba(255, 7, 58, .3); - } - h1 { - background: linear-gradient(45deg, #FF073A, #0AEFFF); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - text-align: center; - font-size: 2.5em; - } - .gr-form { - background: rgba(42, 42, 42, 0.9); - border-radius: 10px; - padding: 20px; - border: 1px solid #444; - } - .gr-input { - background-color: #2a2a2a; - color: white; - border: 1px solid #444; - } - .gr-check-radio { - background-color: #2a2a2a; - } - """ - ) as demo: - gr.Markdown(""" - # 🏗️ Advanced AI Grid Cut Optimizer Pro - ### AI-Powered Subdivision Planning with Manual Fine-Tuning - """) - - with gr.Tabs(): - with gr.TabItem("🤖 AI Optimization"): - with gr.Row(): - with gr.Column(scale=1): - with gr.Group(): - gr.Markdown("### 📐 Stage Dimensions") - stage_width = gr.Number( - label="Stage Width (m)", - value=105.0, - info="Width along the street" - ) - stage_depth = gr.Number( - label="Stage Depth (m)", - value=32.0, - info="Depth of lots (perpendicular to street)" - ) - - gr.Markdown("### 📏 Lot Width Options") - - with gr.Group(): - gr.Markdown("**Standard Widths**") - with gr.Row(): - enable_8_5 = gr.Checkbox(label="8.5m SLHC", value=True) - enable_10_5 = gr.Checkbox(label="10.5m SLHC", value=True) - enable_12_5 = gr.Checkbox(label="12.5m", value=True) - with gr.Row(): - enable_14 = gr.Checkbox(label="14.0m", value=True) - enable_16 = gr.Checkbox(label="16.0m", value=True) - enable_18 = gr.Checkbox(label="18.0m", value=False) - - with gr.Group(): - enable_corners = gr.Checkbox( - label="Enable Corner-Specific Widths", - value=True, - info="Adds variety and helps achieve 100%" - ) - with gr.Row(): - enable_11 = gr.Checkbox(label="11.0m", value=True) - enable_13_3 = gr.Checkbox(label="13.3m", value=True) - with gr.Row(): - enable_14_8 = gr.Checkbox(label="14.8m", value=True) - enable_16_8 = gr.Checkbox(label="16.8m", value=True) - - with gr.Column(scale=1): - gr.Markdown("### ⚙️ Advanced Settings") - - allow_custom_corners = gr.Checkbox( - label="🎯 Allow Flexible Corner Widths", - value=True, - info="Enables 13.8m, 13.9m etc. for perfect fits" - ) - - optimization_strategy = gr.Radio( - ["diversity_focus", "balanced"], - label="Optimization Strategy", - value="diversity_focus", - info="Diversity creates more interesting layouts" - ) - - color_scheme = gr.Radio( - ["modern", "professional", "neon"], - label="🎨 Color Scheme", - value="neon", - info="Neon colors work best with dark background" - ) - - optimize_btn = gr.Button( - "🚀 Optimize with AI", - variant="primary", - size="lg", - elem_id="optimize-button" - ) - - gr.Markdown(""" - ### 💡 Quick Tips: - - **Visual Fix**: All lots now align at rear boundary - - **Corner Lots**: Always wider than internals - - **Grid Variance**: Shows if layout is perfect (0.0m) - - **Manual Adjust**: Edit the result below after optimization - """) - - with gr.Row(): - plot_2d = gr.Plot(label="2D Layout with Corner Splays") - - # Manual adjustment section - gr.Markdown("### ✏️ Fine-Tune AI Result") - with gr.Row(): - with gr.Column(scale=2): - manual_widths = gr.Textbox( - label="Manually Adjust Lot Widths", - placeholder="Widths will appear here after optimization", - info="Edit the widths (comma-separated) and click 'Update Layout'", - lines=2 - ) - with gr.Column(scale=1): - update_btn = gr.Button("🔄 Update Layout", variant="secondary") - adjustment_feedback = gr.Markdown( - value="", - label="Adjustment Feedback" - ) - - with gr.Row(): - results_table = gr.DataFrame(label="Lot Distribution Analysis") - - with gr.Row(): - with gr.Column(): - summary_output = gr.Markdown(label="Optimization Summary") - with gr.Column(): - report_output = gr.Markdown(label="Professional Report") - - with gr.TabItem("📊 Plan Reader"): - gr.Markdown(""" - ## 🏢 AI Plan Reader - ### Upload your subdivision plan to automatically extract lot information - """) - - with gr.Row(): - with gr.Column(scale=1): - plan_upload = gr.File( - label="Upload Subdivision Plan", - file_types=["image", "pdf"], - type="filepath" - ) - - gr.Markdown(""" - **Supported Formats:** - - PDF plans - - PNG/JPG images - - CAD exports - - **Best Results:** - - High resolution (300+ DPI) - - Clear lot numbers - - Visible frontage dimensions - - North arrow included - """) - - process_plan_btn = gr.Button( - "🔍 Analyze Plan", - variant="primary", - size="lg" - ) - - # Analysis options - with gr.Group(): - gr.Markdown("**Analysis Settings**") - scale_input = gr.Number( - label="Scale (1:X)", - value=1000, - info="Drawing scale ratio" - ) - - auto_detect_scale = gr.Checkbox( - label="Auto-detect scale from plan", - value=True - ) - - confidence_threshold = gr.Slider( - label="Detection Confidence", - minimum=0.5, - maximum=0.95, - value=0.75, - step=0.05, - info="Higher = more accurate but may miss some lots" - ) - - with gr.Column(scale=2): - # Preview with annotations - plan_preview = gr.Image( - label="Analyzed Plan Preview", - type="numpy" - ) - - analysis_status = gr.Markdown( - value="Upload a plan to begin analysis", - label="Analysis Status" - ) - - # Results section - gr.Markdown("### 📊 Extracted Lot Data") - - with gr.Row(): - extracted_data = gr.DataFrame( - headers=["Lot #", "Frontage (m)", "Depth (m)", "Area (m²)", "Type"], - label="Detected Lots", - interactive=True - ) - - with gr.Column(): - extraction_summary = gr.Markdown( - label="Extraction Summary" - ) - - export_btn = gr.Button( - "📥 Export to CSV", - variant="secondary" - ) - - send_to_optimizer_btn = gr.Button( - "➡️ Send to Optimizer", - variant="primary" - ) - - # Manual correction section - gr.Markdown("### ✏️ Manual Corrections") - with gr.Row(): - with gr.Column(): - gr.Markdown(""" - **Quick Edit Tools:** - - Double-click cells to edit - - Add missing lots manually - - Correct misread numbers - - Adjust frontages - """) - - add_lot_btn = gr.Button("➕ Add Lot", size="sm") - remove_selected_btn = gr.Button("➖ Remove Selected", size="sm") - - with gr.Column(): - validation_result = gr.Markdown( - label="Data Validation" - ) - - # Wire up the buttons - optimize_btn.click( - optimize_grid, - inputs=[ - stage_width, - stage_depth, - enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18, - enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8, - allow_custom_corners, optimization_strategy, color_scheme - ], - outputs=[plot_2d, results_table, summary_output, report_output, manual_widths] - ) - - update_btn.click( - update_manual_adjustment, - inputs=[manual_widths, stage_width, stage_depth, color_scheme], - outputs=[plot_2d, adjustment_feedback] - ) - - return demo - -# Create and launch -if __name__ == "__main__": - app = create_advanced_app() - app.launch(), text_val): - text_elements.append({ - 'text': text_val, - 'x': data['left'][i], - 'y': data['top'][i], - 'w': data['width'][i], - 'h': data['height'][i] - }) - - return text_elements - except: - return [] - - def match_lots_with_dimensions(self, lots, text_data, scale, auto_detect_scale): - """Match detected lots with their dimensions and numbers""" - lot_info = [] - - # Simple matching based on proximity - for i, lot in enumerate(lots): - x, y, w, h = lot['bbox'] - lot_center = (x + w/2, y + h/2) - - # Find nearby text - lot_number = None - frontage = None - depth = None - - for text in text_data: - text_center = (text['x'] + text['w']/2, text['y'] + text['h']/2) - distance = np.sqrt((lot_center[0] - text_center[0])**2 + - (lot_center[1] - text_center[1])**2) - - # If text is close to lot - if distance < max(w, h) * 0.5: - text_val = text['text'] - - # Check if it's a lot number - if re.match(r'^L\d+ - -def create_advanced_app(): - optimizer = AdvancedGridOptimizer() - - def optimize_grid( - stage_width, - stage_depth, - enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18, - enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8, - allow_custom_corners, optimization_strategy, color_scheme - ): - # Update color scheme - optimizer.current_scheme = color_scheme - - # Collect enabled widths - enabled_widths = [] - if enable_8_5: enabled_widths.append(8.5) - if enable_10_5: enabled_widths.append(10.5) - if enable_12_5: enabled_widths.append(12.5) - if enable_14: enabled_widths.append(14.0) - if enable_16: enabled_widths.append(16.0) - if enable_18: enabled_widths.append(18.0) - - if enable_corners: - if enable_11: enabled_widths.append(11.0) - if enable_13_3: enabled_widths.append(13.3) - if enable_14_8: enabled_widths.append(14.8) - if enable_16_8: enabled_widths.append(16.8) - - if not enabled_widths: - return None, None, pd.DataFrame(), "Please select at least one lot width!", "", "" - - # Run optimization based on strategy - if optimization_strategy == "diversity_focus": - optimized_solution = optimizer.optimize_with_flexible_corners( - stage_width, enabled_widths, allow_custom_corners - ) - else: # balanced approach - optimized_solution = optimizer.optimize_with_corners_diverse( - stage_width, enabled_widths, None - ) - - # Store current solution for manual adjustment - optimizer.current_solution = optimized_solution - - # Calculate variance for display - if optimized_solution: - total_width = sum(w for w, _ in optimized_solution) - variance = total_width - stage_width - else: - variance = None - - # Verify solution - if not optimized_solution or abs(sum(w for w, _ in optimized_solution) - stage_width) > 0.001: - # Provide suggestions - return None, pd.DataFrame(), f""" -### ❌ Cannot achieve 100% usage with selected widths - -**Stage Width**: {stage_width}m -**Available Widths**: {', '.join([f"{w}m" for w in sorted(enabled_widths)])} - -**Try:** -1. Enable more lot types for flexibility -2. Enable "Custom Corners" option -3. Try common stage widths: 84m, 105m, 126m -""", "", "" - - # Create visualizations with variance indicator - fig_2d = optimizer.create_enhanced_visualization( - optimized_solution, stage_width, stage_depth, - "AI-Optimized Diverse Subdivision Layout", - show_variance=variance - ) - - # Create results table - width_counts = {} - for width, lot_type in optimized_solution: - key = f"{width:.1f}m" - if key in width_counts: - width_counts[key]['count'] += 1 - else: - # Handle both standard and custom widths - if width in optimizer.lot_specifications: - spec = optimizer.lot_specifications[width] - elif int(width) in optimizer.lot_specifications: - spec = optimizer.lot_specifications[int(width)] - else: - # Custom width - find closest - closest = min(optimizer.lot_specifications.keys(), - key=lambda x: abs(x - width)) - spec = optimizer.lot_specifications[closest] - spec = {**spec, 'type': 'Custom', 'squares': 'Custom'} - - width_counts[key] = { - 'count': 1, - 'type': spec.get('type', 'Custom'), - 'squares': spec.get('squares', 'N/A'), - 'area': width * stage_depth - } - - results_data = [] - for width, info in sorted(width_counts.items()): - results_data.append({ - 'Lot Width': width, - 'Count': info['count'], - 'Type': info['type'], - 'Area Each': f"{info['area']:.0f}m²", - 'Total Width': f"{float(width[:-1]) * info['count']:.1f}m", - 'Total Area': f"{info['area'] * info['count']:.0f}m²" - }) - - results_df = pd.DataFrame(results_data) - - # Generate report - report = optimizer.generate_report(optimized_solution, stage_width, stage_depth, None) - - # Create summary - total_lots = len(optimized_solution) - unique_widths = len(set(w for w, _ in optimized_solution)) - - # Count SLHC pairs - slhc_pairs = sum(1 for i in range(len(optimized_solution) - 1) - if optimized_solution[i][0] <= 10.5 and optimized_solution[i+1][0] <= 10.5) - - # Analyze corners - corner_info = "N/A" - if len(optimized_solution) >= 2: - first = optimized_solution[0][0] - last = optimized_solution[-1][0] - diff = abs(first - last) - - if diff < 0.1: - corner_info = f"✨ PERFECT ({first:.1f}m × 2)" - elif diff <= 1.0: - corner_info = f"✅ Excellent ({first:.1f}m + {last:.1f}m)" - elif diff <= 2.0: - corner_info = f"👍 Good ({first:.1f}m + {last:.1f}m)" - else: - corner_info = f"⚠️ Unbalanced ({first:.1f}m + {last:.1f}m)" - - summary = f""" -**Stage**: {stage_width}m × {stage_depth}m = {stage_width * stage_depth}m² -**Total Lots**: {total_lots} -**Unique Lot Types**: {unique_widths} -**Grid Variance**: {variance:+.2f}m {"✅" if abs(variance) < 0.001 else "⚠️"} -""" - - # Convert solution to string for manual editing - manual_edit_string = optimizer.solution_to_string(optimized_solution) - - return fig_2d, results_df, summary, report, manual_edit_string - - def update_manual_adjustment(manual_widths_text, stage_width, stage_depth, color_scheme): - """Update visualization based on manual adjustment""" - optimizer.current_scheme = color_scheme - - # Parse manual widths - widths = optimizer.parse_manual_adjustments(manual_widths_text) - - if not widths: - return None, "Please enter lot widths (e.g., '14.0, 8.5, 10.5, 8.5, 14.0')" - - # Validate and get feedback - solution, feedback = optimizer.validate_manual_solution(widths, stage_width) - - if not solution: - return None, feedback - - # Calculate variance - total_width = sum(widths) - variance = total_width - stage_width - - # Create visualization with variance - fig = optimizer.create_enhanced_visualization( - solution, stage_width, stage_depth, - "Manually Adjusted Layout", - show_variance=variance - ) - - return fig, feedback - - # Create Gradio interface - with gr.Blocks( - title="Advanced AI Grid Optimizer", - theme=gr.themes.Base(), - css=""" - .gradio-container { - font-family: 'Segoe UI', sans-serif; - background: #1a1a1a; - color: white; - } - .gr-button-primary { - background: linear-gradient(45deg, #FF073A 30%, #0AEFFF 90%); - border: none; - box-shadow: 0 3px 5px 2px rgba(255, 7, 58, .3); - } - h1 { - background: linear-gradient(45deg, #FF073A, #0AEFFF); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - text-align: center; - font-size: 2.5em; - } - .gr-form { - background: rgba(42, 42, 42, 0.9); - border-radius: 10px; - padding: 20px; - border: 1px solid #444; - } - .gr-input { - background-color: #2a2a2a; - color: white; - border: 1px solid #444; - } - .gr-check-radio { - background-color: #2a2a2a; - } - """ - ) as demo: - gr.Markdown(""" - # 🏗️ Advanced AI Grid Cut Optimizer Pro - ### AI-Powered Subdivision Planning with Manual Fine-Tuning - """) - - with gr.Tabs(): - with gr.TabItem("🤖 AI Optimization"): - with gr.Row(): - with gr.Column(scale=1): - with gr.Group(): - gr.Markdown("### 📐 Stage Dimensions") - stage_width = gr.Number( - label="Stage Width (m)", - value=105.0, - info="Width along the street" - ) - stage_depth = gr.Number( - label="Stage Depth (m)", - value=32.0, - info="Depth of lots (perpendicular to street)" - ) - - gr.Markdown("### 📏 Lot Width Options") - - with gr.Group(): - gr.Markdown("**Standard Widths**") - with gr.Row(): - enable_8_5 = gr.Checkbox(label="8.5m SLHC", value=True) - enable_10_5 = gr.Checkbox(label="10.5m SLHC", value=True) - enable_12_5 = gr.Checkbox(label="12.5m", value=True) - with gr.Row(): - enable_14 = gr.Checkbox(label="14.0m", value=True) - enable_16 = gr.Checkbox(label="16.0m", value=True) - enable_18 = gr.Checkbox(label="18.0m", value=False) - - with gr.Group(): - enable_corners = gr.Checkbox( - label="Enable Corner-Specific Widths", - value=True, - info="Adds variety and helps achieve 100%" - ) - with gr.Row(): - enable_11 = gr.Checkbox(label="11.0m", value=True) - enable_13_3 = gr.Checkbox(label="13.3m", value=True) - with gr.Row(): - enable_14_8 = gr.Checkbox(label="14.8m", value=True) - enable_16_8 = gr.Checkbox(label="16.8m", value=True) - - with gr.Column(scale=1): - gr.Markdown("### ⚙️ Advanced Settings") - - allow_custom_corners = gr.Checkbox( - label="🎯 Allow Flexible Corner Widths", - value=True, - info="Enables 13.8m, 13.9m etc. for perfect fits" - ) - - optimization_strategy = gr.Radio( - ["diversity_focus", "balanced"], - label="Optimization Strategy", - value="diversity_focus", - info="Diversity creates more interesting layouts" - ) - - color_scheme = gr.Radio( - ["modern", "professional", "neon"], - label="🎨 Color Scheme", - value="neon", - info="Neon colors work best with dark background" - ) - - optimize_btn = gr.Button( - "🚀 Optimize with AI", - variant="primary", - size="lg", - elem_id="optimize-button" - ) - - gr.Markdown(""" - ### 💡 Quick Tips: - - **Visual Fix**: All lots now align at rear boundary - - **Corner Lots**: Always wider than internals - - **Grid Variance**: Shows if layout is perfect (0.0m) - - **Manual Adjust**: Edit the result below after optimization - """) - - with gr.Row(): - plot_2d = gr.Plot(label="2D Layout with Corner Splays") - - # Manual adjustment section - gr.Markdown("### ✏️ Fine-Tune AI Result") - with gr.Row(): - with gr.Column(scale=2): - manual_widths = gr.Textbox( - label="Manually Adjust Lot Widths", - placeholder="Widths will appear here after optimization", - info="Edit the widths (comma-separated) and click 'Update Layout'", - lines=2 - ) - with gr.Column(scale=1): - update_btn = gr.Button("🔄 Update Layout", variant="secondary") - adjustment_feedback = gr.Markdown( - value="", - label="Adjustment Feedback" - ) - - with gr.Row(): - results_table = gr.DataFrame(label="Lot Distribution Analysis") - - with gr.Row(): - with gr.Column(): - summary_output = gr.Markdown(label="Optimization Summary") - with gr.Column(): - report_output = gr.Markdown(label="Professional Report") - - with gr.TabItem("📊 Plan Reader"): - gr.Markdown(""" - ## 🏢 AI Plan Reader - ### Upload your subdivision plan to automatically extract lot information - """) - - with gr.Row(): - with gr.Column(scale=1): - plan_upload = gr.File( - label="Upload Subdivision Plan", - file_types=["image", "pdf"], - type="filepath" - ) - - gr.Markdown(""" - **Supported Formats:** - - PDF plans - - PNG/JPG images - - CAD exports - - **Best Results:** - - High resolution (300+ DPI) - - Clear lot numbers - - Visible frontage dimensions - - North arrow included - """) - - process_plan_btn = gr.Button( - "🔍 Analyze Plan", - variant="primary", - size="lg" - ) - - # Analysis options - with gr.Group(): - gr.Markdown("**Analysis Settings**") - scale_input = gr.Number( - label="Scale (1:X)", - value=1000, - info="Drawing scale ratio" - ) - - auto_detect_scale = gr.Checkbox( - label="Auto-detect scale from plan", - value=True - ) - - confidence_threshold = gr.Slider( - label="Detection Confidence", - minimum=0.5, - maximum=0.95, - value=0.75, - step=0.05, - info="Higher = more accurate but may miss some lots" - ) - - with gr.Column(scale=2): - # Preview with annotations - plan_preview = gr.Image( - label="Analyzed Plan Preview", - type="numpy" - ) - - analysis_status = gr.Markdown( - value="Upload a plan to begin analysis", - label="Analysis Status" - ) - - # Results section - gr.Markdown("### 📊 Extracted Lot Data") - - with gr.Row(): - extracted_data = gr.DataFrame( - headers=["Lot #", "Frontage (m)", "Depth (m)", "Area (m²)", "Type"], - label="Detected Lots", - interactive=True - ) - - with gr.Column(): - extraction_summary = gr.Markdown( - label="Extraction Summary" - ) - - export_btn = gr.Button( - "📥 Export to CSV", - variant="secondary" - ) - - send_to_optimizer_btn = gr.Button( - "➡️ Send to Optimizer", - variant="primary" - ) - - # Manual correction section - gr.Markdown("### ✏️ Manual Corrections") - with gr.Row(): - with gr.Column(): - gr.Markdown(""" - **Quick Edit Tools:** - - Double-click cells to edit - - Add missing lots manually - - Correct misread numbers - - Adjust frontages - """) - - add_lot_btn = gr.Button("➕ Add Lot", size="sm") - remove_selected_btn = gr.Button("➖ Remove Selected", size="sm") - - with gr.Column(): - validation_result = gr.Markdown( - label="Data Validation" - ) - - # Wire up the buttons - optimize_btn.click( - optimize_grid, - inputs=[ - stage_width, - stage_depth, - enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18, - enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8, - allow_custom_corners, optimization_strategy, color_scheme - ], - outputs=[plot_2d, results_table, summary_output, report_output, manual_widths] - ) - - update_btn.click( - update_manual_adjustment, - inputs=[manual_widths, stage_width, stage_depth, color_scheme], - outputs=[plot_2d, adjustment_feedback] - ) - - return demo - -# Create and launch -if __name__ == "__main__": - app = create_advanced_app() - app.launch(), text_val): - lot_number = text_val - # Check if it's a dimension - elif re.match(r'^\d+\.?\d*m? - -def create_advanced_app(): - optimizer = AdvancedGridOptimizer() - - def optimize_grid( - stage_width, - stage_depth, - enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18, - enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8, - allow_custom_corners, optimization_strategy, color_scheme - ): - # Update color scheme - optimizer.current_scheme = color_scheme - - # Collect enabled widths - enabled_widths = [] - if enable_8_5: enabled_widths.append(8.5) - if enable_10_5: enabled_widths.append(10.5) - if enable_12_5: enabled_widths.append(12.5) - if enable_14: enabled_widths.append(14.0) - if enable_16: enabled_widths.append(16.0) - if enable_18: enabled_widths.append(18.0) - - if enable_corners: - if enable_11: enabled_widths.append(11.0) - if enable_13_3: enabled_widths.append(13.3) - if enable_14_8: enabled_widths.append(14.8) - if enable_16_8: enabled_widths.append(16.8) - - if not enabled_widths: - return None, None, pd.DataFrame(), "Please select at least one lot width!", "", "" - - # Run optimization based on strategy - if optimization_strategy == "diversity_focus": - optimized_solution = optimizer.optimize_with_flexible_corners( - stage_width, enabled_widths, allow_custom_corners - ) - else: # balanced approach - optimized_solution = optimizer.optimize_with_corners_diverse( - stage_width, enabled_widths, None - ) - - # Store current solution for manual adjustment - optimizer.current_solution = optimized_solution - - # Calculate variance for display - if optimized_solution: - total_width = sum(w for w, _ in optimized_solution) - variance = total_width - stage_width - else: - variance = None - - # Verify solution - if not optimized_solution or abs(sum(w for w, _ in optimized_solution) - stage_width) > 0.001: - # Provide suggestions - return None, pd.DataFrame(), f""" -### ❌ Cannot achieve 100% usage with selected widths - -**Stage Width**: {stage_width}m -**Available Widths**: {', '.join([f"{w}m" for w in sorted(enabled_widths)])} - -**Try:** -1. Enable more lot types for flexibility -2. Enable "Custom Corners" option -3. Try common stage widths: 84m, 105m, 126m -""", "", "" - - # Create visualizations with variance indicator - fig_2d = optimizer.create_enhanced_visualization( - optimized_solution, stage_width, stage_depth, - "AI-Optimized Diverse Subdivision Layout", - show_variance=variance - ) - - # Create results table - width_counts = {} - for width, lot_type in optimized_solution: - key = f"{width:.1f}m" - if key in width_counts: - width_counts[key]['count'] += 1 - else: - # Handle both standard and custom widths - if width in optimizer.lot_specifications: - spec = optimizer.lot_specifications[width] - elif int(width) in optimizer.lot_specifications: - spec = optimizer.lot_specifications[int(width)] - else: - # Custom width - find closest - closest = min(optimizer.lot_specifications.keys(), - key=lambda x: abs(x - width)) - spec = optimizer.lot_specifications[closest] - spec = {**spec, 'type': 'Custom', 'squares': 'Custom'} - - width_counts[key] = { - 'count': 1, - 'type': spec.get('type', 'Custom'), - 'squares': spec.get('squares', 'N/A'), - 'area': width * stage_depth - } - - results_data = [] - for width, info in sorted(width_counts.items()): - results_data.append({ - 'Lot Width': width, - 'Count': info['count'], - 'Type': info['type'], - 'Area Each': f"{info['area']:.0f}m²", - 'Total Width': f"{float(width[:-1]) * info['count']:.1f}m", - 'Total Area': f"{info['area'] * info['count']:.0f}m²" - }) - - results_df = pd.DataFrame(results_data) - - # Generate report - report = optimizer.generate_report(optimized_solution, stage_width, stage_depth, None) - - # Create summary - total_lots = len(optimized_solution) - unique_widths = len(set(w for w, _ in optimized_solution)) - - # Count SLHC pairs - slhc_pairs = sum(1 for i in range(len(optimized_solution) - 1) - if optimized_solution[i][0] <= 10.5 and optimized_solution[i+1][0] <= 10.5) - - # Analyze corners - corner_info = "N/A" - if len(optimized_solution) >= 2: - first = optimized_solution[0][0] - last = optimized_solution[-1][0] - diff = abs(first - last) - - if diff < 0.1: - corner_info = f"✨ PERFECT ({first:.1f}m × 2)" - elif diff <= 1.0: - corner_info = f"✅ Excellent ({first:.1f}m + {last:.1f}m)" - elif diff <= 2.0: - corner_info = f"👍 Good ({first:.1f}m + {last:.1f}m)" - else: - corner_info = f"⚠️ Unbalanced ({first:.1f}m + {last:.1f}m)" - - summary = f""" -**Stage**: {stage_width}m × {stage_depth}m = {stage_width * stage_depth}m² -**Total Lots**: {total_lots} -**Unique Lot Types**: {unique_widths} -**Grid Variance**: {variance:+.2f}m {"✅" if abs(variance) < 0.001 else "⚠️"} -""" - - # Convert solution to string for manual editing - manual_edit_string = optimizer.solution_to_string(optimized_solution) - - return fig_2d, results_df, summary, report, manual_edit_string - - def update_manual_adjustment(manual_widths_text, stage_width, stage_depth, color_scheme): - """Update visualization based on manual adjustment""" - optimizer.current_scheme = color_scheme - - # Parse manual widths - widths = optimizer.parse_manual_adjustments(manual_widths_text) - - if not widths: - return None, "Please enter lot widths (e.g., '14.0, 8.5, 10.5, 8.5, 14.0')" - - # Validate and get feedback - solution, feedback = optimizer.validate_manual_solution(widths, stage_width) - - if not solution: - return None, feedback - - # Calculate variance - total_width = sum(widths) - variance = total_width - stage_width - - # Create visualization with variance - fig = optimizer.create_enhanced_visualization( - solution, stage_width, stage_depth, - "Manually Adjusted Layout", - show_variance=variance - ) - - return fig, feedback - - # Create Gradio interface - with gr.Blocks( - title="Advanced AI Grid Optimizer", - theme=gr.themes.Base(), - css=""" - .gradio-container { - font-family: 'Segoe UI', sans-serif; - background: #1a1a1a; - color: white; - } - .gr-button-primary { - background: linear-gradient(45deg, #FF073A 30%, #0AEFFF 90%); - border: none; - box-shadow: 0 3px 5px 2px rgba(255, 7, 58, .3); - } - h1 { - background: linear-gradient(45deg, #FF073A, #0AEFFF); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - text-align: center; - font-size: 2.5em; - } - .gr-form { - background: rgba(42, 42, 42, 0.9); - border-radius: 10px; - padding: 20px; - border: 1px solid #444; - } - .gr-input { - background-color: #2a2a2a; - color: white; - border: 1px solid #444; - } - .gr-check-radio { - background-color: #2a2a2a; - } - """ - ) as demo: - gr.Markdown(""" - # 🏗️ Advanced AI Grid Cut Optimizer Pro - ### AI-Powered Subdivision Planning with Manual Fine-Tuning - """) - - with gr.Tabs(): - with gr.TabItem("🤖 AI Optimization"): - with gr.Row(): - with gr.Column(scale=1): - with gr.Group(): - gr.Markdown("### 📐 Stage Dimensions") - stage_width = gr.Number( - label="Stage Width (m)", - value=105.0, - info="Width along the street" - ) - stage_depth = gr.Number( - label="Stage Depth (m)", - value=32.0, - info="Depth of lots (perpendicular to street)" - ) - - gr.Markdown("### 📏 Lot Width Options") - - with gr.Group(): - gr.Markdown("**Standard Widths**") - with gr.Row(): - enable_8_5 = gr.Checkbox(label="8.5m SLHC", value=True) - enable_10_5 = gr.Checkbox(label="10.5m SLHC", value=True) - enable_12_5 = gr.Checkbox(label="12.5m", value=True) - with gr.Row(): - enable_14 = gr.Checkbox(label="14.0m", value=True) - enable_16 = gr.Checkbox(label="16.0m", value=True) - enable_18 = gr.Checkbox(label="18.0m", value=False) - - with gr.Group(): - enable_corners = gr.Checkbox( - label="Enable Corner-Specific Widths", - value=True, - info="Adds variety and helps achieve 100%" - ) - with gr.Row(): - enable_11 = gr.Checkbox(label="11.0m", value=True) - enable_13_3 = gr.Checkbox(label="13.3m", value=True) - with gr.Row(): - enable_14_8 = gr.Checkbox(label="14.8m", value=True) - enable_16_8 = gr.Checkbox(label="16.8m", value=True) - - with gr.Column(scale=1): - gr.Markdown("### ⚙️ Advanced Settings") - - allow_custom_corners = gr.Checkbox( - label="🎯 Allow Flexible Corner Widths", - value=True, - info="Enables 13.8m, 13.9m etc. for perfect fits" - ) - - optimization_strategy = gr.Radio( - ["diversity_focus", "balanced"], - label="Optimization Strategy", - value="diversity_focus", - info="Diversity creates more interesting layouts" - ) - - color_scheme = gr.Radio( - ["modern", "professional", "neon"], - label="🎨 Color Scheme", - value="neon", - info="Neon colors work best with dark background" - ) - - optimize_btn = gr.Button( - "🚀 Optimize with AI", - variant="primary", - size="lg", - elem_id="optimize-button" - ) - - gr.Markdown(""" - ### 💡 Quick Tips: - - **Visual Fix**: All lots now align at rear boundary - - **Corner Lots**: Always wider than internals - - **Grid Variance**: Shows if layout is perfect (0.0m) - - **Manual Adjust**: Edit the result below after optimization - """) - - with gr.Row(): - plot_2d = gr.Plot(label="2D Layout with Corner Splays") - - # Manual adjustment section - gr.Markdown("### ✏️ Fine-Tune AI Result") - with gr.Row(): - with gr.Column(scale=2): - manual_widths = gr.Textbox( - label="Manually Adjust Lot Widths", - placeholder="Widths will appear here after optimization", - info="Edit the widths (comma-separated) and click 'Update Layout'", - lines=2 - ) - with gr.Column(scale=1): - update_btn = gr.Button("🔄 Update Layout", variant="secondary") - adjustment_feedback = gr.Markdown( - value="", - label="Adjustment Feedback" - ) - - with gr.Row(): - results_table = gr.DataFrame(label="Lot Distribution Analysis") - - with gr.Row(): - with gr.Column(): - summary_output = gr.Markdown(label="Optimization Summary") - with gr.Column(): - report_output = gr.Markdown(label="Professional Report") - - with gr.TabItem("📊 Plan Reader"): - gr.Markdown(""" - ## 🏢 AI Plan Reader - ### Upload your subdivision plan to automatically extract lot information - """) - - with gr.Row(): - with gr.Column(scale=1): - plan_upload = gr.File( - label="Upload Subdivision Plan", - file_types=["image", "pdf"], - type="filepath" - ) - - gr.Markdown(""" - **Supported Formats:** - - PDF plans - - PNG/JPG images - - CAD exports - - **Best Results:** - - High resolution (300+ DPI) - - Clear lot numbers - - Visible frontage dimensions - - North arrow included - """) - - process_plan_btn = gr.Button( - "🔍 Analyze Plan", - variant="primary", - size="lg" - ) - - # Analysis options - with gr.Group(): - gr.Markdown("**Analysis Settings**") - scale_input = gr.Number( - label="Scale (1:X)", - value=1000, - info="Drawing scale ratio" - ) - - auto_detect_scale = gr.Checkbox( - label="Auto-detect scale from plan", - value=True - ) - - confidence_threshold = gr.Slider( - label="Detection Confidence", - minimum=0.5, - maximum=0.95, - value=0.75, - step=0.05, - info="Higher = more accurate but may miss some lots" - ) - - with gr.Column(scale=2): - # Preview with annotations - plan_preview = gr.Image( - label="Analyzed Plan Preview", - type="numpy" - ) - - analysis_status = gr.Markdown( - value="Upload a plan to begin analysis", - label="Analysis Status" - ) - - # Results section - gr.Markdown("### 📊 Extracted Lot Data") - - with gr.Row(): - extracted_data = gr.DataFrame( - headers=["Lot #", "Frontage (m)", "Depth (m)", "Area (m²)", "Type"], - label="Detected Lots", - interactive=True - ) - - with gr.Column(): - extraction_summary = gr.Markdown( - label="Extraction Summary" - ) - - export_btn = gr.Button( - "📥 Export to CSV", - variant="secondary" - ) - - send_to_optimizer_btn = gr.Button( - "➡️ Send to Optimizer", - variant="primary" - ) - - # Manual correction section - gr.Markdown("### ✏️ Manual Corrections") - with gr.Row(): - with gr.Column(): - gr.Markdown(""" - **Quick Edit Tools:** - - Double-click cells to edit - - Add missing lots manually - - Correct misread numbers - - Adjust frontages - """) - - add_lot_btn = gr.Button("➕ Add Lot", size="sm") - remove_selected_btn = gr.Button("➖ Remove Selected", size="sm") - - with gr.Column(): - validation_result = gr.Markdown( - label="Data Validation" - ) - - # Wire up the buttons - optimize_btn.click( - optimize_grid, - inputs=[ - stage_width, - stage_depth, - enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18, - enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8, - allow_custom_corners, optimization_strategy, color_scheme - ], - outputs=[plot_2d, results_table, summary_output, report_output, manual_widths] - ) - - update_btn.click( - update_manual_adjustment, - inputs=[manual_widths, stage_width, stage_depth, color_scheme], - outputs=[plot_2d, adjustment_feedback] - ) - - return demo - -# Create and launch -if __name__ == "__main__": - app = create_advanced_app() - app.launch(), text_val): - dim_val = float(re.findall(r'\d+\.?\d*', text_val)[0]) - # Assign to frontage or depth based on position - if abs(text_center[1] - lot_center[1]) < h * 0.3: - frontage = dim_val - else: - depth = dim_val - - # If no lot number found, assign sequential - if not lot_number: - lot_number = f"L{i+1}" - - # If no dimensions found, estimate from pixel measurements - if not frontage: - frontage = round(w / scale * 1000, 1) # Convert to meters - if not depth: - depth = round(h / scale * 1000, 1) # Convert to meters - - # Determine lot type based on frontage - if frontage <= 10.5: - lot_type = "SLHC" - elif frontage <= 14: - lot_type = "Standard" - else: - lot_type = "Premium" - - lot_info.append({ - 'lot_number': lot_number, - 'frontage': frontage, - 'depth': depth, - 'area': frontage * depth, - 'type': lot_type, - 'bbox': lot['bbox'] - }) - - # Sort by lot number if possible - try: - def get_lot_number(lot_info): - lot_num = lot_info['lot_number'] - if lot_num.startswith('L'): - return int(lot_num[1:]) - return 999999 # Put non-standard lot numbers at the end - - lot_info.sort(key=get_lot_number) - except: - pass - - return lot_info - - def create_annotated_preview(self, img, lot_data): - """Create preview image with annotations""" - if not PLAN_READER_AVAILABLE: - return img - - annotated = img.copy() - - # Define colors for different lot types - colors = { - 'SLHC': (255, 0, 0), # Red - 'Standard': (0, 255, 0), # Green - 'Premium': (0, 0, 255) # Blue - } - - # Draw lot boundaries and labels - for lot in lot_data: - if 'bbox' in lot: - x, y, w, h = lot['bbox'] - color = colors.get(lot['type'], (128, 128, 128)) - - # Draw rectangle - cv2.rectangle(annotated, (x, y), (x + w, y + h), color, 2) - - # Draw lot number - label = f"{lot['lot_number']}: {lot['frontage']}m" - cv2.putText(annotated, label, (x + 5, y + 20), - cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) - - return annotated - - def lot_data_to_dataframe(self, lot_data): - """Convert lot data to DataFrame format""" - if not lot_data: - return pd.DataFrame(columns=["Lot #", "Frontage (m)", "Depth (m)", "Area (m²)", "Type"]) - - df_data = [] - for lot in lot_data: - df_data.append({ - "Lot #": lot['lot_number'], - "Frontage (m)": lot['frontage'], - "Depth (m)": lot['depth'], - "Area (m²)": round(lot['area'], 1), - "Type": lot['type'] - }) - - return pd.DataFrame(df_data) - - def export_lot_data_to_csv(self, df): - """Export lot data to CSV format""" - if df is None or df.empty: - return None - - csv_buffer = io.StringIO() - df.to_csv(csv_buffer, index=False) - return csv_buffer.getvalue() - - def convert_lot_data_to_stage_format(self, df): - """Convert lot data to format suitable for optimizer""" - if df is None or df.empty: - return None, None - - # Group by frontage and count - frontage_counts = {} - for _, row in df.iterrows(): - frontage = float(row['Frontage (m)']) - if frontage in frontage_counts: - frontage_counts[frontage] += 1 - else: - frontage_counts[frontage] = 1 - - # Calculate total width - total_width = sum(f * c for f, c in frontage_counts.items()) - - # Find common depth (mode) - depths = df['Depth (m)'].mode() - common_depth = depths[0] if len(depths) > 0 else 32 - - return total_width, common_depth - - def darken_color(self, hex_color, factor=0.8): - """Darken a hex color by a factor""" - try: - hex_color = hex_color.lstrip('#') - rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) - darker_rgb = tuple(int(c * factor) for c in rgb) - return '#' + ''.join(f'{c:02x}' for c in darker_rgb) - except: - return hex_color + extracted_data.change( + validate_lot_data, + inputs=[extracted_data], + outputs=[validation_result] + ) -def create_advanced_app(): - optimizer = AdvancedGridOptimizer() - - def optimize_grid( - stage_width, - stage_depth, - enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18, - enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8, - allow_custom_corners, optimization_strategy, color_scheme - ): - # Update color scheme - optimizer.current_scheme = color_scheme - - # Collect enabled widths - enabled_widths = [] - if enable_8_5: enabled_widths.append(8.5) - if enable_10_5: enabled_widths.append(10.5) - if enable_12_5: enabled_widths.append(12.5) - if enable_14: enabled_widths.append(14.0) - if enable_16: enabled_widths.append(16.0) - if enable_18: enabled_widths.append(18.0) - - if enable_corners: - if enable_11: enabled_widths.append(11.0) - if enable_13_3: enabled_widths.append(13.3) - if enable_14_8: enabled_widths.append(14.8) - if enable_16_8: enabled_widths.append(16.8) - - if not enabled_widths: - return None, None, pd.DataFrame(), "Please select at least one lot width!", "", "" - - # Run optimization based on strategy - if optimization_strategy == "diversity_focus": - optimized_solution = optimizer.optimize_with_flexible_corners( - stage_width, enabled_widths, allow_custom_corners - ) - else: # balanced approach - optimized_solution = optimizer.optimize_with_corners_diverse( - stage_width, enabled_widths, None - ) - - # Store current solution for manual adjustment - optimizer.current_solution = optimized_solution - - # Calculate variance for display - if optimized_solution: - total_width = sum(w for w, _ in optimized_solution) - variance = total_width - stage_width - else: - variance = None - - # Verify solution - if not optimized_solution or abs(sum(w for w, _ in optimized_solution) - stage_width) > 0.001: - # Provide suggestions - return None, pd.DataFrame(), f""" -### ❌ Cannot achieve 100% usage with selected widths + add_lot_btn.click( + add_lot_row, + inputs=[extracted_data], + outputs=[extracted_data] + ) -**Stage Width**: {stage_width}m -**Available Widths**: {', '.join([f"{w}m" for w in sorted(enabled_widths)])} + remove_selected_btn.click( + remove_selected_rows, + inputs=[extracted_data, gr.State([])], + outputs=[extracted_data] + ) -**Try:** -1. Enable more lot types for flexibility -2. Enable "Custom Corners" option -3. Try common stage widths: 84m, 105m, 126m -""", "", "" - - # Create visualizations with variance indicator - fig_2d = optimizer.create_enhanced_visualization( - optimized_solution, stage_width, stage_depth, - "AI-Optimized Diverse Subdivision Layout", - show_variance=variance - ) - - # Create results table - width_counts = {} - for width, lot_type in optimized_solution: - key = f"{width:.1f}m" - if key in width_counts: - width_counts[key]['count'] += 1 - else: - # Handle both standard and custom widths - if width in optimizer.lot_specifications: - spec = optimizer.lot_specifications[width] - elif int(width) in optimizer.lot_specifications: - spec = optimizer.lot_specifications[int(width)] - else: - # Custom width - find closest - closest = min(optimizer.lot_specifications.keys(), - key=lambda x: abs(x - width)) - spec = optimizer.lot_specifications[closest] - spec = {**spec, 'type': 'Custom', 'squares': 'Custom'} - - width_counts[key] = { - 'count': 1, - 'type': spec.get('type', 'Custom'), - 'squares': spec.get('squares', 'N/A'), - 'area': width * stage_depth - } - - results_data = [] - for width, info in sorted(width_counts.items()): - results_data.append({ - 'Lot Width': width, - 'Count': info['count'], - 'Type': info['type'], - 'Area Each': f"{info['area']:.0f}m²", - 'Total Width': f"{float(width[:-1]) * info['count']:.1f}m", - 'Total Area': f"{info['area'] * info['count']:.0f}m²" - }) - - results_df = pd.DataFrame(results_data) - - # Generate report - report = optimizer.generate_report(optimized_solution, stage_width, stage_depth, None) - - # Create summary - total_lots = len(optimized_solution) - unique_widths = len(set(w for w, _ in optimized_solution)) - - # Count SLHC pairs - slhc_pairs = sum(1 for i in range(len(optimized_solution) - 1) - if optimized_solution[i][0] <= 10.5 and optimized_solution[i+1][0] <= 10.5) - - # Analyze corners - corner_info = "N/A" - if len(optimized_solution) >= 2: - first = optimized_solution[0][0] - last = optimized_solution[-1][0] - diff = abs(first - last) - - if diff < 0.1: - corner_info = f"✨ PERFECT ({first:.1f}m × 2)" - elif diff <= 1.0: - corner_info = f"✅ Excellent ({first:.1f}m + {last:.1f}m)" - elif diff <= 2.0: - corner_info = f"👍 Good ({first:.1f}m + {last:.1f}m)" - else: - corner_info = f"⚠️ Unbalanced ({first:.1f}m + {last:.1f}m)" - - summary = f""" -**Stage**: {stage_width}m × {stage_depth}m = {stage_width * stage_depth}m² -**Total Lots**: {total_lots} -**Unique Lot Types**: {unique_widths} -**Grid Variance**: {variance:+.2f}m {"✅" if abs(variance) < 0.001 else "⚠️"} -""" - - # Convert solution to string for manual editing - manual_edit_string = optimizer.solution_to_string(optimized_solution) - - return fig_2d, results_df, summary, report, manual_edit_string - - def update_manual_adjustment(manual_widths_text, stage_width, stage_depth, color_scheme): - """Update visualization based on manual adjustment""" - optimizer.current_scheme = color_scheme - - # Parse manual widths - widths = optimizer.parse_manual_adjustments(manual_widths_text) - - if not widths: - return None, "Please enter lot widths (e.g., '14.0, 8.5, 10.5, 8.5, 14.0')" - - # Validate and get feedback - solution, feedback = optimizer.validate_manual_solution(widths, stage_width) - - if not solution: - return None, feedback - - # Calculate variance - total_width = sum(widths) - variance = total_width - stage_width - - # Create visualization with variance - fig = optimizer.create_enhanced_visualization( - solution, stage_width, stage_depth, - "Manually Adjusted Layout", - show_variance=variance - ) - - return fig, feedback - - # Create Gradio interface - with gr.Blocks( - title="Advanced AI Grid Optimizer", - theme=gr.themes.Base(), - css=""" - .gradio-container { - font-family: 'Segoe UI', sans-serif; - background: #1a1a1a; - color: white; - } - .gr-button-primary { - background: linear-gradient(45deg, #FF073A 30%, #0AEFFF 90%); - border: none; - box-shadow: 0 3px 5px 2px rgba(255, 7, 58, .3); - } - h1 { - background: linear-gradient(45deg, #FF073A, #0AEFFF); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - text-align: center; - font-size: 2.5em; - } - .gr-form { - background: rgba(42, 42, 42, 0.9); - border-radius: 10px; - padding: 20px; - border: 1px solid #444; - } - .gr-input { - background-color: #2a2a2a; - color: white; - border: 1px solid #444; - } - .gr-check-radio { - background-color: #2a2a2a; - } - """ - ) as demo: - gr.Markdown(""" - # 🏗️ Advanced AI Grid Cut Optimizer Pro - ### AI-Powered Subdivision Planning with Manual Fine-Tuning - """) - - with gr.Tabs(): - with gr.TabItem("🤖 AI Optimization"): - with gr.Row(): - with gr.Column(scale=1): - with gr.Group(): - gr.Markdown("### 📐 Stage Dimensions") - stage_width = gr.Number( - label="Stage Width (m)", - value=105.0, - info="Width along the street" - ) - stage_depth = gr.Number( - label="Stage Depth (m)", - value=32.0, - info="Depth of lots (perpendicular to street)" - ) - - gr.Markdown("### 📏 Lot Width Options") - - with gr.Group(): - gr.Markdown("**Standard Widths**") - with gr.Row(): - enable_8_5 = gr.Checkbox(label="8.5m SLHC", value=True) - enable_10_5 = gr.Checkbox(label="10.5m SLHC", value=True) - enable_12_5 = gr.Checkbox(label="12.5m", value=True) - with gr.Row(): - enable_14 = gr.Checkbox(label="14.0m", value=True) - enable_16 = gr.Checkbox(label="16.0m", value=True) - enable_18 = gr.Checkbox(label="18.0m", value=False) - - with gr.Group(): - enable_corners = gr.Checkbox( - label="Enable Corner-Specific Widths", - value=True, - info="Adds variety and helps achieve 100%" - ) - with gr.Row(): - enable_11 = gr.Checkbox(label="11.0m", value=True) - enable_13_3 = gr.Checkbox(label="13.3m", value=True) - with gr.Row(): - enable_14_8 = gr.Checkbox(label="14.8m", value=True) - enable_16_8 = gr.Checkbox(label="16.8m", value=True) - - with gr.Column(scale=1): - gr.Markdown("### ⚙️ Advanced Settings") - - allow_custom_corners = gr.Checkbox( - label="🎯 Allow Flexible Corner Widths", - value=True, - info="Enables 13.8m, 13.9m etc. for perfect fits" - ) - - optimization_strategy = gr.Radio( - ["diversity_focus", "balanced"], - label="Optimization Strategy", - value="diversity_focus", - info="Diversity creates more interesting layouts" - ) - - color_scheme = gr.Radio( - ["modern", "professional", "neon"], - label="🎨 Color Scheme", - value="neon", - info="Neon colors work best with dark background" - ) - - optimize_btn = gr.Button( - "🚀 Optimize with AI", - variant="primary", - size="lg", - elem_id="optimize-button" - ) - - gr.Markdown(""" - ### 💡 Quick Tips: - - **Visual Fix**: All lots now align at rear boundary - - **Corner Lots**: Always wider than internals - - **Grid Variance**: Shows if layout is perfect (0.0m) - - **Manual Adjust**: Edit the result below after optimization - """) - - with gr.Row(): - plot_2d = gr.Plot(label="2D Layout with Corner Splays") - - # Manual adjustment section - gr.Markdown("### ✏️ Fine-Tune AI Result") - with gr.Row(): - with gr.Column(scale=2): - manual_widths = gr.Textbox( - label="Manually Adjust Lot Widths", - placeholder="Widths will appear here after optimization", - info="Edit the widths (comma-separated) and click 'Update Layout'", - lines=2 - ) - with gr.Column(scale=1): - update_btn = gr.Button("🔄 Update Layout", variant="secondary") - adjustment_feedback = gr.Markdown( - value="", - label="Adjustment Feedback" - ) - - with gr.Row(): - results_table = gr.DataFrame(label="Lot Distribution Analysis") - - with gr.Row(): - with gr.Column(): - summary_output = gr.Markdown(label="Optimization Summary") - with gr.Column(): - report_output = gr.Markdown(label="Professional Report") - - with gr.TabItem("📊 Plan Reader"): - gr.Markdown(""" - ## 🏢 AI Plan Reader - ### Upload your subdivision plan to automatically extract lot information - """) - - with gr.Row(): - with gr.Column(scale=1): - plan_upload = gr.File( - label="Upload Subdivision Plan", - file_types=["image", "pdf"], - type="filepath" - ) - - gr.Markdown(""" - **Supported Formats:** - - PDF plans - - PNG/JPG images - - CAD exports - - **Best Results:** - - High resolution (300+ DPI) - - Clear lot numbers - - Visible frontage dimensions - - North arrow included - """) - - process_plan_btn = gr.Button( - "🔍 Analyze Plan", - variant="primary", - size="lg" - ) - - # Analysis options - with gr.Group(): - gr.Markdown("**Analysis Settings**") - scale_input = gr.Number( - label="Scale (1:X)", - value=1000, - info="Drawing scale ratio" - ) - - auto_detect_scale = gr.Checkbox( - label="Auto-detect scale from plan", - value=True - ) - - confidence_threshold = gr.Slider( - label="Detection Confidence", - minimum=0.5, - maximum=0.95, - value=0.75, - step=0.05, - info="Higher = more accurate but may miss some lots" - ) - - with gr.Column(scale=2): - # Preview with annotations - plan_preview = gr.Image( - label="Analyzed Plan Preview", - type="numpy" - ) - - analysis_status = gr.Markdown( - value="Upload a plan to begin analysis", - label="Analysis Status" - ) - - # Results section - gr.Markdown("### 📊 Extracted Lot Data") - - with gr.Row(): - extracted_data = gr.DataFrame( - headers=["Lot #", "Frontage (m)", "Depth (m)", "Area (m²)", "Type"], - label="Detected Lots", - interactive=True - ) - - with gr.Column(): - extraction_summary = gr.Markdown( - label="Extraction Summary" - ) - - export_btn = gr.Button( - "📥 Export to CSV", - variant="secondary" - ) - - send_to_optimizer_btn = gr.Button( - "➡️ Send to Optimizer", - variant="primary" - ) - - # Manual correction section - gr.Markdown("### ✏️ Manual Corrections") - with gr.Row(): - with gr.Column(): - gr.Markdown(""" - **Quick Edit Tools:** - - Double-click cells to edit - - Add missing lots manually - - Correct misread numbers - - Adjust frontages - """) - - add_lot_btn = gr.Button("➕ Add Lot", size="sm") - remove_selected_btn = gr.Button("➖ Remove Selected", size="sm") - - with gr.Column(): - validation_result = gr.Markdown( - label="Data Validation" - ) - - # Wire up the buttons - optimize_btn.click( - optimize_grid, - inputs=[ - stage_width, - stage_depth, - enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18, - enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8, - allow_custom_corners, optimization_strategy, color_scheme - ], - outputs=[plot_2d, results_table, summary_output, report_output, manual_widths] - ) - - update_btn.click( - update_manual_adjustment, - inputs=[manual_widths, stage_width, stage_depth, color_scheme], - outputs=[plot_2d, adjustment_feedback] - ) - return demo -# Create and launch + if __name__ == "__main__": app = create_advanced_app() - app.launch() \ No newline at end of file + app.launch()