diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,14 +1,972 @@ +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.lower().endswith('.pdf'): + if image_path.endswith('.pdf'): + # Convert PDF to image 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" @@ -16,7 +974,7 @@ img = cv2.imread(image_path) if img is None: return None, None, "Failed to load image" - + # Convert to RGB if needed if len(img.shape) == 2: img_rgb = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) @@ -24,43 +982,64 @@ 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**: {int(confidence * 100)}% +- **Confidence**: {confidence:.0%} **Next Steps:** 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""" 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: + if area > 1000: # Minimum area threshold + # Approximate polygon 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 if h > 0 else 0 + aspect_ratio = float(w) / h + + # Lots typically have aspect ratios between 0.3 and 3.0 if 0.3 <= aspect_ratio <= 3.0: lots.append({ 'contour': approx, @@ -68,62 +1047,2094 @@ '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 try: - _, thresh = cv2.threshold(gray_img, 150, 255, cv2.THRESH_BINARY) text = pytesseract.image_to_string(thresh) 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: text_val = data['text'][i].strip() - 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 [] + # Look for lot numbers and dimensions + if 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() as main_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 + + **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( + 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") + + 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 + +# Create and launch +if __name__ == "__main__": + app = create_advanced_app() + app.launch(), text_val) or 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): + 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 - def match_lots_with_dimensions(self, lots, text_data, scale, auto_detect_scale): - """Match detected lots with their dimensions and numbers""" - 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'] - if re.match(r'^L\d+', text_val): - lot_number = text_val - elif re.match(r'^\d+\.?\d*m?$', text_val): +# 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 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_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, @@ -132,38 +3143,56 @@ 'type': lot_type, 'bbox': lot['bbox'] }) - + + # Sort by lot number if possible try: - lot_info.sort(key=lambda x: int(re.findall(r'\d+', x['lot_number'])[0])) - except Exception: + 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() - colors = {'SLHC': (255, 0, 0), 'Standard': (0, 255, 0), 'Premium': (0, 0, 255)} - + + # 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) - + 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({ @@ -173,54 +3202,65 @@ "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)']) - frontage_counts[frontage] = frontage_counts.get(frontage, 0) + 1 + 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)) + 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 Exception: + except: 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, - allow_custom_corners, optimization_strategy, - color_scheme + 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) @@ -228,34 +3268,39 @@ def create_advanced_app(): 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, pd.DataFrame(), "❌ Please select at least one lot width!", "", "" - + 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: + 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 - - if (not optimized_solution) or abs(sum(w for w, _ in optimized_solution) - stage_width) > 0.001: + + # 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 @@ -263,39 +3308,44 @@ 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 fig_2d = optimizer.create_enhanced_visualization( optimized_solution, stage_width, stage_depth, - "AI-Optimized Diverse Subdivision Layout", show_variance=variance + "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: - closest = min(optimizer.lot_specifications.keys(), - key=lambda x: abs(x - width)) + # 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({ @@ -306,21 +3356,27 @@ def create_advanced_app(): '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)) - - 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) - + + # 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: @@ -329,105 +3385,51 @@ 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""" 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 + "Manually Adjusted Layout", + show_variance=variance ) + return fig, feedback - - 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): - """Remove selected rows by index list""" - 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) - + + # Create Gradio interface with gr.Blocks( - title="Advanced AI Grid Optimizer", + title="Advanced AI Grid Optimizer", theme=gr.themes.Base(), css=""" .gradio-container { @@ -467,7 +3469,7 @@ def create_advanced_app(): # 🏗️ 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(): @@ -475,17 +3477,18 @@ def create_advanced_app(): 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(): @@ -496,10 +3499,10 @@ def create_advanced_app(): 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%" ) @@ -509,43 +3512,49 @@ def create_advanced_app(): 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", + "🚀 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") with gr.Row(): with gr.Column(scale=2): @@ -561,39 +3570,22 @@ 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 ### Upload your subdivision plan to automatically extract lot information """) - + with gr.Row(): with gr.Column(scale=1): plan_upload = gr.File( @@ -601,23 +3593,27 @@ 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( @@ -625,10 +3621,12 @@ 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, @@ -637,83 +3635,86 @@ 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") - + 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 + - 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") - - 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=[gr.Textbox(label="CSV Export (copy & save)", lines=8, visible=False), 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] - ) - - remove_selected_btn.click( - remove_selected_rows, - inputs=[extracted_data, gr.State([])], - outputs=[extracted_data] - ) - + 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() + app.launch() \ No newline at end of file