Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -14,17 +14,6 @@ import io
|
|
| 14 |
import base64
|
| 15 |
import tempfile
|
| 16 |
|
| 17 |
-
# Try to import plan reading libraries
|
| 18 |
-
try:
|
| 19 |
-
import cv2
|
| 20 |
-
import pytesseract
|
| 21 |
-
from PIL import Image
|
| 22 |
-
from pdf2image import convert_from_path
|
| 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
|
|
@@ -42,6 +31,17 @@ class AdvancedGridOptimizer:
|
|
| 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]
|
|
@@ -93,8 +93,8 @@ class AdvancedGridOptimizer:
|
|
| 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
|
| 98 |
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(18, 12), gridspec_kw={'height_ratios': [3, 1]},
|
| 99 |
facecolor='#1a1a1a')
|
| 100 |
|
|
@@ -134,6 +134,9 @@ class AdvancedGridOptimizer:
|
|
| 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:
|
|
@@ -205,6 +208,63 @@ class AdvancedGridOptimizer:
|
|
| 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],
|
|
@@ -253,6 +313,21 @@ class AdvancedGridOptimizer:
|
|
| 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)
|
|
@@ -289,11 +364,21 @@ class AdvancedGridOptimizer:
|
|
| 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}",
|
| 295 |
f"🎯 DIVERSITY: {diversity_score:.0%} ({unique_widths} types)",
|
| 296 |
f"📏 GRID VARIANCE: {variance:+.2f}m",
|
|
|
|
| 297 |
"",
|
| 298 |
f"SLHC (≤10.5m): {slhc_count} lots",
|
| 299 |
f"Standard (11-14m): {standard_count} lots",
|
|
@@ -303,8 +388,8 @@ class AdvancedGridOptimizer:
|
|
| 303 |
f"💰 Revenue: ${total_lots * 0.5:.1f}M - ${total_lots * 1.2:.1f}M"
|
| 304 |
]
|
| 305 |
|
| 306 |
-
col1_text = '\n'.join(metrics_lines[:
|
| 307 |
-
col2_text = '\n'.join(metrics_lines[
|
| 308 |
|
| 309 |
ax2.text(0.05, 0.5, col1_text, transform=ax2.transAxes,
|
| 310 |
fontsize=14, verticalalignment='center', fontweight='bold',
|
|
@@ -922,337 +1007,12 @@ class AdvancedGridOptimizer:
|
|
| 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:
|
|
@@ -1271,7 +1031,7 @@ def create_advanced_app():
|
|
| 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
|
|
@@ -1329,11 +1089,12 @@ def create_advanced_app():
|
|
| 1329 |
3. Try common stage widths: 84m, 105m, 126m
|
| 1330 |
""", "", ""
|
| 1331 |
|
| 1332 |
-
# Create visualizations with variance indicator
|
| 1333 |
fig_2d = optimizer.create_enhanced_visualization(
|
| 1334 |
optimized_solution, stage_width, stage_depth,
|
| 1335 |
"AI-Optimized Diverse Subdivision Layout",
|
| 1336 |
-
show_variance=variance
|
|
|
|
| 1337 |
)
|
| 1338 |
|
| 1339 |
# Create results table
|
|
@@ -1414,7 +1175,7 @@ def create_advanced_app():
|
|
| 1414 |
|
| 1415 |
return fig_2d, results_df, summary, report, manual_edit_string
|
| 1416 |
|
| 1417 |
-
def update_manual_adjustment(manual_widths_text, stage_width, stage_depth, color_scheme):
|
| 1418 |
"""Update visualization based on manual adjustment"""
|
| 1419 |
optimizer.current_scheme = color_scheme
|
| 1420 |
|
|
@@ -1438,7 +1199,8 @@ def create_advanced_app():
|
|
| 1438 |
fig = optimizer.create_enhanced_visualization(
|
| 1439 |
solution, stage_width, stage_depth,
|
| 1440 |
"Manually Adjusted Layout",
|
| 1441 |
-
show_variance=variance
|
|
|
|
| 1442 |
)
|
| 1443 |
|
| 1444 |
return fig, feedback
|
|
@@ -1483,239 +1245,122 @@ def create_advanced_app():
|
|
| 1483 |
) as demo:
|
| 1484 |
gr.Markdown("""
|
| 1485 |
# 🏗️ Advanced AI Grid Cut Optimizer Pro
|
| 1486 |
-
### AI-Powered Subdivision Planning with
|
| 1487 |
""")
|
| 1488 |
|
| 1489 |
-
with gr.
|
| 1490 |
-
with gr.
|
| 1491 |
-
with gr.
|
| 1492 |
-
|
| 1493 |
-
|
| 1494 |
-
|
| 1495 |
-
|
| 1496 |
-
|
| 1497 |
-
|
| 1498 |
-
|
| 1499 |
-
|
| 1500 |
-
|
| 1501 |
-
|
| 1502 |
-
|
| 1503 |
-
info="Depth of lots (perpendicular to street)"
|
| 1504 |
-
)
|
| 1505 |
-
|
| 1506 |
-
gr.Markdown("### 📏 Lot Width Options")
|
| 1507 |
-
|
| 1508 |
-
with gr.Group():
|
| 1509 |
-
gr.Markdown("**Standard Widths**")
|
| 1510 |
-
with gr.Row():
|
| 1511 |
-
enable_8_5 = gr.Checkbox(label="8.5m SLHC", value=True)
|
| 1512 |
-
enable_10_5 = gr.Checkbox(label="10.5m SLHC", value=True)
|
| 1513 |
-
enable_12_5 = gr.Checkbox(label="12.5m", value=True)
|
| 1514 |
-
with gr.Row():
|
| 1515 |
-
enable_14 = gr.Checkbox(label="14.0m", value=True)
|
| 1516 |
-
enable_16 = gr.Checkbox(label="16.0m", value=True)
|
| 1517 |
-
enable_18 = gr.Checkbox(label="18.0m", value=False)
|
| 1518 |
-
|
| 1519 |
-
with gr.Group():
|
| 1520 |
-
enable_corners = gr.Checkbox(
|
| 1521 |
-
label="Enable Corner-Specific Widths",
|
| 1522 |
-
value=True,
|
| 1523 |
-
info="Adds variety and helps achieve 100%"
|
| 1524 |
-
)
|
| 1525 |
-
with gr.Row():
|
| 1526 |
-
enable_11 = gr.Checkbox(label="11.0m", value=True)
|
| 1527 |
-
enable_13_3 = gr.Checkbox(label="13.3m", value=True)
|
| 1528 |
-
with gr.Row():
|
| 1529 |
-
enable_14_8 = gr.Checkbox(label="14.8m", value=True)
|
| 1530 |
-
enable_16_8 = gr.Checkbox(label="16.8m", value=True)
|
| 1531 |
-
|
| 1532 |
-
with gr.Column(scale=1):
|
| 1533 |
-
gr.Markdown("### ⚙️ Advanced Settings")
|
| 1534 |
-
|
| 1535 |
-
allow_custom_corners = gr.Checkbox(
|
| 1536 |
-
label="🎯 Allow Flexible Corner Widths",
|
| 1537 |
-
value=True,
|
| 1538 |
-
info="Enables 13.8m, 13.9m etc. for perfect fits"
|
| 1539 |
-
)
|
| 1540 |
-
|
| 1541 |
-
optimization_strategy = gr.Radio(
|
| 1542 |
-
["diversity_focus", "balanced"],
|
| 1543 |
-
label="Optimization Strategy",
|
| 1544 |
-
value="diversity_focus",
|
| 1545 |
-
info="Diversity creates more interesting layouts"
|
| 1546 |
-
)
|
| 1547 |
-
|
| 1548 |
-
color_scheme = gr.Radio(
|
| 1549 |
-
["modern", "professional", "neon"],
|
| 1550 |
-
label="🎨 Color Scheme",
|
| 1551 |
-
value="neon",
|
| 1552 |
-
info="Neon colors work best with dark background"
|
| 1553 |
-
)
|
| 1554 |
-
|
| 1555 |
-
optimize_btn = gr.Button(
|
| 1556 |
-
"🚀 Optimize with AI",
|
| 1557 |
-
variant="primary",
|
| 1558 |
-
size="lg",
|
| 1559 |
-
elem_id="optimize-button"
|
| 1560 |
-
)
|
| 1561 |
-
|
| 1562 |
-
gr.Markdown("""
|
| 1563 |
-
### 💡 Quick Tips:
|
| 1564 |
-
- **Visual Fix**: All lots now align at rear boundary
|
| 1565 |
-
- **Corner Lots**: Always wider than internals
|
| 1566 |
-
- **Grid Variance**: Shows if layout is perfect (0.0m)
|
| 1567 |
-
- **Manual Adjust**: Edit the result below after optimization
|
| 1568 |
-
""")
|
| 1569 |
-
|
| 1570 |
-
with gr.Row():
|
| 1571 |
-
plot_2d = gr.Plot(label="2D Layout with Corner Splays")
|
| 1572 |
|
| 1573 |
-
|
| 1574 |
-
gr.Markdown("### ✏️ Fine-Tune AI Result")
|
| 1575 |
-
with gr.Row():
|
| 1576 |
-
with gr.Column(scale=2):
|
| 1577 |
-
manual_widths = gr.Textbox(
|
| 1578 |
-
label="Manually Adjust Lot Widths",
|
| 1579 |
-
placeholder="Widths will appear here after optimization",
|
| 1580 |
-
info="Edit the widths (comma-separated) and click 'Update Layout'",
|
| 1581 |
-
lines=2
|
| 1582 |
-
)
|
| 1583 |
-
with gr.Column(scale=1):
|
| 1584 |
-
update_btn = gr.Button("🔄 Update Layout", variant="secondary")
|
| 1585 |
-
adjustment_feedback = gr.Markdown(
|
| 1586 |
-
value="",
|
| 1587 |
-
label="Adjustment Feedback"
|
| 1588 |
-
)
|
| 1589 |
|
| 1590 |
-
with gr.
|
| 1591 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1592 |
|
| 1593 |
-
with gr.
|
| 1594 |
-
|
| 1595 |
-
|
| 1596 |
-
|
| 1597 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1598 |
|
| 1599 |
-
with gr.
|
| 1600 |
-
gr.Markdown(""
|
| 1601 |
-
## 🏢 AI Plan Reader
|
| 1602 |
-
### Upload your subdivision plan to automatically extract lot information
|
| 1603 |
|
| 1604 |
-
|
| 1605 |
-
|
| 1606 |
-
|
|
|
|
|
|
|
| 1607 |
|
| 1608 |
-
|
| 1609 |
-
|
| 1610 |
-
|
| 1611 |
-
|
| 1612 |
-
|
| 1613 |
-
type="filepath"
|
| 1614 |
-
)
|
| 1615 |
-
|
| 1616 |
-
gr.Markdown("""
|
| 1617 |
-
**Supported Formats:**
|
| 1618 |
-
- PDF plans
|
| 1619 |
-
- PNG/JPG images
|
| 1620 |
-
- CAD exports
|
| 1621 |
-
|
| 1622 |
-
**Best Results:**
|
| 1623 |
-
- High resolution (300+ DPI)
|
| 1624 |
-
- Clear lot numbers
|
| 1625 |
-
- Visible frontage dimensions
|
| 1626 |
-
- North arrow included
|
| 1627 |
-
""")
|
| 1628 |
-
|
| 1629 |
-
process_plan_btn = gr.Button(
|
| 1630 |
-
"🔍 Analyze Plan",
|
| 1631 |
-
variant="primary",
|
| 1632 |
-
size="lg"
|
| 1633 |
-
)
|
| 1634 |
-
|
| 1635 |
-
# Analysis options
|
| 1636 |
-
with gr.Group():
|
| 1637 |
-
gr.Markdown("**Analysis Settings**")
|
| 1638 |
-
scale_input = gr.Number(
|
| 1639 |
-
label="Scale (1:X)",
|
| 1640 |
-
value=1000,
|
| 1641 |
-
info="Drawing scale ratio"
|
| 1642 |
-
)
|
| 1643 |
-
|
| 1644 |
-
auto_detect_scale = gr.Checkbox(
|
| 1645 |
-
label="Auto-detect scale from plan",
|
| 1646 |
-
value=True
|
| 1647 |
-
)
|
| 1648 |
-
|
| 1649 |
-
confidence_threshold = gr.Slider(
|
| 1650 |
-
label="Detection Confidence",
|
| 1651 |
-
minimum=0.5,
|
| 1652 |
-
maximum=0.95,
|
| 1653 |
-
value=0.75,
|
| 1654 |
-
step=0.05,
|
| 1655 |
-
info="Higher = more accurate but may miss some lots"
|
| 1656 |
-
)
|
| 1657 |
-
|
| 1658 |
-
with gr.Column(scale=2):
|
| 1659 |
-
# Preview with annotations
|
| 1660 |
-
plan_preview = gr.Image(
|
| 1661 |
-
label="Analyzed Plan Preview",
|
| 1662 |
-
type="numpy"
|
| 1663 |
-
)
|
| 1664 |
-
|
| 1665 |
-
analysis_status = gr.Markdown(
|
| 1666 |
-
value="Upload a plan to begin analysis",
|
| 1667 |
-
label="Analysis Status"
|
| 1668 |
-
)
|
| 1669 |
|
| 1670 |
-
|
| 1671 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1672 |
|
| 1673 |
-
|
| 1674 |
-
|
| 1675 |
-
|
| 1676 |
-
|
| 1677 |
-
|
| 1678 |
-
|
| 1679 |
-
|
| 1680 |
-
with gr.Column():
|
| 1681 |
-
extraction_summary = gr.Markdown(
|
| 1682 |
-
label="Extraction Summary"
|
| 1683 |
-
)
|
| 1684 |
-
|
| 1685 |
-
export_btn = gr.Button(
|
| 1686 |
-
"📥 Export to CSV",
|
| 1687 |
-
variant="secondary"
|
| 1688 |
-
)
|
| 1689 |
-
|
| 1690 |
-
send_to_optimizer_btn = gr.Button(
|
| 1691 |
-
"➡️ Send to Optimizer",
|
| 1692 |
-
variant="primary"
|
| 1693 |
-
)
|
| 1694 |
|
| 1695 |
-
|
| 1696 |
-
|
| 1697 |
-
|
| 1698 |
-
|
| 1699 |
-
|
| 1700 |
-
|
| 1701 |
-
|
| 1702 |
-
|
| 1703 |
-
|
| 1704 |
-
|
| 1705 |
-
|
| 1706 |
-
|
| 1707 |
-
|
| 1708 |
-
|
| 1709 |
-
|
| 1710 |
-
|
| 1711 |
-
|
| 1712 |
-
|
| 1713 |
-
|
| 1714 |
-
|
| 1715 |
-
|
| 1716 |
-
|
| 1717 |
-
|
| 1718 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1719 |
|
| 1720 |
# Wire up the buttons
|
| 1721 |
optimize_btn.click(
|
|
@@ -1725,124 +1370,17 @@ def create_advanced_app():
|
|
| 1725 |
stage_depth,
|
| 1726 |
enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18,
|
| 1727 |
enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8,
|
| 1728 |
-
allow_custom_corners, optimization_strategy, color_scheme
|
| 1729 |
],
|
| 1730 |
outputs=[plot_2d, results_table, summary_output, report_output, manual_widths]
|
| 1731 |
)
|
| 1732 |
|
| 1733 |
update_btn.click(
|
| 1734 |
update_manual_adjustment,
|
| 1735 |
-
inputs=[manual_widths, stage_width, stage_depth, color_scheme],
|
| 1736 |
outputs=[plot_2d, adjustment_feedback]
|
| 1737 |
)
|
| 1738 |
|
| 1739 |
-
# Plan reader functions
|
| 1740 |
-
def process_uploaded_plan(file_path, scale, auto_detect, confidence):
|
| 1741 |
-
if not file_path:
|
| 1742 |
-
return None, pd.DataFrame(), "Please upload a plan file"
|
| 1743 |
-
|
| 1744 |
-
preview, lot_data, status = optimizer.process_plan_image(
|
| 1745 |
-
file_path, scale, auto_detect, confidence
|
| 1746 |
-
)
|
| 1747 |
-
|
| 1748 |
-
if lot_data:
|
| 1749 |
-
df = optimizer.lot_data_to_dataframe(lot_data)
|
| 1750 |
-
return preview, df, status
|
| 1751 |
-
else:
|
| 1752 |
-
return preview, pd.DataFrame(), status
|
| 1753 |
-
|
| 1754 |
-
def export_to_csv(df):
|
| 1755 |
-
if df is None or df.empty:
|
| 1756 |
-
return gr.update(visible=False), "No data to export"
|
| 1757 |
-
|
| 1758 |
-
csv_content = optimizer.export_lot_data_to_csv(df)
|
| 1759 |
-
return gr.update(value=csv_content, visible=True), "✅ CSV data ready - copy and save as .csv file"
|
| 1760 |
-
|
| 1761 |
-
def send_to_optimizer(df):
|
| 1762 |
-
if df is None or df.empty:
|
| 1763 |
-
return 0, 32, "No data to send"
|
| 1764 |
-
|
| 1765 |
-
width, depth = optimizer.convert_lot_data_to_stage_format(df)
|
| 1766 |
-
return width, depth, f"✅ Stage dimensions set to {width:.1f}m × {depth:.1f}m\nSwitch to 'AI Optimization' tab to continue"
|
| 1767 |
-
|
| 1768 |
-
def validate_lot_data(df):
|
| 1769 |
-
if df is None or df.empty:
|
| 1770 |
-
return "No data to validate"
|
| 1771 |
-
|
| 1772 |
-
# Check for common issues
|
| 1773 |
-
issues = []
|
| 1774 |
-
|
| 1775 |
-
# Check for missing values
|
| 1776 |
-
if df.isnull().any().any():
|
| 1777 |
-
issues.append("⚠️ Missing values detected")
|
| 1778 |
-
|
| 1779 |
-
# Check for unrealistic dimensions
|
| 1780 |
-
if (df['Frontage (m)'] < 6).any():
|
| 1781 |
-
issues.append("⚠️ Some lots have frontage < 6m")
|
| 1782 |
-
if (df['Frontage (m)'] > 30).any():
|
| 1783 |
-
issues.append("⚠️ Some lots have frontage > 30m")
|
| 1784 |
-
|
| 1785 |
-
# Check total lots
|
| 1786 |
-
total_lots = len(df)
|
| 1787 |
-
if total_lots < 5:
|
| 1788 |
-
issues.append("ℹ️ Few lots detected - check if all were found")
|
| 1789 |
-
|
| 1790 |
-
if not issues:
|
| 1791 |
-
return f"✅ Data looks good! {total_lots} lots ready for optimization"
|
| 1792 |
-
else:
|
| 1793 |
-
return "\n".join(issues)
|
| 1794 |
-
|
| 1795 |
-
def add_lot_row(df):
|
| 1796 |
-
if df is None or df.empty:
|
| 1797 |
-
new_row = pd.DataFrame({
|
| 1798 |
-
"Lot #": ["L1"],
|
| 1799 |
-
"Frontage (m)": [12.5],
|
| 1800 |
-
"Depth (m)": [32.0],
|
| 1801 |
-
"Area (m²)": [400.0],
|
| 1802 |
-
"Type": ["Standard"]
|
| 1803 |
-
})
|
| 1804 |
-
return new_row
|
| 1805 |
-
else:
|
| 1806 |
-
last_lot_num = len(df) + 1
|
| 1807 |
-
new_row = pd.DataFrame({
|
| 1808 |
-
"Lot #": [f"L{last_lot_num}"],
|
| 1809 |
-
"Frontage (m)": [12.5],
|
| 1810 |
-
"Depth (m)": [32.0],
|
| 1811 |
-
"Area (m²)": [400.0],
|
| 1812 |
-
"Type": ["Standard"]
|
| 1813 |
-
})
|
| 1814 |
-
return pd.concat([df, new_row], ignore_index=True)
|
| 1815 |
-
|
| 1816 |
-
process_plan_btn.click(
|
| 1817 |
-
process_uploaded_plan,
|
| 1818 |
-
inputs=[plan_upload, scale_input, auto_detect_scale, confidence_threshold],
|
| 1819 |
-
outputs=[plan_preview, extracted_data, analysis_status]
|
| 1820 |
-
)
|
| 1821 |
-
|
| 1822 |
-
export_btn.click(
|
| 1823 |
-
export_to_csv,
|
| 1824 |
-
inputs=[extracted_data],
|
| 1825 |
-
outputs=[export_output, extraction_summary]
|
| 1826 |
-
)
|
| 1827 |
-
|
| 1828 |
-
send_to_optimizer_btn.click(
|
| 1829 |
-
send_to_optimizer,
|
| 1830 |
-
inputs=[extracted_data],
|
| 1831 |
-
outputs=[stage_width, stage_depth, extraction_summary]
|
| 1832 |
-
)
|
| 1833 |
-
|
| 1834 |
-
extracted_data.change(
|
| 1835 |
-
validate_lot_data,
|
| 1836 |
-
inputs=[extracted_data],
|
| 1837 |
-
outputs=[validation_result]
|
| 1838 |
-
)
|
| 1839 |
-
|
| 1840 |
-
add_lot_btn.click(
|
| 1841 |
-
add_lot_row,
|
| 1842 |
-
inputs=[extracted_data],
|
| 1843 |
-
outputs=[extracted_data]
|
| 1844 |
-
)
|
| 1845 |
-
|
| 1846 |
return demo
|
| 1847 |
|
| 1848 |
# Create and launch
|
|
|
|
| 14 |
import base64
|
| 15 |
import tempfile
|
| 16 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
class AdvancedGridOptimizer:
|
| 18 |
def __init__(self):
|
| 19 |
# Standard lot widths and their typical depths
|
|
|
|
| 31 |
16.8: {"depths": [30, 32], "type": "Corner-Premium", "squares": "26-32"}
|
| 32 |
}
|
| 33 |
|
| 34 |
+
# Rescode setback requirements
|
| 35 |
+
self.setback_rules = {
|
| 36 |
+
'front': 6.0, # 6m front setback
|
| 37 |
+
'rear': 6.0, # 6m rear setback
|
| 38 |
+
'side': 1.0, # 1m side setback (single story)
|
| 39 |
+
'side_2story': 1.5, # 1.5m side setback (two story)
|
| 40 |
+
'corner_secondary': 3.0, # 3m setback on secondary street for corners
|
| 41 |
+
'garage_front': 5.5, # 5.5m garage setback from front
|
| 42 |
+
'alfresco_rear': 3.0 # 3m minimum for alfresco area
|
| 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]
|
|
|
|
| 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, show_setbacks=True):
|
| 97 |
+
"""Create a clean 2D visualization with corner splays and buildable boundaries"""
|
| 98 |
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(18, 12), gridspec_kw={'height_ratios': [3, 1]},
|
| 99 |
facecolor='#1a1a1a')
|
| 100 |
|
|
|
|
| 134 |
splay_size = 3 # 3m corner splay
|
| 135 |
lot_height = 28 # UNIFORM HEIGHT FOR ALL LOTS
|
| 136 |
|
| 137 |
+
# Track maximum buildable area info
|
| 138 |
+
max_buildable_areas = []
|
| 139 |
+
|
| 140 |
for i, (width, lot_type) in enumerate(solution):
|
| 141 |
# Get base color
|
| 142 |
if width in colors:
|
|
|
|
| 208 |
zorder=2)
|
| 209 |
ax1.add_patch(glow)
|
| 210 |
|
| 211 |
+
# Add buildable boundaries if enabled
|
| 212 |
+
if show_setbacks:
|
| 213 |
+
# Determine if lot likely has 2 stories
|
| 214 |
+
two_story = width > 14 # Premium lots likely 2 story
|
| 215 |
+
side_setback = self.setback_rules['side_2story'] if two_story else self.setback_rules['side']
|
| 216 |
+
|
| 217 |
+
if is_corner:
|
| 218 |
+
# Corner lot buildable area
|
| 219 |
+
if i == 0: # First corner
|
| 220 |
+
buildable_x = x_pos + max(splay_size + self.setback_rules['corner_secondary'], side_setback)
|
| 221 |
+
buildable_y = 8 + self.setback_rules['front']
|
| 222 |
+
buildable_width = width - max(splay_size + self.setback_rules['corner_secondary'], side_setback) - side_setback
|
| 223 |
+
buildable_height = lot_height - self.setback_rules['front'] - self.setback_rules['rear']
|
| 224 |
+
else: # Last corner
|
| 225 |
+
buildable_x = x_pos + side_setback
|
| 226 |
+
buildable_y = 8 + self.setback_rules['front']
|
| 227 |
+
buildable_width = width - side_setback - max(splay_size + self.setback_rules['corner_secondary'], side_setback)
|
| 228 |
+
buildable_height = lot_height - self.setback_rules['front'] - self.setback_rules['rear']
|
| 229 |
+
else:
|
| 230 |
+
# Regular lot buildable area
|
| 231 |
+
buildable_x = x_pos + side_setback
|
| 232 |
+
buildable_y = 8 + self.setback_rules['front']
|
| 233 |
+
buildable_width = width - 2 * side_setback
|
| 234 |
+
buildable_height = lot_height - self.setback_rules['front'] - self.setback_rules['rear']
|
| 235 |
+
|
| 236 |
+
# Draw buildable boundary with dotted lines
|
| 237 |
+
buildable_rect = Rectangle(
|
| 238 |
+
(buildable_x, buildable_y),
|
| 239 |
+
buildable_width,
|
| 240 |
+
buildable_height,
|
| 241 |
+
fill=False,
|
| 242 |
+
edgecolor='white',
|
| 243 |
+
linewidth=1.5,
|
| 244 |
+
linestyle='--',
|
| 245 |
+
alpha=0.7,
|
| 246 |
+
zorder=4
|
| 247 |
+
)
|
| 248 |
+
ax1.add_patch(buildable_rect)
|
| 249 |
+
|
| 250 |
+
# Add small label for max house size
|
| 251 |
+
max_house_area = buildable_width * buildable_height
|
| 252 |
+
max_buildable_areas.append(max_house_area)
|
| 253 |
+
|
| 254 |
+
# Show dimensions on hover-like annotation
|
| 255 |
+
ax1.text(buildable_x + buildable_width/2, buildable_y + buildable_height/2,
|
| 256 |
+
f'{buildable_width:.1f}×{buildable_height:.1f}m\n({max_house_area:.0f}m²)',
|
| 257 |
+
ha='center', va='center', fontsize=8, color='white', alpha=0.5,
|
| 258 |
+
bbox=dict(boxstyle="round,pad=0.2", facecolor='black', alpha=0.5))
|
| 259 |
+
|
| 260 |
+
# Add setback dimension indicators
|
| 261 |
+
# Front setback
|
| 262 |
+
ax1.plot([x_pos + width*0.1, x_pos + width*0.1], [8, buildable_y],
|
| 263 |
+
'gray', linewidth=0.5, alpha=0.3)
|
| 264 |
+
ax1.text(x_pos + width*0.1, 8 + self.setback_rules['front']/2,
|
| 265 |
+
f"{self.setback_rules['front']}m",
|
| 266 |
+
ha='right', va='center', fontsize=6, color='gray', alpha=0.5)
|
| 267 |
+
|
| 268 |
# Add rear alignment line to emphasize equal depth
|
| 269 |
rear_y = 8 + lot_height
|
| 270 |
ax1.plot([x_pos, x_pos + width], [rear_y, rear_y],
|
|
|
|
| 313 |
bbox=dict(boxstyle="round,pad=0.3", facecolor='#1a1a1a',
|
| 314 |
edgecolor='cyan', alpha=0.8))
|
| 315 |
|
| 316 |
+
# Add setback legend
|
| 317 |
+
if show_setbacks:
|
| 318 |
+
legend_text = (
|
| 319 |
+
"SETBACK REQUIREMENTS:\n"
|
| 320 |
+
f"Front: {self.setback_rules['front']}m | Rear: {self.setback_rules['rear']}m\n"
|
| 321 |
+
f"Side (1 story): {self.setback_rules['side']}m | Side (2 story): {self.setback_rules['side_2story']}m\n"
|
| 322 |
+
f"Corner secondary street: {self.setback_rules['corner_secondary']}m"
|
| 323 |
+
)
|
| 324 |
+
ax1.text(0.02, 0.98, legend_text,
|
| 325 |
+
transform=ax1.transAxes,
|
| 326 |
+
fontsize=10, color='white', alpha=0.7,
|
| 327 |
+
bbox=dict(boxstyle="round,pad=0.5", facecolor='#2a2a2a',
|
| 328 |
+
edgecolor='white', alpha=0.7),
|
| 329 |
+
verticalalignment='top')
|
| 330 |
+
|
| 331 |
# Add stage dimensions
|
| 332 |
arrow_props = dict(arrowstyle='<->', color='white', lw=3)
|
| 333 |
ax1.annotate('', xy=(0, -6), xytext=(stage_width, -6), arrowprops=arrow_props)
|
|
|
|
| 364 |
variance = total_width - stage_width
|
| 365 |
efficiency = "100%" if abs(variance) < 0.001 else f"{(total_width/stage_width)*100:.1f}%"
|
| 366 |
|
| 367 |
+
# Calculate buildable area stats
|
| 368 |
+
if max_buildable_areas:
|
| 369 |
+
avg_buildable = np.mean(max_buildable_areas)
|
| 370 |
+
min_buildable = min(max_buildable_areas)
|
| 371 |
+
max_buildable = max(max_buildable_areas)
|
| 372 |
+
buildable_stats = f"Buildable: {min_buildable:.0f}-{max_buildable:.0f}m² (avg: {avg_buildable:.0f}m²)"
|
| 373 |
+
else:
|
| 374 |
+
buildable_stats = "Buildable area calculation pending"
|
| 375 |
+
|
| 376 |
metrics_lines = [
|
| 377 |
f"📊 TOTAL LOTS: {total_lots}",
|
| 378 |
f"📐 LAND EFFICIENCY: {efficiency}",
|
| 379 |
f"🎯 DIVERSITY: {diversity_score:.0%} ({unique_widths} types)",
|
| 380 |
f"📏 GRID VARIANCE: {variance:+.2f}m",
|
| 381 |
+
f"🏠 {buildable_stats}",
|
| 382 |
"",
|
| 383 |
f"SLHC (≤10.5m): {slhc_count} lots",
|
| 384 |
f"Standard (11-14m): {standard_count} lots",
|
|
|
|
| 388 |
f"💰 Revenue: ${total_lots * 0.5:.1f}M - ${total_lots * 1.2:.1f}M"
|
| 389 |
]
|
| 390 |
|
| 391 |
+
col1_text = '\n'.join(metrics_lines[:6])
|
| 392 |
+
col2_text = '\n'.join(metrics_lines[6:])
|
| 393 |
|
| 394 |
ax2.text(0.05, 0.5, col1_text, transform=ax2.transAxes,
|
| 395 |
fontsize=14, verticalalignment='center', fontweight='bold',
|
|
|
|
| 1007 |
report += f"- All lots have identical rear alignment for visual consistency\n"
|
| 1008 |
report += f"- Diverse lot mix ensures varied streetscape\n"
|
| 1009 |
report += f"- SLHC lots grouped for efficient garbage collection\n"
|
| 1010 |
+
report += f"- Buildable areas shown with rescode-compliant setbacks\n"
|
| 1011 |
|
| 1012 |
report += f"\n---\n*Report generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*"
|
| 1013 |
|
| 1014 |
return report
|
| 1015 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1016 |
def darken_color(self, hex_color, factor=0.8):
|
| 1017 |
"""Darken a hex color by a factor"""
|
| 1018 |
try:
|
|
|
|
| 1031 |
stage_depth,
|
| 1032 |
enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18,
|
| 1033 |
enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8,
|
| 1034 |
+
allow_custom_corners, optimization_strategy, color_scheme, show_setbacks
|
| 1035 |
):
|
| 1036 |
# Update color scheme
|
| 1037 |
optimizer.current_scheme = color_scheme
|
|
|
|
| 1089 |
3. Try common stage widths: 84m, 105m, 126m
|
| 1090 |
""", "", ""
|
| 1091 |
|
| 1092 |
+
# Create visualizations with variance indicator and setbacks
|
| 1093 |
fig_2d = optimizer.create_enhanced_visualization(
|
| 1094 |
optimized_solution, stage_width, stage_depth,
|
| 1095 |
"AI-Optimized Diverse Subdivision Layout",
|
| 1096 |
+
show_variance=variance,
|
| 1097 |
+
show_setbacks=show_setbacks
|
| 1098 |
)
|
| 1099 |
|
| 1100 |
# Create results table
|
|
|
|
| 1175 |
|
| 1176 |
return fig_2d, results_df, summary, report, manual_edit_string
|
| 1177 |
|
| 1178 |
+
def update_manual_adjustment(manual_widths_text, stage_width, stage_depth, color_scheme, show_setbacks):
|
| 1179 |
"""Update visualization based on manual adjustment"""
|
| 1180 |
optimizer.current_scheme = color_scheme
|
| 1181 |
|
|
|
|
| 1199 |
fig = optimizer.create_enhanced_visualization(
|
| 1200 |
solution, stage_width, stage_depth,
|
| 1201 |
"Manually Adjusted Layout",
|
| 1202 |
+
show_variance=variance,
|
| 1203 |
+
show_setbacks=show_setbacks
|
| 1204 |
)
|
| 1205 |
|
| 1206 |
return fig, feedback
|
|
|
|
| 1245 |
) as demo:
|
| 1246 |
gr.Markdown("""
|
| 1247 |
# 🏗️ Advanced AI Grid Cut Optimizer Pro
|
| 1248 |
+
### AI-Powered Subdivision Planning with Buildable Boundaries
|
| 1249 |
""")
|
| 1250 |
|
| 1251 |
+
with gr.Row():
|
| 1252 |
+
with gr.Column(scale=1):
|
| 1253 |
+
with gr.Group():
|
| 1254 |
+
gr.Markdown("### 📐 Stage Dimensions")
|
| 1255 |
+
stage_width = gr.Number(
|
| 1256 |
+
label="Stage Width (m)",
|
| 1257 |
+
value=105.0,
|
| 1258 |
+
info="Width along the street"
|
| 1259 |
+
)
|
| 1260 |
+
stage_depth = gr.Number(
|
| 1261 |
+
label="Stage Depth (m)",
|
| 1262 |
+
value=32.0,
|
| 1263 |
+
info="Depth of lots (perpendicular to street)"
|
| 1264 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1265 |
|
| 1266 |
+
gr.Markdown("### 📏 Lot Width Options")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1267 |
|
| 1268 |
+
with gr.Group():
|
| 1269 |
+
gr.Markdown("**Standard Widths**")
|
| 1270 |
+
with gr.Row():
|
| 1271 |
+
enable_8_5 = gr.Checkbox(label="8.5m SLHC", value=True)
|
| 1272 |
+
enable_10_5 = gr.Checkbox(label="10.5m SLHC", value=True)
|
| 1273 |
+
enable_12_5 = gr.Checkbox(label="12.5m", value=True)
|
| 1274 |
+
with gr.Row():
|
| 1275 |
+
enable_14 = gr.Checkbox(label="14.0m", value=True)
|
| 1276 |
+
enable_16 = gr.Checkbox(label="16.0m", value=True)
|
| 1277 |
+
enable_18 = gr.Checkbox(label="18.0m", value=False)
|
| 1278 |
|
| 1279 |
+
with gr.Group():
|
| 1280 |
+
enable_corners = gr.Checkbox(
|
| 1281 |
+
label="Enable Corner-Specific Widths",
|
| 1282 |
+
value=True,
|
| 1283 |
+
info="Adds variety and helps achieve 100%"
|
| 1284 |
+
)
|
| 1285 |
+
with gr.Row():
|
| 1286 |
+
enable_11 = gr.Checkbox(label="11.0m", value=True)
|
| 1287 |
+
enable_13_3 = gr.Checkbox(label="13.3m", value=True)
|
| 1288 |
+
with gr.Row():
|
| 1289 |
+
enable_14_8 = gr.Checkbox(label="14.8m", value=True)
|
| 1290 |
+
enable_16_8 = gr.Checkbox(label="16.8m", value=True)
|
| 1291 |
|
| 1292 |
+
with gr.Column(scale=1):
|
| 1293 |
+
gr.Markdown("### ⚙️ Advanced Settings")
|
|
|
|
|
|
|
| 1294 |
|
| 1295 |
+
allow_custom_corners = gr.Checkbox(
|
| 1296 |
+
label="🎯 Allow Flexible Corner Widths",
|
| 1297 |
+
value=True,
|
| 1298 |
+
info="Enables 13.8m, 13.9m etc. for perfect fits"
|
| 1299 |
+
)
|
| 1300 |
|
| 1301 |
+
show_setbacks = gr.Checkbox(
|
| 1302 |
+
label="🏠 Show Buildable Boundaries",
|
| 1303 |
+
value=True,
|
| 1304 |
+
info="Display maximum house envelope with rescode setbacks"
|
| 1305 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1306 |
|
| 1307 |
+
optimization_strategy = gr.Radio(
|
| 1308 |
+
["diversity_focus", "balanced"],
|
| 1309 |
+
label="Optimization Strategy",
|
| 1310 |
+
value="diversity_focus",
|
| 1311 |
+
info="Diversity creates more interesting layouts"
|
| 1312 |
+
)
|
| 1313 |
|
| 1314 |
+
color_scheme = gr.Radio(
|
| 1315 |
+
["modern", "professional", "neon"],
|
| 1316 |
+
label="🎨 Color Scheme",
|
| 1317 |
+
value="neon",
|
| 1318 |
+
info="Neon colors work best with dark background"
|
| 1319 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1320 |
|
| 1321 |
+
optimize_btn = gr.Button(
|
| 1322 |
+
"🚀 Optimize with AI",
|
| 1323 |
+
variant="primary",
|
| 1324 |
+
size="lg",
|
| 1325 |
+
elem_id="optimize-button"
|
| 1326 |
+
)
|
| 1327 |
+
|
| 1328 |
+
gr.Markdown("""
|
| 1329 |
+
### 💡 Quick Tips:
|
| 1330 |
+
- **Buildable Areas**: Dotted lines show max house size
|
| 1331 |
+
- **Setbacks**: Front/Rear: 6m, Side: 1m (1.5m for 2-story)
|
| 1332 |
+
- **Corner Lots**: Extra 3m setback on secondary street
|
| 1333 |
+
- **Manual Adjust**: Edit the result below after optimization
|
| 1334 |
+
""")
|
| 1335 |
+
|
| 1336 |
+
with gr.Row():
|
| 1337 |
+
plot_2d = gr.Plot(label="2D Layout with Buildable Boundaries")
|
| 1338 |
+
|
| 1339 |
+
# Manual adjustment section
|
| 1340 |
+
gr.Markdown("### ✏️ Fine-Tune AI Result")
|
| 1341 |
+
with gr.Row():
|
| 1342 |
+
with gr.Column(scale=2):
|
| 1343 |
+
manual_widths = gr.Textbox(
|
| 1344 |
+
label="Manually Adjust Lot Widths",
|
| 1345 |
+
placeholder="Widths will appear here after optimization",
|
| 1346 |
+
info="Edit the widths (comma-separated) and click 'Update Layout'",
|
| 1347 |
+
lines=2
|
| 1348 |
+
)
|
| 1349 |
+
with gr.Column(scale=1):
|
| 1350 |
+
update_btn = gr.Button("🔄 Update Layout", variant="secondary")
|
| 1351 |
+
adjustment_feedback = gr.Markdown(
|
| 1352 |
+
value="",
|
| 1353 |
+
label="Adjustment Feedback"
|
| 1354 |
+
)
|
| 1355 |
+
|
| 1356 |
+
with gr.Row():
|
| 1357 |
+
results_table = gr.DataFrame(label="Lot Distribution Analysis")
|
| 1358 |
+
|
| 1359 |
+
with gr.Row():
|
| 1360 |
+
with gr.Column():
|
| 1361 |
+
summary_output = gr.Markdown(label="Optimization Summary")
|
| 1362 |
+
with gr.Column():
|
| 1363 |
+
report_output = gr.Markdown(label="Professional Report")
|
| 1364 |
|
| 1365 |
# Wire up the buttons
|
| 1366 |
optimize_btn.click(
|
|
|
|
| 1370 |
stage_depth,
|
| 1371 |
enable_8_5, enable_10_5, enable_12_5, enable_14, enable_16, enable_18,
|
| 1372 |
enable_corners, enable_11, enable_13_3, enable_14_8, enable_16_8,
|
| 1373 |
+
allow_custom_corners, optimization_strategy, color_scheme, show_setbacks
|
| 1374 |
],
|
| 1375 |
outputs=[plot_2d, results_table, summary_output, report_output, manual_widths]
|
| 1376 |
)
|
| 1377 |
|
| 1378 |
update_btn.click(
|
| 1379 |
update_manual_adjustment,
|
| 1380 |
+
inputs=[manual_widths, stage_width, stage_depth, color_scheme, show_setbacks],
|
| 1381 |
outputs=[plot_2d, adjustment_feedback]
|
| 1382 |
)
|
| 1383 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1384 |
return demo
|
| 1385 |
|
| 1386 |
# Create and launch
|