buildinves commited on
Commit
3247bda
·
verified ·
1 Parent(s): 5909b52

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +996 -467
app.py CHANGED
@@ -3,13 +3,16 @@ import pandas as pd
3
  import numpy as np
4
  import matplotlib.pyplot as plt
5
  import matplotlib.patches as patches
6
- from matplotlib.patches import FancyBboxPatch, Path, PathPatch, Rectangle
7
  from matplotlib.collections import PatchCollection
 
 
 
 
8
  from datetime import datetime
9
  import io
10
- import json
11
  import tempfile
12
- import re
13
 
14
  # Try to import plan reading libraries
15
  try:
@@ -20,264 +23,272 @@ try:
20
  PLAN_READER_AVAILABLE = True
21
  except ImportError:
22
  PLAN_READER_AVAILABLE = False
23
- print("Warning: Plan reader libraries not available. "
24
- "Install opencv-python, pytesseract, pillow, and pdf2image for full functionality.")
25
-
26
 
27
  class AdvancedGridOptimizer:
28
  def __init__(self):
29
  # Standard lot widths and their typical depths
30
  self.lot_specifications = {
31
- 8.5: {"depths": [21, 25, 28], "type": "SLHC", "squares": "11-16"},
32
- 10.5: {"depths": [21, 25, 28, 32, 35], "type": "SLHC", "squares": "13-21.5"},
33
- 12.5: {"depths": [21, 25, 28, 30, 32], "type": "Standard", "squares": "16-24"},
34
- 14.0: {"depths": [21, 25, 28, 30, 32, 34], "type": "Standard", "squares": "17-28"},
35
- 16.0: {"depths": [28, 30, 32, 34, 36, 40], "type": "Premium", "squares": "24-38"},
36
- 18.0: {"depths": [32, 34, 36], "type": "Premium", "squares": "32-39"},
37
  # Traditional corner lots
38
- 11.0: {"depths": [21, 25], "type": "Corner-SLHC", "squares": "13-17"},
39
- 13.3: {"depths": [25, 28], "type": "Corner-Standard", "squares": "18-22"},
40
- 14.8: {"depths": [28, 30], "type": "Corner-Standard", "squares": "22-26"},
41
- 16.8: {"depths": [30, 32], "type": "Corner-Premium", "squares": "26-32"}
42
  }
43
-
44
  self.slhc_widths = [8.5, 10.5]
45
  self.standard_widths = [12.5, 14.0]
46
  self.premium_widths = [16.0, 18.0]
47
  self.corner_specific = [11.0, 13.3, 14.8, 16.8]
48
-
49
  # Define corner_widths as all widths suitable for corners
50
  self.corner_widths = self.corner_specific + [14.0, 16.0, 18.0]
51
-
52
  # Enhanced color palette with gradients
53
  self.color_schemes = {
54
  'modern': {
55
- 8.5: '#FF6B6B', 10.5: '#4ECDC4', 12.5: '#45B7D1',
56
- 14.0: '#96CEB4', 16.0: '#DDA0DD', 18.0: '#FFD93D',
57
- 11.0: '#FFA07A', 13.3: '#98D8C8', 14.8: '#F7DC6F',
58
- 16.8: '#BB8FCE'
 
 
 
 
 
 
59
  },
60
  'professional': {
61
- 8.5: '#E74C3C', 10.5: '#3498DB', 12.5: '#2ECC71',
62
- 14.0: '#F39C12', 16.0: '#9B59B6', 18.0: '#1ABC9C',
63
- 11.0: '#E67E22', 13.3: '#16A085', 14.8: '#F1C40F',
64
- 16.8: '#8E44AD'
 
 
 
 
 
 
65
  },
66
  'neon': {
67
- 8.5: '#FF073A', 10.5: '#0AEFFF', 12.5: '#39FF14',
68
- 14.0: '#FF6600', 16.0: '#BF00FF', 18.0: '#FFFF00',
69
- 11.0: '#FF1493', 13.3: '#00FFFF', 14.8: '#FFF700',
70
- 16.8: '#FF00FF'
 
 
 
 
 
 
71
  }
72
  }
73
-
74
  self.current_scheme = 'neon'
75
  self.current_solution = None # Store current AI solution
76
-
77
- def create_enhanced_visualization(self, solution, stage_width, stage_depth=32,
78
- title="Premium Grid Layout", show_variance=None):
79
  """Create a clean 2D visualization with corner splays and proper alignment"""
80
- fig, (ax1, ax2) = plt.subplots(
81
- 2, 1, figsize=(18, 12),
82
- gridspec_kw={'height_ratios': [3, 1]},
83
- facecolor='#1a1a1a'
84
- )
85
-
86
  colors = self.color_schemes[self.current_scheme]
 
87
  x_pos = 0
88
  lot_num = 1
 
 
89
  ax1.set_xlim(-5, stage_width + 5)
90
  ax1.set_ylim(-10, 50)
91
  ax1.set_facecolor('#1a1a1a')
92
-
93
- # Title with variance if provided
94
  if show_variance is not None:
 
95
  title_text = f"{title}\nGrid Variance: {show_variance:+.1f}m"
96
  ax1.set_title(title_text, fontsize=28, fontweight='bold', pad=25, color='white')
97
  else:
98
  ax1.set_title(title, fontsize=28, fontweight='bold', pad=25, color='white')
99
-
100
- # Subtle dark gradient background
101
  gradient = np.linspace(0.2, 0, 100).reshape(1, -1)
102
- ax1.imshow(gradient, extent=[-5, stage_width + 5, -10, 50],
103
- aspect='auto', cmap='Greys', alpha=0.3, zorder=0)
104
-
105
- # Draw street rectangle
106
- street = Rectangle((-5, -8), stage_width + 10, 12,
107
- facecolor='#2c2c2c', alpha=0.9, zorder=1,
108
- edgecolor='#444444', linewidth=2)
109
  ax1.add_patch(street)
110
- ax1.text(stage_width / 2, -2, 'STREET', ha='center', va='center',
111
- fontsize=20, color='white', fontweight='bold')
112
-
113
- # Corner splay size and uniform lot height
114
- splay_size = 3
115
- lot_height = 28
116
-
117
  for i, (width, lot_type) in enumerate(solution):
118
- # Pick base color, or nearest if missing
119
  if width in colors:
120
  base_color = colors[width]
121
  else:
122
  closest_width = min(colors.keys(), key=lambda x: abs(x - width))
123
  base_color = colors[closest_width]
124
-
 
125
  is_corner = (i == 0 or i == len(solution) - 1)
 
 
126
  face_color = base_color
127
  edge_color = 'white'
128
  linewidth = 4.0 if is_corner else 3.0
129
-
 
130
  if is_corner:
131
- # Build a 5‐vertex corner polygon (splay at front) &
132
- # keep uniform lot height
133
- if i == 0:
134
  vertices = [
135
- (x_pos + splay_size, 8), # start after splay
136
  (x_pos + width, 8),
137
- (x_pos + width, 8 + lot_height),
138
- (x_pos, 8 + lot_height),
139
- (x_pos, 8 + splay_size)
140
  ]
141
- else:
142
  vertices = [
143
- (x_pos, 8),
144
  (x_pos + width - splay_size, 8),
145
- (x_pos + width, 8 + splay_size),
146
- (x_pos + width, 8 + lot_height),
147
- (x_pos, 8 + lot_height)
148
  ]
149
-
150
- # Close polygon path
151
  codes = [Path.MOVETO] + [Path.LINETO] * (len(vertices) - 1) + [Path.CLOSEPOLY]
152
- vertices.append(vertices[0])
153
  path = Path(vertices, codes)
154
- lot_patch = PathPatch(path, facecolor=face_color,
155
- edgecolor=edge_color, linewidth=linewidth, zorder=3)
156
- ax1.add_patch(lot_patch)
157
-
158
- # Draw splay line
159
  if i == 0:
160
- ax1.plot([x_pos, x_pos + splay_size], [8 + splay_size, 8],
161
- 'white', linewidth=2, alpha=0.8)
162
  else:
163
- ax1.plot([x_pos + width - splay_size, x_pos + width],
164
- [8, 8 + splay_size], 'white', linewidth=2, alpha=0.8)
165
  else:
166
- # Regular rectangular (rounded) lot
167
- lot_patch = FancyBboxPatch(
168
- (x_pos, 8), width, lot_height,
169
- boxstyle="round,pad=0.1",
170
- facecolor=face_color,
171
- edgecolor=edge_color,
172
- linewidth=linewidth,
173
- zorder=3
174
- )
175
- ax1.add_patch(lot_patch)
176
-
177
- # Add glow underneath
178
- glow = FancyBboxPatch(
179
- (x_pos - 0.2, 7.8), width + 0.4, lot_height + 0.4,
180
- boxstyle="round,pad=0.15",
181
- facecolor='none',
182
- edgecolor=face_color,
183
- linewidth=1,
184
- alpha=0.5,
185
- zorder=2
186
- )
187
  ax1.add_patch(glow)
188
-
189
- # Rear alignment dashed line
190
  rear_y = 8 + lot_height
191
- ax1.plot([x_pos, x_pos + width], [rear_y, rear_y],
192
- color=edge_color, linewidth=1, alpha=0.3, linestyle='--')
193
-
194
- # Lot labels (number & width)
195
- ax1.text(x_pos + width / 2, 40, f"L{lot_num}",
196
- ha='center', va='center', fontsize=16, fontweight='bold', color='white')
197
- ax1.text(x_pos + width / 2, 35, f"{width:.1f}m",
198
- ha='center', va='center', fontsize=14, fontweight='bold', color='white')
199
-
200
- # Lot type text inside a dark box
 
201
  if int(width) in self.lot_specifications:
202
  spec = self.lot_specifications[int(width)]
203
  elif width in self.lot_specifications:
204
  spec = self.lot_specifications[width]
205
  else:
206
- closest_width = min(self.lot_specifications.keys(),
207
- key=lambda x: abs(x - width))
208
  spec = self.lot_specifications[closest_width]
209
  spec = {**spec, 'type': 'Custom'}
210
-
211
  lot_type_text = spec['type']
212
  if is_corner:
213
  lot_type_text = "CORNER"
214
-
215
- ax1.text(
216
- x_pos + width / 2, 23, lot_type_text,
217
- ha='center', va='center', fontsize=11,
218
- bbox=dict(
219
- boxstyle="round,pad=0.3",
220
- facecolor='#333333',
221
- edgecolor='white',
222
- alpha=0.9
223
- ),
224
- color='white'
225
- )
226
-
227
- # Dimension indicator lines at front of lot
228
  ax1.plot([x_pos, x_pos + width], [12, 12], 'w-', linewidth=1, alpha=0.3)
229
  ax1.plot([x_pos, x_pos], [10, 14], 'w-', linewidth=1, alpha=0.3)
230
  ax1.plot([x_pos + width, x_pos + width], [10, 14], 'w-', linewidth=1, alpha=0.3)
231
-
232
  x_pos += width
233
  lot_num += 1
234
-
235
- # Draw one final “rear alignment line across everything
236
- ax1.plot([0, stage_width], [8 + lot_height, 8 + lot_height],
237
- 'cyan', linewidth=2, alpha=0.8, linestyle='-')
238
- ax1.text(
239
- stage_width / 2, 8 + lot_height + 1, 'REAR ALIGNMENT LINE',
240
- ha='center', va='bottom', fontsize=12, color='cyan', alpha=0.8,
241
- bbox=dict(
242
- boxstyle="round,pad=0.3",
243
- facecolor='#1a1a1a',
244
- edgecolor='cyan',
245
- alpha=0.8
246
- )
247
- )
248
-
249
- # Draw stage dimension arrow & text
250
  arrow_props = dict(arrowstyle='<->', color='white', lw=3)
251
  ax1.annotate('', xy=(0, -6), xytext=(stage_width, -6), arrowprops=arrow_props)
252
- ax1.text(stage_width / 2, -7, f'{stage_width}m × {stage_depth}m',
253
- ha='center', va='top', fontsize=16, fontweight='bold', color='white')
254
-
255
- # Hide all axes spines/ticks
256
- ax1.set_xticks([]); ax1.set_yticks([])
 
257
  for spine in ax1.spines.values():
258
  spine.set_visible(False)
259
-
260
- # ========== Metrics panel below ========== #
261
  ax2.axis('off')
262
  ax2.set_facecolor('#1a1a1a')
263
-
 
264
  total_lots = len(solution)
265
  unique_widths = len(set(w for w, _ in solution))
266
  diversity_score = unique_widths / len(set(self.lot_specifications.keys()))
267
-
268
  slhc_count = sum(1 for w, _ in solution if w <= 10.5)
269
  standard_count = sum(1 for w, _ in solution if 10.5 < w <= 14)
270
  premium_count = sum(1 for w, _ in solution if w > 14)
271
-
272
- slhc_pairs = sum(
273
- 1 for i in range(len(solution) - 1)
274
- if solution[i][0] <= 10.5 and solution[i + 1][0] <= 10.5
275
- )
276
-
 
 
277
  total_width = sum(w for w, _ in solution)
278
  variance = total_width - stage_width
279
- efficiency = "100%" if abs(variance) < 0.001 else f"{(total_width / stage_width) * 100:.1f}%"
280
-
281
  metrics_lines = [
282
  f"📊 TOTAL LOTS: {total_lots}",
283
  f"📐 LAND EFFICIENCY: {efficiency}",
@@ -285,436 +296,594 @@ class AdvancedGridOptimizer:
285
  f"📏 GRID VARIANCE: {variance:+.2f}m",
286
  "",
287
  f"SLHC (≤10.5m): {slhc_count} lots",
288
- f"Standard (1114m): {standard_count} lots",
289
  f"Premium (>14m): {premium_count} lots",
290
  "",
291
  f"🚗 SLHC Pairs: {slhc_pairs}",
292
- f"💰 Revenue: ${total_lots * 0.5:.1f}M ${total_lots * 1.2:.1f}M"
293
  ]
294
-
295
  col1_text = '\n'.join(metrics_lines[:5])
296
  col2_text = '\n'.join(metrics_lines[5:])
297
-
298
- ax2.text(
299
- 0.05, 0.5, col1_text, transform=ax2.transAxes,
300
- fontsize=14, verticalalignment='center', fontweight='bold',
301
- color='white',
302
- bbox=dict(
303
- boxstyle="round,pad=0.5",
304
- facecolor='#2a2a2a',
305
- edgecolor='#444444',
306
- alpha=0.8
307
- )
308
- )
309
- ax2.text(
310
- 0.55, 0.5, col2_text, transform=ax2.transAxes,
311
- fontsize=14, verticalalignment='center', fontweight='bold',
312
- color='white',
313
- bbox=dict(
314
- boxstyle="round,pad=0.5",
315
- facecolor='#2a2a2a',
316
- edgecolor='#444444',
317
- alpha=0.8
318
- )
319
- )
320
-
321
  plt.tight_layout()
322
  return fig
323
-
324
  def parse_manual_adjustments(self, adjustment_text):
325
  """Parse manual adjustment input into a list of widths"""
326
  try:
327
  if not adjustment_text:
328
  return []
 
 
329
  adjustment_text = adjustment_text.strip()
330
- parts = re.split(r'[,\s]+', adjustment_text)
331
- widths = [float(w.strip()) for w in parts if w.strip()]
 
 
 
 
 
 
 
 
 
 
 
 
332
  return widths
333
  except Exception as e:
334
  print(f"Error parsing manual adjustments: {e}")
335
  return []
336
-
337
  def validate_manual_solution(self, widths, stage_width):
338
  """Validate and provide feedback on manual solution"""
339
  if not widths:
340
  return None, "No widths provided"
341
-
342
  total_width = sum(widths)
343
  variance = total_width - stage_width
344
-
345
- solution = [
346
- (w, 'corner' if i in [0, len(widths) - 1] else 'standard')
347
- for i, w in enumerate(widths)
348
- ]
349
-
350
  if abs(variance) < 0.001:
351
  feedback = "✅ Perfect fit! Grid is exactly aligned."
352
  elif variance > 0:
353
- feedback = f"⚠️ Grid is {variance:.2f}m too wide."
354
  else:
355
- feedback = f"⚠️ Grid is {-variance:.2f}m too narrow."
356
-
 
357
  if abs(variance) > 0.001:
358
  if variance > 0:
 
359
  suggestions = []
360
  for i, w in enumerate(widths):
361
- if w - variance >= 8.5:
362
- suggestions.append(f"L{i + 1}: reduce from {w:.1f}m to {w - variance:.1f}m")
363
  if suggestions:
364
- feedback += "\n\nSuggestions:\n" + "\n".join(suggestions[:3])
365
  else:
366
- add_each = -variance / len(widths)
367
- feedback += f"\n\nSuggestion: Add {add_each:.2f}m to each lot"
368
-
 
 
369
  return solution, feedback
370
-
371
  def solution_to_string(self, solution):
372
- """Convert solution to comma‐separated string for manual editing"""
373
  if not solution:
374
  return ""
375
  return ", ".join([f"{w:.1f}" for w, _ in solution])
376
-
377
- def optimize_with_corners_diverse(self, stage_width, enabled_widths, manual_allocation=None):
378
- """Find lot arrangement with emphasis on diversity & proper corners"""
379
- all_widths = sorted(enabled_widths)
380
- if not all_widths:
381
- return None
382
-
383
- min_internal = min(all_widths)
384
- corner_options = [w for w in enabled_widths if w >= max(11.0, min_internal)]
385
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  best_solution = None
387
  best_fitness = -float('inf')
388
-
389
- for corner1 in corner_options:
390
- for corner2 in corner_options:
391
- if abs(corner1 - corner2) > 3.0:
392
- continue
393
- internal_space = stage_width - corner1 - corner2
394
- if internal_space <= 0:
 
 
 
 
 
 
 
 
395
  continue
396
- internal_sols = self.find_diverse_combinations(internal_space, all_widths, max_solutions=20)
397
- for internal_list in internal_sols:
398
- if not internal_list:
 
 
 
 
 
399
  continue
400
- if max(internal_list) > min(corner1, corner2):
401
- continue
402
- solution = [(corner1, 'corner')]
403
- solution.extend([(w, 'standard') for w in internal_list])
404
- solution.append((corner2, 'corner'))
405
- optimized = self.optimize_slhc_grouping(solution)
406
- fitness = self.evaluate_solution_with_diversity(optimized, stage_width)
 
 
407
  if fitness > best_fitness:
408
  best_fitness = fitness
409
- best_solution = optimized
410
-
411
- if not best_solution:
412
- all_combos = []
413
- self.find_all_combinations_recursive(stage_width, sorted(enabled_widths), [], all_combos, 20)
414
- for combo in all_combos[:50]:
415
- sorted_combo = sorted(combo)
416
- if len(sorted_combo) >= 2:
417
- sol = [(sorted_combo[-1], 'corner')]
418
- sol.extend((w, 'standard') for w in sorted_combo[:-2])
419
- sol.append((sorted_combo[-2], 'corner'))
420
- else:
421
- sol = [(w, 'standard') for w in sorted_combo]
422
- optimized = self.optimize_slhc_grouping(sol)
423
- fitness = self.evaluate_solution_with_diversity(optimized, stage_width)
424
- if fitness > best_fitness:
425
- best_fitness = fitness
426
- best_solution = optimized
427
-
428
  return best_solution
429
-
430
  def optimize_with_flexible_corners(self, stage_width, enabled_widths, allow_custom_corners=True):
431
- """Enhanced optimization allowing corner widths ±0.5m variation"""
 
 
432
  standard_internal = [w for w in enabled_widths if w not in self.corner_specific]
433
-
434
  best_solution = None
435
  best_fitness = -float('inf')
436
-
437
- # Try strict corner widths first
438
- sol = self.optimize_with_corners_diverse(stage_width, enabled_widths, None)
439
- if sol:
440
- fitness = self.evaluate_solution_with_diversity(sol, stage_width)
441
  if fitness > best_fitness:
442
  best_fitness = fitness
443
- best_solution = sol
444
-
445
- # If custom corners allowed, vary around each base corner
446
  if allow_custom_corners and standard_internal:
447
- for base in [11.0, 13.3, 14.8, 16.8, 14.0, 16.0]:
448
- if any(abs(w - base) < 2 for w in enabled_widths):
449
- custom_sol = self.find_optimal_custom_corners(
450
- stage_width, standard_internal, base, tolerance=0.5
 
451
  )
452
- if custom_sol:
453
- fitness = self.evaluate_solution_with_diversity(custom_sol, stage_width)
454
  if fitness > best_fitness:
455
  best_fitness = fitness
456
- best_solution = custom_sol
457
-
458
  return best_solution
459
-
460
- def find_optimal_custom_corners(self, stage_width, internal_widths, base_corner_width, tolerance=0.5):
461
- """Search corner widths ±0.5m to maximize diversity & fit exactly"""
 
 
 
 
 
 
 
 
462
  best_solution = None
463
  best_fitness = -float('inf')
464
-
465
- min_internal = min(internal_widths) if internal_widths else 8.5
466
- min_corner = max(base_corner_width - tolerance, min_internal)
467
- variations = np.arange(min_corner, base_corner_width + tolerance + 0.1, 0.1)
468
-
469
- for c1 in variations:
470
- for c2 in variations:
471
- internal_space = stage_width - c1 - c2
472
- if internal_space <= 0:
473
  continue
474
- internal_sol = self.find_exact_solution_with_diversity(internal_space, internal_widths)
475
- if internal_sol:
476
- if max(internal_sol) > min(c1, c2):
477
- continue
478
- solution = [(round(c1, 1), 'corner')]
479
- solution.extend((w, 'standard') for w in internal_sol)
480
- solution.append((round(c2, 1), 'corner'))
481
- fitness = self.evaluate_solution_with_diversity(solution, stage_width)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  if fitness > best_fitness:
483
  best_fitness = fitness
484
- best_solution = solution
485
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
486
  return best_solution
487
-
488
  def find_diverse_combinations(self, target_width, available_widths, max_solutions=20):
489
- """Find all combos that sum to target_width, then pick the top‐20 by diversity"""
490
  all_solutions = []
491
- self.find_all_combinations_recursive(target_width, available_widths, [], all_solutions, 20)
492
- if not all_solutions:
493
- return []
494
-
495
- scored = []
496
  for sol in all_solutions:
497
- scored.append((len(set(sol)), sol))
498
- scored.sort(key=lambda x: (x[0], len(x[1])), reverse=True)
499
- return [sol for _, sol in scored[:max_solutions]]
500
-
 
 
 
 
 
501
  def find_exact_solution_with_diversity(self, target_width, enabled_widths, max_depth=20):
502
- """Dynamic‐programming approach to find an exact‐sum solution that maximizes diversity"""
503
- dp = {0: ([], set())} # {sum: (solution_list, unique_widths)}
504
-
505
- for current in range(1, int(target_width) + 1):
506
- best_div = -1
507
- best_pair = None
508
- for w in enabled_widths:
509
- if w <= current and (current - w) in dp:
510
- prev_list, prev_set = dp[current - w]
511
- if len(prev_list) < max_depth:
512
- new_list = prev_list + [w]
513
- new_set = prev_set | {w}
514
- diversity = len(new_set)
515
- if diversity > best_div:
516
- best_div = diversity
517
- best_pair = (new_list, new_set)
518
- if best_pair:
519
- dp[current] = best_pair
520
-
 
 
 
 
 
 
 
 
 
 
521
  if target_width in dp:
522
  return dp[target_width][0]
523
-
524
- # Fallback to simpler exact‐sum
525
  return self.find_exact_solution(target_width, enabled_widths, max_depth)
526
-
527
  def find_exact_solution(self, target_width, enabled_widths, max_depth=20):
528
- """Classic DP to find ANY combination of enabled_widths summing to target_width"""
529
- # Quick check for a single‐width repetition
530
- for w in enabled_widths:
531
- if abs(target_width % w) < 0.001:
532
- count = int(target_width / w)
 
533
  if count <= max_depth:
534
- return [w] * count
535
-
536
- dp = {0: []} # {sum: solution_list}
537
- for s in range(1, int(target_width) + 1):
538
- dp[s] = None
539
- for w in enabled_widths:
540
- if w <= s and dp[s - w] is not None and len(dp[s - w]) < max_depth:
541
- dp[s] = dp[s - w] + [w]
542
- break
543
-
544
- if dp.get(target_width) is not None:
 
 
 
545
  return dp[target_width]
546
-
547
- # Last‐resort: brute‐force recursion
548
  all_solutions = []
549
- self.find_all_combinations_recursive(target_width, sorted(enabled_widths), [], all_solutions, max_depth)
550
- return min(all_solutions, key=len) if all_solutions else None
551
-
 
 
 
 
 
 
552
  def find_all_combinations_recursive(self, remaining, widths, current, all_solutions, max_depth):
553
- """Recursively build up lists of widths to exactly match remaining"""
554
  if abs(remaining) < 0.001:
555
  all_solutions.append(current[:])
556
  return
 
557
  if remaining < 0 or len(current) >= max_depth or len(all_solutions) >= 100:
558
  return
559
- for i, w in enumerate(widths):
560
- if w <= remaining + 0.001:
561
- current.append(w)
562
- self.find_all_combinations_recursive(remaining - w, widths[i:], current, all_solutions, max_depth)
 
 
563
  current.pop()
564
-
565
  def optimize_slhc_grouping(self, lots):
566
- """Re‐order an existing (corner, slhc, standard, custom) list for best grouping"""
567
  if not lots or len(lots) <= 1:
568
  return lots
569
-
 
570
  corner_specific = []
571
  slhc_lots = []
572
  standard_lots = []
573
  custom_lots = []
574
-
575
- for w, lot_type in lots:
576
- if w in self.corner_specific:
577
- corner_specific.append((w, lot_type))
578
- elif w <= 10.5:
579
- slhc_lots.append((w, lot_type))
580
- elif w in self.standard_widths + self.premium_widths:
581
- standard_lots.append((w, lot_type))
582
  else:
583
- # Custom widths that are not exact matches
584
- if 10.8 < w < 17:
585
- custom_lots.append((w, lot_type))
586
  else:
587
- standard_lots.append((w, lot_type))
588
-
589
- slhc_8_5 = [(w, t) for w, t in slhc_lots if abs(w - 8.5) < 0.1]
 
590
  slhc_10_5 = [(w, t) for w, t in slhc_lots if abs(w - 10.5) < 0.1]
591
-
 
592
  corner_solution = self._determine_best_corners(corner_specific + custom_lots, standard_lots)
593
-
 
594
  optimized = []
 
595
  # Place first corner
596
  if corner_solution and corner_solution[0]:
597
  optimized.append((corner_solution[0][0], 'corner'))
 
598
  for lst in [corner_specific, custom_lots, standard_lots]:
599
  if corner_solution[0] in lst:
600
  lst.remove(corner_solution[0])
601
  break
602
-
603
- # Add SLHC in optimal pairs
604
  optimized.extend(self._arrange_slhc_optimally(slhc_8_5, slhc_10_5))
605
-
606
- # Add remaining standard & custom
607
  optimized.extend(standard_lots)
608
  optimized.extend(custom_lots)
609
  optimized.extend(corner_specific)
610
-
611
  # Place second corner
612
  if corner_solution and len(corner_solution) > 1 and corner_solution[1]:
613
  optimized.append((corner_solution[1][0], 'corner'))
614
-
615
  return optimized
616
-
617
  def _determine_best_corners(self, corner_suitable, standard_lots):
618
- """Pick two lots (among corner‐suitable or large standard) that are closest in width"""
619
  all_suitable = corner_suitable + [(w, t) for w, t in standard_lots if w >= 12.5]
 
620
  if len(all_suitable) < 2:
621
  return None
622
-
 
623
  best_pair = None
624
  min_diff = float('inf')
 
625
  for i in range(len(all_suitable)):
626
  for j in range(i + 1, len(all_suitable)):
627
  diff = abs(all_suitable[i][0] - all_suitable[j][0])
628
  if diff < min_diff:
629
  min_diff = diff
630
  best_pair = (all_suitable[i], all_suitable[j])
 
631
  return best_pair
632
-
633
  def _arrange_slhc_optimally(self, slhc_8_5, slhc_10_5):
634
- """Group SLHC lots in same‐width pairs first, then cross‐mix if needed"""
635
  arranged = []
 
 
636
  while len(slhc_8_5) >= 2:
637
  arranged.extend(slhc_8_5[:2])
638
  slhc_8_5 = slhc_8_5[2:]
 
639
  while len(slhc_10_5) >= 2:
640
  arranged.extend(slhc_10_5[:2])
641
  slhc_10_5 = slhc_10_5[2:]
 
 
642
  while slhc_8_5 and slhc_10_5:
643
- arranged.append(slhc_8_5[0]); slhc_8_5 = slhc_8_5[1:]
644
- arranged.append(slhc_10_5[0]); slhc_10_5 = slhc_10_5[1:]
 
 
 
 
645
  arranged.extend(slhc_8_5)
646
  arranged.extend(slhc_10_5)
 
647
  return arranged
648
-
649
  def evaluate_solution_with_diversity(self, solution, stage_width):
650
- """Fitness = #lots*1000 + (#unique widths)*2000 – (max repetition)*500 + ratio‐bonus + corner‐bonus"""
651
  if not solution:
652
  return -float('inf')
653
-
654
  total_width = sum(w for w, _ in solution)
655
  waste = stage_width - total_width
 
 
656
  if abs(waste) > 0.001:
657
  return -float('inf')
658
-
659
  lot_count = len(solution)
 
 
660
  width_counts = {}
661
  for w, _ in solution:
662
  width_counts[w] = width_counts.get(w, 0) + 1
663
-
664
  unique_widths = len(width_counts)
665
  max_repetition = max(width_counts.values())
666
  diversity_ratio = unique_widths / lot_count if lot_count > 0 else 0
667
-
 
668
  fitness = lot_count * 1000
669
- fitness += unique_widths * 2000
670
- fitness -= max_repetition * 500
671
- fitness += diversity_ratio * 3000
672
-
673
- # Corner: penalize if SLHC on corners, reward if ≥11m
 
 
674
  if len(solution) >= 2:
675
- first_w = solution[0][0]
676
- last_w = solution[-1][0]
677
- if first_w <= 10.5: fitness -= 2000
678
- if last_w <= 10.5: fitness -= 2000
679
- if first_w >= 11.0: fitness += 1000
680
- if last_w >= 11.0: fitness += 1000
681
- diff = abs(first_w - last_w)
682
- if diff < 0.1:
683
- fitness += 1500
684
- elif diff <= 1.0:
 
 
 
685
  fitness += 1000
686
- elif diff <= 2.0:
687
- fitness += 500
 
 
 
 
 
 
 
688
  else:
689
- fitness -= 500
690
-
691
- # Bonus for adjacent SLHC
692
  for i in range(len(solution) - 1):
693
- if solution[i][0] <= 10.5 and solution[i + 1][0] <= 10.5:
694
- fitness += 300
695
-
696
- # Penalty if a cornerspecific width used internally
697
  for i in range(1, len(solution) - 1):
698
  if solution[i][0] in self.corner_specific:
699
  fitness -= 200
700
-
701
  return fitness
702
-
703
  def generate_report(self, solution, stage_width, stage_depth, manual_allocation=None):
704
- """Generate a Markdown‐style report summarizing the layout"""
705
  if not solution:
706
  return None
707
-
708
- custom_widths = [f"{w:.1f}m" for w, _ in solution if w not in self.lot_specifications]
709
-
 
 
 
 
 
710
  unique_widths = len(set(w for w, _ in solution))
711
  width_counts = {}
712
  for w, _ in solution:
713
  width_counts[w] = width_counts.get(w, 0) + 1
714
-
 
715
  total_width = sum(w for w, _ in solution)
716
  variance = total_width - stage_width
717
-
718
  report = f"""
719
  # SUBDIVISION OPTIMIZATION REPORT
720
  ## Project Analysis for {stage_width}m × {stage_depth}m Stage
@@ -727,66 +896,426 @@ class AdvancedGridOptimizer:
727
  - **Stage Dimensions**: {stage_width}m × {stage_depth}m
728
  - **Total Area**: {stage_width * stage_depth}m²
729
  {f"- **Custom Widths Used**: {', '.join(custom_widths)}" if custom_widths else ""}
730
- """
731
 
732
- report += "\n### LOT DIVERSITY ANALYSIS\n"
733
- sorted_counts = sorted(width_counts.items(), key=lambda x: x[1], reverse=True)
734
- for width, count in sorted_counts:
 
 
 
735
  percentage = (count / len(solution)) * 100
736
  if width in self.lot_specifications:
737
  spec = self.lot_specifications[width]
738
  report += f"- **{width:.1f}m** × {count} ({percentage:.1f}%): {spec['type']}\n"
739
  else:
740
  report += f"- **{width:.1f}m** × {count} ({percentage:.1f}%): Custom Width\n"
741
-
 
742
  if len(solution) >= 2:
743
- report += f"""
744
- ### CORNER ANALYSIS
745
- - **Front Corner**: {solution[0][0]:.1f}m with 3m × 3m splay
746
- - **Rear Corner**: {solution[-1][0]:.1f}m with 3m × 3m splay
747
- - **Balance**: {abs(solution[0][0] - solution[-1][0]):.1f}m difference
748
- """
749
-
750
- report += """
751
- ### DESIGN FEATURES
752
- - Corner splays provide safe sight lines at intersections
753
- - All lots have identical rear alignment for visual consistency
754
- - Diverse lot mix ensures varied streetscape
755
- - SLHC lots grouped for efficient garbage collection
756
-
757
- ---
758
- *Report generated: {timestamp}*
759
- """.format(timestamp=datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
760
-
761
  return report
762
-
763
  def process_plan_image(self, image_path, scale=1000, auto_detect_scale=True, confidence=0.75):
764
- """Use OpenCV + Tesseract to identify lots & OCR text; return a preview plus lot data"""
765
  if not PLAN_READER_AVAILABLE:
766
- # Demo mode: return a mock array and mock lot list
767
  mock_lots = []
768
  for i in range(8):
769
  frontage = np.random.choice([8.5, 10.5, 12.5, 14.0, 16.0])
770
  mock_lots.append({
771
- 'lot_number': f'L{i + 1}',
772
  'frontage': frontage,
773
  'depth': 32,
774
  'area': frontage * 32,
775
  'type': 'SLHC' if frontage <= 10.5 else 'Standard' if frontage <= 14 else 'Premium'
776
  })
777
-
778
- # Create a simple “demo” image
779
  fig, ax = plt.subplots(figsize=(10, 8))
780
- ax.text(0.5, 0.5,
781
- 'Plan Reader Demo Mode\n(Install required libraries for actual functionality)',
782
  ha='center', va='center', fontsize=16, transform=ax.transAxes)
783
- ax.set_xlim(0, 1); ax.set_ylim(0, 1); ax.axis('off')
 
 
 
 
784
  fig.canvas.draw()
785
  preview_img = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)
786
  preview_img = preview_img.reshape(fig.canvas.get_width_height()[::-1] + (3,))
787
  plt.close()
 
788
  summary = """
789
  ### Demo Mode Active
790
  Plan reader libraries not installed. Showing sample data.
791
 
792
  **To enable full functionality, install:**
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  import numpy as np
4
  import matplotlib.pyplot as plt
5
  import matplotlib.patches as patches
6
+ from matplotlib.patches import Rectangle, FancyBboxPatch, Circle, Polygon, Wedge, Path, PathPatch
7
  from matplotlib.collections import PatchCollection
8
+ from matplotlib import cm
9
+ from mpl_toolkits.mplot3d import Axes3D
10
+ from mpl_toolkits.mplot3d.art3d import Poly3DCollection
11
+ import json
12
  from datetime import datetime
13
  import io
14
+ import base64
15
  import tempfile
 
16
 
17
  # Try to import plan reading libraries
18
  try:
 
23
  PLAN_READER_AVAILABLE = True
24
  except ImportError:
25
  PLAN_READER_AVAILABLE = False
26
+ print("Warning: Plan reader libraries not available. Install opencv-python, pytesseract, pillow, and pdf2image for full functionality.")
 
 
27
 
28
  class AdvancedGridOptimizer:
29
  def __init__(self):
30
  # Standard lot widths and their typical depths
31
  self.lot_specifications = {
32
+ 8.5: {"depths": [21, 25, 28], "type": "SLHC", "squares": "11-16"},
33
+ 10.5: {"depths": [21, 25, 28, 32, 35], "type": "SLHC", "squares": "13-21.5"},
34
+ 12.5: {"depths": [21, 25, 28, 30, 32], "type": "Standard", "squares": "16-24"},
35
+ 14.0: {"depths": [21, 25, 28, 30, 32, 34], "type": "Standard", "squares": "17-28"},
36
+ 16.0: {"depths": [28, 30, 32, 34, 36, 40], "type": "Premium", "squares": "24-38"},
37
+ 18.0: {"depths": [32, 34, 36], "type": "Premium", "squares": "32-39"},
38
  # Traditional corner lots
39
+ 11.0: {"depths": [21, 25], "type": "Corner-SLHC", "squares": "13-17"},
40
+ 13.3: {"depths": [25, 28], "type": "Corner-Standard", "squares": "18-22"},
41
+ 14.8: {"depths": [28, 30], "type": "Corner-Standard", "squares": "22-26"},
42
+ 16.8: {"depths": [30, 32], "type": "Corner-Premium", "squares": "26-32"}
43
  }
44
+
45
  self.slhc_widths = [8.5, 10.5]
46
  self.standard_widths = [12.5, 14.0]
47
  self.premium_widths = [16.0, 18.0]
48
  self.corner_specific = [11.0, 13.3, 14.8, 16.8]
49
+
50
  # Define corner_widths as all widths suitable for corners
51
  self.corner_widths = self.corner_specific + [14.0, 16.0, 18.0]
52
+
53
  # Enhanced color palette with gradients
54
  self.color_schemes = {
55
  'modern': {
56
+ 8.5: '#FF6B6B', # Vibrant Red
57
+ 10.5: '#4ECDC4', # Teal
58
+ 12.5: '#45B7D1', # Sky Blue
59
+ 14.0: '#96CEB4', # Sage Green
60
+ 16.0: '#DDA0DD', # Lavender
61
+ 18.0: '#FFD93D', # Golden
62
+ 11.0: '#FFA07A', # Coral
63
+ 13.3: '#98D8C8', # Mint
64
+ 14.8: '#F7DC6F', # Butter
65
+ 16.8: '#BB8FCE' # Orchid
66
  },
67
  'professional': {
68
+ 8.5: '#E74C3C', # Professional Red
69
+ 10.5: '#3498DB', # Professional Blue
70
+ 12.5: '#2ECC71', # Professional Green
71
+ 14.0: '#F39C12', # Professional Orange
72
+ 16.0: '#9B59B6', # Professional Purple
73
+ 18.0: '#1ABC9C', # Professional Turquoise
74
+ 11.0: '#E67E22', # Professional Dark Orange
75
+ 13.3: '#16A085', # Professional Teal
76
+ 14.8: '#F1C40F', # Professional Yellow
77
+ 16.8: '#8E44AD' # Professional Dark Purple
78
  },
79
  'neon': {
80
+ 8.5: '#FF073A', # Neon Red
81
+ 10.5: '#0AEFFF', # Neon Cyan
82
+ 12.5: '#39FF14', # Neon Green
83
+ 14.0: '#FF6600', # Neon Orange
84
+ 16.0: '#BF00FF', # Neon Purple
85
+ 18.0: '#FFFF00', # Neon Yellow
86
+ 11.0: '#FF1493', # Neon Pink
87
+ 13.3: '#00FFFF', # Neon Aqua
88
+ 14.8: '#FFF700', # Bright Yellow
89
+ 16.8: '#FF00FF' # Neon Magenta
90
  }
91
  }
92
+
93
  self.current_scheme = 'neon'
94
  self.current_solution = None # Store current AI solution
95
+
96
+ def create_enhanced_visualization(self, solution, stage_width, stage_depth=32, title="Premium Grid Layout", show_variance=None):
 
97
  """Create a clean 2D visualization with corner splays and proper alignment"""
98
+ fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(18, 12), gridspec_kw={'height_ratios': [3, 1]},
99
+ facecolor='#1a1a1a')
100
+
101
+ # Main visualization
 
 
102
  colors = self.color_schemes[self.current_scheme]
103
+
104
  x_pos = 0
105
  lot_num = 1
106
+
107
+ # Set up main plot with dark background
108
  ax1.set_xlim(-5, stage_width + 5)
109
  ax1.set_ylim(-10, 50)
110
  ax1.set_facecolor('#1a1a1a')
111
+
112
+ # Add title with variance if provided
113
  if show_variance is not None:
114
+ variance_color = '#39FF14' if abs(show_variance) < 0.001 else '#FF073A'
115
  title_text = f"{title}\nGrid Variance: {show_variance:+.1f}m"
116
  ax1.set_title(title_text, fontsize=28, fontweight='bold', pad=25, color='white')
117
  else:
118
  ax1.set_title(title, fontsize=28, fontweight='bold', pad=25, color='white')
119
+
120
+ # Add subtle dark gradient background
121
  gradient = np.linspace(0.2, 0, 100).reshape(1, -1)
122
+ ax1.imshow(gradient, extent=[-5, stage_width + 5, -10, 50], aspect='auto',
123
+ cmap='Greys', alpha=0.3, zorder=0)
124
+
125
+ # Add street with label
126
+ street = Rectangle((-5, -8), stage_width + 10, 12,
127
+ facecolor='#2c2c2c', alpha=0.9, zorder=1,
128
+ edgecolor='#444444', linewidth=2)
129
  ax1.add_patch(street)
130
+ ax1.text(stage_width/2, -2, 'STREET', ha='center', va='center',
131
+ fontsize=20, color='white', fontweight='bold')
132
+
133
+ # Draw lots with corner splays - FIXED ALIGNMENT
134
+ splay_size = 3 # 3m corner splay
135
+ lot_height = 28 # UNIFORM HEIGHT FOR ALL LOTS
136
+
137
  for i, (width, lot_type) in enumerate(solution):
138
+ # Get base color
139
  if width in colors:
140
  base_color = colors[width]
141
  else:
142
  closest_width = min(colors.keys(), key=lambda x: abs(x - width))
143
  base_color = colors[closest_width]
144
+
145
+ # Check position
146
  is_corner = (i == 0 or i == len(solution) - 1)
147
+
148
+ # Consistent styling for visual alignment
149
  face_color = base_color
150
  edge_color = 'white'
151
  linewidth = 4.0 if is_corner else 3.0
152
+
153
+ # Create lot shape with SAME HEIGHT for all lots
154
  if is_corner:
155
+ # Corner lot with splay - using same height
156
+ if i == 0: # First corner
 
157
  vertices = [
158
+ (x_pos + splay_size, 8), # Start after splay
159
  (x_pos + width, 8),
160
+ (x_pos + width, 8 + lot_height), # SAME HEIGHT
161
+ (x_pos, 8 + lot_height), # Straight rear
162
+ (x_pos, 8 + splay_size) # Splay corner
163
  ]
164
+ else: # Last corner
165
  vertices = [
166
+ (x_pos, 8),
167
  (x_pos + width - splay_size, 8),
168
+ (x_pos + width, 8 + splay_size), # Splay corner
169
+ (x_pos + width, 8 + lot_height), # SAME HEIGHT
170
+ (x_pos, 8 + lot_height)
171
  ]
172
+
173
+ # Create polygon path
174
  codes = [Path.MOVETO] + [Path.LINETO] * (len(vertices) - 1) + [Path.CLOSEPOLY]
175
+ vertices.append(vertices[0]) # Close the path
176
  path = Path(vertices, codes)
177
+ lot = PathPatch(path, facecolor=face_color, edgecolor=edge_color,
178
+ linewidth=linewidth, zorder=3)
179
+ ax1.add_patch(lot)
180
+
181
+ # Add splay line
182
  if i == 0:
183
+ ax1.plot([x_pos, x_pos + splay_size], [8 + splay_size, 8],
184
+ 'white', linewidth=2, alpha=0.8)
185
  else:
186
+ ax1.plot([x_pos + width - splay_size, x_pos + width],
187
+ [8, 8 + splay_size], 'white', linewidth=2, alpha=0.8)
188
  else:
189
+ # Regular lot with SAME HEIGHT
190
+ lot = FancyBboxPatch((x_pos, 8), width, lot_height,
191
+ boxstyle="round,pad=0.1",
192
+ facecolor=face_color,
193
+ edgecolor=edge_color,
194
+ linewidth=linewidth,
195
+ zorder=3)
196
+ ax1.add_patch(lot)
197
+
198
+ # Add subtle glow (same for all)
199
+ glow = FancyBboxPatch((x_pos - 0.2, 7.8), width + 0.4, lot_height + 0.4,
200
+ boxstyle="round,pad=0.15",
201
+ facecolor='none',
202
+ edgecolor=face_color,
203
+ linewidth=1,
204
+ alpha=0.5,
205
+ zorder=2)
 
 
 
 
206
  ax1.add_patch(glow)
207
+
208
+ # Add rear alignment line to emphasize equal depth
209
  rear_y = 8 + lot_height
210
+ ax1.plot([x_pos, x_pos + width], [rear_y, rear_y],
211
+ color=edge_color, linewidth=1, alpha=0.3, linestyle='--')
212
+
213
+ # Add lot information (positioned consistently)
214
+ ax1.text(x_pos + width/2, 40, f'L{lot_num}',
215
+ ha='center', va='center', fontsize=16, fontweight='bold', color='white')
216
+
217
+ ax1.text(x_pos + width/2, 35, f'{width:.1f}m',
218
+ ha='center', va='center', fontsize=14, fontweight='bold', color='white')
219
+
220
+ # Lot type
221
  if int(width) in self.lot_specifications:
222
  spec = self.lot_specifications[int(width)]
223
  elif width in self.lot_specifications:
224
  spec = self.lot_specifications[width]
225
  else:
226
+ closest_width = min(self.lot_specifications.keys(),
227
+ key=lambda x: abs(x - width))
228
  spec = self.lot_specifications[closest_width]
229
  spec = {**spec, 'type': 'Custom'}
230
+
231
  lot_type_text = spec['type']
232
  if is_corner:
233
  lot_type_text = "CORNER"
234
+
235
+ ax1.text(x_pos + width/2, 23, lot_type_text,
236
+ ha='center', va='center', fontsize=11,
237
+ bbox=dict(boxstyle="round,pad=0.3", facecolor='#333333',
238
+ edgecolor='white', alpha=0.9), color='white')
239
+
240
+ # Dimension lines
 
 
 
 
 
 
 
241
  ax1.plot([x_pos, x_pos + width], [12, 12], 'w-', linewidth=1, alpha=0.3)
242
  ax1.plot([x_pos, x_pos], [10, 14], 'w-', linewidth=1, alpha=0.3)
243
  ax1.plot([x_pos + width, x_pos + width], [10, 14], 'w-', linewidth=1, alpha=0.3)
244
+
245
  x_pos += width
246
  lot_num += 1
247
+
248
+ # Add rear alignment line across all lots
249
+ ax1.plot([0, stage_width], [8 + lot_height, 8 + lot_height],
250
+ 'cyan', linewidth=2, alpha=0.8, linestyle='-')
251
+ ax1.text(stage_width/2, 8 + lot_height + 1, 'REAR ALIGNMENT LINE',
252
+ ha='center', va='bottom', fontsize=12, color='cyan', alpha=0.8,
253
+ bbox=dict(boxstyle="round,pad=0.3", facecolor='#1a1a1a',
254
+ edgecolor='cyan', alpha=0.8))
255
+
256
+ # Add stage dimensions
 
 
 
 
 
 
257
  arrow_props = dict(arrowstyle='<->', color='white', lw=3)
258
  ax1.annotate('', xy=(0, -6), xytext=(stage_width, -6), arrowprops=arrow_props)
259
+ ax1.text(stage_width/2, -7, f'{stage_width}m × {stage_depth}m',
260
+ ha='center', va='top', fontsize=16, fontweight='bold', color='white')
261
+
262
+ # Style axes
263
+ ax1.set_xticks([])
264
+ ax1.set_yticks([])
265
  for spine in ax1.spines.values():
266
  spine.set_visible(False)
267
+
268
+ # Metrics panel
269
  ax2.axis('off')
270
  ax2.set_facecolor('#1a1a1a')
271
+
272
+ # Calculate metrics with diversity score
273
  total_lots = len(solution)
274
  unique_widths = len(set(w for w, _ in solution))
275
  diversity_score = unique_widths / len(set(self.lot_specifications.keys()))
276
+
277
  slhc_count = sum(1 for w, _ in solution if w <= 10.5)
278
  standard_count = sum(1 for w, _ in solution if 10.5 < w <= 14)
279
  premium_count = sum(1 for w, _ in solution if w > 14)
280
+
281
+ # SLHC pairs
282
+ slhc_pairs = 0
283
+ for i in range(len(solution) - 1):
284
+ if solution[i][0] <= 10.5 and solution[i+1][0] <= 10.5:
285
+ slhc_pairs += 1
286
+
287
+ # Calculate actual total width and variance
288
  total_width = sum(w for w, _ in solution)
289
  variance = total_width - stage_width
290
+ efficiency = "100%" if abs(variance) < 0.001 else f"{(total_width/stage_width)*100:.1f}%"
291
+
292
  metrics_lines = [
293
  f"📊 TOTAL LOTS: {total_lots}",
294
  f"📐 LAND EFFICIENCY: {efficiency}",
 
296
  f"📏 GRID VARIANCE: {variance:+.2f}m",
297
  "",
298
  f"SLHC (≤10.5m): {slhc_count} lots",
299
+ f"Standard (11-14m): {standard_count} lots",
300
  f"Premium (>14m): {premium_count} lots",
301
  "",
302
  f"🚗 SLHC Pairs: {slhc_pairs}",
303
+ f"💰 Revenue: ${total_lots * 0.5:.1f}M - ${total_lots * 1.2:.1f}M"
304
  ]
305
+
306
  col1_text = '\n'.join(metrics_lines[:5])
307
  col2_text = '\n'.join(metrics_lines[5:])
308
+
309
+ ax2.text(0.05, 0.5, col1_text, transform=ax2.transAxes,
310
+ fontsize=14, verticalalignment='center', fontweight='bold',
311
+ color='white',
312
+ bbox=dict(boxstyle="round,pad=0.5", facecolor='#2a2a2a',
313
+ edgecolor='#444444', alpha=0.8))
314
+
315
+ ax2.text(0.55, 0.5, col2_text, transform=ax2.transAxes,
316
+ fontsize=14, verticalalignment='center', fontweight='bold',
317
+ color='white',
318
+ bbox=dict(boxstyle="round,pad=0.5", facecolor='#2a2a2a',
319
+ edgecolor='#444444', alpha=0.8))
320
+
 
 
 
 
 
 
 
 
 
 
 
321
  plt.tight_layout()
322
  return fig
323
+
324
  def parse_manual_adjustments(self, adjustment_text):
325
  """Parse manual adjustment input into a list of widths"""
326
  try:
327
  if not adjustment_text:
328
  return []
329
+
330
+ # Remove any whitespace and split by commas or spaces
331
  adjustment_text = adjustment_text.strip()
332
+
333
+ # Try parsing as comma-separated values
334
+ if ',' in adjustment_text:
335
+ widths = [float(w.strip()) for w in adjustment_text.split(',') if w.strip()]
336
+ # Try parsing as space-separated values
337
+ elif ' ' in adjustment_text:
338
+ widths = [float(w.strip()) for w in adjustment_text.split() if w.strip()]
339
+ # Try parsing as newline-separated values
340
+ elif '\n' in adjustment_text:
341
+ widths = [float(w.strip()) for w in adjustment_text.split('\n') if w.strip()]
342
+ else:
343
+ # Single value
344
+ widths = [float(adjustment_text)]
345
+
346
  return widths
347
  except Exception as e:
348
  print(f"Error parsing manual adjustments: {e}")
349
  return []
350
+
351
  def validate_manual_solution(self, widths, stage_width):
352
  """Validate and provide feedback on manual solution"""
353
  if not widths:
354
  return None, "No widths provided"
355
+
356
  total_width = sum(widths)
357
  variance = total_width - stage_width
358
+
359
+ # Create solution format
360
+ solution = [(w, 'corner' if i in [0, len(widths)-1] else 'standard')
361
+ for i, w in enumerate(widths)]
362
+
363
+ # Provide feedback
364
  if abs(variance) < 0.001:
365
  feedback = "✅ Perfect fit! Grid is exactly aligned."
366
  elif variance > 0:
367
+ feedback = f"⚠️ Grid is {variance:.2f}m too wide. Remove {variance:.2f}m total width."
368
  else:
369
+ feedback = f"⚠️ Grid is {-variance:.2f}m too narrow. Add {-variance:.2f}m total width."
370
+
371
+ # Add suggestions if not perfect
372
  if abs(variance) > 0.001:
373
  if variance > 0:
374
+ # Suggest which lots could be reduced
375
  suggestions = []
376
  for i, w in enumerate(widths):
377
+ if w - variance >= 8.5: # Minimum viable width
378
+ suggestions.append(f"L{i+1}: reduce from {w:.1f}m to {w-variance:.1f}m")
379
  if suggestions:
380
+ feedback += f"\n\nSuggestions:\n" + "\n".join(suggestions[:3])
381
  else:
382
+ # Suggest which lots could be increased
383
+ suggestions = []
384
+ add_per_lot = -variance / len(widths)
385
+ feedback += f"\n\nSuggestion: Add {add_per_lot:.2f}m to each lot"
386
+
387
  return solution, feedback
388
+
389
  def solution_to_string(self, solution):
390
+ """Convert solution to string format for manual editing"""
391
  if not solution:
392
  return ""
393
  return ", ".join([f"{w:.1f}" for w, _ in solution])
394
+
395
+ def parse_manual_input(self, manual_text):
396
+ """Parse manual input into structured data"""
397
+ try:
398
+ if not manual_text:
399
+ return {}
400
+
401
+ # Try JSON format first
402
+ if manual_text.strip().startswith('{'):
403
+ return json.loads(manual_text)
404
+
405
+ # Otherwise parse line by line
406
+ result = {}
407
+ for line in manual_text.strip().split('\n'):
408
+ line = line.strip()
409
+ if not line:
410
+ continue
411
+
412
+ if '=' in line:
413
+ parts = line.split('=')
414
+ width_str = parts[0].strip().replace('m', '')
415
+ count_str = parts[1].strip()
416
+ try:
417
+ width_val = float(width_str)
418
+ result[width_val] = int(count_str)
419
+ except:
420
+ pass
421
+ elif ':' in line:
422
+ parts = line.split(':')
423
+ width_str = parts[0].strip().replace('m', '')
424
+ count_str = parts[1].strip()
425
+ try:
426
+ width_val = float(width_str)
427
+ result[width_val] = int(count_str)
428
+ except:
429
+ pass
430
+ return result
431
+ except Exception as e:
432
+ print(f"Error parsing manual input: {e}")
433
+ return {}
434
+
435
+ def find_optimal_custom_corners(self, stage_width, internal_widths, base_corner_width, tolerance=0.5):
436
+ """Find optimal corner widths that can vary slightly from base width"""
437
  best_solution = None
438
  best_fitness = -float('inf')
439
+
440
+ # Ensure corners are at least as wide as smallest internal lot
441
+ min_internal = min(internal_widths) if internal_widths else 8.5
442
+ min_corner_width = max(base_corner_width - tolerance, min_internal)
443
+
444
+ # Try variations of corner widths within tolerance
445
+ variations = np.arange(min_corner_width,
446
+ base_corner_width + tolerance + 0.1,
447
+ 0.1)
448
+
449
+ for corner1 in variations:
450
+ for corner2 in variations:
451
+ # Calculate internal space
452
+ internal_width = stage_width - corner1 - corner2
453
+ if internal_width <= 0:
454
  continue
455
+
456
+ # Try to fill internal space exactly
457
+ internal_solution = self.find_exact_solution_with_diversity(internal_width, internal_widths)
458
+
459
+ if internal_solution:
460
+ # Verify no internal lot is wider than corners
461
+ max_internal = max(internal_solution) if internal_solution else 0
462
+ if max_internal > min(corner1, corner2):
463
  continue
464
+
465
+ # Build complete solution
466
+ solution = [(round(corner1, 1), 'corner')]
467
+ solution.extend([(w, 'standard') for w in internal_solution])
468
+ solution.append((round(corner2, 1), 'corner'))
469
+
470
+ # Evaluate (prefer balanced corners and diversity)
471
+ fitness = self.evaluate_solution_with_diversity(solution, stage_width)
472
+
473
  if fitness > best_fitness:
474
  best_fitness = fitness
475
+ best_solution = solution
476
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
477
  return best_solution
478
+
479
  def optimize_with_flexible_corners(self, stage_width, enabled_widths, allow_custom_corners=True):
480
+ """Enhanced optimization allowing flexible corner sizes with diversity"""
481
+
482
+ # Separate widths by type
483
  standard_internal = [w for w in enabled_widths if w not in self.corner_specific]
484
+
485
  best_solution = None
486
  best_fitness = -float('inf')
487
+
488
+ # Strategy 1: Try exact widths first with diversity
489
+ solution = self.optimize_with_corners_diverse(stage_width, enabled_widths, None)
490
+ if solution:
491
+ fitness = self.evaluate_solution_with_diversity(solution, stage_width)
492
  if fitness > best_fitness:
493
  best_fitness = fitness
494
+ best_solution = solution
495
+
496
+ # Strategy 2: Try flexible corners if enabled
497
  if allow_custom_corners and standard_internal:
498
+ # Try variations around each corner-suitable width
499
+ for base_width in [11.0, 13.3, 14.8, 16.8, 14.0, 16.0]:
500
+ if any(abs(w - base_width) < 2 for w in enabled_widths):
501
+ custom_solution = self.find_optimal_custom_corners(
502
+ stage_width, standard_internal, base_width, tolerance=0.5
503
  )
504
+ if custom_solution:
505
+ fitness = self.evaluate_solution_with_diversity(custom_solution, stage_width)
506
  if fitness > best_fitness:
507
  best_fitness = fitness
508
+ best_solution = custom_solution
509
+
510
  return best_solution
511
+
512
+ def optimize_with_corners_diverse(self, stage_width, enabled_widths, manual_allocation=None):
513
+ """Find lot arrangement with emphasis on diversity and proper corner sizing"""
514
+
515
+ # Separate widths by size
516
+ all_widths = sorted(enabled_widths)
517
+ min_internal_width = min(all_widths)
518
+
519
+ # Corner lots must be at least as wide as smallest internal lot
520
+ corner_options = [w for w in enabled_widths if w >= max(11.0, min_internal_width)]
521
+
522
  best_solution = None
523
  best_fitness = -float('inf')
524
+
525
+ # Try different corner combinations
526
+ for corner1 in corner_options:
527
+ for corner2 in corner_options:
528
+ if abs(corner1 - corner2) > 3.0: # Skip very unbalanced
 
 
 
 
529
  continue
530
+
531
+ # Calculate internal space
532
+ internal_width = stage_width - corner1 - corner2
533
+ if internal_width <= 0:
534
+ continue
535
+
536
+ # Find diverse internal solutions
537
+ internal_solutions = self.find_diverse_combinations(
538
+ internal_width, all_widths, max_solutions=20
539
+ )
540
+
541
+ for internal_widths in internal_solutions:
542
+ # Verify no internal lot is wider than corners
543
+ max_internal = max(internal_widths) if internal_widths else 0
544
+ if max_internal > min(corner1, corner2):
545
+ continue # Skip if internal lots are wider than corners
546
+
547
+ # Build complete solution
548
+ solution = [(corner1, 'corner')]
549
+ solution.extend([(w, 'standard') for w in internal_widths])
550
+ solution.append((corner2, 'corner'))
551
+
552
+ # Optimize arrangement
553
+ optimized = self.optimize_slhc_grouping(solution)
554
+ fitness = self.evaluate_solution_with_diversity(optimized, stage_width)
555
+
556
  if fitness > best_fitness:
557
  best_fitness = fitness
558
+ best_solution = optimized
559
+
560
+ # If no good solution, try without strict corner rules but maintain size hierarchy
561
+ if not best_solution:
562
+ all_solutions = []
563
+ self.find_all_combinations_recursive(stage_width, sorted(enabled_widths),
564
+ [], all_solutions, 20)
565
+
566
+ for widths in all_solutions[:50]:
567
+ # Ensure corners are among the largest lots
568
+ sorted_widths = sorted(widths)
569
+ if len(sorted_widths) >= 2:
570
+ # Put two largest widths at corners
571
+ solution = [(sorted_widths[-1], 'corner')] # Largest
572
+ solution.extend([(w, 'standard') for w in sorted_widths[:-2]])
573
+ solution.append((sorted_widths[-2], 'corner')) # Second largest
574
+ else:
575
+ solution = [(w, 'standard') for w in widths]
576
+
577
+ optimized = self.optimize_slhc_grouping(solution)
578
+ fitness = self.evaluate_solution_with_diversity(optimized, stage_width)
579
+
580
+ if fitness > best_fitness:
581
+ best_fitness = fitness
582
+ best_solution = optimized
583
+
584
  return best_solution
585
+
586
  def find_diverse_combinations(self, target_width, available_widths, max_solutions=20):
587
+ """Find combinations that maximize diversity"""
588
  all_solutions = []
589
+ self.find_all_combinations_recursive(target_width, available_widths,
590
+ [], all_solutions, 20)
591
+
592
+ # Sort by diversity (number of unique widths)
593
+ diverse_solutions = []
594
  for sol in all_solutions:
595
+ unique_count = len(set(sol))
596
+ diverse_solutions.append((unique_count, sol))
597
+
598
+ # Sort by diversity, then by total lots
599
+ diverse_solutions.sort(key=lambda x: (x[0], len(x[1])), reverse=True)
600
+
601
+ # Return the most diverse solutions
602
+ return [sol[1] for sol in diverse_solutions[:max_solutions]]
603
+
604
  def find_exact_solution_with_diversity(self, target_width, enabled_widths, max_depth=20):
605
+ """Find exact solution prioritizing diversity"""
606
+
607
+ # Try to use multiple different widths
608
+ solutions = []
609
+
610
+ # Dynamic programming with diversity tracking
611
+ dp = {}
612
+ dp[0] = ([], set()) # (solution, unique_widths)
613
+
614
+ for current_target in range(1, int(target_width) + 1):
615
+ best_diversity = -1
616
+ best_solution = None
617
+
618
+ for width in enabled_widths:
619
+ if width <= current_target and (current_target - width) in dp:
620
+ prev_solution, prev_unique = dp[current_target - width]
621
+ if len(prev_solution) < max_depth:
622
+ new_solution = prev_solution + [width]
623
+ new_unique = prev_unique.copy()
624
+ new_unique.add(width)
625
+
626
+ diversity = len(new_unique)
627
+ if diversity > best_diversity:
628
+ best_diversity = diversity
629
+ best_solution = (new_solution, new_unique)
630
+
631
+ if best_solution:
632
+ dp[current_target] = best_solution
633
+
634
  if target_width in dp:
635
  return dp[target_width][0]
636
+
637
+ # Fallback to regular solution
638
  return self.find_exact_solution(target_width, enabled_widths, max_depth)
639
+
640
  def find_exact_solution(self, target_width, enabled_widths, max_depth=20):
641
+ """Find exact combination that sums to target_width"""
642
+
643
+ # Quick check for simple solutions
644
+ for width in enabled_widths:
645
+ if abs(target_width % width) < 0.001:
646
+ count = int(target_width / width)
647
  if count <= max_depth:
648
+ return [width] * count
649
+
650
+ # Dynamic programming solution
651
+ dp = {}
652
+ dp[0] = []
653
+
654
+ for current_target in range(1, int(target_width) + 1):
655
+ for width in enabled_widths:
656
+ if width <= current_target and (current_target - width) in dp:
657
+ prev_solution = dp[current_target - width]
658
+ if len(prev_solution) < max_depth:
659
+ dp[current_target] = prev_solution + [width]
660
+
661
+ if target_width in dp:
662
  return dp[target_width]
663
+
664
+ # Try exhaustive search
665
  all_solutions = []
666
+ self.find_all_combinations_recursive(target_width, sorted(enabled_widths),
667
+ [], all_solutions, max_depth)
668
+
669
+ if all_solutions:
670
+ # Return shortest solution
671
+ return min(all_solutions, key=len)
672
+
673
+ return None
674
+
675
  def find_all_combinations_recursive(self, remaining, widths, current, all_solutions, max_depth):
676
+ """Recursively find all exact combinations"""
677
  if abs(remaining) < 0.001:
678
  all_solutions.append(current[:])
679
  return
680
+
681
  if remaining < 0 or len(current) >= max_depth or len(all_solutions) >= 100:
682
  return
683
+
684
+ for i, width in enumerate(widths):
685
+ if width <= remaining + 0.001:
686
+ current.append(width)
687
+ self.find_all_combinations_recursive(remaining - width, widths[i:],
688
+ current, all_solutions, max_depth)
689
  current.pop()
690
+
691
  def optimize_slhc_grouping(self, lots):
692
+ """Optimize lot arrangement with sophisticated rules"""
693
  if not lots or len(lots) <= 1:
694
  return lots
695
+
696
+ # Separate lots by type
697
  corner_specific = []
698
  slhc_lots = []
699
  standard_lots = []
700
  custom_lots = []
701
+
702
+ for width, lot_type in lots:
703
+ if width in self.corner_specific:
704
+ corner_specific.append((width, lot_type))
705
+ elif width <= 10.5:
706
+ slhc_lots.append((width, lot_type))
707
+ elif width in self.standard_widths + self.premium_widths:
708
+ standard_lots.append((width, lot_type))
709
  else:
710
+ # Custom width
711
+ if width > 10.8 and width < 17:
712
+ custom_lots.append((width, lot_type))
713
  else:
714
+ standard_lots.append((width, lot_type))
715
+
716
+ # Further separate SLHC by width
717
+ slhc_8_5 = [(w, t) for w, t in slhc_lots if abs(w - 8.5) < 0.1]
718
  slhc_10_5 = [(w, t) for w, t in slhc_lots if abs(w - 10.5) < 0.1]
719
+
720
+ # Determine corner placement
721
  corner_solution = self._determine_best_corners(corner_specific + custom_lots, standard_lots)
722
+
723
+ # Build optimized layout
724
  optimized = []
725
+
726
  # Place first corner
727
  if corner_solution and corner_solution[0]:
728
  optimized.append((corner_solution[0][0], 'corner'))
729
+ # Remove from appropriate list
730
  for lst in [corner_specific, custom_lots, standard_lots]:
731
  if corner_solution[0] in lst:
732
  lst.remove(corner_solution[0])
733
  break
734
+
735
+ # Add SLHC groups optimally
736
  optimized.extend(self._arrange_slhc_optimally(slhc_8_5, slhc_10_5))
737
+
738
+ # Add remaining lots
739
  optimized.extend(standard_lots)
740
  optimized.extend(custom_lots)
741
  optimized.extend(corner_specific)
742
+
743
  # Place second corner
744
  if corner_solution and len(corner_solution) > 1 and corner_solution[1]:
745
  optimized.append((corner_solution[1][0], 'corner'))
746
+
747
  return optimized
748
+
749
  def _determine_best_corners(self, corner_suitable, standard_lots):
750
+ """Determine the best corner placement strategy"""
751
  all_suitable = corner_suitable + [(w, t) for w, t in standard_lots if w >= 12.5]
752
+
753
  if len(all_suitable) < 2:
754
  return None
755
+
756
+ # Find best matching pair
757
  best_pair = None
758
  min_diff = float('inf')
759
+
760
  for i in range(len(all_suitable)):
761
  for j in range(i + 1, len(all_suitable)):
762
  diff = abs(all_suitable[i][0] - all_suitable[j][0])
763
  if diff < min_diff:
764
  min_diff = diff
765
  best_pair = (all_suitable[i], all_suitable[j])
766
+
767
  return best_pair
768
+
769
  def _arrange_slhc_optimally(self, slhc_8_5, slhc_10_5):
770
+ """Arrange SLHC lots for optimal garage adjacency"""
771
  arranged = []
772
+
773
+ # Pair matching widths first
774
  while len(slhc_8_5) >= 2:
775
  arranged.extend(slhc_8_5[:2])
776
  slhc_8_5 = slhc_8_5[2:]
777
+
778
  while len(slhc_10_5) >= 2:
779
  arranged.extend(slhc_10_5[:2])
780
  slhc_10_5 = slhc_10_5[2:]
781
+
782
+ # Mixed pairing
783
  while slhc_8_5 and slhc_10_5:
784
+ arranged.append(slhc_8_5[0])
785
+ arranged.append(slhc_10_5[0])
786
+ slhc_8_5 = slhc_8_5[1:]
787
+ slhc_10_5 = slhc_10_5[1:]
788
+
789
+ # Add remaining
790
  arranged.extend(slhc_8_5)
791
  arranged.extend(slhc_10_5)
792
+
793
  return arranged
794
+
795
  def evaluate_solution_with_diversity(self, solution, stage_width):
796
+ """Evaluate fitness with strong emphasis on diversity"""
797
  if not solution:
798
  return -float('inf')
799
+
800
  total_width = sum(w for w, _ in solution)
801
  waste = stage_width - total_width
802
+
803
+ # Must have 100% usage
804
  if abs(waste) > 0.001:
805
  return -float('inf')
806
+
807
  lot_count = len(solution)
808
+
809
+ # Calculate diversity metrics
810
  width_counts = {}
811
  for w, _ in solution:
812
  width_counts[w] = width_counts.get(w, 0) + 1
813
+
814
  unique_widths = len(width_counts)
815
  max_repetition = max(width_counts.values())
816
  diversity_ratio = unique_widths / lot_count if lot_count > 0 else 0
817
+
818
+ # Base fitness
819
  fitness = lot_count * 1000
820
+
821
+ # STRONG diversity bonus
822
+ fitness += unique_widths * 2000 # Big bonus for each unique width
823
+ fitness -= max_repetition * 500 # Penalty for too many of same width
824
+ fitness += diversity_ratio * 3000 # Bonus for good diversity ratio
825
+
826
+ # Corner evaluation
827
  if len(solution) >= 2:
828
+ first_width = solution[0][0]
829
+ last_width = solution[-1][0]
830
+
831
+ # Penalty for SLHC on corners
832
+ if first_width <= 10.5:
833
+ fitness -= 2000
834
+ if last_width <= 10.5:
835
+ fitness -= 2000
836
+
837
+ # Bonus for good corners
838
+ if first_width >= 11.0:
839
+ fitness += 1000
840
+ if last_width >= 11.0:
841
  fitness += 1000
842
+
843
+ # Balance bonus
844
+ corner_diff = abs(first_width - last_width)
845
+ if corner_diff < 0.1:
846
+ fitness += 1500 # Perfect match
847
+ elif corner_diff <= 1.0:
848
+ fitness += 1000 # Very good
849
+ elif corner_diff <= 2.0:
850
+ fitness += 500 # Good
851
  else:
852
+ fitness -= 500 # Poor balance
853
+
854
+ # SLHC grouping bonus
855
  for i in range(len(solution) - 1):
856
+ if solution[i][0] <= 10.5 and solution[i+1][0] <= 10.5:
857
+ fitness += 300 # Adjacent SLHC bonus
858
+
859
+ # Penalize corner-specific widths used internally
860
  for i in range(1, len(solution) - 1):
861
  if solution[i][0] in self.corner_specific:
862
  fitness -= 200
863
+
864
  return fitness
865
+
866
  def generate_report(self, solution, stage_width, stage_depth, manual_allocation=None):
867
+ """Generate a professional report"""
868
  if not solution:
869
  return None
870
+
871
+ # Check for custom widths
872
+ custom_widths = []
873
+ for width, _ in solution:
874
+ if width not in self.lot_specifications:
875
+ custom_widths.append(f"{width:.1f}m")
876
+
877
+ # Calculate diversity
878
  unique_widths = len(set(w for w, _ in solution))
879
  width_counts = {}
880
  for w, _ in solution:
881
  width_counts[w] = width_counts.get(w, 0) + 1
882
+
883
+ # Calculate variance
884
  total_width = sum(w for w, _ in solution)
885
  variance = total_width - stage_width
886
+
887
  report = f"""
888
  # SUBDIVISION OPTIMIZATION REPORT
889
  ## Project Analysis for {stage_width}m × {stage_depth}m Stage
 
896
  - **Stage Dimensions**: {stage_width}m × {stage_depth}m
897
  - **Total Area**: {stage_width * stage_depth}m²
898
  {f"- **Custom Widths Used**: {', '.join(custom_widths)}" if custom_widths else ""}
 
899
 
900
+ ### LOT DIVERSITY ANALYSIS
901
+ """
902
+
903
+ # Sort by count to show distribution
904
+ sorted_widths = sorted(width_counts.items(), key=lambda x: x[1], reverse=True)
905
+ for width, count in sorted_widths:
906
  percentage = (count / len(solution)) * 100
907
  if width in self.lot_specifications:
908
  spec = self.lot_specifications[width]
909
  report += f"- **{width:.1f}m** × {count} ({percentage:.1f}%): {spec['type']}\n"
910
  else:
911
  report += f"- **{width:.1f}m** × {count} ({percentage:.1f}%): Custom Width\n"
912
+
913
+ # Corner analysis
914
  if len(solution) >= 2:
915
+ report += f"\n### CORNER ANALYSIS\n"
916
+ report += f"- **Front Corner**: {solution[0][0]:.1f}m with 3m × 3m splay\n"
917
+ report += f"- **Rear Corner**: {solution[-1][0]:.1f}m with 3m × 3m splay\n"
918
+ report += f"- **Balance**: {abs(solution[0][0] - solution[-1][0]):.1f}m difference\n"
919
+
920
+ report += f"\n### DESIGN FEATURES\n"
921
+ report += f"- Corner splays provide safe sight lines at intersections\n"
922
+ report += f"- All lots have identical rear alignment for visual consistency\n"
923
+ report += f"- Diverse lot mix ensures varied streetscape\n"
924
+ report += f"- SLHC lots grouped for efficient garbage collection\n"
925
+
926
+ report += f"\n---\n*Report generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*"
927
+
 
 
 
 
 
928
  return report
929
+
930
  def process_plan_image(self, image_path, scale=1000, auto_detect_scale=True, confidence=0.75):
931
+ """Process a plan image to extract lot information"""
932
  if not PLAN_READER_AVAILABLE:
933
+ # Return mock data for demonstration
934
  mock_lots = []
935
  for i in range(8):
936
  frontage = np.random.choice([8.5, 10.5, 12.5, 14.0, 16.0])
937
  mock_lots.append({
938
+ 'lot_number': f'L{i+1}',
939
  'frontage': frontage,
940
  'depth': 32,
941
  'area': frontage * 32,
942
  'type': 'SLHC' if frontage <= 10.5 else 'Standard' if frontage <= 14 else 'Premium'
943
  })
944
+
945
+ # Create a simple preview image
946
  fig, ax = plt.subplots(figsize=(10, 8))
947
+ ax.text(0.5, 0.5, 'Plan Reader Demo Mode\n(Install required libraries for actual functionality)',
 
948
  ha='center', va='center', fontsize=16, transform=ax.transAxes)
949
+ ax.set_xlim(0, 1)
950
+ ax.set_ylim(0, 1)
951
+ ax.axis('off')
952
+
953
+ # Convert plot to numpy array
954
  fig.canvas.draw()
955
  preview_img = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)
956
  preview_img = preview_img.reshape(fig.canvas.get_width_height()[::-1] + (3,))
957
  plt.close()
958
+
959
  summary = """
960
  ### Demo Mode Active
961
  Plan reader libraries not installed. Showing sample data.
962
 
963
  **To enable full functionality, install:**
964
+ ```
965
+ pip install opencv-python pytesseract pillow pdf2image
966
+ ```
967
+
968
+ **Sample lots generated for demonstration.**
969
+ """
970
+ return preview_img, mock_lots, summary
971
+
972
+ try:
973
+ # Load image
974
+ if image_path.endswith('.pdf'):
975
+ # Convert PDF to image
976
+ with tempfile.TemporaryDirectory() as temp_dir:
977
+ images = convert_from_path(image_path, dpi=300)
978
+ if images:
979
+ # Convert PIL image to numpy array
980
+ img = np.array(images[0])
981
+ else:
982
+ return None, None, "Failed to convert PDF"
983
+ else:
984
+ img = cv2.imread(image_path)
985
+ if img is None:
986
+ return None, None, "Failed to load image"
987
+
988
+ # Convert to RGB if needed
989
+ if len(img.shape) == 2:
990
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB)
991
+ elif img.shape[2] == 4:
992
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGRA2RGB)
993
+ else:
994
+ img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
995
+
996
+ # Process image for lot detection
997
+ gray = cv2.cvtColor(img_rgb, cv2.COLOR_RGB2GRAY)
998
+
999
+ # Detect lot boundaries
1000
+ lots_detected = self.detect_lot_boundaries(gray, img_rgb, confidence)
1001
+
1002
+ # Extract text using OCR
1003
+ text_data = self.extract_text_from_plan(gray)
1004
+
1005
+ # Match lots with dimensions
1006
+ lot_data = self.match_lots_with_dimensions(lots_detected, text_data, scale, auto_detect_scale)
1007
+
1008
+ # Create annotated preview
1009
+ preview_img = self.create_annotated_preview(img_rgb, lot_data)
1010
+
1011
+ # Create summary
1012
+ summary = f"""
1013
+ ### Analysis Complete!
1014
+ - **Lots Detected**: {len(lot_data)}
1015
+ - **Scale Used**: 1:{scale if not auto_detect_scale else 'Auto-detected'}
1016
+ - **Confidence**: {confidence:.0%}
1017
+
1018
+ **Next Steps:**
1019
+ 1. Review detected lots in the table below
1020
+ 2. Make any necessary corrections
1021
+ 3. Click "Send to Optimizer" to analyze the layout
1022
+ """
1023
+
1024
+ return preview_img, lot_data, summary
1025
+
1026
+ except Exception as e:
1027
+ return None, None, f"Error processing plan: {str(e)}"
1028
+
1029
+ def detect_lot_boundaries(self, gray_img, rgb_img, confidence):
1030
+ """Detect lot boundaries in the plan"""
1031
+ if not PLAN_READER_AVAILABLE:
1032
+ return []
1033
+
1034
+ lots = []
1035
+
1036
+ # Apply edge detection
1037
+ edges = cv2.Canny(gray_img, 50, 150)
1038
+
1039
+ # Find contours
1040
+ contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
1041
+
1042
+ # Filter and process contours
1043
+ for contour in contours:
1044
+ area = cv2.contourArea(contour)
1045
+ if area > 1000: # Minimum area threshold
1046
+ # Approximate polygon
1047
+ epsilon = 0.02 * cv2.arcLength(contour, True)
1048
+ approx = cv2.approxPolyDP(contour, epsilon, True)
1049
+
1050
+ # Check if shape is roughly rectangular (4-6 vertices)
1051
+ if 4 <= len(approx) <= 6:
1052
+ x, y, w, h = cv2.boundingRect(contour)
1053
+ aspect_ratio = float(w) / h
1054
+
1055
+ # Lots typically have aspect ratios between 0.3 and 3.0
1056
+ if 0.3 <= aspect_ratio <= 3.0:
1057
+ lots.append({
1058
+ 'contour': approx,
1059
+ 'bbox': (x, y, w, h),
1060
+ 'area': area,
1061
+ 'confidence': confidence
1062
+ })
1063
+
1064
+ return lots
1065
+
1066
+ def extract_text_from_plan(self, gray_img):
1067
+ """Extract text from plan using OCR"""
1068
+ if not PLAN_READER_AVAILABLE:
1069
+ return []
1070
+
1071
+ # Preprocess for better OCR
1072
+ _, thresh = cv2.threshold(gray_img, 150, 255, cv2.THRESH_BINARY)
1073
+
1074
+ # Use Tesseract OCR
1075
+ try:
1076
+ text = pytesseract.image_to_string(thresh)
1077
+ data = pytesseract.image_to_data(thresh, output_type=pytesseract.Output.DICT)
1078
+
1079
+ # Extract numbers and dimensions
1080
+ text_elements = []
1081
+ for i in range(len(data['text'])):
1082
+ if int(data['conf'][i]) > 0:
1083
+ text_val = data['text'][i].strip()
1084
+ # Look for lot numbers (L followed by digits) and dimensions (numbers possibly with 'm')
1085
+ is_lot_number = text_val.startswith('L') and text_val[1:].isdigit()
1086
+ is_dimension = text_val.replace('.', '').replace('m', '').isdigit()
1087
+
1088
+ if is_lot_number or is_dimension:
1089
+ text_elements.append({
1090
+ 'text': text_val,
1091
+ 'x': data['left'][i],
1092
+ 'y': data['top'][i],
1093
+ 'w': data['width'][i],
1094
+ 'h': data['height'][i]
1095
+ })
1096
+
1097
+ return text_elements
1098
+ except:
1099
+ return []
1100
+
1101
+ def match_lots_with_dimensions(self, lots, text_data, scale, auto_detect_scale):
1102
+ """Match detected lots with their dimensions and numbers"""
1103
+ lot_info = []
1104
+
1105
+ # Simple matching based on proximity
1106
+ for i, lot in enumerate(lots):
1107
+ x, y, w, h = lot['bbox']
1108
+ lot_center = (x + w/2, y + h/2)
1109
+
1110
+ # Find nearby text
1111
+ lot_number = None
1112
+ frontage = None
1113
+ depth = None
1114
+
1115
+ for text in text_data:
1116
+ text_center = (text['x'] + text['w']/2, text['y'] + text['h']/2)
1117
+ distance = np.sqrt((lot_center[0] - text_center[0])**2 +
1118
+ (lot_center[1] - text_center[1])**2)
1119
+
1120
+ # If text is close to lot
1121
+ if distance < max(w, h) * 0.5:
1122
+ text_val = text['text']
1123
+
1124
+ # Check if it's a lot number or dimension
1125
+ if text_val.startswith('L') and text_val[1:].isdigit():
1126
+ lot_number = text_val
1127
+ # Check if it's a dimension (number possibly followed by 'm')
1128
+ elif text_val.replace('.', '').replace('m', '').isdigit():
1129
+ dim_val = float(text_val.replace('m', ''))
1130
+ # Assign to frontage or depth based on position
1131
+ if abs(text_center[1] - lot_center[1]) < h * 0.3:
1132
+ frontage = dim_val
1133
+ else:
1134
+ depth = dim_val
1135
+
1136
+ # If no lot number found, assign sequential
1137
+ if not lot_number:
1138
+ lot_number = f"L{i+1}"
1139
+
1140
+ # If no dimensions found, estimate from pixel measurements
1141
+ if not frontage:
1142
+ frontage = round(w / scale * 1000, 1) # Convert to meters
1143
+ if not depth:
1144
+ depth = round(h / scale * 1000, 1) # Convert to meters
1145
+
1146
+ # Determine lot type based on frontage
1147
+ if frontage <= 10.5:
1148
+ lot_type = "SLHC"
1149
+ elif frontage <= 14:
1150
+ lot_type = "Standard"
1151
+ else:
1152
+ lot_type = "Premium"
1153
+
1154
+ lot_info.append({
1155
+ 'lot_number': lot_number,
1156
+ 'frontage': frontage,
1157
+ 'depth': depth,
1158
+ 'area': frontage * depth,
1159
+ 'type': lot_type,
1160
+ 'bbox': lot['bbox']
1161
+ })
1162
+
1163
+ # Sort by lot number if possible
1164
+ try:
1165
+ def get_lot_number(lot_info):
1166
+ lot_num = lot_info['lot_number']
1167
+ if lot_num.startswith('L'):
1168
+ return int(lot_num[1:])
1169
+ return 999999 # Put non-standard lot numbers at the end
1170
+
1171
+ lot_info.sort(key=get_lot_number)
1172
+ except:
1173
+ pass
1174
+
1175
+ return lot_info
1176
+
1177
+ def create_annotated_preview(self, img, lot_data):
1178
+ """Create preview image with annotations"""
1179
+ if not PLAN_READER_AVAILABLE:
1180
+ return img
1181
+
1182
+ annotated = img.copy()
1183
+
1184
+ # Define colors for different lot types
1185
+ colors = {
1186
+ 'SLHC': (255, 0, 0), # Red
1187
+ 'Standard': (0, 255, 0), # Green
1188
+ 'Premium': (0, 0, 255) # Blue
1189
+ }
1190
+
1191
+ # Draw lot boundaries and labels
1192
+ for lot in lot_data:
1193
+ if 'bbox' in lot:
1194
+ x, y, w, h = lot['bbox']
1195
+ color = colors.get(lot['type'], (128, 128, 128))
1196
+
1197
+ # Draw rectangle
1198
+ cv2.rectangle(annotated, (x, y), (x + w, y + h), color, 2)
1199
+
1200
+ # Draw lot number
1201
+ label = f"{lot['lot_number']}: {lot['frontage']}m"
1202
+ cv2.putText(annotated, label, (x + 5, y + 20),
1203
+ cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
1204
+
1205
+ return annotated
1206
+
1207
+ def lot_data_to_dataframe(self, lot_data):
1208
+ """Convert lot data to DataFrame format"""
1209
+ if not lot_data:
1210
+ return pd.DataFrame(columns=["Lot #", "Frontage (m)", "Depth (m)", "Area (m²)", "Type"])
1211
+
1212
+ df_data = []
1213
+ for lot in lot_data:
1214
+ df_data.append({
1215
+ "Lot #": lot['lot_number'],
1216
+ "Frontage (m)": lot['frontage'],
1217
+ "Depth (m)": lot['depth'],
1218
+ "Area (m²)": round(lot['area'], 1),
1219
+ "Type": lot['type']
1220
+ })
1221
+
1222
+ return pd.DataFrame(df_data)
1223
+
1224
+ def export_lot_data_to_csv(self, df):
1225
+ """Export lot data to CSV format"""
1226
+ if df is None or df.empty:
1227
+ return None
1228
+
1229
+ csv_buffer = io.StringIO()
1230
+ df.to_csv(csv_buffer, index=False)
1231
+ return csv_buffer.getvalue()
1232
+
1233
+ def convert_lot_data_to_stage_format(self, df):
1234
+ """Convert lot data to format suitable for optimizer"""
1235
+ if df is None or df.empty:
1236
+ return None, None
1237
+
1238
+ # Group by frontage and count
1239
+ frontage_counts = {}
1240
+ for _, row in df.iterrows():
1241
+ frontage = float(row['Frontage (m)'])
1242
+ if frontage in frontage_counts:
1243
+ frontage_counts[frontage] += 1
1244
+ else:
1245
+ frontage_counts[frontage] = 1
1246
+
1247
+ # Calculate total width
1248
+ total_width = sum(f * c for f, c in frontage_counts.items())
1249
+
1250
+ # Find common depth (mode)
1251
+ depths = df['Depth (m)'].mode()
1252
+ common_depth = depths[0] if len(depths) > 0 else 32
1253
+
1254
+ return total_width, common_depth
1255
+
1256
+ def darken_color(self, hex_color, factor=0.8):
1257
+ """Darken a hex color by a factor"""
1258
+ try:
1259
+ hex_color = hex_color.lstrip('#')
1260
+ rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
1261
+ darker_rgb = tuple(int(c * factor) for c in rgb)
1262
+ return '#' + ''.join(f'{c:02x}' for c in darker_rgb)
1263
+ except:
1264
+ return hex_color
1265
+
1266
+ def create_advanced_app():
1267
+ optimizer = AdvancedGridOptimizer()
1268
+
1269
+ def optimize_grid(
1270
+ stage_width,
1271
+ stage_depth,
1272
+ enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18,
1273
+ enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8,
1274
+ allow_custom_corners, optimization_strategy, color_scheme
1275
+ ):
1276
+ # Update color scheme
1277
+ optimizer.current_scheme = color_scheme
1278
+
1279
+ # Collect enabled widths
1280
+ enabled_widths = []
1281
+ if enable_8_5: enabled_widths.append(8.5)
1282
+ if enable_10_5: enabled_widths.append(10.5)
1283
+ if enable_12_5: enabled_widths.append(12.5)
1284
+ if enable_14: enabled_widths.append(14.0)
1285
+ if enable_16: enabled_widths.append(16.0)
1286
+ if enable_18: enabled_widths.append(18.0)
1287
+
1288
+ if enable_corners:
1289
+ if enable_11: enabled_widths.append(11.0)
1290
+ if enable_13_3: enabled_widths.append(13.3)
1291
+ if enable_14_8: enabled_widths.append(14.8)
1292
+ if enable_16_8: enabled_widths.append(16.8)
1293
+
1294
+ if not enabled_widths:
1295
+ return None, None, pd.DataFrame(), "Please select at least one lot width!", "", ""
1296
+
1297
+ # Run optimization based on strategy
1298
+ if optimization_strategy == "diversity_focus":
1299
+ optimized_solution = optimizer.optimize_with_flexible_corners(
1300
+ stage_width, enabled_widths, allow_custom_corners
1301
+ )
1302
+ else: # balanced approach
1303
+ optimized_solution = optimizer.optimize_with_corners_diverse(
1304
+ stage_width, enabled_widths, None
1305
+ )
1306
+
1307
+ # Store current solution for manual adjustment
1308
+ optimizer.current_solution = optimized_solution
1309
+
1310
+ # Calculate variance for display
1311
+ if optimized_solution:
1312
+ total_width = sum(w for w, _ in optimized_solution)
1313
+ variance = total_width - stage_width
1314
+ else:
1315
+ variance = None
1316
+
1317
+ # Verify solution
1318
+ if not optimized_solution or abs(sum(w for w, _ in optimized_solution) - stage_width) > 0.001:
1319
+ # Provide suggestions
1320
+ return None, pd.DataFrame(), f"""
1321
+ ### ❌ Cannot achieve 100% usage