Commit ·
b010f1b
0
Parent(s):
Initial commit: REMB - AI-Powered Industrial Estate Master Plan Optimization Engine
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env.example +29 -0
- .gitignore +86 -0
- README.md +0 -0
- config/__init__.py +1 -0
- config/regulations.yaml +61 -0
- config/settings.py +50 -0
- docs/Core_document.md +40 -0
- docs/MVP-24h.md +1783 -0
- docs/MVP-STATUS.md +120 -0
- docs/Proposal_ AI-Powered Industrial Estate Master Plan Optimization Engine.md +0 -0
- docs/Requirement.md +8 -0
- examples/api-cw750-details.dxf +0 -0
- frontend/.gitignore +24 -0
- frontend/README.md +73 -0
- frontend/eslint.config.js +23 -0
- frontend/index.html +13 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +34 -0
- frontend/public/vite.svg +1 -0
- frontend/src/App.css +724 -0
- frontend/src/App.tsx +333 -0
- frontend/src/assets/react.svg +1 -0
- frontend/src/components/ChatInterface.tsx +127 -0
- frontend/src/components/ExportPanel.tsx +35 -0
- frontend/src/components/FileUploadPanel.tsx +75 -0
- frontend/src/components/LayoutOptionsPanel.tsx +91 -0
- frontend/src/components/Map2DPlotter.tsx +274 -0
- frontend/src/components/index.ts +6 -0
- frontend/src/index.css +26 -0
- frontend/src/main.tsx +10 -0
- frontend/src/services/api.ts +102 -0
- frontend/src/types/index.ts +86 -0
- frontend/tsconfig.app.json +28 -0
- frontend/tsconfig.json +7 -0
- frontend/tsconfig.node.json +26 -0
- frontend/vite.config.ts +7 -0
- requirements.txt +41 -0
- src/__init__.py +7 -0
- src/algorithms/__init__.py +1 -0
- src/algorithms/ga_optimizer.py +405 -0
- src/algorithms/milp_solver.py +659 -0
- src/algorithms/nsga2_optimizer.py +336 -0
- src/algorithms/regulation_checker.py +329 -0
- src/api/__init__.py +1 -0
- src/api/main.py +182 -0
- src/api/mvp_api.py +677 -0
- src/core/__init__.py +2 -0
- src/core/orchestrator.py +662 -0
- src/export/__init__.py +2 -0
- src/export/dxf_exporter.py +444 -0
.env.example
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment Configuration for REMB
|
| 2 |
+
|
| 3 |
+
# Database Configuration
|
| 4 |
+
POSTGRES_USER=remb_user
|
| 5 |
+
POSTGRES_PASSWORD=remb_password
|
| 6 |
+
POSTGRES_SERVER=localhost
|
| 7 |
+
POSTGRES_PORT=5432
|
| 8 |
+
POSTGRES_DB=remb_db
|
| 9 |
+
|
| 10 |
+
# API Configuration
|
| 11 |
+
API_V1_STR=/api/v1
|
| 12 |
+
PROJECT_NAME="REMB - Industrial Estate Master Planning Engine"
|
| 13 |
+
VERSION=0.1.0
|
| 14 |
+
|
| 15 |
+
# Optimization Settings
|
| 16 |
+
NSGA2_POPULATION_SIZE=100
|
| 17 |
+
NSGA2_GENERATIONS=200
|
| 18 |
+
NSGA2_CROSSOVER_RATE=0.9
|
| 19 |
+
NSGA2_MUTATION_RATE=0.1
|
| 20 |
+
|
| 21 |
+
# MILP Solver Settings
|
| 22 |
+
MILP_TIME_LIMIT_SECONDS=3600
|
| 23 |
+
MILP_SOLVER=SCIP
|
| 24 |
+
|
| 25 |
+
# File Upload Settings
|
| 26 |
+
MAX_UPLOAD_SIZE_MB=50
|
| 27 |
+
|
| 28 |
+
# Processing Settings
|
| 29 |
+
MAX_CONCURRENT_OPTIMIZATIONS=2
|
.gitignore
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
share/python-wheels/
|
| 20 |
+
*.egg-info/
|
| 21 |
+
.installed.cfg
|
| 22 |
+
*.egg
|
| 23 |
+
MANIFEST
|
| 24 |
+
*.manifest
|
| 25 |
+
*.spec
|
| 26 |
+
pip-log.txt
|
| 27 |
+
pip-delete-this-directory.txt
|
| 28 |
+
|
| 29 |
+
# Virtual Environment
|
| 30 |
+
venv/
|
| 31 |
+
ENV/
|
| 32 |
+
env/
|
| 33 |
+
.venv
|
| 34 |
+
|
| 35 |
+
# IDEs
|
| 36 |
+
.vscode/
|
| 37 |
+
.idea/
|
| 38 |
+
*.swp
|
| 39 |
+
*.swo
|
| 40 |
+
*~
|
| 41 |
+
.DS_Store
|
| 42 |
+
|
| 43 |
+
# Environment variables
|
| 44 |
+
.env
|
| 45 |
+
.env.local
|
| 46 |
+
.env.*.local
|
| 47 |
+
|
| 48 |
+
# Testing
|
| 49 |
+
.pytest_cache/
|
| 50 |
+
.coverage
|
| 51 |
+
htmlcov/
|
| 52 |
+
.tox/
|
| 53 |
+
.nox/
|
| 54 |
+
|
| 55 |
+
# Jupyter Notebook
|
| 56 |
+
.ipynb_checkpoints
|
| 57 |
+
|
| 58 |
+
# Frontend - Node
|
| 59 |
+
frontend/node_modules/
|
| 60 |
+
frontend/dist/
|
| 61 |
+
frontend/.vite/
|
| 62 |
+
frontend/.cache/
|
| 63 |
+
|
| 64 |
+
# Logs
|
| 65 |
+
*.log
|
| 66 |
+
logs/
|
| 67 |
+
npm-debug.log*
|
| 68 |
+
yarn-debug.log*
|
| 69 |
+
yarn-error.log*
|
| 70 |
+
pnpm-debug.log*
|
| 71 |
+
lerna-debug.log*
|
| 72 |
+
|
| 73 |
+
# Output files
|
| 74 |
+
output/*.dxf
|
| 75 |
+
output/*.pdf
|
| 76 |
+
output/*.png
|
| 77 |
+
!output/.gitkeep
|
| 78 |
+
|
| 79 |
+
# OS
|
| 80 |
+
Thumbs.db
|
| 81 |
+
.DS_Store
|
| 82 |
+
|
| 83 |
+
# Temporary files
|
| 84 |
+
*.tmp
|
| 85 |
+
*.temp
|
| 86 |
+
.cache/
|
README.md
ADDED
|
Binary file (7.26 kB). View file
|
|
|
config/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Config package"""
|
config/regulations.yaml
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Vietnamese Industrial Estate Regulations
|
| 2 |
+
# Quy định khu công nghiệp Việt Nam
|
| 3 |
+
|
| 4 |
+
# Setbacks (khoảng cách lùi ranh giới) - in meters
|
| 5 |
+
setbacks:
|
| 6 |
+
boundary_minimum: 50 # Khoảng cách tối thiểu từ ranh giới
|
| 7 |
+
fire_safety_distance: 30 # Khoảng cách an toàn PCCC
|
| 8 |
+
waterway_buffer: 100 # Vùng đệm sông, suối
|
| 9 |
+
highway_buffer: 50 # Vùng đệm đường cao tốc
|
| 10 |
+
residential_buffer: 300 # Khoảng cách từ khu dân cư
|
| 11 |
+
|
| 12 |
+
# Floor Area Ratio (Tỷ lệ diện tích sàn)
|
| 13 |
+
far:
|
| 14 |
+
maximum: 0.7 # Hệ số sử dụng đất tối đa
|
| 15 |
+
minimum: 0.3 # Hệ số sử dụng đất tối thiểu
|
| 16 |
+
|
| 17 |
+
# Green Space Requirements (Yêu cầu cây xanh)
|
| 18 |
+
green_space:
|
| 19 |
+
minimum_percentage: 0.15 # 15% diện tích tối thiểu
|
| 20 |
+
preferred_percentage: 0.20 # 20% diện tích khuyến khích
|
| 21 |
+
|
| 22 |
+
# Plot Requirements (Yêu cầu lô đất)
|
| 23 |
+
plot:
|
| 24 |
+
minimum_area_sqm: 1000 # Diện tích lô tối thiểu (m²)
|
| 25 |
+
maximum_area_sqm: 50000 # Diện tích lô tối đa (m²)
|
| 26 |
+
minimum_width_m: 20 # Chiều rộng tối thiểu (m)
|
| 27 |
+
minimum_frontage_m: 15 # Mặt tiền tối thiểu (m)
|
| 28 |
+
|
| 29 |
+
# Road Network (Mạng lưới đường)
|
| 30 |
+
roads:
|
| 31 |
+
primary_width_m: 24 # Đường chính
|
| 32 |
+
secondary_width_m: 16 # Đường nhánh
|
| 33 |
+
tertiary_width_m: 12 # Đường nội bộ
|
| 34 |
+
minimum_turning_radius_m: 12 # Bán kính quay đầu tối thiểu
|
| 35 |
+
maximum_distance_to_road_m: 200 # Khoảng cách tối đa đến đường
|
| 36 |
+
|
| 37 |
+
# Hazard Zones (Khu vực nguy hiểm)
|
| 38 |
+
hazard_zones:
|
| 39 |
+
chemical_storage_buffer_m: 200 # Vùng đệm kho hóa chất
|
| 40 |
+
fuel_storage_buffer_m: 150 # Vùng đệm kho nhiên liệu
|
| 41 |
+
warehouse_buffer_m: 50 # Vùng đệm kho hàng thường
|
| 42 |
+
utility_corridor_width_m: 10 # Hành lang kỹ thuật
|
| 43 |
+
|
| 44 |
+
# Infrastructure (Hạ tầng)
|
| 45 |
+
infrastructure:
|
| 46 |
+
utility_corridor_percentage: 0.05 # 5% diện tích cho hành lang kỹ thuật
|
| 47 |
+
drainage_slope_minimum: 0.003 # Độ dốc thoát nước tối thiểu (0.3%)
|
| 48 |
+
|
| 49 |
+
# Land Use Distribution (Phân bổ đất đai khuyến nghị)
|
| 50 |
+
land_use:
|
| 51 |
+
industrial_plots: 0.65 # 65% đất công nghiệp
|
| 52 |
+
roads_and_infrastructure: 0.20 # 20% đường và hạ tầng
|
| 53 |
+
green_space: 0.15 # 15% cây xanh
|
| 54 |
+
|
| 55 |
+
# Optimization Objectives (Mục tiêu tối ưu)
|
| 56 |
+
objectives:
|
| 57 |
+
weights:
|
| 58 |
+
sellable_area: 0.5 # Trọng số diện tích có thể bán
|
| 59 |
+
green_space: 0.2 # Trọng số cây xanh
|
| 60 |
+
road_efficiency: 0.15 # Trọng số hiệu quả đường
|
| 61 |
+
regulatory_compliance: 0.15 # Trọng số tuân thủ quy định
|
config/settings.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Configuration settings for REMB Optimization Engine
|
| 3 |
+
"""
|
| 4 |
+
from pydantic_settings import BaseSettings
|
| 5 |
+
from typing import Optional
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Settings(BaseSettings):
|
| 9 |
+
"""Application settings"""
|
| 10 |
+
|
| 11 |
+
# API Settings
|
| 12 |
+
API_V1_STR: str = "/api/v1"
|
| 13 |
+
PROJECT_NAME: str = "REMB - Industrial Estate Master Planning Engine"
|
| 14 |
+
VERSION: str = "0.1.0"
|
| 15 |
+
|
| 16 |
+
# Database Settings
|
| 17 |
+
POSTGRES_USER: str = "remb_user"
|
| 18 |
+
POSTGRES_PASSWORD: str = "remb_password"
|
| 19 |
+
POSTGRES_SERVER: str = "localhost"
|
| 20 |
+
POSTGRES_PORT: str = "5432"
|
| 21 |
+
POSTGRES_DB: str = "remb_db"
|
| 22 |
+
|
| 23 |
+
@property
|
| 24 |
+
def DATABASE_URL(self) -> str:
|
| 25 |
+
return f"postgresql://{self.POSTGRES_USER}:{self.POSTGRES_PASSWORD}@{self.POSTGRES_SERVER}:{self.POSTGRES_PORT}/{self.POSTGRES_DB}"
|
| 26 |
+
|
| 27 |
+
# Optimization Settings
|
| 28 |
+
NSGA2_POPULATION_SIZE: int = 100
|
| 29 |
+
NSGA2_GENERATIONS: int = 200
|
| 30 |
+
NSGA2_CROSSOVER_RATE: float = 0.9
|
| 31 |
+
NSGA2_MUTATION_RATE: float = 0.1
|
| 32 |
+
|
| 33 |
+
# MILP Solver Settings
|
| 34 |
+
MILP_TIME_LIMIT_SECONDS: int = 3600 # 1 hour
|
| 35 |
+
MILP_SOLVER: str = "SCIP" # OR-Tools solver
|
| 36 |
+
|
| 37 |
+
# File Upload Settings
|
| 38 |
+
MAX_UPLOAD_SIZE_MB: int = 50
|
| 39 |
+
ALLOWED_EXTENSIONS: list = [".shp", ".dxf", ".geojson"]
|
| 40 |
+
|
| 41 |
+
# Processing Settings
|
| 42 |
+
MAX_CONCURRENT_OPTIMIZATIONS: int = 2
|
| 43 |
+
CELERY_BROKER_URL: Optional[str] = None
|
| 44 |
+
|
| 45 |
+
class Config:
|
| 46 |
+
env_file = ".env"
|
| 47 |
+
case_sensitive = True
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
settings = Settings()
|
docs/Core_document.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Cơ chế hoạt động của phần Core Engine theo mô hình "Nhạc trưởng & Kỹ sư" (Orchestrator-Solver Model):
|
| 2 |
+
|
| 3 |
+
1. Phân chia vai trò
|
| 4 |
+
🧠 LLM (The Brain/Nhạc trưởng): Chịu trách nhiệm hiểu ngữ nghĩa, điều phối luồng đi, xử lý logic nghiệp vụ và giao tiếp với con người. LLM không trực tiếp tính toán hình học.
|
| 5 |
+
|
| 6 |
+
⚙️ CP Module (The Muscle/Kỹ sư): Chịu trách nhiệm tính toán toán học, giải quyết các ràng buộc hình học (Geometry) và tối ưu hóa (Optimization) chính xác tuyệt đối. CP hoạt động như một "hộp đen" (black-box) xử lý dữ liệu.
|
| 7 |
+
|
| 8 |
+
2. Quy trình "Bắt tay" (The Handshake Loop)
|
| 9 |
+
Quy trình diễn ra theo vòng lặp khép kín 4 bước:
|
| 10 |
+
|
| 11 |
+
Dịch (Translation):
|
| 12 |
+
|
| 13 |
+
LLM nhận yêu cầu tự nhiên (VD: "Tránh kho xăng 200m").
|
| 14 |
+
|
| 15 |
+
LLM chuyển đổi yêu cầu thành tham số kỹ thuật chuẩn (JSON) để gọi hàm (Function Calling).
|
| 16 |
+
|
| 17 |
+
Giải (Execution):
|
| 18 |
+
|
| 19 |
+
CP Module nhận JSON, chạy thuật toán (MILP/GeoPandas).
|
| 20 |
+
|
| 21 |
+
CP trả về kết quả thô (số liệu, tọa độ) hoặc trạng thái lỗi (nếu bài toán vô nghiệm).
|
| 22 |
+
|
| 23 |
+
Hiểu (Interpretation):
|
| 24 |
+
|
| 25 |
+
LLM đọc kết quả thô từ CP.
|
| 26 |
+
|
| 27 |
+
LLM so sánh với yêu cầu ban đầu để đánh giá: Thành công hay Thất bại.
|
| 28 |
+
|
| 29 |
+
Quyết định (Reasoning & Action):
|
| 30 |
+
|
| 31 |
+
Nếu thành công: LLM ra lệnh xuất bản vẽ và báo cáo cho người dùng.
|
| 32 |
+
|
| 33 |
+
Nếu thất bại (Xung đột): LLM tự động suy luận nguyên nhân (VD: "Quá chật chội"), đề xuất phương án nới lỏng ràng buộc và hỏi ý kiến người dùng.
|
| 34 |
+
|
| 35 |
+
3. Ưu điểm cốt lõi
|
| 36 |
+
Cơ chế này khắc phục được điểm yếu chết người của từng công nghệ riêng lẻ:
|
| 37 |
+
|
| 38 |
+
Loại bỏ sự "ảo giác" (hallucination) của AI vì mọi con số đều do CP tính.
|
| 39 |
+
|
| 40 |
+
Loại bỏ sự "cứng nhắc" của thuật toán truyền thống vì LLM giúp linh hoạt trong việc nhập liệu và xử lý tình huống phát sinh.
|
docs/MVP-24h.md
ADDED
|
@@ -0,0 +1,1783 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# **AIOptimize™ COMPLETE ARCHITECTURE, WORKFLOW & IMPLEMENTATION GUIDE**
|
| 2 |
+
|
| 3 |
+
## **Enterprise-Ready System Design Architecture & Strategy**
|
| 4 |
+
|
| 5 |
+
**Complete technical architecture, workflow diagrams, technology stack, and implementation strategy**
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
# **🎯 EXECUTIVE OVERVIEW**
|
| 10 |
+
|
| 11 |
+
## **What This System Does**
|
| 12 |
+
|
| 13 |
+
AIOptimize™ is an AI-powered industrial estate planning engine that: 1\. Analyzes site boundaries 2\. Generates multiple optimized layout options 3\. Explains optimization choices via AI 4\. Exports professional CAD files
|
| 14 |
+
|
| 15 |
+
## **System Maturity Levels**
|
| 16 |
+
|
| 17 |
+
### **Level 1: MVP (6 hours)**
|
| 18 |
+
|
| 19 |
+
* Basic UI for file upload
|
| 20 |
+
|
| 21 |
+
* GeoJSON parsing
|
| 22 |
+
|
| 23 |
+
* Simple visualization
|
| 24 |
+
|
| 25 |
+
* No optimization
|
| 26 |
+
|
| 27 |
+
### **Level 2: Smart Demo (12 hours)**
|
| 28 |
+
|
| 29 |
+
* Real genetic algorithm optimization
|
| 30 |
+
|
| 31 |
+
* Multiple intelligent layout options
|
| 32 |
+
|
| 33 |
+
* Hardcoded AI chat explanations
|
| 34 |
+
|
| 35 |
+
* Professional 2D visualization
|
| 36 |
+
|
| 37 |
+
### **Level 2+: Enterprise (24 hours)**
|
| 38 |
+
|
| 39 |
+
* Real Gemini Flash 2.0 AI (replaces hardcoded)
|
| 40 |
+
|
| 41 |
+
* Professional DXF CAD export
|
| 42 |
+
|
| 43 |
+
* Complete error handling
|
| 44 |
+
|
| 45 |
+
* Production-ready deployment
|
| 46 |
+
|
| 47 |
+
---
|
| 48 |
+
|
| 49 |
+
# **📊 COMPLETE SYSTEM ARCHITECTURE**
|
| 50 |
+
|
| 51 |
+
## **High-Level System Design**
|
| 52 |
+
|
| 53 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 54 |
+
│ USER INTERFACE (Browser) │
|
| 55 |
+
│ ┌──────────────┐ ┌────────────────┐ ┌──────────────┐ │
|
| 56 |
+
│ │ Upload UI │ │ 2D Visualizer │ │ Chat Panel │ │
|
| 57 |
+
│ │ (React) │ │ (Konva.js) │ │ (React) │ │
|
| 58 |
+
│ └──────────────┘ └────────────────┘ └──────────────┘ │
|
| 59 |
+
│ │ │ │
|
| 60 |
+
│ ┌─────────────────────┴──────────────────────┘ │
|
| 61 |
+
│ │ Metrics Display │ Export Buttons (DXF/ZIP) │
|
| 62 |
+
└──┼──────────────────────────────────────────────────────────┘
|
| 63 |
+
│ REST API (HTTP/JSON)
|
| 64 |
+
▼
|
| 65 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 66 |
+
│ APPLICATION BACKEND │
|
| 67 |
+
│ (FastAPI \- Python) │
|
| 68 |
+
│ │
|
| 69 |
+
│ ┌──────────────────────────────────────────────────────┐ │
|
| 70 |
+
│ │ API Layer (REST Endpoints) │ │
|
| 71 |
+
│ │ • /upload-boundary (POST) │ │
|
| 72 |
+
│ │ • /generate-layouts (POST) │ │
|
| 73 |
+
│ │ • /chat (POST) │ │
|
| 74 |
+
│ │ • /export-dxf (POST) │ │
|
| 75 |
+
│ │ • /export-all-dxf (POST) │ │
|
| 76 |
+
│ │ • /health (GET) │ │
|
| 77 |
+
│ └──────────────────────────────────────────────────────┘ │
|
| 78 |
+
│ │ │
|
| 79 |
+
│ ┌──────────────────────┴───────────────────────────────┐ │
|
| 80 |
+
│ │ Service Layer (Business Logic) │ │
|
| 81 |
+
│ │ ┌──────────────────┐ ┌──────────────────────┐ │ │
|
| 82 |
+
│ │ │ Geometry Service │ │ GA Optimization │ │ │
|
| 83 |
+
│ │ │ (Shapely) │ │ (Genetic Algorithm) │ │ │
|
| 84 |
+
│ │ └──────────────────┘ └──────────────────────┘ │ │
|
| 85 |
+
│ │ ┌──────────────────┐ ┌──────────────────────┐ │ │
|
| 86 |
+
│ │ │ Chat Service │ │ Gemini LLM Service │ │ │
|
| 87 |
+
│ │ │ (Hardcoded) │ │ (Real AI) │ │ │
|
| 88 |
+
│ │ └──────────────────┘ └──────────────────────┘ │ │
|
| 89 |
+
│ │ ���──────────────────┐ ┌──────────────────────┐ │ │
|
| 90 |
+
│ │ │ DXF Export │ │ Session Management │ │ │
|
| 91 |
+
│ │ │ (ezdxf) │ │ (In-memory store) │ │ │
|
| 92 |
+
│ │ └──────────────────┘ └──────────────────────┘ │ │
|
| 93 |
+
│ └──────────────────────────────────────────────────────┘ │
|
| 94 |
+
│ │
|
| 95 |
+
│ ┌──────────────────────────────────────────────────────┐ │
|
| 96 |
+
│ │ Data Layer │ │
|
| 97 |
+
│ │ • Session storage (UUID → site data) │ │
|
| 98 |
+
│ │ • Geometry data (Shapely Polygon objects) │ │
|
| 99 |
+
│ │ • Layout options (plot coordinates, metrics) │ │
|
| 100 |
+
│ │ • Export cache (temporary DXF files) │ │
|
| 101 |
+
│ └──────────────────────────────────────────────────────┘ │
|
| 102 |
+
└─────────────────────────────────────────────────────────────┘
|
| 103 |
+
│ External API Calls
|
| 104 |
+
├─→ Google Gemini API (AI Chat)
|
| 105 |
+
├─→ GeoJSON parsing (geospatial data)
|
| 106 |
+
└─→ File system (export storage)
|
| 107 |
+
|
| 108 |
+
---
|
| 109 |
+
|
| 110 |
+
# **🏗️ DETAILED SYSTEM COMPONENTS**
|
| 111 |
+
|
| 112 |
+
## **Component 1: Frontend Application Layer**
|
| 113 |
+
|
| 114 |
+
### **Purpose**
|
| 115 |
+
|
| 116 |
+
Interactive user interface for site analysis and visualization.
|
| 117 |
+
|
| 118 |
+
### **Responsibilities**
|
| 119 |
+
|
| 120 |
+
* File upload handling
|
| 121 |
+
|
| 122 |
+
* 2D site visualization
|
| 123 |
+
|
| 124 |
+
* Layout options display
|
| 125 |
+
|
| 126 |
+
* Chat interface
|
| 127 |
+
|
| 128 |
+
* Export button management
|
| 129 |
+
|
| 130 |
+
* Real-time state management
|
| 131 |
+
|
| 132 |
+
### **Technology Choices & Why**
|
| 133 |
+
|
| 134 |
+
* **React** (instead of Vue/Angular)
|
| 135 |
+
|
| 136 |
+
* Large ecosystem (tools, libraries, components)
|
| 137 |
+
|
| 138 |
+
* TypeScript support (type safety)
|
| 139 |
+
|
| 140 |
+
* Easier learning curve for full-stack developers
|
| 141 |
+
|
| 142 |
+
* Better component reusability
|
| 143 |
+
|
| 144 |
+
* **TypeScript** (instead of plain JavaScript)
|
| 145 |
+
|
| 146 |
+
* Catches errors at compile time
|
| 147 |
+
|
| 148 |
+
* Better IDE support and autocomplete
|
| 149 |
+
|
| 150 |
+
* Self-documenting code
|
| 151 |
+
|
| 152 |
+
* Enterprise standard
|
| 153 |
+
|
| 154 |
+
* **Konva.js** (instead of Canvas API / D3 / Three.js)
|
| 155 |
+
|
| 156 |
+
* Specifically built for 2D graphics
|
| 157 |
+
|
| 158 |
+
* Simpler API than raw Canvas
|
| 159 |
+
|
| 160 |
+
* Built-in event handling
|
| 161 |
+
|
| 162 |
+
* Better performance for GIS visualization
|
| 163 |
+
|
| 164 |
+
* **Axios** (instead of Fetch API / GraphQL)
|
| 165 |
+
|
| 166 |
+
* Simpler request/response handling
|
| 167 |
+
|
| 168 |
+
* Built-in interceptors (error handling, logging)
|
| 169 |
+
|
| 170 |
+
* Request/response transformation
|
| 171 |
+
|
| 172 |
+
* Backward compatible
|
| 173 |
+
|
| 174 |
+
* **Lucide React** (for icons)
|
| 175 |
+
|
| 176 |
+
* Lightweight icon library
|
| 177 |
+
|
| 178 |
+
* Consistent icon set
|
| 179 |
+
|
| 180 |
+
* Simple integration with React
|
| 181 |
+
|
| 182 |
+
### **Data Flow (Frontend)**
|
| 183 |
+
|
| 184 |
+
User Action (Upload/Generate/Chat)
|
| 185 |
+
↓
|
| 186 |
+
React State Update
|
| 187 |
+
↓
|
| 188 |
+
API Call via Axios
|
| 189 |
+
↓
|
| 190 |
+
Wait for Response
|
| 191 |
+
↓
|
| 192 |
+
Update UI Components
|
| 193 |
+
↓
|
| 194 |
+
Display Results
|
| 195 |
+
|
| 196 |
+
### **Component Hierarchy**
|
| 197 |
+
|
| 198 |
+
\<App\> (Main)
|
| 199 |
+
├─ Header (Logo, Title)
|
| 200 |
+
├─ FileUploadPanel
|
| 201 |
+
│ ├─ Upload Button
|
| 202 |
+
│ └─ Sample Data Button
|
| 203 |
+
├─ MainContent (60% width)
|
| 204 |
+
│ ├─ Map2DPlotter (Konva Stage)
|
| 205 |
+
│ │ ├─ Boundary Polygon
|
| 206 |
+
│ │ ├─ Setback Zone
|
| 207 |
+
│ │ └─ Grid/Reference Lines
|
| 208 |
+
│ ├─ LayoutOptionsPanel
|
| 209 |
+
│ │ ├─ OptionCard 1
|
| 210 |
+
│ │ ├─ OptionCard 2
|
| 211 |
+
│ │ └─ OptionCard 3
|
| 212 |
+
│ └─ ExportPanel
|
| 213 |
+
│ ├─ Export Individual Buttons
|
| 214 |
+
│ └─ Export All Zip Button
|
| 215 |
+
└─ ChatSidebar (40% width)
|
| 216 |
+
└─ ChatInterface
|
| 217 |
+
├─ Message History
|
| 218 |
+
├─ Message Input
|
| 219 |
+
└─ Send Button
|
| 220 |
+
|
| 221 |
+
---
|
| 222 |
+
|
| 223 |
+
## **Component 2: Backend Application Layer**
|
| 224 |
+
|
| 225 |
+
### **Purpose**
|
| 226 |
+
|
| 227 |
+
Business logic, optimization algorithms, and data processing.
|
| 228 |
+
|
| 229 |
+
### **Responsibilities**
|
| 230 |
+
|
| 231 |
+
* REST API endpoint management
|
| 232 |
+
|
| 233 |
+
* GeoJSON parsing and validation
|
| 234 |
+
|
| 235 |
+
* Session management
|
| 236 |
+
|
| 237 |
+
* Genetic algorithm execution
|
| 238 |
+
|
| 239 |
+
* AI response generation
|
| 240 |
+
|
| 241 |
+
* DXF file creation
|
| 242 |
+
|
| 243 |
+
* Error handling and logging
|
| 244 |
+
|
| 245 |
+
### **Technology Choices & Why**
|
| 246 |
+
|
| 247 |
+
* **FastAPI** (instead of Flask / Django / Starlette)
|
| 248 |
+
|
| 249 |
+
* Automatic OpenAPI documentation
|
| 250 |
+
|
| 251 |
+
* Built-in request validation (Pydantic)
|
| 252 |
+
|
| 253 |
+
* Asynchronous support (async/await)
|
| 254 |
+
|
| 255 |
+
* Type hints integration
|
| 256 |
+
|
| 257 |
+
* Very fast (ASGI-based)
|
| 258 |
+
|
| 259 |
+
* Smaller learning curve than Django
|
| 260 |
+
|
| 261 |
+
* **Python 3.8+** (language choice)
|
| 262 |
+
|
| 263 |
+
* Large scientific computing ecosystem
|
| 264 |
+
|
| 265 |
+
* Quick prototyping and development
|
| 266 |
+
|
| 267 |
+
* Strong geospatial libraries (Shapely, GeoPandas)
|
| 268 |
+
|
| 269 |
+
* Good AI/ML library support
|
| 270 |
+
|
| 271 |
+
* Easy deployment
|
| 272 |
+
|
| 273 |
+
* **Shapely** (instead of GDAL / PostGIS / Turf.js)
|
| 274 |
+
|
| 275 |
+
* Pure Python (no native dependencies)
|
| 276 |
+
|
| 277 |
+
* Simple geometry operations
|
| 278 |
+
|
| 279 |
+
* Built-in validation
|
| 280 |
+
|
| 281 |
+
* Good performance for 2D operations
|
| 282 |
+
|
| 283 |
+
* Well-documented
|
| 284 |
+
|
| 285 |
+
* **NumPy** (for numerical operations)
|
| 286 |
+
|
| 287 |
+
* Industry standard for numerical computing
|
| 288 |
+
|
| 289 |
+
* Fast matrix/array operations
|
| 290 |
+
|
| 291 |
+
* Genetic algorithm fitness calculations
|
| 292 |
+
|
| 293 |
+
* Statistical functions
|
| 294 |
+
|
| 295 |
+
* **google-generativeai** (for Gemini API)
|
| 296 |
+
|
| 297 |
+
* Official Google library
|
| 298 |
+
|
| 299 |
+
* Maintained and updated
|
| 300 |
+
|
| 301 |
+
* Simple API for chat completion
|
| 302 |
+
|
| 303 |
+
* Free tier available
|
| 304 |
+
|
| 305 |
+
* **ezdxf** (instead of pyDXF / cadquery / LibreCAD)
|
| 306 |
+
|
| 307 |
+
* Comprehensive DXF support (R2010 standard)
|
| 308 |
+
|
| 309 |
+
* Specific for CAD file creation
|
| 310 |
+
|
| 311 |
+
* Active maintenance
|
| 312 |
+
|
| 313 |
+
* Good layer/attribute support
|
| 314 |
+
|
| 315 |
+
* Works with all CAD software
|
| 316 |
+
|
| 317 |
+
### **Service Architecture**
|
| 318 |
+
|
| 319 |
+
API Layer (REST Endpoints)
|
| 320 |
+
↓
|
| 321 |
+
├─ Authentication/Validation
|
| 322 |
+
├─ Request Routing
|
| 323 |
+
└─ Response Formatting
|
| 324 |
+
↓
|
| 325 |
+
Service Layer (Business Logic)
|
| 326 |
+
├─ GeometryService
|
| 327 |
+
│ ├─ Parse GeoJSON → Polygon
|
| 328 |
+
│ ├─ Calculate setback zones
|
| 329 |
+
│ ├─ Validate boundaries
|
| 330 |
+
│ └─ Compute metrics
|
| 331 |
+
│
|
| 332 |
+
├─ OptimizationService
|
| 333 |
+
│ ├─ Genetic Algorithm
|
| 334 |
+
│ ├─ Population management
|
| 335 |
+
│ ├─ Fitness evaluation
|
| 336 |
+
│ └─ Layout generation
|
| 337 |
+
│
|
| 338 |
+
├─ ChatService
|
| 339 |
+
│ ├─ Message analysis
|
| 340 |
+
│ ├─ Response generation
|
| 341 |
+
│ └─ Context management
|
| 342 |
+
│
|
| 343 |
+
├─ GeminiService
|
| 344 |
+
│ ├─ API communication
|
| 345 |
+
│ ├─ Prompt engineering
|
| 346 |
+
│ └─ Error handling
|
| 347 |
+
│
|
| 348 |
+
└─ ExportService
|
| 349 |
+
├─ DXF document creation
|
| 350 |
+
├─ Layer management
|
| 351 |
+
└─ File export
|
| 352 |
+
|
| 353 |
+
↓
|
| 354 |
+
Data Layer (Storage & Access)
|
| 355 |
+
├─ Session Store (In-memory)
|
| 356 |
+
├─ File System (Export cache)
|
| 357 |
+
└─ External APIs (Gemini, etc.)
|
| 358 |
+
|
| 359 |
+
---
|
| 360 |
+
|
| 361 |
+
## **Component 3: Optimization Algorithm (Genetic Algorithm)**
|
| 362 |
+
|
| 363 |
+
### **Purpose**
|
| 364 |
+
|
| 365 |
+
Generate multiple intelligent layout options that maximize different objectives.
|
| 366 |
+
|
| 367 |
+
### **How It Works (Conceptual)**
|
| 368 |
+
|
| 369 |
+
**Phase 1: Initialization** \- Create 10 random layout candidates \- Each layout has 8 plots positioned randomly within boundary \- Each plot respects 50m setback rule
|
| 370 |
+
|
| 371 |
+
**Phase 2: Evaluation** \- Calculate fitness score for each layout \- Fitness \= (Profit Score × 0.5) \+ (Compliance Score × 0.3) \+ (Space Efficiency × 0.2) \- Profit \= total plot area (more area \= higher profit) \- Compliance \= 1.0 if all setback rules met, 0.8 if violated \- Space Efficiency \= (used area / total boundary area)
|
| 372 |
+
|
| 373 |
+
**Phase 3: Selection** \- Keep top 3 best performers (elitism) \- Discard bottom 7 layouts
|
| 374 |
+
|
| 375 |
+
**Phase 4: Reproduction** \- Create 7 new layouts from the elite 3 \- New layouts are mutations of elite layouts \- 30% of plots in each new layout are randomly repositioned
|
| 376 |
+
|
| 377 |
+
**Phase 5: Mutation** \- Randomly adjust plot positions (±30 meters) \- Small probability of adding/removing plots \- Ensures genetic diversity
|
| 378 |
+
|
| 379 |
+
**Phase 6: Repeat** \- Run phases 2-5 for 20 generations \- Track best solution from each generation \- Stop if improvement plateaus
|
| 380 |
+
|
| 381 |
+
**Result: Top 3 layouts with different strategies** \- Option 1: Maximum profit (most plots) \- Option 2: Balanced (medium plots, more space) \- Option 3: Premium (fewer plots, larger sizes)
|
| 382 |
+
|
| 383 |
+
### **Why Genetic Algorithm?**
|
| 384 |
+
|
| 385 |
+
| Approach | Pros | Cons | Used Here? |
|
| 386 |
+
| :---- | :---- | :---- | :---- |
|
| 387 |
+
| Random Search | Simple | Very slow (1000s of tries) | ❌ |
|
| 388 |
+
| Greedy Algorithm | Fast | Gets stuck in local optimum | ❌ |
|
| 389 |
+
| Simulated Annealing | Good for some problems | Limited diversity | ❌ |
|
| 390 |
+
| **Genetic Algorithm** | **Finds diverse solutions** | **Reasonable time** | ✅ |
|
| 391 |
+
| Linear Programming | Optimal solutions | Complex setup | ❌ |
|
| 392 |
+
|
| 393 |
+
### **Algorithm Parameters (Tuned for Demo)**
|
| 394 |
+
|
| 395 |
+
| Parameter | Value | Reason |
|
| 396 |
+
| :---- | :---- | :---- |
|
| 397 |
+
| Population Size | 10 | Balance speed vs. diversity |
|
| 398 |
+
| Generations | 20 | Enough iterations for convergence |
|
| 399 |
+
| Elite Size | 3 | Keep best performers |
|
| 400 |
+
| Mutation Rate | 0.3 (30%) | Enough randomness for diversity |
|
| 401 |
+
| Target Plots | 8 | Realistic for industrial estates |
|
| 402 |
+
| Setback Distance | 50m | Typical zoning requirement |
|
| 403 |
+
|
| 404 |
+
---
|
| 405 |
+
|
| 406 |
+
## **Component 4: AI Chat System**
|
| 407 |
+
|
| 408 |
+
### **Level 2: Hardcoded Responses**
|
| 409 |
+
|
| 410 |
+
**How it works:** 1\. User asks question 2\. Analyze question keywords 3\. Match to predefined category 4\. Return scripted response
|
| 411 |
+
|
| 412 |
+
**Categories:** \- Layout differences → Explain trade-offs \- Best option → Recommend based on fitness scores \- Compliance questions → Explain setback rules \- Metrics questions → Define each metric \- Algorithm questions → Explain GA process \- Default → Generic helpful response
|
| 413 |
+
|
| 414 |
+
**Advantages:** \- Fast response (no API latency) \- Completely free \- Predictable behavior \- Works offline
|
| 415 |
+
|
| 416 |
+
**Disadvantages:** \- Rigid responses (no true understanding) \- Limited conversational ability \- Can’t handle new question types
|
| 417 |
+
|
| 418 |
+
### **Level 2+: Real Gemini LLM**
|
| 419 |
+
|
| 420 |
+
**How it works:** 1\. User asks question 2\. Build context from current layouts 3\. Send to Google Gemini API 4\. Get intelligent response 5\. Return response to user 6\. Fall back to hardcoded if API fails
|
| 421 |
+
|
| 422 |
+
**Advantages:** \- Real AI understanding \- Handles unlimited question variations \- Context-aware responses \- Professional appearance
|
| 423 |
+
|
| 424 |
+
**Disadvantages:** \- API latency (1-2 seconds) \- Requires internet connection \- Rate-limited free tier \- Costs money at scale
|
| 425 |
+
|
| 426 |
+
**Gemini Choice Rationale:**
|
| 427 |
+
|
| 428 |
+
| Provider | Cost | Speed | Quality | Integration | Chosen? |
|
| 429 |
+
| :---- | :---- | :---- | :---- | :---- | :---- |
|
| 430 |
+
| OpenAI GPT-4 | $$$ (expensive) | Fast | Best | Simple | ❌ || Anthropic Claude | $$ (moderate) | Slower | Very Good | Simple | ❌ |
|
| 431 |
+
| **Google Gemini** | **FREE** | **Fast** | **Good** | **Simple** | ✅ |
|
| 432 |
+
| Open Source (LLaMA) | Free | Slow | OK | Complex | ❌ |
|
| 433 |
+
| Self-hosted LLM | Free | Slow | OK | Complex | ❌ |
|
| 434 |
+
|
| 435 |
+
**Why Gemini Flash 2.0 specifically:** \- Free tier: 15 requests/minute, 1.5M tokens/day \- Fast: \<1 second response time \- Latest: Up-to-date training (knowledge cutoff 2024\) \- Reliable: Google infrastructure \- Easy: Simple Python library
|
| 436 |
+
|
| 437 |
+
---
|
| 438 |
+
|
| 439 |
+
## **Component 5: CAD Export System**
|
| 440 |
+
|
| 441 |
+
### **Purpose**
|
| 442 |
+
|
| 443 |
+
Create professional, industry-standard CAD files for architects and planners.
|
| 444 |
+
|
| 445 |
+
### **DXF Format Choice Rationale**
|
| 446 |
+
|
| 447 |
+
| Format | Use Case | Pros | Cons | Chosen? |
|
| 448 |
+
| :---- | :---- | :---- | :---- | :---- |
|
| 449 |
+
| PDF | Drawings, reports | Universal | Not editable | ❌ |
|
| 450 |
+
| **DXF** | **CAD software** | **Universal, editable** | **Old format** | ✅ |
|
| 451 |
+
| SVG | Web graphics | Modern, scalable | Limited CAD support | ❌ |
|
| 452 |
+
| GeoJSON | Geospatial data | Standard, portable | Not CAD format | ❌ |
|
| 453 |
+
| AutoCAD DWG | Professional | Industry standard | Proprietary, paid | ❌ |
|
| 454 |
+
|
| 455 |
+
**DXF Advantages:** \- Open standard (40+ years old) \- Works with all CAD software (AutoCAD, LibreCAD, DraftSight, etc.) \- Works with free online viewers \- Professional appearance \- Contains all necessary information
|
| 456 |
+
|
| 457 |
+
### **DXF File Structure**
|
| 458 |
+
|
| 459 |
+
DXF Document
|
| 460 |
+
├─ Header (Version, units, etc.)
|
| 461 |
+
├─ Layers (Organizational hierarchy)
|
| 462 |
+
│ ├─ BOUNDARY (Site edge \- black, solid)
|
| 463 |
+
│ ├─ SETBACK (50m buffer zone \- red, dashed)
|
| 464 |
+
│ ├─ PLOTS (Individual plots \- cyan, solid)
|
| 465 |
+
│ ├─ LABELS (Plot names P1,P2,etc. \- white)
|
| 466 |
+
│ ├─ ANNOTATIONS (Area labels 1200m² \- yellow)
|
| 467 |
+
│ └─ TITLEBLOCK (Metadata \- black)
|
| 468 |
+
├─ Entities (Drawing elements)
|
| 469 |
+
│ ├─ Polylines (Plot boundaries)
|
| 470 |
+
│ ├─ Circles (Reference points)
|
| 471 |
+
│ ├─ Text (Labels and annotations)
|
| 472 |
+
│ └─ Lines (Grid, dimensions)
|
| 473 |
+
└─ Blocks (Reusable components)
|
| 474 |
+
|
| 475 |
+
### **Export Options**
|
| 476 |
+
|
| 477 |
+
**Option 1: Single Layout Export** \- Download individual DXF file \- Filename: option\_1\_20251204\_123456.dxf \- \~50-100KB file size \- Immediate download
|
| 478 |
+
|
| 479 |
+
**Option 2: All Layouts ZIP** \- Download ZIP containing 3 DXF files \- Filename: layouts\_20251204\_123456.zip \- \~150-300KB total \- Immediate download \- User extracts to get individual files
|
| 480 |
+
|
| 481 |
+
### **Why ezdxf Library?**
|
| 482 |
+
|
| 483 |
+
| Library | Purpose | Pros | Cons | Chosen? |
|
| 484 |
+
| :---- | :---- | :---- | :---- | :---- |
|
| 485 |
+
| **ezdxf** | **DXF creation** | **Complete, Python native** | **Not for plotting** | ✅ |
|
| 486 |
+
| pyDXF | DXF creation | Lightweight | Limited features | ❌ |
|
| 487 |
+
| CADQuery | CAD design | Parametric | Heavy (depends on OpenCASCADE) | ❌ |
|
| 488 |
+
| GDAL | Geospatial I/O | Comprehensive | Complex | ❌ |
|
| 489 |
+
| Inkscape | Vector graphics | GUI-based | Not programmable | ❌ |
|
| 490 |
+
|
| 491 |
+
**ezdxf Advantages:** \- Pure Python (no native dependencies) \- Complete DXF R2010 support \- Easy layer management \- Good performance \- Well-documented \- Active maintenance
|
| 492 |
+
|
| 493 |
+
---
|
| 494 |
+
|
| 495 |
+
# **🔄 COMPLETE WORKFLOW (USER PERSPECTIVE)**
|
| 496 |
+
|
| 497 |
+
## **User Journey \- Step by Step**
|
| 498 |
+
|
| 499 |
+
### **Step 1: Upload Site Boundary**
|
| 500 |
+
|
| 501 |
+
User Action: Click "Upload" button and select GeoJSON file
|
| 502 |
+
↓
|
| 503 |
+
Frontend: Read file using FileReader API
|
| 504 |
+
↓
|
| 505 |
+
Frontend: Send to backend /api/upload-boundary endpoint
|
| 506 |
+
↓
|
| 507 |
+
Backend: Parse GeoJSON
|
| 508 |
+
\- Extract coordinates
|
| 509 |
+
\- Create Shapely Polygon
|
| 510 |
+
\- Validate geometry (is\_valid check)
|
| 511 |
+
\- Create session with UUID
|
| 512 |
+
\- Store in memory
|
| 513 |
+
↓
|
| 514 |
+
Backend: Return session\_id \+ boundary coordinates \+ metadata
|
| 515 |
+
↓
|
| 516 |
+
Frontend: Store session\_id in React state
|
| 517 |
+
↓
|
| 518 |
+
Frontend: Extract boundary coordinates
|
| 519 |
+
↓
|
| 520 |
+
Frontend: Render on 2D canvas using Konva
|
| 521 |
+
\- Create Polygon shape
|
| 522 |
+
\- Set scale to fit canvas
|
| 523 |
+
\- Render with black line (1px)
|
| 524 |
+
\- Add reference grid
|
| 525 |
+
↓
|
| 526 |
+
User Sees: 2D plot of site boundary with dimensions (area, perimeter)
|
| 527 |
+
|
| 528 |
+
### **Step 2: Generate Optimized Layouts**
|
| 529 |
+
|
| 530 |
+
User Action: Click "Generate Layouts" button
|
| 531 |
+
↓
|
| 532 |
+
Frontend: Make POST request to /api/generate-layouts with session\_id
|
| 533 |
+
↓
|
| 534 |
+
Backend: Retrieve session using session\_id
|
| 535 |
+
↓
|
| 536 |
+
Backend: Initialize Genetic Algorithm
|
| 537 |
+
\- Create population of 10 random layouts
|
| 538 |
+
\- Each layout has 8 plots
|
| 539 |
+
\- Each plot respects 50m setback
|
| 540 |
+
↓
|
| 541 |
+
Backend: Run GA evolution loop (20 generations)
|
| 542 |
+
For each generation:
|
| 543 |
+
1\. Evaluate fitness of all 10 layouts
|
| 544 |
+
2\. Select top 3 (elitism)
|
| 545 |
+
3\. Create 7 new layouts from elite (mutation)
|
| 546 |
+
4\. Replace population
|
| 547 |
+
↓
|
| 548 |
+
Backend: Extract top 3 final layouts
|
| 549 |
+
↓
|
| 550 |
+
Backend: Calculate metrics for each layout
|
| 551 |
+
\- Total plots count
|
| 552 |
+
\- Total area (sum of plot areas)
|
| 553 |
+
\- Average plot size
|
| 554 |
+
\- Fitness score
|
| 555 |
+
↓
|
| 556 |
+
Backend: Return options array with plot data \+ metrics
|
| 557 |
+
↓
|
| 558 |
+
Frontend: Receive options data
|
| 559 |
+
↓
|
| 560 |
+
Frontend: Render 3 option cards
|
| 561 |
+
\- Each card shows option name (Option 1/2/3)
|
| 562 |
+
\- Display icon (💰/⚖️/🏢)
|
| 563 |
+
\- Show metrics (plots, area, avg, fitness)
|
| 564 |
+
\- Show compliance status (PASS)
|
| 565 |
+
↓
|
| 566 |
+
User Sees: 3 layout options with different characteristics
|
| 567 |
+
|
| 568 |
+
### **Step 3: Ask Chat Questions**
|
| 569 |
+
|
| 570 |
+
User Action: Type question in chat input, press Enter
|
| 571 |
+
↓
|
| 572 |
+
Frontend: Add user message to message history
|
| 573 |
+
↓
|
| 574 |
+
Frontend: Send POST to /api/chat with session\_id \+ message
|
| 575 |
+
↓
|
| 576 |
+
Backend: Receive question \+ session data
|
| 577 |
+
↓
|
| 578 |
+
Backend: Check if Gemini API available
|
| 579 |
+
├─ YES: Call GeminiService
|
| 580 |
+
│ \- Build context from current layouts
|
| 581 |
+
│ \- Create prompt with system instructions
|
| 582 |
+
│ \- Send to Google Gemini API
|
| 583 |
+
│ \- Get response
|
| 584 |
+
│ \- Return with model="gemini-2.0-flash"
|
| 585 |
+
│
|
| 586 |
+
└─ NO: Use fallback ChatService
|
| 587 |
+
\- Analyze question keywords
|
| 588 |
+
\- Match to category
|
| 589 |
+
\- Return scripted response
|
| 590 |
+
\- Return with model="fallback"
|
| 591 |
+
↓
|
| 592 |
+
Frontend: Receive response \+ model type
|
| 593 |
+
↓
|
| 594 |
+
Frontend: Add assistant message to chat
|
| 595 |
+
↓
|
| 596 |
+
Frontend: Display model indicator badge
|
| 597 |
+
\- "🤖 Powered by Gemini" if real AI
|
| 598 |
+
\- "💬 Fallback Mode" if hardcoded
|
| 599 |
+
↓
|
| 600 |
+
Frontend: Auto-scroll to show latest message
|
| 601 |
+
↓
|
| 602 |
+
User Sees: AI response explaining the layouts
|
| 603 |
+
|
| 604 |
+
### **Step 4: Export to CAD**
|
| 605 |
+
|
| 606 |
+
User Action: Click "Option 1 DXF" button
|
| 607 |
+
↓
|
| 608 |
+
Frontend: Make POST to /api/export-dxf with session\_id \+ option\_id
|
| 609 |
+
↓
|
| 610 |
+
Backend: Retrieve layout from session
|
| 611 |
+
↓
|
| 612 |
+
Backend: Call DXFExportService
|
| 613 |
+
├─ Create new DXF document
|
| 614 |
+
├─ Setup layers (BOUNDARY, SETBACK, PLOTS, etc.)
|
| 615 |
+
├─ Draw site boundary polygon
|
| 616 |
+
├─ Draw 50m setback zone
|
| 617 |
+
├─ Draw each plot rectangle
|
| 618 |
+
├─ Add plot labels (P1, P2, etc.)
|
| 619 |
+
├─ Add area annotations
|
| 620 |
+
├─ Add title block with metadata
|
| 621 |
+
└─ Save to temporary file
|
| 622 |
+
↓
|
| 623 |
+
Backend: Stream DXF file to frontend as blob
|
| 624 |
+
↓
|
| 625 |
+
Frontend: Create blob from response
|
| 626 |
+
↓
|
| 627 |
+
Frontend: Create temporary download link
|
| 628 |
+
↓
|
| 629 |
+
Frontend: Trigger browser download
|
| 630 |
+
\- Filename: option\_1\_20251204\_123456.dxf
|
| 631 |
+
\- MIME type: application/x-autocad-dxf
|
| 632 |
+
↓
|
| 633 |
+
Browser: Downloads file to user's Downloads folder
|
| 634 |
+
↓
|
| 635 |
+
User Can: Open in AutoCAD, LibreCAD, or online viewers
|
| 636 |
+
|
| 637 |
+
### **Step 5: Export All as ZIP**
|
| 638 |
+
|
| 639 |
+
User Action: Click "Export All as ZIP" button
|
| 640 |
+
↓
|
| 641 |
+
Frontend: Make POST to /api/export-all-dxf with session\_id
|
| 642 |
+
↓
|
| 643 |
+
Backend: Get all 3 layouts from session
|
| 644 |
+
↓
|
| 645 |
+
Backend: For each layout:
|
| 646 |
+
\- Call DXFExportService
|
| 647 |
+
\- Generate DXF file
|
| 648 |
+
\- Add to ZIP archive
|
| 649 |
+
↓
|
| 650 |
+
Backend: Create ZIP file containing 3 DXF files
|
| 651 |
+
↓
|
| 652 |
+
Backend: Stream ZIP to frontend
|
| 653 |
+
↓
|
| 654 |
+
Frontend: Trigger browser download
|
| 655 |
+
\- Filename: layouts\_20251204\_123456.zip
|
| 656 |
+
\- MIME type: application/zip
|
| 657 |
+
↓
|
| 658 |
+
User Can: Unzip and open each DXF in CAD software
|
| 659 |
+
|
| 660 |
+
---
|
| 661 |
+
|
| 662 |
+
# **🛠️ COMPLETE TECHNOLOGY STACK**
|
| 663 |
+
|
| 664 |
+
## **Frontend Stack**
|
| 665 |
+
|
| 666 |
+
### **Core Framework**
|
| 667 |
+
|
| 668 |
+
* **React 18** \- UI library
|
| 669 |
+
|
| 670 |
+
* Component-based architecture
|
| 671 |
+
|
| 672 |
+
* Virtual DOM optimization
|
| 673 |
+
|
| 674 |
+
* Hooks for state management
|
| 675 |
+
|
| 676 |
+
* Functional components
|
| 677 |
+
|
| 678 |
+
* **TypeScript 5** \- Type safety
|
| 679 |
+
|
| 680 |
+
* Type checking at compile time
|
| 681 |
+
|
| 682 |
+
* Better IDE support
|
| 683 |
+
|
| 684 |
+
* Self-documenting
|
| 685 |
+
|
| 686 |
+
* Catches errors early
|
| 687 |
+
|
| 688 |
+
### **UI & Visualization**
|
| 689 |
+
|
| 690 |
+
* **Konva.js** \- 2D Canvas library
|
| 691 |
+
|
| 692 |
+
* Stage (canvas container)
|
| 693 |
+
|
| 694 |
+
* Layers (grouping elements)
|
| 695 |
+
|
| 696 |
+
* Shapes (Polygon, Rect, Text)
|
| 697 |
+
|
| 698 |
+
* Event handling
|
| 699 |
+
|
| 700 |
+
* Performance optimization
|
| 701 |
+
|
| 702 |
+
* **Lucide React** \- Icon library
|
| 703 |
+
|
| 704 |
+
* Upload, Download, Zap, MessageCircle icons
|
| 705 |
+
|
| 706 |
+
* Lightweight (SVG-based)
|
| 707 |
+
|
| 708 |
+
* Consistent styling
|
| 709 |
+
|
| 710 |
+
* **CSS/Styling**
|
| 711 |
+
|
| 712 |
+
* Inline styles (React style objects)
|
| 713 |
+
|
| 714 |
+
* Tailwind CSS (optional)
|
| 715 |
+
|
| 716 |
+
* CSS Flexbox/Grid for layout
|
| 717 |
+
|
| 718 |
+
* Responsive design media queries
|
| 719 |
+
|
| 720 |
+
### **Data & Communication**
|
| 721 |
+
|
| 722 |
+
* **Axios** \- HTTP client
|
| 723 |
+
|
| 724 |
+
* REST API calls
|
| 725 |
+
|
| 726 |
+
* Request/response handling
|
| 727 |
+
|
| 728 |
+
* Error handling
|
| 729 |
+
|
| 730 |
+
* Request interceptors
|
| 731 |
+
|
| 732 |
+
* **React Hooks** \- State management
|
| 733 |
+
|
| 734 |
+
* useState (component state)
|
| 735 |
+
|
| 736 |
+
* useEffect (side effects)
|
| 737 |
+
|
| 738 |
+
* useRef (direct DOM access)
|
| 739 |
+
|
| 740 |
+
* useCallback (memoization)
|
| 741 |
+
|
| 742 |
+
### **Build & Development**
|
| 743 |
+
|
| 744 |
+
* **Create React App** \- Build tool
|
| 745 |
+
|
| 746 |
+
* Webpack configuration
|
| 747 |
+
|
| 748 |
+
* Babel transpiling
|
| 749 |
+
|
| 750 |
+
* Development server
|
| 751 |
+
|
| 752 |
+
* Production optimization
|
| 753 |
+
|
| 754 |
+
* **npm** \- Package manager
|
| 755 |
+
|
| 756 |
+
* Dependency management
|
| 757 |
+
|
| 758 |
+
* Version control
|
| 759 |
+
|
| 760 |
+
* Scripts execution
|
| 761 |
+
|
| 762 |
+
### **Browser APIs Used**
|
| 763 |
+
|
| 764 |
+
* **FileReader API** \- File upload handling
|
| 765 |
+
|
| 766 |
+
* **Fetch API** / Axios \- HTTP requests
|
| 767 |
+
|
| 768 |
+
* **Blob API** \- File downloads
|
| 769 |
+
|
| 770 |
+
* **LocalStorage** \- Session persistence (optional)
|
| 771 |
+
|
| 772 |
+
---
|
| 773 |
+
|
| 774 |
+
## **Backend Stack**
|
| 775 |
+
|
| 776 |
+
### **Core Framework**
|
| 777 |
+
|
| 778 |
+
* **FastAPI** \- Web framework
|
| 779 |
+
|
| 780 |
+
* ASGI (async support)
|
| 781 |
+
|
| 782 |
+
* Automatic API documentation
|
| 783 |
+
|
| 784 |
+
* Request validation (Pydantic)
|
| 785 |
+
|
| 786 |
+
* Type hints integration
|
| 787 |
+
|
| 788 |
+
* Middleware support
|
| 789 |
+
|
| 790 |
+
* **Python 3.8+** \- Language
|
| 791 |
+
|
| 792 |
+
* Type hints
|
| 793 |
+
|
| 794 |
+
* Async/await support
|
| 795 |
+
|
| 796 |
+
* Rich ecosystem
|
| 797 |
+
|
| 798 |
+
* Easy deployment
|
| 799 |
+
|
| 800 |
+
### **Geospatial & Geometry**
|
| 801 |
+
|
| 802 |
+
* **Shapely** \- Geometry operations
|
| 803 |
+
|
| 804 |
+
* Polygon creation from coordinates
|
| 805 |
+
|
| 806 |
+
* Buffer operations (setback zones)
|
| 807 |
+
|
| 808 |
+
* Geometry validation
|
| 809 |
+
|
| 810 |
+
* Intersection/containment checks
|
| 811 |
+
|
| 812 |
+
* Distance calculations
|
| 813 |
+
|
| 814 |
+
* **NumPy** \- Numerical computing
|
| 815 |
+
|
| 816 |
+
* Array operations
|
| 817 |
+
|
| 818 |
+
* Mathematical functions
|
| 819 |
+
|
| 820 |
+
* Random number generation
|
| 821 |
+
|
| 822 |
+
* Statistical calculations
|
| 823 |
+
|
| 824 |
+
* **GeoJSON** \- Data format
|
| 825 |
+
|
| 826 |
+
* Standard geospatial format
|
| 827 |
+
|
| 828 |
+
* JSON-based
|
| 829 |
+
|
| 830 |
+
* Supported by most GIS tools
|
| 831 |
+
|
| 832 |
+
* Web-friendly
|
| 833 |
+
|
| 834 |
+
### **Optimization**
|
| 835 |
+
|
| 836 |
+
* **Genetic Algorithm** (custom implementation)
|
| 837 |
+
|
| 838 |
+
* Population management
|
| 839 |
+
|
| 840 |
+
* Fitness calculation
|
| 841 |
+
|
| 842 |
+
* Selection operators
|
| 843 |
+
|
| 844 |
+
* Crossover/mutation
|
| 845 |
+
|
| 846 |
+
* Convergence detection
|
| 847 |
+
|
| 848 |
+
### **AI & LLM**
|
| 849 |
+
|
| 850 |
+
* **google-generativeai** \- Gemini API client
|
| 851 |
+
|
| 852 |
+
* Chat completion
|
| 853 |
+
|
| 854 |
+
* Context window management
|
| 855 |
+
|
| 856 |
+
* Token counting
|
| 857 |
+
|
| 858 |
+
* Error handling
|
| 859 |
+
|
| 860 |
+
### **CAD & Export**
|
| 861 |
+
|
| 862 |
+
* **ezdxf** \- DXF file creation
|
| 863 |
+
|
| 864 |
+
* Document creation
|
| 865 |
+
|
| 866 |
+
* Layer management
|
| 867 |
+
|
| 868 |
+
* Entity creation (polylines, text)
|
| 869 |
+
|
| 870 |
+
* Attributes and styling
|
| 871 |
+
|
| 872 |
+
* File output
|
| 873 |
+
|
| 874 |
+
### **Utilities**
|
| 875 |
+
|
| 876 |
+
* **python-multipart** \- File upload handling
|
| 877 |
+
|
| 878 |
+
* **python-dotenv** \- Environment variables (.env)
|
| 879 |
+
|
| 880 |
+
* **uvicorn** \- ASGI server
|
| 881 |
+
|
| 882 |
+
* Production-ready
|
| 883 |
+
|
| 884 |
+
* Hot reload (development)
|
| 885 |
+
|
| 886 |
+
* Multiple worker support
|
| 887 |
+
|
| 888 |
+
### **Package Management**
|
| 889 |
+
|
| 890 |
+
* **pip** \- Python package manager
|
| 891 |
+
|
| 892 |
+
* **requirements.txt** \- Dependency specification
|
| 893 |
+
|
| 894 |
+
* **Virtual environment** \- Isolation
|
| 895 |
+
|
| 896 |
+
---
|
| 897 |
+
|
| 898 |
+
## **DevOps & Infrastructure**
|
| 899 |
+
|
| 900 |
+
### **Development**
|
| 901 |
+
|
| 902 |
+
* **Local Development Server**
|
| 903 |
+
|
| 904 |
+
* Backend: uvicorn (localhost:8000)
|
| 905 |
+
|
| 906 |
+
* Frontend: npm (localhost:3000)
|
| 907 |
+
|
| 908 |
+
* CORS enabled for local testing
|
| 909 |
+
|
| 910 |
+
### **Version Control**
|
| 911 |
+
|
| 912 |
+
* **Git** \- Source control
|
| 913 |
+
|
| 914 |
+
* Code tracking
|
| 915 |
+
|
| 916 |
+
* Collaboration
|
| 917 |
+
|
| 918 |
+
* Version history
|
| 919 |
+
|
| 920 |
+
* **GitHub** \- Repository hosting
|
| 921 |
+
|
| 922 |
+
* Remote backup
|
| 923 |
+
|
| 924 |
+
* CI/CD integration
|
| 925 |
+
|
| 926 |
+
* Collaboration features
|
| 927 |
+
|
| 928 |
+
### **Deployment Targets**
|
| 929 |
+
|
| 930 |
+
#### *Frontend Deployment*
|
| 931 |
+
|
| 932 |
+
* **Vercel** (recommended for React)
|
| 933 |
+
|
| 934 |
+
* Git integration
|
| 935 |
+
|
| 936 |
+
* Automatic deployments
|
| 937 |
+
|
| 938 |
+
* Global CDN
|
| 939 |
+
|
| 940 |
+
* Environment variables
|
| 941 |
+
|
| 942 |
+
* Free tier available
|
| 943 |
+
|
| 944 |
+
* **Netlify** (alternative)
|
| 945 |
+
|
| 946 |
+
* Similar features
|
| 947 |
+
|
| 948 |
+
* Lambda functions (optional)
|
| 949 |
+
|
| 950 |
+
* Form handling
|
| 951 |
+
|
| 952 |
+
#### *Backend Deployment*
|
| 953 |
+
|
| 954 |
+
* **Railway** (recommended for Python)
|
| 955 |
+
|
| 956 |
+
* Docker support
|
| 957 |
+
|
| 958 |
+
* Git integration
|
| 959 |
+
|
| 960 |
+
* Automatic deployments
|
| 961 |
+
|
| 962 |
+
* PostgreSQL addon available
|
| 963 |
+
|
| 964 |
+
* Free tier available
|
| 965 |
+
|
| 966 |
+
* **Heroku** (alternative)
|
| 967 |
+
|
| 968 |
+
* Python support
|
| 969 |
+
|
| 970 |
+
* Addons (database, etc.)
|
| 971 |
+
|
| 972 |
+
* Procfile configuration
|
| 973 |
+
|
| 974 |
+
* Paid only
|
| 975 |
+
|
| 976 |
+
* **AWS / Google Cloud / Azure**
|
| 977 |
+
|
| 978 |
+
* More complex setup
|
| 979 |
+
|
| 980 |
+
* More control
|
| 981 |
+
|
| 982 |
+
* Pay-as-you-go pricing
|
| 983 |
+
|
| 984 |
+
* Enterprise scale
|
| 985 |
+
|
| 986 |
+
### **Database (Future Enhancement)**
|
| 987 |
+
|
| 988 |
+
* **PostgreSQL** \- Relational database
|
| 989 |
+
|
| 990 |
+
* Project persistence
|
| 991 |
+
|
| 992 |
+
* User data storage
|
| 993 |
+
|
| 994 |
+
* PostGIS extension (geospatial queries)
|
| 995 |
+
|
| 996 |
+
* **Redis** \- Caching (optional)
|
| 997 |
+
|
| 998 |
+
* Session caching
|
| 999 |
+
|
| 1000 |
+
* Job queue
|
| 1001 |
+
|
| 1002 |
+
* Rate limiting
|
| 1003 |
+
|
| 1004 |
+
---
|
| 1005 |
+
|
| 1006 |
+
# **📋 SYSTEM REQUIREMENTS & SPECIFICATIONS**
|
| 1007 |
+
|
| 1008 |
+
## **Frontend Requirements**
|
| 1009 |
+
|
| 1010 |
+
### **Browser Compatibility**
|
| 1011 |
+
|
| 1012 |
+
* Chrome 90+
|
| 1013 |
+
|
| 1014 |
+
* Firefox 88+
|
| 1015 |
+
|
| 1016 |
+
* Safari 14+
|
| 1017 |
+
|
| 1018 |
+
* Edge 90+
|
| 1019 |
+
|
| 1020 |
+
### **Minimum System Specs**
|
| 1021 |
+
|
| 1022 |
+
* 1GB RAM
|
| 1023 |
+
|
| 1024 |
+
* Modern CPU (2010+)
|
| 1025 |
+
|
| 1026 |
+
* 50MB disk space
|
| 1027 |
+
|
| 1028 |
+
* Broadband internet (2+ Mbps)
|
| 1029 |
+
|
| 1030 |
+
### **Screen Resolutions Supported**
|
| 1031 |
+
|
| 1032 |
+
* Desktop: 1024x768 minimum (1920x1080 optimal)
|
| 1033 |
+
|
| 1034 |
+
* Tablet: 768x1024 minimum
|
| 1035 |
+
|
| 1036 |
+
* Mobile: 320x480 (basic support)
|
| 1037 |
+
|
| 1038 |
+
### **Network Requirements**
|
| 1039 |
+
|
| 1040 |
+
* HTTPS for production
|
| 1041 |
+
|
| 1042 |
+
* CORS enabled
|
| 1043 |
+
|
| 1044 |
+
* WebSocket support (optional, for future features)
|
| 1045 |
+
|
| 1046 |
+
---
|
| 1047 |
+
|
| 1048 |
+
## **Backend Requirements**
|
| 1049 |
+
|
| 1050 |
+
### **Server Specs (Minimum)**
|
| 1051 |
+
|
| 1052 |
+
* **CPU**: 1 core
|
| 1053 |
+
|
| 1054 |
+
* **RAM**: 512MB
|
| 1055 |
+
|
| 1056 |
+
* **Disk**: 2GB
|
| 1057 |
+
|
| 1058 |
+
* **Network**: Broadband (10+ Mbps)
|
| 1059 |
+
|
| 1060 |
+
### **Python Version**
|
| 1061 |
+
|
| 1062 |
+
* 3.8+ required
|
| 1063 |
+
|
| 1064 |
+
* 3.10+ recommended
|
| 1065 |
+
|
| 1066 |
+
### **Operating System**
|
| 1067 |
+
|
| 1068 |
+
* Linux (production)
|
| 1069 |
+
|
| 1070 |
+
* macOS (development)
|
| 1071 |
+
|
| 1072 |
+
* Windows 10+ (development)
|
| 1073 |
+
|
| 1074 |
+
### **Dependencies**
|
| 1075 |
+
|
| 1076 |
+
* FastAPI
|
| 1077 |
+
|
| 1078 |
+
* Shapely
|
| 1079 |
+
|
| 1080 |
+
* NumPy
|
| 1081 |
+
|
| 1082 |
+
* google-generativeai
|
| 1083 |
+
|
| 1084 |
+
* ezdxf
|
| 1085 |
+
|
| 1086 |
+
* python-multipart
|
| 1087 |
+
|
| 1088 |
+
* python-dotenv
|
| 1089 |
+
|
| 1090 |
+
* uvicorn
|
| 1091 |
+
|
| 1092 |
+
---
|
| 1093 |
+
|
| 1094 |
+
# **🔐 SECURITY CONSIDERATIONS**
|
| 1095 |
+
|
| 1096 |
+
## **Frontend Security**
|
| 1097 |
+
|
| 1098 |
+
### **File Upload Security**
|
| 1099 |
+
|
| 1100 |
+
* Validate file type (only .geojson, .json)
|
| 1101 |
+
|
| 1102 |
+
* Limit file size (5MB maximum)
|
| 1103 |
+
|
| 1104 |
+
* No executable file types
|
| 1105 |
+
|
| 1106 |
+
* Scan for malicious content (optional)
|
| 1107 |
+
|
| 1108 |
+
### **API Communication**
|
| 1109 |
+
|
| 1110 |
+
* Use HTTPS only
|
| 1111 |
+
|
| 1112 |
+
* CORS validation
|
| 1113 |
+
|
| 1114 |
+
* Input validation before sending
|
| 1115 |
+
|
| 1116 |
+
* Sanitize displayed content
|
| 1117 |
+
|
| 1118 |
+
### **Data Privacy**
|
| 1119 |
+
|
| 1120 |
+
* No sensitive data stored locally
|
| 1121 |
+
|
| 1122 |
+
* Use httpOnly cookies (if session tokens used)
|
| 1123 |
+
|
| 1124 |
+
* Clear session on logout
|
| 1125 |
+
|
| 1126 |
+
* Implement CSP headers
|
| 1127 |
+
|
| 1128 |
+
---
|
| 1129 |
+
|
| 1130 |
+
## **Backend Security**
|
| 1131 |
+
|
| 1132 |
+
### **Input Validation**
|
| 1133 |
+
|
| 1134 |
+
* Validate GeoJSON format
|
| 1135 |
+
|
| 1136 |
+
* Check coordinate bounds
|
| 1137 |
+
|
| 1138 |
+
* Validate session IDs
|
| 1139 |
+
|
| 1140 |
+
* Check file paths (prevent directory traversal)
|
| 1141 |
+
|
| 1142 |
+
### **API Security**
|
| 1143 |
+
|
| 1144 |
+
* Rate limiting (to prevent abuse)
|
| 1145 |
+
|
| 1146 |
+
* CORS restrictions (whitelist allowed origins)
|
| 1147 |
+
|
| 1148 |
+
* Input sanitization
|
| 1149 |
+
|
| 1150 |
+
* Error handling (no sensitive info in errors)
|
| 1151 |
+
|
| 1152 |
+
### **Authentication (Future)**
|
| 1153 |
+
|
| 1154 |
+
* API keys for external access
|
| 1155 |
+
|
| 1156 |
+
* User authentication (OAuth2/JWT)
|
| 1157 |
+
|
| 1158 |
+
* Role-based access control
|
| 1159 |
+
|
| 1160 |
+
* Audit logging
|
| 1161 |
+
|
| 1162 |
+
### **AI API Security**
|
| 1163 |
+
|
| 1164 |
+
* Store GEMINI\_API\_KEY in environment variables
|
| 1165 |
+
|
| 1166 |
+
* Never commit keys to Git
|
| 1167 |
+
|
| 1168 |
+
* Rotate keys periodically
|
| 1169 |
+
|
| 1170 |
+
* Monitor API usage
|
| 1171 |
+
|
| 1172 |
+
* Set spending limits
|
| 1173 |
+
|
| 1174 |
+
### **File Handling**
|
| 1175 |
+
|
| 1176 |
+
* Validate DXF file paths
|
| 1177 |
+
|
| 1178 |
+
* Use secure temporary directories
|
| 1179 |
+
|
| 1180 |
+
* Auto-delete old export files
|
| 1181 |
+
|
| 1182 |
+
* Limit export directory size
|
| 1183 |
+
|
| 1184 |
+
---
|
| 1185 |
+
|
| 1186 |
+
# **⚡ PERFORMANCE OPTIMIZATION STRATEGIES**
|
| 1187 |
+
|
| 1188 |
+
## **Frontend Optimization**
|
| 1189 |
+
|
| 1190 |
+
### **Code Splitting**
|
| 1191 |
+
|
| 1192 |
+
* Lazy load components
|
| 1193 |
+
|
| 1194 |
+
* Code splitting by route
|
| 1195 |
+
|
| 1196 |
+
* Dynamic imports for heavy libraries
|
| 1197 |
+
|
| 1198 |
+
### **Asset Optimization**
|
| 1199 |
+
|
| 1200 |
+
* Minify JavaScript/CSS
|
| 1201 |
+
|
| 1202 |
+
* Compress images
|
| 1203 |
+
|
| 1204 |
+
* Use WebP format
|
| 1205 |
+
|
| 1206 |
+
* Cache static assets
|
| 1207 |
+
|
| 1208 |
+
### **Rendering Optimization**
|
| 1209 |
+
|
| 1210 |
+
* Memoize expensive components
|
| 1211 |
+
|
| 1212 |
+
* Virtual scrolling for large lists
|
| 1213 |
+
|
| 1214 |
+
* Debounce resize events
|
| 1215 |
+
|
| 1216 |
+
* Optimize Konva rendering
|
| 1217 |
+
|
| 1218 |
+
### **Bundle Size**
|
| 1219 |
+
|
| 1220 |
+
* Tree-shaking unused code
|
| 1221 |
+
|
| 1222 |
+
* Remove development dependencies
|
| 1223 |
+
|
| 1224 |
+
* Use production builds
|
| 1225 |
+
|
| 1226 |
+
* Monitor with webpack-bundle-analyzer
|
| 1227 |
+
|
| 1228 |
+
---
|
| 1229 |
+
|
| 1230 |
+
## **Backend Optimization**
|
| 1231 |
+
|
| 1232 |
+
### **Algorithm Optimization**
|
| 1233 |
+
|
| 1234 |
+
* GA parameters tuned for performance
|
| 1235 |
+
|
| 1236 |
+
* Early termination if converged
|
| 1237 |
+
|
| 1238 |
+
* Parallel population evaluation (optional)
|
| 1239 |
+
|
| 1240 |
+
* Caching of fitness calculations
|
| 1241 |
+
|
| 1242 |
+
### **API Optimization**
|
| 1243 |
+
|
| 1244 |
+
* Pagination for large responses
|
| 1245 |
+
|
| 1246 |
+
* Compression (gzip)
|
| 1247 |
+
|
| 1248 |
+
* Caching headers
|
| 1249 |
+
|
| 1250 |
+
* Connection pooling
|
| 1251 |
+
|
| 1252 |
+
### **Memory Optimization**
|
| 1253 |
+
|
| 1254 |
+
* Session cleanup (remove old sessions)
|
| 1255 |
+
|
| 1256 |
+
* Stream large file downloads
|
| 1257 |
+
|
| 1258 |
+
* Limit file size
|
| 1259 |
+
|
| 1260 |
+
* Garbage collection tuning
|
| 1261 |
+
|
| 1262 |
+
### **Database Optimization (Future)**
|
| 1263 |
+
|
| 1264 |
+
* Indexes on frequently queried fields
|
| 1265 |
+
|
| 1266 |
+
* Query optimization
|
| 1267 |
+
|
| 1268 |
+
* Connection pooling
|
| 1269 |
+
|
| 1270 |
+
* Replication for redundancy
|
| 1271 |
+
|
| 1272 |
+
---
|
| 1273 |
+
|
| 1274 |
+
# **🔄 DATA FLOW & STATE MANAGEMENT**
|
| 1275 |
+
|
| 1276 |
+
## **Frontend State Management**
|
| 1277 |
+
|
| 1278 |
+
### **React State Hierarchy**
|
| 1279 |
+
|
| 1280 |
+
App (Root)
|
| 1281 |
+
├─ sessionId (string, UUID)
|
| 1282 |
+
├─ boundary (GeoJSON polygon)
|
| 1283 |
+
├─ options (array of layout options)
|
| 1284 |
+
├─ siteMetadata (object: area, perimeter)
|
| 1285 |
+
├─ messages (array of chat messages)
|
| 1286 |
+
├─ loading (boolean, for loading states)
|
| 1287 |
+
└─ errors (array of error messages)
|
| 1288 |
+
|
| 1289 |
+
### **State Updates**
|
| 1290 |
+
|
| 1291 |
+
User Action
|
| 1292 |
+
↓
|
| 1293 |
+
Event Handler (onClick, onChange, etc.)
|
| 1294 |
+
↓
|
| 1295 |
+
Call setState or useReducer
|
| 1296 |
+
↓
|
| 1297 |
+
Trigger re-render of affected components
|
| 1298 |
+
↓
|
| 1299 |
+
Virtual DOM diff
|
| 1300 |
+
↓
|
| 1301 |
+
Update actual DOM
|
| 1302 |
+
↓
|
| 1303 |
+
Display changes to user
|
| 1304 |
+
|
| 1305 |
+
---
|
| 1306 |
+
|
| 1307 |
+
## **Backend Session Management**
|
| 1308 |
+
|
| 1309 |
+
### **Session Lifecycle**
|
| 1310 |
+
|
| 1311 |
+
User Uploads File
|
| 1312 |
+
↓
|
| 1313 |
+
Backend creates Session object
|
| 1314 |
+
\- Generate UUID
|
| 1315 |
+
\- Store in memory dictionary
|
| 1316 |
+
\- Initialize with empty data
|
| 1317 |
+
↓
|
| 1318 |
+
Return session\_id to frontend
|
| 1319 |
+
↓
|
| 1320 |
+
Frontend stores session\_id in state
|
| 1321 |
+
↓
|
| 1322 |
+
All subsequent requests include session\_id
|
| 1323 |
+
↓
|
| 1324 |
+
Backend retrieves session from dictionary
|
| 1325 |
+
↓
|
| 1326 |
+
Add/update session data (layouts, metadata)
|
| 1327 |
+
↓
|
| 1328 |
+
Session remains available for 24 hours (optional cleanup)
|
| 1329 |
+
↓
|
| 1330 |
+
User closes browser/session expires
|
| 1331 |
+
↓
|
| 1332 |
+
Backend periodically cleans up old sessions
|
| 1333 |
+
|
| 1334 |
+
---
|
| 1335 |
+
|
| 1336 |
+
# **📊 INTEGRATION POINTS & DEPENDENCIES**
|
| 1337 |
+
|
| 1338 |
+
## **External Services**
|
| 1339 |
+
|
| 1340 |
+
### **Google Gemini API**
|
| 1341 |
+
|
| 1342 |
+
* **Purpose**: Real AI chat responses
|
| 1343 |
+
|
| 1344 |
+
* **Integration**: google-generativeai Python library
|
| 1345 |
+
|
| 1346 |
+
* **Authentication**: API key in environment variable
|
| 1347 |
+
|
| 1348 |
+
* **Rate Limits**: 15 requests/minute (free tier)
|
| 1349 |
+
|
| 1350 |
+
* **Fallback**: Use hardcoded responses if unavailable
|
| 1351 |
+
|
| 1352 |
+
### **GeoJSON Input**
|
| 1353 |
+
|
| 1354 |
+
* **Source**: User file upload
|
| 1355 |
+
|
| 1356 |
+
* **Format**: RFC 7946 standard
|
| 1357 |
+
|
| 1358 |
+
* **Validation**: Shapely geometry checks
|
| 1359 |
+
|
| 1360 |
+
* **Expected Data**: Polygon geometry (site boundary)
|
| 1361 |
+
|
| 1362 |
+
### **File System**
|
| 1363 |
+
|
| 1364 |
+
* **Purpose**: Store temporary export files
|
| 1365 |
+
|
| 1366 |
+
* **Location**: backend/exports/ directory
|
| 1367 |
+
|
| 1368 |
+
* **Cleanup**: Remove files older than 24 hours
|
| 1369 |
+
|
| 1370 |
+
* **Permissions**: Read/write/delete
|
| 1371 |
+
|
| 1372 |
+
---
|
| 1373 |
+
|
| 1374 |
+
# **📈 SCALABILITY & GROWTH PATH**
|
| 1375 |
+
|
| 1376 |
+
## **Current System (Single Server)**
|
| 1377 |
+
|
| 1378 |
+
Frontend (Vercel CDN)
|
| 1379 |
+
↓ HTTPS
|
| 1380 |
+
Backend (Single Railway container)
|
| 1381 |
+
└─ All processing
|
| 1382 |
+
└─ In-memory session storage
|
| 1383 |
+
└─ Temporary file storage
|
| 1384 |
+
|
| 1385 |
+
### **Limitations**
|
| 1386 |
+
|
| 1387 |
+
* \~100 concurrent sessions
|
| 1388 |
+
|
| 1389 |
+
* \~1000 requests/minute
|
| 1390 |
+
|
| 1391 |
+
* Data lost on restart
|
| 1392 |
+
|
| 1393 |
+
* No redundancy
|
| 1394 |
+
|
| 1395 |
+
---
|
| 1396 |
+
|
| 1397 |
+
## **Future: Scalable Architecture**
|
| 1398 |
+
|
| 1399 |
+
External
|
| 1400 |
+
Services
|
| 1401 |
+
(Gemini)
|
| 1402 |
+
↑
|
| 1403 |
+
User (Browser) │
|
| 1404 |
+
↓ HTTPS │
|
| 1405 |
+
└─ Vercel CDN ←──────────────────┬─┘
|
| 1406 |
+
↓
|
| 1407 |
+
Load Balancer
|
| 1408 |
+
↓
|
| 1409 |
+
┌───────────┼───────────┐
|
| 1410 |
+
↓ ↓ ↓
|
| 1411 |
+
Backend Backend Backend
|
| 1412 |
+
Container Container Container
|
| 1413 |
+
↓ ↓ ↓
|
| 1414 |
+
└───────────┼───────────┘
|
| 1415 |
+
↓
|
| 1416 |
+
PostgreSQL
|
| 1417 |
+
(Persistent)
|
| 1418 |
+
↓
|
| 1419 |
+
Redis Cache
|
| 1420 |
+
(Session, GA cache)
|
| 1421 |
+
|
| 1422 |
+
### **Improvements**
|
| 1423 |
+
|
| 1424 |
+
* Horizontal scaling (multiple backend containers)
|
| 1425 |
+
|
| 1426 |
+
* Database persistence (PostgreSQL)
|
| 1427 |
+
|
| 1428 |
+
* Session caching (Redis)
|
| 1429 |
+
|
| 1430 |
+
* Load balancing
|
| 1431 |
+
|
| 1432 |
+
* Monitoring & logging (Datadog, New Relic)
|
| 1433 |
+
|
| 1434 |
+
* CDN for static files
|
| 1435 |
+
|
| 1436 |
+
* API gateway
|
| 1437 |
+
|
| 1438 |
+
---
|
| 1439 |
+
|
| 1440 |
+
# **🎯 DEPLOYMENT STRATEGY**
|
| 1441 |
+
|
| 1442 |
+
## **Development Environment**
|
| 1443 |
+
|
| 1444 |
+
Local Machine
|
| 1445 |
+
├─ Backend: localhost:8000 (uvicorn \--reload)
|
| 1446 |
+
├─ Frontend: localhost:3000 (npm start)
|
| 1447 |
+
├─ CORS: Localhost only
|
| 1448 |
+
├─ Database: None (in-memory)
|
| 1449 |
+
└─ Logging: Console
|
| 1450 |
+
|
| 1451 |
+
## **Staging Environment**
|
| 1452 |
+
|
| 1453 |
+
Staging Server (Railway/AWS)
|
| 1454 |
+
├─ Backend: staging-api.aioptimize.com
|
| 1455 |
+
├─ Frontend: staging.aioptimize.com
|
| 1456 |
+
├─ CORS: Staging domain only
|
| 1457 |
+
├─ Database: PostgreSQL (optional)
|
| 1458 |
+
└─ Logging: Structured logging service
|
| 1459 |
+
|
| 1460 |
+
## **Production Environment**
|
| 1461 |
+
|
| 1462 |
+
Production Server (Railway/AWS/Google Cloud)
|
| 1463 |
+
├─ Backend: api.aioptimize.com
|
| 1464 |
+
├─ Frontend: app.aioptimize.com (Vercel CDN)
|
| 1465 |
+
├─ CORS: Production domains only
|
| 1466 |
+
├─ Database: PostgreSQL with backups
|
| 1467 |
+
├─ Logging: Enterprise logging (Datadog)
|
| 1468 |
+
├─ Monitoring: Performance monitoring
|
| 1469 |
+
├─ Alerting: Email/Slack notifications
|
| 1470 |
+
└─ Backup: Daily automated backups
|
| 1471 |
+
|
| 1472 |
+
---
|
| 1473 |
+
|
| 1474 |
+
# **📊 MONITORING & OBSERVABILITY**
|
| 1475 |
+
|
| 1476 |
+
## **Metrics to Track (Current)**
|
| 1477 |
+
|
| 1478 |
+
* API response times
|
| 1479 |
+
|
| 1480 |
+
* Error rates by endpoint
|
| 1481 |
+
|
| 1482 |
+
* Session count
|
| 1483 |
+
|
| 1484 |
+
* Gemini API latency
|
| 1485 |
+
|
| 1486 |
+
* File export success rate
|
| 1487 |
+
|
| 1488 |
+
* User download counts
|
| 1489 |
+
|
| 1490 |
+
## **Metrics to Track (Future)**
|
| 1491 |
+
|
| 1492 |
+
* User engagement
|
| 1493 |
+
|
| 1494 |
+
* Conversion funnel
|
| 1495 |
+
|
| 1496 |
+
* Cost per session
|
| 1497 |
+
|
| 1498 |
+
* GA optimization efficiency
|
| 1499 |
+
|
| 1500 |
+
* User satisfaction (feedback)
|
| 1501 |
+
|
| 1502 |
+
## **Logging Strategy**
|
| 1503 |
+
|
| 1504 |
+
* Info level: Major operations
|
| 1505 |
+
|
| 1506 |
+
* Warning level: Non-critical errors
|
| 1507 |
+
|
| 1508 |
+
* Error level: Critical failures
|
| 1509 |
+
|
| 1510 |
+
* Debug level: Development only
|
| 1511 |
+
|
| 1512 |
+
---
|
| 1513 |
+
|
| 1514 |
+
# **🔍 ERROR HANDLING & RESILIENCE**
|
| 1515 |
+
|
| 1516 |
+
## **Error Categories & Handling**
|
| 1517 |
+
|
| 1518 |
+
### **User Input Errors**
|
| 1519 |
+
|
| 1520 |
+
* Invalid GeoJSON → User-friendly message
|
| 1521 |
+
|
| 1522 |
+
* Missing file → Prompt to upload
|
| 1523 |
+
|
| 1524 |
+
* Invalid coordinates → Suggest bounds
|
| 1525 |
+
|
| 1526 |
+
### **API Errors**
|
| 1527 |
+
|
| 1528 |
+
* Gemini API timeout → Use fallback chat
|
| 1529 |
+
|
| 1530 |
+
* Rate limit exceeded → Queue message or inform user
|
| 1531 |
+
|
| 1532 |
+
* Network error → Retry with exponential backoff
|
| 1533 |
+
|
| 1534 |
+
### **System Errors**
|
| 1535 |
+
|
| 1536 |
+
* Out of memory → Reject large file
|
| 1537 |
+
|
| 1538 |
+
* File system full → Clean up old exports
|
| 1539 |
+
|
| 1540 |
+
* Database connection → Use in-memory fallback
|
| 1541 |
+
|
| 1542 |
+
### **Graceful Degradation**
|
| 1543 |
+
|
| 1544 |
+
Gemini AI Available?
|
| 1545 |
+
├─ YES → Use real AI
|
| 1546 |
+
└─ NO → Use hardcoded responses (system still works)
|
| 1547 |
+
|
| 1548 |
+
DXF Export Available?
|
| 1549 |
+
├─ YES → Generate professional CAD
|
| 1550 |
+
└─ NO → Return JSON alternative
|
| 1551 |
+
|
| 1552 |
+
Database Available?
|
| 1553 |
+
├─ YES → Persist to database
|
| 1554 |
+
└─ NO → Use in-memory storage
|
| 1555 |
+
|
| 1556 |
+
---
|
| 1557 |
+
|
| 1558 |
+
# **✅ QUALITY ASSURANCE STRATEGY**
|
| 1559 |
+
|
| 1560 |
+
## **Testing Levels**
|
| 1561 |
+
|
| 1562 |
+
### **Unit Testing**
|
| 1563 |
+
|
| 1564 |
+
* Test individual functions
|
| 1565 |
+
|
| 1566 |
+
* Test geometry operations
|
| 1567 |
+
|
| 1568 |
+
* Test GA fitness calculations
|
| 1569 |
+
|
| 1570 |
+
* Test response generation logic
|
| 1571 |
+
|
| 1572 |
+
### **Integration Testing**
|
| 1573 |
+
|
| 1574 |
+
* Test API endpoints
|
| 1575 |
+
|
| 1576 |
+
* Test frontend-backend communication
|
| 1577 |
+
|
| 1578 |
+
* Test file upload flow
|
| 1579 |
+
|
| 1580 |
+
* Test export generation
|
| 1581 |
+
|
| 1582 |
+
### **End-to-End Testing**
|
| 1583 |
+
|
| 1584 |
+
* Complete user workflows
|
| 1585 |
+
|
| 1586 |
+
* Multi-step scenarios
|
| 1587 |
+
|
| 1588 |
+
* Error recovery
|
| 1589 |
+
|
| 1590 |
+
* Performance under load
|
| 1591 |
+
|
| 1592 |
+
### **Performance Testing**
|
| 1593 |
+
|
| 1594 |
+
* Load testing (concurrent users)
|
| 1595 |
+
|
| 1596 |
+
* Stress testing (resource limits)
|
| 1597 |
+
|
| 1598 |
+
* Latency testing (response times)
|
| 1599 |
+
|
| 1600 |
+
* Scalability testing
|
| 1601 |
+
|
| 1602 |
+
---
|
| 1603 |
+
|
| 1604 |
+
# **📋 IMPLEMENTATION CHECKLIST**
|
| 1605 |
+
|
| 1606 |
+
## **Phase 1: Level 2 Smart Demo (12 hours)**
|
| 1607 |
+
|
| 1608 |
+
### **Frontend**
|
| 1609 |
+
|
| 1610 |
+
* ☐ React \+ TypeScript setup
|
| 1611 |
+
|
| 1612 |
+
* ☐ Component architecture designed
|
| 1613 |
+
|
| 1614 |
+
* ☐ File upload UI implemented
|
| 1615 |
+
|
| 1616 |
+
* ☐ 2D Konva canvas integrated
|
| 1617 |
+
|
| 1618 |
+
* ☐ Layout options display created
|
| 1619 |
+
|
| 1620 |
+
* ☐ Chat UI built
|
| 1621 |
+
|
| 1622 |
+
* ☐ Styling finalized
|
| 1623 |
+
|
| 1624 |
+
### **Backend**
|
| 1625 |
+
|
| 1626 |
+
* ☐ FastAPI project initialized
|
| 1627 |
+
|
| 1628 |
+
* ☐ Virtual environment created
|
| 1629 |
+
|
| 1630 |
+
* ☐ Dependencies installed
|
| 1631 |
+
|
| 1632 |
+
* ☐ API endpoints designed
|
| 1633 |
+
|
| 1634 |
+
* ☐ GeoJSON parsing implemented
|
| 1635 |
+
|
| 1636 |
+
* ☐ Genetic algorithm coded
|
| 1637 |
+
|
| 1638 |
+
* ☐ Chat logic implemented
|
| 1639 |
+
|
| 1640 |
+
* ☐ Error handling added
|
| 1641 |
+
|
| 1642 |
+
### **Integration**
|
| 1643 |
+
|
| 1644 |
+
* ☐ CORS enabled
|
| 1645 |
+
|
| 1646 |
+
* ☐ Frontend connects to backend
|
| 1647 |
+
|
| 1648 |
+
* ☐ File upload works end-to-end
|
| 1649 |
+
|
| 1650 |
+
* ☐ Layout generation works
|
| 1651 |
+
|
| 1652 |
+
* ☐ Chat responds
|
| 1653 |
+
|
| 1654 |
+
* ☐ No console errors
|
| 1655 |
+
|
| 1656 |
+
### **Testing**
|
| 1657 |
+
|
| 1658 |
+
* ☐ Manual workflow testing
|
| 1659 |
+
|
| 1660 |
+
* ☐ Error case testing
|
| 1661 |
+
|
| 1662 |
+
* ☐ Performance verified
|
| 1663 |
+
|
| 1664 |
+
* ☐ Browser compatibility checked
|
| 1665 |
+
|
| 1666 |
+
---
|
| 1667 |
+
|
| 1668 |
+
## **Phase 2: Level 2+ Enhancements (12 hours)**
|
| 1669 |
+
|
| 1670 |
+
### **Gemini Integration**
|
| 1671 |
+
|
| 1672 |
+
* ☐ API key obtained
|
| 1673 |
+
|
| 1674 |
+
* ☐ google-generativeai library installed
|
| 1675 |
+
|
| 1676 |
+
* ☐ GeminiService class created
|
| 1677 |
+
|
| 1678 |
+
* ☐ Context building implemented
|
| 1679 |
+
|
| 1680 |
+
* ☐ Fallback mechanism tested
|
| 1681 |
+
|
| 1682 |
+
* ☐ Badge indicator added
|
| 1683 |
+
|
| 1684 |
+
* ☐ Real responses verified
|
| 1685 |
+
|
| 1686 |
+
### **DXF Export**
|
| 1687 |
+
|
| 1688 |
+
* ☐ ezdxf library installed
|
| 1689 |
+
|
| 1690 |
+
* ☐ DXFExportService class created
|
| 1691 |
+
|
| 1692 |
+
* ☐ Layer setup implemented
|
| 1693 |
+
|
| 1694 |
+
* ☐ Geometry drawing implemented
|
| 1695 |
+
|
| 1696 |
+
* ☐ Title block added
|
| 1697 |
+
|
| 1698 |
+
* ☐ Export endpoints created
|
| 1699 |
+
|
| 1700 |
+
* ☐ UI buttons added
|
| 1701 |
+
|
| 1702 |
+
* ☐ Download mechanism tested
|
| 1703 |
+
|
| 1704 |
+
* ☐ ZIP creation implemented
|
| 1705 |
+
|
| 1706 |
+
* ☐ File opening verified (CAD software)
|
| 1707 |
+
|
| 1708 |
+
### **Final Testing**
|
| 1709 |
+
|
| 1710 |
+
* ☐ Both features working
|
| 1711 |
+
|
| 1712 |
+
* ☐ No console errors
|
| 1713 |
+
|
| 1714 |
+
* ☐ No backend errors
|
| 1715 |
+
|
| 1716 |
+
* ☐ Complete workflows tested
|
| 1717 |
+
|
| 1718 |
+
* ☐ Performance acceptable
|
| 1719 |
+
|
| 1720 |
+
* ☐ Documentation complete
|
| 1721 |
+
|
| 1722 |
+
* ☐ Code committed to Git
|
| 1723 |
+
|
| 1724 |
+
* ☐ Ready for production
|
| 1725 |
+
|
| 1726 |
+
---
|
| 1727 |
+
|
| 1728 |
+
# **🚀 DEPLOYMENT READINESS CHECKLIST**
|
| 1729 |
+
|
| 1730 |
+
## **Pre-Deployment**
|
| 1731 |
+
|
| 1732 |
+
* ☐ All tests passing
|
| 1733 |
+
|
| 1734 |
+
* ☐ Code reviewed
|
| 1735 |
+
|
| 1736 |
+
* ☐ Documentation complete
|
| 1737 |
+
|
| 1738 |
+
* ☐ Security audit done
|
| 1739 |
+
|
| 1740 |
+
* ☐ Performance baseline established
|
| 1741 |
+
|
| 1742 |
+
* ☐ Backup strategy defined
|
| 1743 |
+
|
| 1744 |
+
* ☐ Monitoring setup
|
| 1745 |
+
|
| 1746 |
+
* ☐ Alert rules defined
|
| 1747 |
+
|
| 1748 |
+
## **Deployment**
|
| 1749 |
+
|
| 1750 |
+
* ☐ Frontend deployed (Vercel)
|
| 1751 |
+
|
| 1752 |
+
* ☐ Backend deployed (Railway)
|
| 1753 |
+
|
| 1754 |
+
* ☐ Environment variables set
|
| 1755 |
+
|
| 1756 |
+
* ☐ API keys secured
|
| 1757 |
+
|
| 1758 |
+
* ☐ CORS configured
|
| 1759 |
+
|
| 1760 |
+
* ☐ HTTPS enforced
|
| 1761 |
+
|
| 1762 |
+
* ☐ DNS configured
|
| 1763 |
+
|
| 1764 |
+
## **Post-Deployment**
|
| 1765 |
+
|
| 1766 |
+
* ☐ Smoke tests pass
|
| 1767 |
+
|
| 1768 |
+
* ☐ Performance monitoring active
|
| 1769 |
+
|
| 1770 |
+
* ☐ User feedback collected
|
| 1771 |
+
|
| 1772 |
+
* ☐ Error tracking enabled
|
| 1773 |
+
|
| 1774 |
+
* ☐ Incident response plan ready
|
| 1775 |
+
|
| 1776 |
+
* ☐ Runbooks documented
|
| 1777 |
+
|
| 1778 |
+
---
|
| 1779 |
+
|
| 1780 |
+
**This architecture is battle-tested, production-ready, and designed for rapid iteration and scaling.**
|
| 1781 |
+
|
| 1782 |
+
**Everything is documented, realistic, and achievable.**
|
| 1783 |
+
|
docs/MVP-STATUS.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AIOptimize™ MVP - Final Status Report
|
| 2 |
+
|
| 3 |
+
## Tổng quan hoàn thành
|
| 4 |
+
|
| 5 |
+
| Component | Status | Details |
|
| 6 |
+
|-----------|--------|---------|
|
| 7 |
+
| **Backend API** | ✅ 100% | 10 endpoints |
|
| 8 |
+
| **Frontend UI** | ✅ 100% | React + Konva |
|
| 9 |
+
| **DXF Input** | ✅ NEW | Parse LWPOLYLINE |
|
| 10 |
+
| **DXF Output** | ✅ Fixed | Export layouts |
|
| 11 |
+
| **Gemini AI** | ✅ Ready | API key configured |
|
| 12 |
+
| **Zoom/Pan** | ✅ NEW | Mouse wheel + drag |
|
| 13 |
+
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
## Những gì đã tối ưu
|
| 17 |
+
|
| 18 |
+
### Frontend
|
| 19 |
+
1. **Map2DPlotter hoàn toàn mới**
|
| 20 |
+
- Zoom: mouse wheel + buttons
|
| 21 |
+
- Pan: kéo thả canvas
|
| 22 |
+
- Canvas size: 720x500px
|
| 23 |
+
- Transform coordinates chính xác
|
| 24 |
+
|
| 25 |
+
2. **Layout tối ưu**
|
| 26 |
+
- Left panel: `max-width: 800px`
|
| 27 |
+
- Map section chiếm phần lớn
|
| 28 |
+
- Controls: zoom in/out/reset
|
| 29 |
+
|
| 30 |
+
3. **CSS**
|
| 31 |
+
- Map controls styling
|
| 32 |
+
- Zoom level indicator
|
| 33 |
+
- Grab cursor khi drag
|
| 34 |
+
|
| 35 |
+
### Backend
|
| 36 |
+
1. **Endpoint mới `/api/upload-dxf`**
|
| 37 |
+
- Parse file DXF
|
| 38 |
+
- Extract LWPOLYLINE/POLYLINE
|
| 39 |
+
- Tự động chọn polygon lớn nhất
|
| 40 |
+
|
| 41 |
+
2. **Gemini API**
|
| 42 |
+
- API key từ `.env`
|
| 43 |
+
- Model: `gemini-2.0-flash-exp`
|
| 44 |
+
- Fallback responses nếu lỗi
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
## Files đã sửa
|
| 49 |
+
|
| 50 |
+
| File | Changes |
|
| 51 |
+
|------|---------|
|
| 52 |
+
| `src/api/mvp_api.py` | +100 lines (upload-dxf) |
|
| 53 |
+
| `frontend/src/components/Map2DPlotter.tsx` | Rewritten (zoom/pan) |
|
| 54 |
+
| `frontend/src/App.tsx` | Map size 720x500 |
|
| 55 |
+
| `frontend/src/App.css` | Map controls CSS |
|
| 56 |
+
| `frontend/src/services/api.ts` | DXF routing |
|
| 57 |
+
|
| 58 |
+
---
|
| 59 |
+
|
| 60 |
+
## Build Status
|
| 61 |
+
|
| 62 |
+
```
|
| 63 |
+
Frontend:
|
| 64 |
+
✓ 1818 modules transformed
|
| 65 |
+
✓ built in 4.83s
|
| 66 |
+
- index.js: 562KB (gzip: 178KB)
|
| 67 |
+
- index.css: 9.5KB (gzip: 2.5KB)
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
## Cách test
|
| 73 |
+
|
| 74 |
+
```bash
|
| 75 |
+
# Terminal 1 - Backend
|
| 76 |
+
cd d:\Workspace\Project\REMB
|
| 77 |
+
.\venv\Scripts\activate
|
| 78 |
+
uvicorn src.api.mvp_api:app --reload --port 8000
|
| 79 |
+
|
| 80 |
+
# Terminal 2 - Frontend
|
| 81 |
+
cd frontend
|
| 82 |
+
npm run dev
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
**Test DXF upload:**
|
| 86 |
+
1. Tạo file DXF với LWPOLYLINE
|
| 87 |
+
2. Upload qua UI
|
| 88 |
+
3. Verify boundary hiển thị
|
| 89 |
+
4. Generate layouts
|
| 90 |
+
5. Export DXF
|
| 91 |
+
|
| 92 |
+
---
|
| 93 |
+
|
| 94 |
+
## API Endpoints
|
| 95 |
+
|
| 96 |
+
| Method | Endpoint | Description |
|
| 97 |
+
|--------|----------|-------------|
|
| 98 |
+
| GET | `/api/health` | Health check |
|
| 99 |
+
| GET | `/api/sample-data` | Sample GeoJSON |
|
| 100 |
+
| POST | `/api/upload-dxf` | **NEW** DXF input |
|
| 101 |
+
| POST | `/api/upload-boundary-json` | JSON input |
|
| 102 |
+
| POST | `/api/generate-layouts` | GA optimizer |
|
| 103 |
+
| POST | `/api/chat` | Gemini AI |
|
| 104 |
+
| POST | `/api/export-dxf` | Single DXF |
|
| 105 |
+
| POST | `/api/export-all-dxf` | ZIP all layouts |
|
| 106 |
+
|
| 107 |
+
---
|
| 108 |
+
|
| 109 |
+
## Gemini API
|
| 110 |
+
|
| 111 |
+
```
|
| 112 |
+
Model: gemini-2.0-flash-exp
|
| 113 |
+
Key: Configured in .env
|
| 114 |
+
Status: Ready
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
Test chat với các câu hỏi:
|
| 118 |
+
- "Which option is best?"
|
| 119 |
+
- "Compare the layouts"
|
| 120 |
+
- "How does the algorithm work?"
|
docs/Proposal_ AI-Powered Industrial Estate Master Plan Optimization Engine.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
docs/Requirement.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Tóm tắt "Tech Stack" mã nguồn mở cho MVP khuyến nghị:
|
| 2 |
+
|
| 3 |
+
Ngôn ngữ: Python 3.10+
|
| 4 |
+
Tối ưu hóa (AI): Pymoo (NSGA-II) + Google OR-Tools (dùng solver HiGHS cho MILP).
|
| 5 |
+
Xử lý hình học: GeoPandas + Shapely.
|
| 6 |
+
Xuất bản vẽ: ezdxf.
|
| 7 |
+
Backend/API: FastAPI.
|
| 8 |
+
Database: PostgreSQL + PostGIS.
|
examples/api-cw750-details.dxf
ADDED
|
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
*.local
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.vscode/*
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.suo
|
| 21 |
+
*.ntvs*
|
| 22 |
+
*.njsproj
|
| 23 |
+
*.sln
|
| 24 |
+
*.sw?
|
frontend/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# React + TypeScript + Vite
|
| 2 |
+
|
| 3 |
+
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
| 4 |
+
|
| 5 |
+
Currently, two official plugins are available:
|
| 6 |
+
|
| 7 |
+
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
| 8 |
+
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
| 9 |
+
|
| 10 |
+
## React Compiler
|
| 11 |
+
|
| 12 |
+
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
| 13 |
+
|
| 14 |
+
## Expanding the ESLint configuration
|
| 15 |
+
|
| 16 |
+
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
| 17 |
+
|
| 18 |
+
```js
|
| 19 |
+
export default defineConfig([
|
| 20 |
+
globalIgnores(['dist']),
|
| 21 |
+
{
|
| 22 |
+
files: ['**/*.{ts,tsx}'],
|
| 23 |
+
extends: [
|
| 24 |
+
// Other configs...
|
| 25 |
+
|
| 26 |
+
// Remove tseslint.configs.recommended and replace with this
|
| 27 |
+
tseslint.configs.recommendedTypeChecked,
|
| 28 |
+
// Alternatively, use this for stricter rules
|
| 29 |
+
tseslint.configs.strictTypeChecked,
|
| 30 |
+
// Optionally, add this for stylistic rules
|
| 31 |
+
tseslint.configs.stylisticTypeChecked,
|
| 32 |
+
|
| 33 |
+
// Other configs...
|
| 34 |
+
],
|
| 35 |
+
languageOptions: {
|
| 36 |
+
parserOptions: {
|
| 37 |
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
| 38 |
+
tsconfigRootDir: import.meta.dirname,
|
| 39 |
+
},
|
| 40 |
+
// other options...
|
| 41 |
+
},
|
| 42 |
+
},
|
| 43 |
+
])
|
| 44 |
+
```
|
| 45 |
+
|
| 46 |
+
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
| 47 |
+
|
| 48 |
+
```js
|
| 49 |
+
// eslint.config.js
|
| 50 |
+
import reactX from 'eslint-plugin-react-x'
|
| 51 |
+
import reactDom from 'eslint-plugin-react-dom'
|
| 52 |
+
|
| 53 |
+
export default defineConfig([
|
| 54 |
+
globalIgnores(['dist']),
|
| 55 |
+
{
|
| 56 |
+
files: ['**/*.{ts,tsx}'],
|
| 57 |
+
extends: [
|
| 58 |
+
// Other configs...
|
| 59 |
+
// Enable lint rules for React
|
| 60 |
+
reactX.configs['recommended-typescript'],
|
| 61 |
+
// Enable lint rules for React DOM
|
| 62 |
+
reactDom.configs.recommended,
|
| 63 |
+
],
|
| 64 |
+
languageOptions: {
|
| 65 |
+
parserOptions: {
|
| 66 |
+
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
| 67 |
+
tsconfigRootDir: import.meta.dirname,
|
| 68 |
+
},
|
| 69 |
+
// other options...
|
| 70 |
+
},
|
| 71 |
+
},
|
| 72 |
+
])
|
| 73 |
+
```
|
frontend/eslint.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
+
import tseslint from 'typescript-eslint'
|
| 6 |
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
| 7 |
+
|
| 8 |
+
export default defineConfig([
|
| 9 |
+
globalIgnores(['dist']),
|
| 10 |
+
{
|
| 11 |
+
files: ['**/*.{ts,tsx}'],
|
| 12 |
+
extends: [
|
| 13 |
+
js.configs.recommended,
|
| 14 |
+
tseslint.configs.recommended,
|
| 15 |
+
reactHooks.configs.flat.recommended,
|
| 16 |
+
reactRefresh.configs.vite,
|
| 17 |
+
],
|
| 18 |
+
languageOptions: {
|
| 19 |
+
ecmaVersion: 2020,
|
| 20 |
+
globals: globals.browser,
|
| 21 |
+
},
|
| 22 |
+
},
|
| 23 |
+
])
|
frontend/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>frontend</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "frontend",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "tsc -b && vite build",
|
| 9 |
+
"lint": "eslint .",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"axios": "^1.13.2",
|
| 14 |
+
"konva": "^10.0.12",
|
| 15 |
+
"lucide-react": "^0.555.0",
|
| 16 |
+
"react": "^19.2.0",
|
| 17 |
+
"react-dom": "^19.2.0",
|
| 18 |
+
"react-konva": "^19.2.1"
|
| 19 |
+
},
|
| 20 |
+
"devDependencies": {
|
| 21 |
+
"@eslint/js": "^9.39.1",
|
| 22 |
+
"@types/node": "^24.10.1",
|
| 23 |
+
"@types/react": "^19.2.5",
|
| 24 |
+
"@types/react-dom": "^19.2.3",
|
| 25 |
+
"@vitejs/plugin-react": "^5.1.1",
|
| 26 |
+
"eslint": "^9.39.1",
|
| 27 |
+
"eslint-plugin-react-hooks": "^7.0.1",
|
| 28 |
+
"eslint-plugin-react-refresh": "^0.4.24",
|
| 29 |
+
"globals": "^16.5.0",
|
| 30 |
+
"typescript": "~5.9.3",
|
| 31 |
+
"typescript-eslint": "^8.46.4",
|
| 32 |
+
"vite": "^7.2.4"
|
| 33 |
+
}
|
| 34 |
+
}
|
frontend/public/vite.svg
ADDED
|
|
frontend/src/App.css
ADDED
|
@@ -0,0 +1,724 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* AIOptimize™ MVP - Styles
|
| 3 |
+
* Modern, premium design system
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
/* === CSS Variables === */
|
| 7 |
+
:root {
|
| 8 |
+
--primary: #6366f1;
|
| 9 |
+
--primary-dark: #4f46e5;
|
| 10 |
+
--primary-light: #818cf8;
|
| 11 |
+
--secondary: #10b981;
|
| 12 |
+
--accent: #f59e0b;
|
| 13 |
+
--danger: #ef4444;
|
| 14 |
+
|
| 15 |
+
--bg-dark: #0f172a;
|
| 16 |
+
--bg-card: #1e293b;
|
| 17 |
+
--bg-hover: #334155;
|
| 18 |
+
--bg-light: #f8fafc;
|
| 19 |
+
|
| 20 |
+
--text-primary: #f8fafc;
|
| 21 |
+
--text-secondary: #94a3b8;
|
| 22 |
+
--text-muted: #64748b;
|
| 23 |
+
|
| 24 |
+
--border: #334155;
|
| 25 |
+
--border-light: #475569;
|
| 26 |
+
|
| 27 |
+
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
|
| 28 |
+
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4);
|
| 29 |
+
|
| 30 |
+
--radius: 12px;
|
| 31 |
+
--radius-sm: 8px;
|
| 32 |
+
|
| 33 |
+
--transition: all 0.2s ease;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* === Base Styles === */
|
| 37 |
+
* {
|
| 38 |
+
box-sizing: border-box;
|
| 39 |
+
margin: 0;
|
| 40 |
+
padding: 0;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
body {
|
| 44 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 45 |
+
background: linear-gradient(135deg, var(--bg-dark) 0%, #1a1f35 100%);
|
| 46 |
+
color: var(--text-primary);
|
| 47 |
+
min-height: 100vh;
|
| 48 |
+
line-height: 1.5;
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
/* === Layout === */
|
| 52 |
+
.app {
|
| 53 |
+
display: flex;
|
| 54 |
+
flex-direction: column;
|
| 55 |
+
min-height: 100vh;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
.app-header {
|
| 59 |
+
display: flex;
|
| 60 |
+
align-items: center;
|
| 61 |
+
justify-content: space-between;
|
| 62 |
+
padding: 1rem 2rem;
|
| 63 |
+
background: rgba(15, 23, 42, 0.8);
|
| 64 |
+
backdrop-filter: blur(10px);
|
| 65 |
+
border-bottom: 1px solid var(--border);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.logo {
|
| 69 |
+
display: flex;
|
| 70 |
+
align-items: center;
|
| 71 |
+
gap: 0.75rem;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.logo svg {
|
| 75 |
+
color: var(--primary);
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.logo h1 {
|
| 79 |
+
font-size: 1.5rem;
|
| 80 |
+
font-weight: 700;
|
| 81 |
+
background: linear-gradient(135deg, var(--primary-light), var(--secondary));
|
| 82 |
+
-webkit-background-clip: text;
|
| 83 |
+
-webkit-text-fill-color: transparent;
|
| 84 |
+
background-clip: text;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.tagline {
|
| 88 |
+
color: var(--text-secondary);
|
| 89 |
+
font-size: 0.875rem;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.app-main {
|
| 93 |
+
display: flex;
|
| 94 |
+
flex: 1;
|
| 95 |
+
padding: 1.5rem;
|
| 96 |
+
gap: 1.5rem;
|
| 97 |
+
max-width: 1600px;
|
| 98 |
+
margin: 0 auto;
|
| 99 |
+
width: 100%;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.left-panel {
|
| 103 |
+
flex: 1;
|
| 104 |
+
display: flex;
|
| 105 |
+
flex-direction: column;
|
| 106 |
+
gap: 1.5rem;
|
| 107 |
+
max-width: 800px;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.right-panel {
|
| 111 |
+
width: 400px;
|
| 112 |
+
flex-shrink: 0;
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
.app-footer {
|
| 116 |
+
padding: 1rem 2rem;
|
| 117 |
+
text-align: center;
|
| 118 |
+
color: var(--text-muted);
|
| 119 |
+
font-size: 0.75rem;
|
| 120 |
+
border-top: 1px solid var(--border);
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
/* === Buttons === */
|
| 124 |
+
.btn {
|
| 125 |
+
display: inline-flex;
|
| 126 |
+
align-items: center;
|
| 127 |
+
justify-content: center;
|
| 128 |
+
gap: 0.5rem;
|
| 129 |
+
padding: 0.75rem 1.25rem;
|
| 130 |
+
border-radius: var(--radius-sm);
|
| 131 |
+
font-weight: 600;
|
| 132 |
+
font-size: 0.875rem;
|
| 133 |
+
cursor: pointer;
|
| 134 |
+
border: none;
|
| 135 |
+
transition: var(--transition);
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
.btn:disabled {
|
| 139 |
+
opacity: 0.5;
|
| 140 |
+
cursor: not-allowed;
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.btn-primary {
|
| 144 |
+
background: linear-gradient(135deg, var(--primary), var(--primary-dark));
|
| 145 |
+
color: white;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.btn-primary:hover:not(:disabled) {
|
| 149 |
+
transform: translateY(-1px);
|
| 150 |
+
box-shadow: var(--shadow-lg);
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
.btn-secondary {
|
| 154 |
+
background: var(--bg-card);
|
| 155 |
+
color: var(--text-primary);
|
| 156 |
+
border: 1px solid var(--border);
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.btn-secondary:hover:not(:disabled) {
|
| 160 |
+
background: var(--bg-hover);
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
.btn-sm {
|
| 164 |
+
padding: 0.5rem 0.75rem;
|
| 165 |
+
font-size: 0.75rem;
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
.btn-export {
|
| 169 |
+
background: var(--secondary);
|
| 170 |
+
color: white;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.btn-export:hover:not(:disabled) {
|
| 174 |
+
background: #059669;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.btn-generate {
|
| 178 |
+
width: 100%;
|
| 179 |
+
padding: 1rem;
|
| 180 |
+
font-size: 1rem;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.btn-export-all {
|
| 184 |
+
width: 100%;
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.btn-send {
|
| 188 |
+
padding: 0.75rem;
|
| 189 |
+
background: var(--primary);
|
| 190 |
+
color: white;
|
| 191 |
+
border-radius: var(--radius-sm);
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.spin {
|
| 195 |
+
animation: spin 1s linear infinite;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
@keyframes spin {
|
| 199 |
+
from {
|
| 200 |
+
transform: rotate(0deg);
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
to {
|
| 204 |
+
transform: rotate(360deg);
|
| 205 |
+
}
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
/* === Upload Panel === */
|
| 209 |
+
.upload-panel {
|
| 210 |
+
background: var(--bg-card);
|
| 211 |
+
border-radius: var(--radius);
|
| 212 |
+
padding: 1.5rem;
|
| 213 |
+
border: 1px solid var(--border);
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.upload-header {
|
| 217 |
+
display: flex;
|
| 218 |
+
align-items: center;
|
| 219 |
+
gap: 0.75rem;
|
| 220 |
+
margin-bottom: 1rem;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.upload-header svg {
|
| 224 |
+
color: var(--primary);
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
.upload-header h3 {
|
| 228 |
+
font-size: 1rem;
|
| 229 |
+
font-weight: 600;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.upload-buttons {
|
| 233 |
+
display: flex;
|
| 234 |
+
gap: 0.75rem;
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.upload-status {
|
| 238 |
+
margin-top: 1rem;
|
| 239 |
+
padding: 0.5rem 0.75rem;
|
| 240 |
+
background: rgba(16, 185, 129, 0.1);
|
| 241 |
+
border-radius: var(--radius-sm);
|
| 242 |
+
color: var(--secondary);
|
| 243 |
+
font-size: 0.875rem;
|
| 244 |
+
}
|
| 245 |
+
|
| 246 |
+
/* === Generate Section === */
|
| 247 |
+
.generate-section {
|
| 248 |
+
background: var(--bg-card);
|
| 249 |
+
border-radius: var(--radius);
|
| 250 |
+
padding: 1rem;
|
| 251 |
+
border: 1px solid var(--border);
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
/* === Map Container === */
|
| 255 |
+
.map-section {
|
| 256 |
+
background: var(--bg-card);
|
| 257 |
+
border-radius: var(--radius);
|
| 258 |
+
padding: 1rem;
|
| 259 |
+
border: 1px solid var(--border);
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.map-wrapper {
|
| 263 |
+
position: relative;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.map-controls {
|
| 267 |
+
position: absolute;
|
| 268 |
+
top: 10px;
|
| 269 |
+
right: 10px;
|
| 270 |
+
display: flex;
|
| 271 |
+
gap: 4px;
|
| 272 |
+
z-index: 10;
|
| 273 |
+
background: white;
|
| 274 |
+
padding: 4px;
|
| 275 |
+
border-radius: var(--radius-sm);
|
| 276 |
+
box-shadow: var(--shadow);
|
| 277 |
+
}
|
| 278 |
+
|
| 279 |
+
.map-controls button {
|
| 280 |
+
width: 32px;
|
| 281 |
+
height: 32px;
|
| 282 |
+
display: flex;
|
| 283 |
+
align-items: center;
|
| 284 |
+
justify-content: center;
|
| 285 |
+
background: #f8fafc;
|
| 286 |
+
border: 1px solid #e2e8f0;
|
| 287 |
+
border-radius: 6px;
|
| 288 |
+
cursor: pointer;
|
| 289 |
+
color: #475569;
|
| 290 |
+
transition: var(--transition);
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.map-controls button:hover {
|
| 294 |
+
background: #e2e8f0;
|
| 295 |
+
color: #1e293b;
|
| 296 |
+
}
|
| 297 |
+
|
| 298 |
+
.map-controls .zoom-level {
|
| 299 |
+
font-size: 11px;
|
| 300 |
+
color: #64748b;
|
| 301 |
+
padding: 0 8px;
|
| 302 |
+
display: flex;
|
| 303 |
+
align-items: center;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
.map-hint {
|
| 307 |
+
position: absolute;
|
| 308 |
+
bottom: 8px;
|
| 309 |
+
left: 50%;
|
| 310 |
+
transform: translateX(-50%);
|
| 311 |
+
font-size: 11px;
|
| 312 |
+
color: #94a3b8;
|
| 313 |
+
background: rgba(255, 255, 255, 0.9);
|
| 314 |
+
padding: 4px 12px;
|
| 315 |
+
border-radius: 20px;
|
| 316 |
+
display: flex;
|
| 317 |
+
align-items: center;
|
| 318 |
+
gap: 6px;
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.map-container {
|
| 322 |
+
background: white;
|
| 323 |
+
border-radius: var(--radius-sm);
|
| 324 |
+
overflow: hidden;
|
| 325 |
+
cursor: grab;
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
.map-container:active {
|
| 329 |
+
cursor: grabbing;
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
/* === Options Panel === */
|
| 333 |
+
.options-panel {
|
| 334 |
+
background: var(--bg-card);
|
| 335 |
+
border-radius: var(--radius);
|
| 336 |
+
padding: 1.5rem;
|
| 337 |
+
border: 1px solid var(--border);
|
| 338 |
+
}
|
| 339 |
+
|
| 340 |
+
.options-panel h3 {
|
| 341 |
+
font-size: 1rem;
|
| 342 |
+
font-weight: 600;
|
| 343 |
+
margin-bottom: 1rem;
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.options-panel.empty {
|
| 347 |
+
text-align: center;
|
| 348 |
+
color: var(--text-muted);
|
| 349 |
+
padding: 2rem;
|
| 350 |
+
}
|
| 351 |
+
|
| 352 |
+
.options-grid {
|
| 353 |
+
display: flex;
|
| 354 |
+
flex-direction: column;
|
| 355 |
+
gap: 1rem;
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
.option-card {
|
| 359 |
+
background: var(--bg-dark);
|
| 360 |
+
border-radius: var(--radius-sm);
|
| 361 |
+
padding: 1rem;
|
| 362 |
+
border: 2px solid var(--border);
|
| 363 |
+
cursor: pointer;
|
| 364 |
+
transition: var(--transition);
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
.option-card:hover {
|
| 368 |
+
border-color: var(--border-light);
|
| 369 |
+
background: var(--bg-hover);
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.option-card.selected {
|
| 373 |
+
border-color: var(--primary);
|
| 374 |
+
background: rgba(99, 102, 241, 0.1);
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
.option-header {
|
| 378 |
+
display: flex;
|
| 379 |
+
align-items: center;
|
| 380 |
+
gap: 0.75rem;
|
| 381 |
+
margin-bottom: 0.5rem;
|
| 382 |
+
}
|
| 383 |
+
|
| 384 |
+
.option-icon {
|
| 385 |
+
font-size: 1.5rem;
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.option-header h4 {
|
| 389 |
+
flex: 1;
|
| 390 |
+
font-size: 1rem;
|
| 391 |
+
font-weight: 600;
|
| 392 |
+
}
|
| 393 |
+
|
| 394 |
+
.selected-check {
|
| 395 |
+
color: var(--primary);
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
.option-description {
|
| 399 |
+
color: var(--text-secondary);
|
| 400 |
+
font-size: 0.75rem;
|
| 401 |
+
margin-bottom: 1rem;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
.option-metrics {
|
| 405 |
+
display: grid;
|
| 406 |
+
grid-template-columns: repeat(4, 1fr);
|
| 407 |
+
gap: 0.75rem;
|
| 408 |
+
margin-bottom: 1rem;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
.metric {
|
| 412 |
+
text-align: center;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
.metric-label {
|
| 416 |
+
display: block;
|
| 417 |
+
font-size: 0.625rem;
|
| 418 |
+
color: var(--text-muted);
|
| 419 |
+
text-transform: uppercase;
|
| 420 |
+
letter-spacing: 0.05em;
|
| 421 |
+
margin-bottom: 0.25rem;
|
| 422 |
+
}
|
| 423 |
+
|
| 424 |
+
.metric-value {
|
| 425 |
+
font-size: 0.875rem;
|
| 426 |
+
font-weight: 600;
|
| 427 |
+
color: var(--text-primary);
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
.metric-value.fitness {
|
| 431 |
+
color: var(--secondary);
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
.option-footer {
|
| 435 |
+
display: flex;
|
| 436 |
+
align-items: center;
|
| 437 |
+
justify-content: space-between;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
.compliance {
|
| 441 |
+
font-size: 0.75rem;
|
| 442 |
+
padding: 0.25rem 0.5rem;
|
| 443 |
+
border-radius: 4px;
|
| 444 |
+
}
|
| 445 |
+
|
| 446 |
+
.compliance.pass {
|
| 447 |
+
background: rgba(16, 185, 129, 0.1);
|
| 448 |
+
color: var(--secondary);
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
.compliance.fail {
|
| 452 |
+
background: rgba(239, 68, 68, 0.1);
|
| 453 |
+
color: var(--danger);
|
| 454 |
+
}
|
| 455 |
+
|
| 456 |
+
/* === Export Panel === */
|
| 457 |
+
.export-panel {
|
| 458 |
+
background: var(--bg-card);
|
| 459 |
+
border-radius: var(--radius);
|
| 460 |
+
padding: 1rem;
|
| 461 |
+
border: 1px solid var(--border);
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
/* === Chat Interface === */
|
| 465 |
+
.chat-interface {
|
| 466 |
+
background: var(--bg-card);
|
| 467 |
+
border-radius: var(--radius);
|
| 468 |
+
border: 1px solid var(--border);
|
| 469 |
+
display: flex;
|
| 470 |
+
flex-direction: column;
|
| 471 |
+
height: calc(100vh - 180px);
|
| 472 |
+
max-height: 700px;
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
.chat-header {
|
| 476 |
+
display: flex;
|
| 477 |
+
align-items: center;
|
| 478 |
+
gap: 0.75rem;
|
| 479 |
+
padding: 1rem 1.25rem;
|
| 480 |
+
border-bottom: 1px solid var(--border);
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
.chat-header svg {
|
| 484 |
+
color: var(--primary);
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
.chat-header .sparkle {
|
| 488 |
+
color: var(--accent);
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
.chat-header h3 {
|
| 492 |
+
flex: 1;
|
| 493 |
+
font-size: 1rem;
|
| 494 |
+
font-weight: 600;
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
.chat-messages {
|
| 498 |
+
flex: 1;
|
| 499 |
+
overflow-y: auto;
|
| 500 |
+
padding: 1rem;
|
| 501 |
+
}
|
| 502 |
+
|
| 503 |
+
.chat-welcome {
|
| 504 |
+
text-align: center;
|
| 505 |
+
padding: 2rem 1rem;
|
| 506 |
+
color: var(--text-secondary);
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
.welcome-icon {
|
| 510 |
+
color: var(--primary);
|
| 511 |
+
margin-bottom: 1rem;
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
.chat-welcome h4 {
|
| 515 |
+
font-size: 1.125rem;
|
| 516 |
+
color: var(--text-primary);
|
| 517 |
+
margin-bottom: 0.5rem;
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
.chat-welcome p {
|
| 521 |
+
font-size: 0.875rem;
|
| 522 |
+
margin-bottom: 1.5rem;
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
.suggested-questions {
|
| 526 |
+
display: flex;
|
| 527 |
+
flex-direction: column;
|
| 528 |
+
gap: 0.5rem;
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
.suggested-btn {
|
| 532 |
+
background: var(--bg-dark);
|
| 533 |
+
border: 1px solid var(--border);
|
| 534 |
+
color: var(--text-secondary);
|
| 535 |
+
padding: 0.75rem 1rem;
|
| 536 |
+
border-radius: var(--radius-sm);
|
| 537 |
+
font-size: 0.75rem;
|
| 538 |
+
cursor: pointer;
|
| 539 |
+
text-align: left;
|
| 540 |
+
transition: var(--transition);
|
| 541 |
+
}
|
| 542 |
+
|
| 543 |
+
.suggested-btn:hover:not(:disabled) {
|
| 544 |
+
background: var(--bg-hover);
|
| 545 |
+
color: var(--text-primary);
|
| 546 |
+
border-color: var(--primary);
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.message {
|
| 550 |
+
display: flex;
|
| 551 |
+
gap: 0.75rem;
|
| 552 |
+
margin-bottom: 1rem;
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
.message-icon {
|
| 556 |
+
width: 28px;
|
| 557 |
+
height: 28px;
|
| 558 |
+
border-radius: 50%;
|
| 559 |
+
display: flex;
|
| 560 |
+
align-items: center;
|
| 561 |
+
justify-content: center;
|
| 562 |
+
flex-shrink: 0;
|
| 563 |
+
}
|
| 564 |
+
|
| 565 |
+
.message.user .message-icon {
|
| 566 |
+
background: var(--primary);
|
| 567 |
+
color: white;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.message.assistant .message-icon {
|
| 571 |
+
background: var(--secondary);
|
| 572 |
+
color: white;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
.message-content {
|
| 576 |
+
flex: 1;
|
| 577 |
+
background: var(--bg-dark);
|
| 578 |
+
padding: 0.75rem 1rem;
|
| 579 |
+
border-radius: var(--radius-sm);
|
| 580 |
+
font-size: 0.875rem;
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
.message.user .message-content {
|
| 584 |
+
background: rgba(99, 102, 241, 0.1);
|
| 585 |
+
}
|
| 586 |
+
|
| 587 |
+
.message-text {
|
| 588 |
+
white-space: pre-wrap;
|
| 589 |
+
line-height: 1.6;
|
| 590 |
+
}
|
| 591 |
+
|
| 592 |
+
.model-badge {
|
| 593 |
+
margin-top: 0.5rem;
|
| 594 |
+
font-size: 0.625rem;
|
| 595 |
+
color: var(--text-muted);
|
| 596 |
+
}
|
| 597 |
+
|
| 598 |
+
.model-badge.gemini {
|
| 599 |
+
color: var(--secondary);
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
.typing-indicator {
|
| 603 |
+
display: flex;
|
| 604 |
+
gap: 4px;
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
.typing-indicator span {
|
| 608 |
+
width: 6px;
|
| 609 |
+
height: 6px;
|
| 610 |
+
background: var(--text-muted);
|
| 611 |
+
border-radius: 50%;
|
| 612 |
+
animation: bounce 1.4s infinite ease-in-out;
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
.typing-indicator span:nth-child(2) {
|
| 616 |
+
animation-delay: 0.2s;
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
.typing-indicator span:nth-child(3) {
|
| 620 |
+
animation-delay: 0.4s;
|
| 621 |
+
}
|
| 622 |
+
|
| 623 |
+
@keyframes bounce {
|
| 624 |
+
|
| 625 |
+
0%,
|
| 626 |
+
80%,
|
| 627 |
+
100% {
|
| 628 |
+
transform: translateY(0);
|
| 629 |
+
}
|
| 630 |
+
|
| 631 |
+
40% {
|
| 632 |
+
transform: translateY(-6px);
|
| 633 |
+
}
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
.chat-input-form {
|
| 637 |
+
display: flex;
|
| 638 |
+
gap: 0.75rem;
|
| 639 |
+
padding: 1rem;
|
| 640 |
+
border-top: 1px solid var(--border);
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
.chat-input {
|
| 644 |
+
flex: 1;
|
| 645 |
+
background: var(--bg-dark);
|
| 646 |
+
border: 1px solid var(--border);
|
| 647 |
+
border-radius: var(--radius-sm);
|
| 648 |
+
padding: 0.75rem 1rem;
|
| 649 |
+
color: var(--text-primary);
|
| 650 |
+
font-size: 0.875rem;
|
| 651 |
+
}
|
| 652 |
+
|
| 653 |
+
.chat-input:focus {
|
| 654 |
+
outline: none;
|
| 655 |
+
border-color: var(--primary);
|
| 656 |
+
}
|
| 657 |
+
|
| 658 |
+
.chat-input:disabled {
|
| 659 |
+
opacity: 0.5;
|
| 660 |
+
}
|
| 661 |
+
|
| 662 |
+
/* === Error Banner === */
|
| 663 |
+
.error-banner {
|
| 664 |
+
display: flex;
|
| 665 |
+
align-items: center;
|
| 666 |
+
gap: 0.75rem;
|
| 667 |
+
padding: 0.75rem 1.5rem;
|
| 668 |
+
background: rgba(239, 68, 68, 0.1);
|
| 669 |
+
border-bottom: 1px solid var(--danger);
|
| 670 |
+
color: var(--danger);
|
| 671 |
+
}
|
| 672 |
+
|
| 673 |
+
.error-banner svg {
|
| 674 |
+
flex-shrink: 0;
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
.error-banner span {
|
| 678 |
+
flex: 1;
|
| 679 |
+
font-size: 0.875rem;
|
| 680 |
+
}
|
| 681 |
+
|
| 682 |
+
.error-banner button {
|
| 683 |
+
background: none;
|
| 684 |
+
border: none;
|
| 685 |
+
color: var(--danger);
|
| 686 |
+
font-size: 1.25rem;
|
| 687 |
+
cursor: pointer;
|
| 688 |
+
padding: 0 0.5rem;
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
/* === Responsive === */
|
| 692 |
+
@media (max-width: 1200px) {
|
| 693 |
+
.app-main {
|
| 694 |
+
flex-direction: column;
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
.left-panel {
|
| 698 |
+
max-width: 100%;
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
.right-panel {
|
| 702 |
+
width: 100%;
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
.chat-interface {
|
| 706 |
+
height: 400px;
|
| 707 |
+
}
|
| 708 |
+
}
|
| 709 |
+
|
| 710 |
+
@media (max-width: 768px) {
|
| 711 |
+
.app-header {
|
| 712 |
+
flex-direction: column;
|
| 713 |
+
gap: 0.5rem;
|
| 714 |
+
text-align: center;
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
.upload-buttons {
|
| 718 |
+
flex-direction: column;
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
.option-metrics {
|
| 722 |
+
grid-template-columns: repeat(2, 1fr);
|
| 723 |
+
}
|
| 724 |
+
}
|
frontend/src/App.tsx
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* AIOptimize™ MVP - Main Application
|
| 3 |
+
* Industrial Estate Planning Optimization
|
| 4 |
+
*/
|
| 5 |
+
import { useState, useCallback, useEffect } from 'react';
|
| 6 |
+
import { Layers, Zap, AlertCircle, Loader2 } from 'lucide-react';
|
| 7 |
+
import { FileUploadPanel } from './components/FileUploadPanel';
|
| 8 |
+
import { Map2DPlotter } from './components/Map2DPlotter';
|
| 9 |
+
import { LayoutOptionsPanel } from './components/LayoutOptionsPanel';
|
| 10 |
+
import { ChatInterface } from './components/ChatInterface';
|
| 11 |
+
import { ExportPanel } from './components/ExportPanel';
|
| 12 |
+
import { apiService } from './services/api';
|
| 13 |
+
import type { AppState, ChatMessage, GeoJSONFeature } from './types';
|
| 14 |
+
import './App.css';
|
| 15 |
+
|
| 16 |
+
function App() {
|
| 17 |
+
// Application state
|
| 18 |
+
const [state, setState] = useState<AppState>({
|
| 19 |
+
sessionId: null,
|
| 20 |
+
boundary: null,
|
| 21 |
+
boundaryCoords: null,
|
| 22 |
+
metadata: null,
|
| 23 |
+
options: [],
|
| 24 |
+
selectedOption: null,
|
| 25 |
+
messages: [],
|
| 26 |
+
loading: false,
|
| 27 |
+
error: null,
|
| 28 |
+
});
|
| 29 |
+
|
| 30 |
+
// Loading states
|
| 31 |
+
const [uploadLoading, setUploadLoading] = useState(false);
|
| 32 |
+
const [generateLoading, setGenerateLoading] = useState(false);
|
| 33 |
+
const [chatLoading, setChatLoading] = useState(false);
|
| 34 |
+
const [exportLoading, setExportLoading] = useState(false);
|
| 35 |
+
|
| 36 |
+
// Debug: monitor options changes
|
| 37 |
+
useEffect(() => {
|
| 38 |
+
console.log('State options changed:', state.options.length, state.options);
|
| 39 |
+
}, [state.options]);
|
| 40 |
+
|
| 41 |
+
// Extract boundary coords from GeoJSON
|
| 42 |
+
const extractCoords = (geojson: GeoJSONFeature): number[][] => {
|
| 43 |
+
if (geojson.type === 'Feature' && geojson.geometry) {
|
| 44 |
+
return geojson.geometry.coordinates[0];
|
| 45 |
+
}
|
| 46 |
+
return [];
|
| 47 |
+
};
|
| 48 |
+
|
| 49 |
+
// Handle file upload
|
| 50 |
+
const handleUpload = useCallback(async (file: File) => {
|
| 51 |
+
setUploadLoading(true);
|
| 52 |
+
setState(prev => ({ ...prev, error: null }));
|
| 53 |
+
|
| 54 |
+
try {
|
| 55 |
+
const response = await apiService.uploadBoundaryFile(file);
|
| 56 |
+
const coords = extractCoords(response.boundary);
|
| 57 |
+
|
| 58 |
+
setState(prev => ({
|
| 59 |
+
...prev,
|
| 60 |
+
sessionId: response.session_id,
|
| 61 |
+
boundary: response.boundary,
|
| 62 |
+
boundaryCoords: coords,
|
| 63 |
+
metadata: response.metadata,
|
| 64 |
+
options: [],
|
| 65 |
+
selectedOption: null,
|
| 66 |
+
}));
|
| 67 |
+
} catch (err) {
|
| 68 |
+
setState(prev => ({
|
| 69 |
+
...prev,
|
| 70 |
+
error: err instanceof Error ? err.message : 'Upload failed'
|
| 71 |
+
}));
|
| 72 |
+
} finally {
|
| 73 |
+
setUploadLoading(false);
|
| 74 |
+
}
|
| 75 |
+
}, []);
|
| 76 |
+
|
| 77 |
+
// Handle sample data
|
| 78 |
+
const handleSampleData = useCallback(async () => {
|
| 79 |
+
setUploadLoading(true);
|
| 80 |
+
setState(prev => ({ ...prev, error: null }));
|
| 81 |
+
|
| 82 |
+
try {
|
| 83 |
+
const sampleData = await apiService.getSampleData();
|
| 84 |
+
const response = await apiService.uploadBoundary(sampleData);
|
| 85 |
+
const coords = extractCoords(response.boundary);
|
| 86 |
+
|
| 87 |
+
setState(prev => ({
|
| 88 |
+
...prev,
|
| 89 |
+
sessionId: response.session_id,
|
| 90 |
+
boundary: response.boundary,
|
| 91 |
+
boundaryCoords: coords,
|
| 92 |
+
metadata: response.metadata,
|
| 93 |
+
options: [],
|
| 94 |
+
selectedOption: null,
|
| 95 |
+
}));
|
| 96 |
+
} catch (err) {
|
| 97 |
+
setState(prev => ({
|
| 98 |
+
...prev,
|
| 99 |
+
error: err instanceof Error ? err.message : 'Failed to load sample data'
|
| 100 |
+
}));
|
| 101 |
+
} finally {
|
| 102 |
+
setUploadLoading(false);
|
| 103 |
+
}
|
| 104 |
+
}, []);
|
| 105 |
+
|
| 106 |
+
// Generate layouts
|
| 107 |
+
const handleGenerate = useCallback(async () => {
|
| 108 |
+
if (!state.sessionId) return;
|
| 109 |
+
|
| 110 |
+
setGenerateLoading(true);
|
| 111 |
+
setState(prev => ({ ...prev, error: null }));
|
| 112 |
+
|
| 113 |
+
try {
|
| 114 |
+
const response = await apiService.generateLayouts(state.sessionId);
|
| 115 |
+
console.log('Generate response:', response);
|
| 116 |
+
|
| 117 |
+
if (response.options && response.options.length > 0) {
|
| 118 |
+
setState(prev => ({
|
| 119 |
+
...prev,
|
| 120 |
+
options: response.options,
|
| 121 |
+
selectedOption: response.options[0]?.id || null,
|
| 122 |
+
}));
|
| 123 |
+
} else {
|
| 124 |
+
setState(prev => ({
|
| 125 |
+
...prev,
|
| 126 |
+
error: 'No layout options returned'
|
| 127 |
+
}));
|
| 128 |
+
}
|
| 129 |
+
} catch (err) {
|
| 130 |
+
console.error('Generate error:', err);
|
| 131 |
+
setState(prev => ({
|
| 132 |
+
...prev,
|
| 133 |
+
error: err instanceof Error ? err.message : 'Generation failed'
|
| 134 |
+
}));
|
| 135 |
+
} finally {
|
| 136 |
+
setGenerateLoading(false);
|
| 137 |
+
}
|
| 138 |
+
}, [state.sessionId]);
|
| 139 |
+
|
| 140 |
+
// Select layout option
|
| 141 |
+
const handleSelectOption = useCallback((optionId: number) => {
|
| 142 |
+
setState(prev => ({ ...prev, selectedOption: optionId }));
|
| 143 |
+
}, []);
|
| 144 |
+
|
| 145 |
+
// Export single DXF
|
| 146 |
+
const handleExportDxf = useCallback(async (optionId: number) => {
|
| 147 |
+
if (!state.sessionId) return;
|
| 148 |
+
|
| 149 |
+
setExportLoading(true);
|
| 150 |
+
|
| 151 |
+
try {
|
| 152 |
+
const blob = await apiService.exportDxf(state.sessionId, optionId);
|
| 153 |
+
|
| 154 |
+
const url = window.URL.createObjectURL(blob);
|
| 155 |
+
const a = document.createElement('a');
|
| 156 |
+
a.href = url;
|
| 157 |
+
a.download = `layout_option_${optionId}.dxf`;
|
| 158 |
+
document.body.appendChild(a);
|
| 159 |
+
a.click();
|
| 160 |
+
document.body.removeChild(a);
|
| 161 |
+
window.URL.revokeObjectURL(url);
|
| 162 |
+
} catch (err) {
|
| 163 |
+
setState(prev => ({
|
| 164 |
+
...prev,
|
| 165 |
+
error: err instanceof Error ? err.message : 'Export failed'
|
| 166 |
+
}));
|
| 167 |
+
} finally {
|
| 168 |
+
setExportLoading(false);
|
| 169 |
+
}
|
| 170 |
+
}, [state.sessionId]);
|
| 171 |
+
|
| 172 |
+
// Export all as ZIP
|
| 173 |
+
const handleExportAll = useCallback(async () => {
|
| 174 |
+
if (!state.sessionId) return;
|
| 175 |
+
|
| 176 |
+
setExportLoading(true);
|
| 177 |
+
|
| 178 |
+
try {
|
| 179 |
+
const blob = await apiService.exportAllDxf(state.sessionId);
|
| 180 |
+
|
| 181 |
+
const url = window.URL.createObjectURL(blob);
|
| 182 |
+
const a = document.createElement('a');
|
| 183 |
+
a.href = url;
|
| 184 |
+
a.download = 'all_layouts.zip';
|
| 185 |
+
document.body.appendChild(a);
|
| 186 |
+
a.click();
|
| 187 |
+
document.body.removeChild(a);
|
| 188 |
+
window.URL.revokeObjectURL(url);
|
| 189 |
+
} catch (err) {
|
| 190 |
+
setState(prev => ({
|
| 191 |
+
...prev,
|
| 192 |
+
error: err instanceof Error ? err.message : 'Export failed'
|
| 193 |
+
}));
|
| 194 |
+
} finally {
|
| 195 |
+
setExportLoading(false);
|
| 196 |
+
}
|
| 197 |
+
}, [state.sessionId]);
|
| 198 |
+
|
| 199 |
+
// Send chat message
|
| 200 |
+
const handleChat = useCallback(async (message: string) => {
|
| 201 |
+
if (!state.sessionId) return;
|
| 202 |
+
|
| 203 |
+
const userMessage: ChatMessage = { role: 'user', content: message };
|
| 204 |
+
setState(prev => ({
|
| 205 |
+
...prev,
|
| 206 |
+
messages: [...prev.messages, userMessage],
|
| 207 |
+
}));
|
| 208 |
+
|
| 209 |
+
setChatLoading(true);
|
| 210 |
+
|
| 211 |
+
try {
|
| 212 |
+
const response = await apiService.chat(state.sessionId, message);
|
| 213 |
+
|
| 214 |
+
const assistantMessage: ChatMessage = {
|
| 215 |
+
role: 'assistant',
|
| 216 |
+
content: response.message,
|
| 217 |
+
model: response.model,
|
| 218 |
+
};
|
| 219 |
+
|
| 220 |
+
setState(prev => ({
|
| 221 |
+
...prev,
|
| 222 |
+
messages: [...prev.messages, assistantMessage],
|
| 223 |
+
}));
|
| 224 |
+
} catch (err) {
|
| 225 |
+
const errorMessage: ChatMessage = {
|
| 226 |
+
role: 'assistant',
|
| 227 |
+
content: 'Sorry, I encountered an error. Please try again.',
|
| 228 |
+
model: 'fallback',
|
| 229 |
+
};
|
| 230 |
+
setState(prev => ({
|
| 231 |
+
...prev,
|
| 232 |
+
messages: [...prev.messages, errorMessage],
|
| 233 |
+
}));
|
| 234 |
+
} finally {
|
| 235 |
+
setChatLoading(false);
|
| 236 |
+
}
|
| 237 |
+
}, [state.sessionId]);
|
| 238 |
+
|
| 239 |
+
// Get selected option object
|
| 240 |
+
const selectedOptionData = state.options.find(o => o.id === state.selectedOption) || null;
|
| 241 |
+
|
| 242 |
+
return (
|
| 243 |
+
<div className="app">
|
| 244 |
+
<header className="app-header">
|
| 245 |
+
<div className="logo">
|
| 246 |
+
<Layers size={28} />
|
| 247 |
+
<h1>AIOptimize™</h1>
|
| 248 |
+
</div>
|
| 249 |
+
<p className="tagline">AI-Powered Industrial Estate Planning</p>
|
| 250 |
+
</header>
|
| 251 |
+
|
| 252 |
+
{state.error && (
|
| 253 |
+
<div className="error-banner">
|
| 254 |
+
<AlertCircle size={18} />
|
| 255 |
+
<span>{state.error}</span>
|
| 256 |
+
<button onClick={() => setState(prev => ({ ...prev, error: null }))}>×</button>
|
| 257 |
+
</div>
|
| 258 |
+
)}
|
| 259 |
+
|
| 260 |
+
<main className="app-main">
|
| 261 |
+
<div className="left-panel">
|
| 262 |
+
<FileUploadPanel
|
| 263 |
+
onUpload={handleUpload}
|
| 264 |
+
onSampleData={handleSampleData}
|
| 265 |
+
loading={uploadLoading}
|
| 266 |
+
hasData={!!state.boundary}
|
| 267 |
+
/>
|
| 268 |
+
|
| 269 |
+
{state.boundary && (
|
| 270 |
+
<div className="generate-section">
|
| 271 |
+
<button
|
| 272 |
+
className="btn btn-primary btn-generate"
|
| 273 |
+
onClick={handleGenerate}
|
| 274 |
+
disabled={generateLoading}
|
| 275 |
+
>
|
| 276 |
+
{generateLoading ? (
|
| 277 |
+
<>
|
| 278 |
+
<Loader2 size={18} className="spin" />
|
| 279 |
+
Optimizing...
|
| 280 |
+
</>
|
| 281 |
+
) : (
|
| 282 |
+
<>
|
| 283 |
+
<Zap size={18} />
|
| 284 |
+
Generate Layouts
|
| 285 |
+
</>
|
| 286 |
+
)}
|
| 287 |
+
</button>
|
| 288 |
+
</div>
|
| 289 |
+
)}
|
| 290 |
+
|
| 291 |
+
<div className="map-section">
|
| 292 |
+
<Map2DPlotter
|
| 293 |
+
boundaryCoords={state.boundaryCoords}
|
| 294 |
+
metadata={state.metadata}
|
| 295 |
+
selectedOption={selectedOptionData}
|
| 296 |
+
width={720}
|
| 297 |
+
height={500}
|
| 298 |
+
/>
|
| 299 |
+
</div>
|
| 300 |
+
|
| 301 |
+
<LayoutOptionsPanel
|
| 302 |
+
options={state.options}
|
| 303 |
+
selectedOptionId={state.selectedOption}
|
| 304 |
+
onSelect={handleSelectOption}
|
| 305 |
+
onExport={handleExportDxf}
|
| 306 |
+
loading={exportLoading}
|
| 307 |
+
/>
|
| 308 |
+
|
| 309 |
+
<ExportPanel
|
| 310 |
+
hasLayouts={state.options.length > 0}
|
| 311 |
+
onExportAll={handleExportAll}
|
| 312 |
+
loading={exportLoading}
|
| 313 |
+
/>
|
| 314 |
+
</div>
|
| 315 |
+
|
| 316 |
+
<div className="right-panel">
|
| 317 |
+
<ChatInterface
|
| 318 |
+
messages={state.messages}
|
| 319 |
+
onSendMessage={handleChat}
|
| 320 |
+
loading={chatLoading}
|
| 321 |
+
disabled={!state.sessionId}
|
| 322 |
+
/>
|
| 323 |
+
</div>
|
| 324 |
+
</main>
|
| 325 |
+
|
| 326 |
+
<footer className="app-footer">
|
| 327 |
+
<p>AIOptimize™ MVP • Built with React + FastAPI + Genetic Algorithm</p>
|
| 328 |
+
</footer>
|
| 329 |
+
</div>
|
| 330 |
+
);
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
export default App;
|
frontend/src/assets/react.svg
ADDED
|
|
frontend/src/components/ChatInterface.tsx
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ChatInterface Component - AI Chat for layout questions
|
| 2 |
+
import React, { useState, useRef, useEffect } from 'react';
|
| 3 |
+
import { Send, Bot, User, Sparkles } from 'lucide-react';
|
| 4 |
+
import type { ChatMessage } from '../types';
|
| 5 |
+
|
| 6 |
+
interface ChatInterfaceProps {
|
| 7 |
+
messages: ChatMessage[];
|
| 8 |
+
onSendMessage: (message: string) => void;
|
| 9 |
+
loading: boolean;
|
| 10 |
+
disabled: boolean;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export const ChatInterface: React.FC<ChatInterfaceProps> = ({
|
| 14 |
+
messages,
|
| 15 |
+
onSendMessage,
|
| 16 |
+
loading,
|
| 17 |
+
disabled,
|
| 18 |
+
}) => {
|
| 19 |
+
const [input, setInput] = useState('');
|
| 20 |
+
const messagesEndRef = useRef<HTMLDivElement>(null);
|
| 21 |
+
|
| 22 |
+
// Auto-scroll to bottom on new messages
|
| 23 |
+
useEffect(() => {
|
| 24 |
+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 25 |
+
}, [messages]);
|
| 26 |
+
|
| 27 |
+
const handleSubmit = (e: React.FormEvent) => {
|
| 28 |
+
e.preventDefault();
|
| 29 |
+
if (input.trim() && !loading && !disabled) {
|
| 30 |
+
onSendMessage(input.trim());
|
| 31 |
+
setInput('');
|
| 32 |
+
}
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
const suggestedQuestions = [
|
| 36 |
+
"What's the difference between the options?",
|
| 37 |
+
"Which layout do you recommend?",
|
| 38 |
+
"How does the optimization work?",
|
| 39 |
+
"What are the compliance requirements?",
|
| 40 |
+
];
|
| 41 |
+
|
| 42 |
+
return (
|
| 43 |
+
<div className="chat-interface">
|
| 44 |
+
<div className="chat-header">
|
| 45 |
+
<Bot size={20} />
|
| 46 |
+
<h3>AI Assistant</h3>
|
| 47 |
+
<Sparkles size={16} className="sparkle" />
|
| 48 |
+
</div>
|
| 49 |
+
|
| 50 |
+
<div className="chat-messages">
|
| 51 |
+
{messages.length === 0 ? (
|
| 52 |
+
<div className="chat-welcome">
|
| 53 |
+
<Bot size={40} className="welcome-icon" />
|
| 54 |
+
<h4>Ask about your layouts</h4>
|
| 55 |
+
<p>I can help you understand optimization options, metrics, and compliance.</p>
|
| 56 |
+
|
| 57 |
+
<div className="suggested-questions">
|
| 58 |
+
{suggestedQuestions.map((q, i) => (
|
| 59 |
+
<button
|
| 60 |
+
key={i}
|
| 61 |
+
className="suggested-btn"
|
| 62 |
+
onClick={() => onSendMessage(q)}
|
| 63 |
+
disabled={disabled || loading}
|
| 64 |
+
>
|
| 65 |
+
{q}
|
| 66 |
+
</button>
|
| 67 |
+
))}
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
) : (
|
| 71 |
+
messages.map((msg, index) => (
|
| 72 |
+
<div key={index} className={`message ${msg.role}`}>
|
| 73 |
+
<div className="message-icon">
|
| 74 |
+
{msg.role === 'user' ? <User size={16} /> : <Bot size={16} />}
|
| 75 |
+
</div>
|
| 76 |
+
<div className="message-content">
|
| 77 |
+
<div className="message-text">{msg.content}</div>
|
| 78 |
+
{msg.role === 'assistant' && msg.model && (
|
| 79 |
+
<div className={`model-badge ${msg.model === 'gemini-2.0-flash' ? 'gemini' : 'fallback'}`}>
|
| 80 |
+
{msg.model === 'gemini-2.0-flash' ? '🤖 Powered by Gemini' : '💬 Fallback Mode'}
|
| 81 |
+
</div>
|
| 82 |
+
)}
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
))
|
| 86 |
+
)}
|
| 87 |
+
|
| 88 |
+
{loading && (
|
| 89 |
+
<div className="message assistant loading">
|
| 90 |
+
<div className="message-icon">
|
| 91 |
+
<Bot size={16} />
|
| 92 |
+
</div>
|
| 93 |
+
<div className="message-content">
|
| 94 |
+
<div className="typing-indicator">
|
| 95 |
+
<span></span>
|
| 96 |
+
<span></span>
|
| 97 |
+
<span></span>
|
| 98 |
+
</div>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
)}
|
| 102 |
+
|
| 103 |
+
<div ref={messagesEndRef} />
|
| 104 |
+
</div>
|
| 105 |
+
|
| 106 |
+
<form className="chat-input-form" onSubmit={handleSubmit}>
|
| 107 |
+
<input
|
| 108 |
+
type="text"
|
| 109 |
+
value={input}
|
| 110 |
+
onChange={(e) => setInput(e.target.value)}
|
| 111 |
+
placeholder={disabled ? "Upload a site first..." : "Ask about your layouts..."}
|
| 112 |
+
disabled={loading || disabled}
|
| 113 |
+
className="chat-input"
|
| 114 |
+
/>
|
| 115 |
+
<button
|
| 116 |
+
type="submit"
|
| 117 |
+
disabled={!input.trim() || loading || disabled}
|
| 118 |
+
className="btn btn-send"
|
| 119 |
+
>
|
| 120 |
+
<Send size={18} />
|
| 121 |
+
</button>
|
| 122 |
+
</form>
|
| 123 |
+
</div>
|
| 124 |
+
);
|
| 125 |
+
};
|
| 126 |
+
|
| 127 |
+
export default ChatInterface;
|
frontend/src/components/ExportPanel.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ExportPanel Component - Export buttons for DXF download
|
| 2 |
+
import React from 'react';
|
| 3 |
+
import { Download, Package } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
interface ExportPanelProps {
|
| 6 |
+
hasLayouts: boolean;
|
| 7 |
+
onExportAll: () => void;
|
| 8 |
+
loading: boolean;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
export const ExportPanel: React.FC<ExportPanelProps> = ({
|
| 12 |
+
hasLayouts,
|
| 13 |
+
onExportAll,
|
| 14 |
+
loading,
|
| 15 |
+
}) => {
|
| 16 |
+
if (!hasLayouts) {
|
| 17 |
+
return null;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
return (
|
| 21 |
+
<div className="export-panel">
|
| 22 |
+
<button
|
| 23 |
+
className="btn btn-primary btn-export-all"
|
| 24 |
+
onClick={onExportAll}
|
| 25 |
+
disabled={loading}
|
| 26 |
+
>
|
| 27 |
+
<Package size={18} />
|
| 28 |
+
Export All as ZIP
|
| 29 |
+
<Download size={16} />
|
| 30 |
+
</button>
|
| 31 |
+
</div>
|
| 32 |
+
);
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
export default ExportPanel;
|
frontend/src/components/FileUploadPanel.tsx
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// FileUploadPanel Component
|
| 2 |
+
import React, { useRef } from 'react';
|
| 3 |
+
import { Upload, FileJson, Zap } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
interface FileUploadPanelProps {
|
| 6 |
+
onUpload: (file: File) => void;
|
| 7 |
+
onSampleData: () => void;
|
| 8 |
+
loading: boolean;
|
| 9 |
+
hasData: boolean;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export const FileUploadPanel: React.FC<FileUploadPanelProps> = ({
|
| 13 |
+
onUpload,
|
| 14 |
+
onSampleData,
|
| 15 |
+
loading,
|
| 16 |
+
hasData,
|
| 17 |
+
}) => {
|
| 18 |
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 19 |
+
|
| 20 |
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
| 21 |
+
const file = e.target.files?.[0];
|
| 22 |
+
if (file) {
|
| 23 |
+
onUpload(file);
|
| 24 |
+
}
|
| 25 |
+
};
|
| 26 |
+
|
| 27 |
+
const handleClick = () => {
|
| 28 |
+
fileInputRef.current?.click();
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
return (
|
| 32 |
+
<div className="upload-panel">
|
| 33 |
+
<div className="upload-header">
|
| 34 |
+
<FileJson size={24} />
|
| 35 |
+
<h3>Site Boundary</h3>
|
| 36 |
+
</div>
|
| 37 |
+
|
| 38 |
+
<div className="upload-buttons">
|
| 39 |
+
<button
|
| 40 |
+
className="btn btn-primary"
|
| 41 |
+
onClick={handleClick}
|
| 42 |
+
disabled={loading}
|
| 43 |
+
>
|
| 44 |
+
<Upload size={18} />
|
| 45 |
+
{hasData ? 'Replace Boundary' : 'Upload DXF / GeoJSON'}
|
| 46 |
+
</button>
|
| 47 |
+
|
| 48 |
+
<button
|
| 49 |
+
className="btn btn-secondary"
|
| 50 |
+
onClick={onSampleData}
|
| 51 |
+
disabled={loading}
|
| 52 |
+
>
|
| 53 |
+
<Zap size={18} />
|
| 54 |
+
Use Sample Data
|
| 55 |
+
</button>
|
| 56 |
+
</div>
|
| 57 |
+
|
| 58 |
+
<input
|
| 59 |
+
ref={fileInputRef}
|
| 60 |
+
type="file"
|
| 61 |
+
accept=".dxf,.geojson,.json"
|
| 62 |
+
onChange={handleFileChange}
|
| 63 |
+
style={{ display: 'none' }}
|
| 64 |
+
/>
|
| 65 |
+
|
| 66 |
+
{hasData && (
|
| 67 |
+
<div className="upload-status">
|
| 68 |
+
✅ Boundary loaded
|
| 69 |
+
</div>
|
| 70 |
+
)}
|
| 71 |
+
</div>
|
| 72 |
+
);
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
export default FileUploadPanel;
|
frontend/src/components/LayoutOptionsPanel.tsx
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// LayoutOptionsPanel - Display 3 layout options with metrics
|
| 2 |
+
import React from 'react';
|
| 3 |
+
import { Download, Check } from 'lucide-react';
|
| 4 |
+
import type { LayoutOption } from '../types';
|
| 5 |
+
|
| 6 |
+
interface LayoutOptionsPanelProps {
|
| 7 |
+
options: LayoutOption[];
|
| 8 |
+
selectedOptionId: number | null;
|
| 9 |
+
onSelect: (optionId: number) => void;
|
| 10 |
+
onExport: (optionId: number) => void;
|
| 11 |
+
loading: boolean;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
export const LayoutOptionsPanel: React.FC<LayoutOptionsPanelProps> = ({
|
| 15 |
+
options,
|
| 16 |
+
selectedOptionId,
|
| 17 |
+
onSelect,
|
| 18 |
+
onExport,
|
| 19 |
+
loading,
|
| 20 |
+
}) => {
|
| 21 |
+
if (options.length === 0) {
|
| 22 |
+
return (
|
| 23 |
+
<div className="options-panel empty">
|
| 24 |
+
<p>Generate layouts to see optimization options</p>
|
| 25 |
+
</div>
|
| 26 |
+
);
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
return (
|
| 30 |
+
<div className="options-panel">
|
| 31 |
+
<h3>Layout Options</h3>
|
| 32 |
+
<div className="options-grid">
|
| 33 |
+
{options.map((option) => (
|
| 34 |
+
<div
|
| 35 |
+
key={option.id}
|
| 36 |
+
className={`option-card ${selectedOptionId === option.id ? 'selected' : ''}`}
|
| 37 |
+
onClick={() => onSelect(option.id)}
|
| 38 |
+
>
|
| 39 |
+
<div className="option-header">
|
| 40 |
+
<span className="option-icon">{option.icon}</span>
|
| 41 |
+
<h4>{option.name}</h4>
|
| 42 |
+
{selectedOptionId === option.id && (
|
| 43 |
+
<Check size={18} className="selected-check" />
|
| 44 |
+
)}
|
| 45 |
+
</div>
|
| 46 |
+
|
| 47 |
+
<p className="option-description">{option.description}</p>
|
| 48 |
+
|
| 49 |
+
<div className="option-metrics">
|
| 50 |
+
<div className="metric">
|
| 51 |
+
<span className="metric-label">Plots</span>
|
| 52 |
+
<span className="metric-value">{option.metrics.total_plots}</span>
|
| 53 |
+
</div>
|
| 54 |
+
<div className="metric">
|
| 55 |
+
<span className="metric-label">Total Area</span>
|
| 56 |
+
<span className="metric-value">{option.metrics.total_area.toLocaleString()} m²</span>
|
| 57 |
+
</div>
|
| 58 |
+
<div className="metric">
|
| 59 |
+
<span className="metric-label">Avg Size</span>
|
| 60 |
+
<span className="metric-value">{Math.round(option.metrics.avg_size).toLocaleString()} m²</span>
|
| 61 |
+
</div>
|
| 62 |
+
<div className="metric">
|
| 63 |
+
<span className="metric-label">Fitness</span>
|
| 64 |
+
<span className="metric-value fitness">{(option.metrics.fitness * 100).toFixed(0)}%</span>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
|
| 68 |
+
<div className="option-footer">
|
| 69 |
+
<span className={`compliance ${option.metrics.compliance === 'PASS' ? 'pass' : 'fail'}`}>
|
| 70 |
+
{option.metrics.compliance === 'PASS' ? '✓ Compliant' : '✗ Issues'}
|
| 71 |
+
</span>
|
| 72 |
+
<button
|
| 73 |
+
className="btn btn-sm btn-export"
|
| 74 |
+
onClick={(e) => {
|
| 75 |
+
e.stopPropagation();
|
| 76 |
+
onExport(option.id);
|
| 77 |
+
}}
|
| 78 |
+
disabled={loading}
|
| 79 |
+
>
|
| 80 |
+
<Download size={14} />
|
| 81 |
+
DXF
|
| 82 |
+
</button>
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
))}
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
);
|
| 89 |
+
};
|
| 90 |
+
|
| 91 |
+
export default LayoutOptionsPanel;
|
frontend/src/components/Map2DPlotter.tsx
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Map2DPlotter Component - Konva.js canvas with zoom/pan for site visualization
|
| 2 |
+
import React, { useMemo, useState, useRef, useCallback } from 'react';
|
| 3 |
+
import { Stage, Layer, Line, Rect, Text, Group } from 'react-konva';
|
| 4 |
+
import { ZoomIn, ZoomOut, Move, Maximize2 } from 'lucide-react';
|
| 5 |
+
import type { LayoutOption, SiteMetadata } from '../types';
|
| 6 |
+
import type Konva from 'konva';
|
| 7 |
+
|
| 8 |
+
interface Map2DPlotterProps {
|
| 9 |
+
boundaryCoords: number[][] | null;
|
| 10 |
+
metadata: SiteMetadata | null;
|
| 11 |
+
selectedOption: LayoutOption | null;
|
| 12 |
+
width?: number;
|
| 13 |
+
height?: number;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
export const Map2DPlotter: React.FC<Map2DPlotterProps> = ({
|
| 17 |
+
boundaryCoords,
|
| 18 |
+
metadata,
|
| 19 |
+
selectedOption,
|
| 20 |
+
width = 800,
|
| 21 |
+
height = 600,
|
| 22 |
+
}) => {
|
| 23 |
+
// Zoom and pan state
|
| 24 |
+
const [scale, setScale] = useState(1);
|
| 25 |
+
const [position, setPosition] = useState({ x: 0, y: 0 });
|
| 26 |
+
const stageRef = useRef<Konva.Stage>(null);
|
| 27 |
+
|
| 28 |
+
// Calculate base transform to fit boundary in canvas
|
| 29 |
+
const baseTransform = useMemo(() => {
|
| 30 |
+
if (!boundaryCoords || boundaryCoords.length === 0) {
|
| 31 |
+
return { scale: 1, offsetX: 0, offsetY: 0, minX: 0, minY: 0 };
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
const xs = boundaryCoords.map(c => c[0]);
|
| 35 |
+
const ys = boundaryCoords.map(c => c[1]);
|
| 36 |
+
|
| 37 |
+
const minX = Math.min(...xs);
|
| 38 |
+
const maxX = Math.max(...xs);
|
| 39 |
+
const minY = Math.min(...ys);
|
| 40 |
+
const maxY = Math.max(...ys);
|
| 41 |
+
|
| 42 |
+
const dataWidth = maxX - minX;
|
| 43 |
+
const dataHeight = maxY - minY;
|
| 44 |
+
|
| 45 |
+
const padding = 60;
|
| 46 |
+
const scaleX = (width - padding * 2) / dataWidth;
|
| 47 |
+
const scaleY = (height - padding * 2) / dataHeight;
|
| 48 |
+
const baseScale = Math.min(scaleX, scaleY) * 0.85;
|
| 49 |
+
|
| 50 |
+
const offsetX = padding + (width - padding * 2 - dataWidth * baseScale) / 2 - minX * baseScale;
|
| 51 |
+
const offsetY = padding + (height - padding * 2 - dataHeight * baseScale) / 2 + maxY * baseScale;
|
| 52 |
+
|
| 53 |
+
return { scale: baseScale, offsetX, offsetY, minX, minY, maxY };
|
| 54 |
+
}, [boundaryCoords, width, height]);
|
| 55 |
+
|
| 56 |
+
// Transform coordinates (flip Y for screen coords)
|
| 57 |
+
const transformPoint = useCallback((x: number, y: number): [number, number] => {
|
| 58 |
+
return [
|
| 59 |
+
x * baseTransform.scale + baseTransform.offsetX,
|
| 60 |
+
baseTransform.offsetY - y * baseTransform.scale,
|
| 61 |
+
];
|
| 62 |
+
}, [baseTransform]);
|
| 63 |
+
|
| 64 |
+
// Flatten boundary coords for Konva Line
|
| 65 |
+
const boundaryPoints = useMemo(() => {
|
| 66 |
+
if (!boundaryCoords) return [];
|
| 67 |
+
return boundaryCoords.flatMap(([x, y]) => transformPoint(x, y));
|
| 68 |
+
}, [boundaryCoords, transformPoint]);
|
| 69 |
+
|
| 70 |
+
// Calculate setback boundary (50m inside)
|
| 71 |
+
const setbackPoints = useMemo(() => {
|
| 72 |
+
if (!boundaryCoords || boundaryCoords.length < 3) return [];
|
| 73 |
+
|
| 74 |
+
const xs = boundaryCoords.map(c => c[0]);
|
| 75 |
+
const ys = boundaryCoords.map(c => c[1]);
|
| 76 |
+
const centerX = xs.reduce((a, b) => a + b, 0) / xs.length;
|
| 77 |
+
const centerY = ys.reduce((a, b) => a + b, 0) / ys.length;
|
| 78 |
+
|
| 79 |
+
const shrinkFactor = 0.82;
|
| 80 |
+
const shrunkCoords = boundaryCoords.map(([x, y]) => [
|
| 81 |
+
centerX + (x - centerX) * shrinkFactor,
|
| 82 |
+
centerY + (y - centerY) * shrinkFactor,
|
| 83 |
+
]);
|
| 84 |
+
|
| 85 |
+
return shrunkCoords.flatMap(([x, y]) => transformPoint(x, y));
|
| 86 |
+
}, [boundaryCoords, transformPoint]);
|
| 87 |
+
|
| 88 |
+
// Handle zoom
|
| 89 |
+
const handleZoom = (delta: number) => {
|
| 90 |
+
const newScale = Math.min(Math.max(scale + delta, 0.5), 3);
|
| 91 |
+
setScale(newScale);
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
+
// Handle wheel zoom
|
| 95 |
+
const handleWheel = (e: Konva.KonvaEventObject<WheelEvent>) => {
|
| 96 |
+
e.evt.preventDefault();
|
| 97 |
+
const delta = e.evt.deltaY > 0 ? -0.1 : 0.1;
|
| 98 |
+
handleZoom(delta);
|
| 99 |
+
};
|
| 100 |
+
|
| 101 |
+
// Reset view
|
| 102 |
+
const resetView = () => {
|
| 103 |
+
setScale(1);
|
| 104 |
+
setPosition({ x: 0, y: 0 });
|
| 105 |
+
};
|
| 106 |
+
|
| 107 |
+
// Render plots from selected option
|
| 108 |
+
const renderPlots = () => {
|
| 109 |
+
if (!selectedOption?.plots) return null;
|
| 110 |
+
|
| 111 |
+
return selectedOption.plots.map((plot, index) => {
|
| 112 |
+
// Transform plot position
|
| 113 |
+
const [x, y] = transformPoint(plot.x, plot.y + plot.height);
|
| 114 |
+
const w = plot.width * baseTransform.scale;
|
| 115 |
+
const h = plot.height * baseTransform.scale;
|
| 116 |
+
|
| 117 |
+
// Color based on index
|
| 118 |
+
const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6'];
|
| 119 |
+
const color = colors[index % colors.length];
|
| 120 |
+
|
| 121 |
+
return (
|
| 122 |
+
<Group key={index}>
|
| 123 |
+
<Rect
|
| 124 |
+
x={x}
|
| 125 |
+
y={y}
|
| 126 |
+
width={w}
|
| 127 |
+
height={h}
|
| 128 |
+
fill={`${color}33`}
|
| 129 |
+
stroke={color}
|
| 130 |
+
strokeWidth={2}
|
| 131 |
+
cornerRadius={2}
|
| 132 |
+
/>
|
| 133 |
+
<Text
|
| 134 |
+
x={x + w / 2 - 12}
|
| 135 |
+
y={y + h / 2 - 10}
|
| 136 |
+
text={`P${index + 1}`}
|
| 137 |
+
fontSize={14}
|
| 138 |
+
fill={color}
|
| 139 |
+
fontStyle="bold"
|
| 140 |
+
/>
|
| 141 |
+
<Text
|
| 142 |
+
x={x + w / 2 - 25}
|
| 143 |
+
y={y + h / 2 + 6}
|
| 144 |
+
text={`${Math.round(plot.area)}m²`}
|
| 145 |
+
fontSize={10}
|
| 146 |
+
fill="#666"
|
| 147 |
+
/>
|
| 148 |
+
</Group>
|
| 149 |
+
);
|
| 150 |
+
});
|
| 151 |
+
};
|
| 152 |
+
|
| 153 |
+
return (
|
| 154 |
+
<div className="map-wrapper">
|
| 155 |
+
{/* Zoom controls */}
|
| 156 |
+
<div className="map-controls">
|
| 157 |
+
<button onClick={() => handleZoom(0.2)} title="Zoom In">
|
| 158 |
+
<ZoomIn size={16} />
|
| 159 |
+
</button>
|
| 160 |
+
<button onClick={() => handleZoom(-0.2)} title="Zoom Out">
|
| 161 |
+
<ZoomOut size={16} />
|
| 162 |
+
</button>
|
| 163 |
+
<button onClick={resetView} title="Reset View">
|
| 164 |
+
<Maximize2 size={16} />
|
| 165 |
+
</button>
|
| 166 |
+
<span className="zoom-level">{Math.round(scale * 100)}%</span>
|
| 167 |
+
</div>
|
| 168 |
+
|
| 169 |
+
<div className="map-container">
|
| 170 |
+
<Stage
|
| 171 |
+
ref={stageRef}
|
| 172 |
+
width={width}
|
| 173 |
+
height={height}
|
| 174 |
+
scaleX={scale}
|
| 175 |
+
scaleY={scale}
|
| 176 |
+
x={position.x}
|
| 177 |
+
y={position.y}
|
| 178 |
+
draggable
|
| 179 |
+
onWheel={handleWheel}
|
| 180 |
+
onDragEnd={(e) => setPosition({ x: e.target.x(), y: e.target.y() })}
|
| 181 |
+
>
|
| 182 |
+
<Layer>
|
| 183 |
+
{/* Background grid */}
|
| 184 |
+
{[...Array(25)].map((_, i) => (
|
| 185 |
+
<Line
|
| 186 |
+
key={`grid-h-${i}`}
|
| 187 |
+
points={[0, i * (height / 25), width, i * (height / 25)]}
|
| 188 |
+
stroke="#f0f0f0"
|
| 189 |
+
strokeWidth={1}
|
| 190 |
+
/>
|
| 191 |
+
))}
|
| 192 |
+
{[...Array(25)].map((_, i) => (
|
| 193 |
+
<Line
|
| 194 |
+
key={`grid-v-${i}`}
|
| 195 |
+
points={[i * (width / 25), 0, i * (width / 25), height]}
|
| 196 |
+
stroke="#f0f0f0"
|
| 197 |
+
strokeWidth={1}
|
| 198 |
+
/>
|
| 199 |
+
))}
|
| 200 |
+
|
| 201 |
+
{/* Site boundary */}
|
| 202 |
+
{boundaryPoints.length > 0 && (
|
| 203 |
+
<Line
|
| 204 |
+
points={boundaryPoints}
|
| 205 |
+
closed
|
| 206 |
+
stroke="#1e293b"
|
| 207 |
+
strokeWidth={3}
|
| 208 |
+
fill="rgba(226, 232, 240, 0.3)"
|
| 209 |
+
/>
|
| 210 |
+
)}
|
| 211 |
+
|
| 212 |
+
{/* Setback zone */}
|
| 213 |
+
{setbackPoints.length > 0 && (
|
| 214 |
+
<Line
|
| 215 |
+
points={setbackPoints}
|
| 216 |
+
closed
|
| 217 |
+
stroke="#ef4444"
|
| 218 |
+
strokeWidth={2}
|
| 219 |
+
dash={[10, 5]}
|
| 220 |
+
fill="rgba(239, 68, 68, 0.05)"
|
| 221 |
+
/>
|
| 222 |
+
)}
|
| 223 |
+
|
| 224 |
+
{/* Plots */}
|
| 225 |
+
{renderPlots()}
|
| 226 |
+
|
| 227 |
+
{/* Legend */}
|
| 228 |
+
<Group x={10} y={10}>
|
| 229 |
+
<Rect x={0} y={0} width={170} height={90} fill="white" opacity={0.95} cornerRadius={8} shadowBlur={5} shadowColor="rgba(0,0,0,0.1)" />
|
| 230 |
+
<Text x={10} y={8} text="Legend" fontSize={12} fontStyle="bold" fill="#333" />
|
| 231 |
+
<Line points={[10, 30, 35, 30]} stroke="#1e293b" strokeWidth={3} />
|
| 232 |
+
<Text x={45} y={24} text="Site Boundary" fontSize={11} fill="#666" />
|
| 233 |
+
<Line points={[10, 50, 35, 50]} stroke="#ef4444" strokeWidth={2} dash={[5, 3]} />
|
| 234 |
+
<Text x={45} y={44} text="Setback (50m)" fontSize={11} fill="#666" />
|
| 235 |
+
<Rect x={10} y={62} width={25} height={18} fill="rgba(59, 130, 246, 0.2)" stroke="#3B82F6" strokeWidth={2} />
|
| 236 |
+
<Text x={45} y={66} text="Industrial Plots" fontSize={11} fill="#666" />
|
| 237 |
+
</Group>
|
| 238 |
+
|
| 239 |
+
{/* Metadata */}
|
| 240 |
+
{metadata && (
|
| 241 |
+
<Group x={width - 160} y={10}>
|
| 242 |
+
<Rect x={0} y={0} width={150} height={60} fill="white" opacity={0.95} cornerRadius={8} shadowBlur={5} shadowColor="rgba(0,0,0,0.1)" />
|
| 243 |
+
<Text x={10} y={8} text="Site Info" fontSize={12} fontStyle="bold" fill="#333" />
|
| 244 |
+
<Text x={10} y={26} text={`Area: ${(metadata.area / 10000).toFixed(2)} ha`} fontSize={11} fill="#666" />
|
| 245 |
+
<Text x={10} y={42} text={`Perimeter: ${metadata.perimeter.toFixed(0)} m`} fontSize={11} fill="#666" />
|
| 246 |
+
</Group>
|
| 247 |
+
)}
|
| 248 |
+
|
| 249 |
+
{/* Empty state */}
|
| 250 |
+
{!boundaryCoords && (
|
| 251 |
+
<Group>
|
| 252 |
+
<Rect x={width / 2 - 120} y={height / 2 - 30} width={240} height={60} fill="#f8fafc" cornerRadius={8} />
|
| 253 |
+
<Text
|
| 254 |
+
x={width / 2 - 100}
|
| 255 |
+
y={height / 2 - 10}
|
| 256 |
+
text="Upload DXF or GeoJSON to start"
|
| 257 |
+
fontSize={14}
|
| 258 |
+
fill="#94a3b8"
|
| 259 |
+
/>
|
| 260 |
+
</Group>
|
| 261 |
+
)}
|
| 262 |
+
</Layer>
|
| 263 |
+
</Stage>
|
| 264 |
+
</div>
|
| 265 |
+
|
| 266 |
+
{/* Drag hint */}
|
| 267 |
+
<div className="map-hint">
|
| 268 |
+
<Move size={12} /> Drag to pan • Scroll to zoom
|
| 269 |
+
</div>
|
| 270 |
+
</div>
|
| 271 |
+
);
|
| 272 |
+
};
|
| 273 |
+
|
| 274 |
+
export default Map2DPlotter;
|
frontend/src/components/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Components index
|
| 2 |
+
export { FileUploadPanel } from './FileUploadPanel';
|
| 3 |
+
export { Map2DPlotter } from './Map2DPlotter';
|
| 4 |
+
export { LayoutOptionsPanel } from './LayoutOptionsPanel';
|
| 5 |
+
export { ChatInterface } from './ChatInterface';
|
| 6 |
+
export { ExportPanel } from './ExportPanel';
|
frontend/src/index.css
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* Base styles - reset default Vite styles */
|
| 2 |
+
:root {
|
| 3 |
+
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
| 4 |
+
line-height: 1.5;
|
| 5 |
+
font-weight: 400;
|
| 6 |
+
font-synthesis: none;
|
| 7 |
+
text-rendering: optimizeLegibility;
|
| 8 |
+
-webkit-font-smoothing: antialiased;
|
| 9 |
+
-moz-osx-font-smoothing: grayscale;
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
* {
|
| 13 |
+
box-sizing: border-box;
|
| 14 |
+
margin: 0;
|
| 15 |
+
padding: 0;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
body {
|
| 19 |
+
margin: 0;
|
| 20 |
+
min-width: 320px;
|
| 21 |
+
min-height: 100vh;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
#root {
|
| 25 |
+
min-height: 100vh;
|
| 26 |
+
}
|
frontend/src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from 'react'
|
| 2 |
+
import { createRoot } from 'react-dom/client'
|
| 3 |
+
import './index.css'
|
| 4 |
+
import App from './App.tsx'
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>,
|
| 10 |
+
)
|
frontend/src/services/api.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// API service for AIOptimize
|
| 2 |
+
|
| 3 |
+
import axios from 'axios';
|
| 4 |
+
import type {
|
| 5 |
+
UploadResponse,
|
| 6 |
+
GenerateResponse,
|
| 7 |
+
ChatResponse,
|
| 8 |
+
GeoJSONFeature
|
| 9 |
+
} from '../types';
|
| 10 |
+
|
| 11 |
+
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
| 12 |
+
|
| 13 |
+
const api = axios.create({
|
| 14 |
+
baseURL: API_BASE,
|
| 15 |
+
headers: {
|
| 16 |
+
'Content-Type': 'application/json',
|
| 17 |
+
},
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
export const apiService = {
|
| 21 |
+
// Health check
|
| 22 |
+
async health() {
|
| 23 |
+
const response = await api.get('/api/health');
|
| 24 |
+
return response.data;
|
| 25 |
+
},
|
| 26 |
+
|
| 27 |
+
// Get sample data
|
| 28 |
+
async getSampleData(): Promise<GeoJSONFeature> {
|
| 29 |
+
const response = await api.get('/api/sample-data');
|
| 30 |
+
return response.data;
|
| 31 |
+
},
|
| 32 |
+
|
| 33 |
+
// Upload boundary (JSON)
|
| 34 |
+
async uploadBoundary(geojson: GeoJSONFeature): Promise<UploadResponse> {
|
| 35 |
+
const response = await api.post('/api/upload-boundary-json', {
|
| 36 |
+
geojson: geojson,
|
| 37 |
+
});
|
| 38 |
+
return response.data;
|
| 39 |
+
},
|
| 40 |
+
|
| 41 |
+
// Upload boundary file (DXF or GeoJSON)
|
| 42 |
+
async uploadBoundaryFile(file: File): Promise<UploadResponse> {
|
| 43 |
+
const formData = new FormData();
|
| 44 |
+
formData.append('file', file);
|
| 45 |
+
|
| 46 |
+
// Use DXF endpoint for .dxf files
|
| 47 |
+
const isDxf = file.name.toLowerCase().endsWith('.dxf');
|
| 48 |
+
const endpoint = isDxf ? '/api/upload-dxf' : '/api/upload-boundary';
|
| 49 |
+
|
| 50 |
+
const response = await api.post(endpoint, formData, {
|
| 51 |
+
headers: { 'Content-Type': 'multipart/form-data' },
|
| 52 |
+
});
|
| 53 |
+
return response.data;
|
| 54 |
+
},
|
| 55 |
+
|
| 56 |
+
// Generate layouts
|
| 57 |
+
async generateLayouts(
|
| 58 |
+
sessionId: string,
|
| 59 |
+
targetPlots: number = 8,
|
| 60 |
+
setback: number = 50
|
| 61 |
+
): Promise<GenerateResponse> {
|
| 62 |
+
const response = await api.post('/api/generate-layouts', {
|
| 63 |
+
session_id: sessionId,
|
| 64 |
+
target_plots: targetPlots,
|
| 65 |
+
setback: setback,
|
| 66 |
+
});
|
| 67 |
+
return response.data;
|
| 68 |
+
},
|
| 69 |
+
|
| 70 |
+
// Chat
|
| 71 |
+
async chat(sessionId: string, message: string): Promise<ChatResponse> {
|
| 72 |
+
const response = await api.post('/api/chat', {
|
| 73 |
+
session_id: sessionId,
|
| 74 |
+
message: message,
|
| 75 |
+
});
|
| 76 |
+
return response.data;
|
| 77 |
+
},
|
| 78 |
+
|
| 79 |
+
// Export DXF
|
| 80 |
+
async exportDxf(sessionId: string, optionId: number): Promise<Blob> {
|
| 81 |
+
const response = await api.post('/api/export-dxf', {
|
| 82 |
+
session_id: sessionId,
|
| 83 |
+
option_id: optionId,
|
| 84 |
+
}, {
|
| 85 |
+
responseType: 'blob',
|
| 86 |
+
});
|
| 87 |
+
return response.data;
|
| 88 |
+
},
|
| 89 |
+
|
| 90 |
+
// Export all as ZIP
|
| 91 |
+
async exportAllDxf(sessionId: string): Promise<Blob> {
|
| 92 |
+
const formData = new FormData();
|
| 93 |
+
formData.append('session_id', sessionId);
|
| 94 |
+
|
| 95 |
+
const response = await api.post('/api/export-all-dxf', formData, {
|
| 96 |
+
responseType: 'blob',
|
| 97 |
+
});
|
| 98 |
+
return response.data;
|
| 99 |
+
},
|
| 100 |
+
};
|
| 101 |
+
|
| 102 |
+
export default apiService;
|
frontend/src/types/index.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Type definitions for AIOptimize MVP
|
| 2 |
+
|
| 3 |
+
export interface Coordinates {
|
| 4 |
+
x: number;
|
| 5 |
+
y: number;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export interface PlotData {
|
| 9 |
+
x: number;
|
| 10 |
+
y: number;
|
| 11 |
+
width: number;
|
| 12 |
+
height: number;
|
| 13 |
+
area: number;
|
| 14 |
+
coords: number[][];
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export interface LayoutMetrics {
|
| 18 |
+
total_plots: number;
|
| 19 |
+
total_area: number;
|
| 20 |
+
avg_size: number;
|
| 21 |
+
fitness: number;
|
| 22 |
+
compliance: string;
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
export interface LayoutOption {
|
| 26 |
+
id: number;
|
| 27 |
+
name: string;
|
| 28 |
+
icon: string;
|
| 29 |
+
description: string;
|
| 30 |
+
plots: PlotData[];
|
| 31 |
+
metrics: LayoutMetrics;
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export interface SiteMetadata {
|
| 35 |
+
area: number;
|
| 36 |
+
perimeter: number;
|
| 37 |
+
bounds: number[];
|
| 38 |
+
centroid: number[];
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
export interface GeoJSONGeometry {
|
| 42 |
+
type: string;
|
| 43 |
+
coordinates: number[][][];
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
export interface GeoJSONFeature {
|
| 47 |
+
type: string;
|
| 48 |
+
geometry: GeoJSONGeometry;
|
| 49 |
+
properties?: Record<string, unknown>;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
export interface ChatMessage {
|
| 53 |
+
role: 'user' | 'assistant';
|
| 54 |
+
content: string;
|
| 55 |
+
model?: string;
|
| 56 |
+
timestamp?: string;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
export interface UploadResponse {
|
| 60 |
+
session_id: string;
|
| 61 |
+
boundary: GeoJSONFeature;
|
| 62 |
+
metadata: SiteMetadata;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
export interface GenerateResponse {
|
| 66 |
+
session_id: string;
|
| 67 |
+
options: LayoutOption[];
|
| 68 |
+
count: number;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
export interface ChatResponse {
|
| 72 |
+
message: string;
|
| 73 |
+
model: 'gemini-2.0-flash' | 'fallback';
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
export interface AppState {
|
| 77 |
+
sessionId: string | null;
|
| 78 |
+
boundary: GeoJSONFeature | null;
|
| 79 |
+
boundaryCoords: number[][] | null;
|
| 80 |
+
metadata: SiteMetadata | null;
|
| 81 |
+
options: LayoutOption[];
|
| 82 |
+
selectedOption: number | null;
|
| 83 |
+
messages: ChatMessage[];
|
| 84 |
+
loading: boolean;
|
| 85 |
+
error: string | null;
|
| 86 |
+
}
|
frontend/tsconfig.app.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
| 4 |
+
"target": "ES2022",
|
| 5 |
+
"useDefineForClassFields": true,
|
| 6 |
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
| 7 |
+
"module": "ESNext",
|
| 8 |
+
"types": ["vite/client"],
|
| 9 |
+
"skipLibCheck": true,
|
| 10 |
+
|
| 11 |
+
/* Bundler mode */
|
| 12 |
+
"moduleResolution": "bundler",
|
| 13 |
+
"allowImportingTsExtensions": true,
|
| 14 |
+
"verbatimModuleSyntax": true,
|
| 15 |
+
"moduleDetection": "force",
|
| 16 |
+
"noEmit": true,
|
| 17 |
+
"jsx": "react-jsx",
|
| 18 |
+
|
| 19 |
+
/* Linting */
|
| 20 |
+
"strict": true,
|
| 21 |
+
"noUnusedLocals": true,
|
| 22 |
+
"noUnusedParameters": true,
|
| 23 |
+
"erasableSyntaxOnly": true,
|
| 24 |
+
"noFallthroughCasesInSwitch": true,
|
| 25 |
+
"noUncheckedSideEffectImports": true
|
| 26 |
+
},
|
| 27 |
+
"include": ["src"]
|
| 28 |
+
}
|
frontend/tsconfig.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"files": [],
|
| 3 |
+
"references": [
|
| 4 |
+
{ "path": "./tsconfig.app.json" },
|
| 5 |
+
{ "path": "./tsconfig.node.json" }
|
| 6 |
+
]
|
| 7 |
+
}
|
frontend/tsconfig.node.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
| 4 |
+
"target": "ES2023",
|
| 5 |
+
"lib": ["ES2023"],
|
| 6 |
+
"module": "ESNext",
|
| 7 |
+
"types": ["node"],
|
| 8 |
+
"skipLibCheck": true,
|
| 9 |
+
|
| 10 |
+
/* Bundler mode */
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"allowImportingTsExtensions": true,
|
| 13 |
+
"verbatimModuleSyntax": true,
|
| 14 |
+
"moduleDetection": "force",
|
| 15 |
+
"noEmit": true,
|
| 16 |
+
|
| 17 |
+
/* Linting */
|
| 18 |
+
"strict": true,
|
| 19 |
+
"noUnusedLocals": true,
|
| 20 |
+
"noUnusedParameters": true,
|
| 21 |
+
"erasableSyntaxOnly": true,
|
| 22 |
+
"noFallthroughCasesInSwitch": true,
|
| 23 |
+
"noUncheckedSideEffectImports": true
|
| 24 |
+
},
|
| 25 |
+
"include": ["vite.config.ts"]
|
| 26 |
+
}
|
frontend/vite.config.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
// https://vite.dev/config/
|
| 5 |
+
export default defineConfig({
|
| 6 |
+
plugins: [react()],
|
| 7 |
+
})
|
requirements.txt
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# REMB Dependencies - Industrial Estate Master Planning Optimization Engine
|
| 2 |
+
|
| 3 |
+
# Core Optimization
|
| 4 |
+
pymoo>=0.6.0
|
| 5 |
+
ortools>=9.7.2876
|
| 6 |
+
|
| 7 |
+
# Geometry Processing
|
| 8 |
+
geopandas>=0.14.0
|
| 9 |
+
shapely>=2.0.0
|
| 10 |
+
ezdxf>=1.1.0
|
| 11 |
+
pyproj>=3.6.0
|
| 12 |
+
|
| 13 |
+
# API Framework
|
| 14 |
+
fastapi>=0.104.0
|
| 15 |
+
uvicorn[standard]>=0.24.0
|
| 16 |
+
pydantic>=2.4.0
|
| 17 |
+
pydantic-settings>=2.1.0
|
| 18 |
+
python-multipart>=0.0.6
|
| 19 |
+
|
| 20 |
+
# Database
|
| 21 |
+
sqlalchemy>=2.0.0
|
| 22 |
+
psycopg2-binary>=2.9.0
|
| 23 |
+
geoalchemy2>=0.14.0
|
| 24 |
+
alembic>=1.12.0
|
| 25 |
+
|
| 26 |
+
# Data Processing
|
| 27 |
+
numpy>=1.24.0
|
| 28 |
+
pandas>=2.0.0
|
| 29 |
+
pyyaml>=6.0
|
| 30 |
+
|
| 31 |
+
# Utilities
|
| 32 |
+
python-dotenv>=1.0.0
|
| 33 |
+
pydantic>=2.4.0
|
| 34 |
+
|
| 35 |
+
# Development & Testing
|
| 36 |
+
pytest>=7.4.0
|
| 37 |
+
pytest-asyncio>=0.21.0
|
| 38 |
+
pytest-cov>=4.1.0
|
| 39 |
+
black>=23.10.0
|
| 40 |
+
flake8>=6.1.0
|
| 41 |
+
mypy>=1.6.0
|
src/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
REMB - Industrial Estate Master Planning Optimization Engine
|
| 3 |
+
Main package initialization
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
__version__ = "0.1.0"
|
| 7 |
+
__author__ = "PiXerse.AI Team"
|
src/algorithms/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Algorithms package"""
|
src/algorithms/ga_optimizer.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Simple Genetic Algorithm Optimizer
|
| 3 |
+
Generates 3 diverse layout options for industrial estate planning
|
| 4 |
+
Following MVP-24h.md specification
|
| 5 |
+
"""
|
| 6 |
+
import random
|
| 7 |
+
import math
|
| 8 |
+
from typing import List, Dict, Tuple, Any
|
| 9 |
+
from dataclasses import dataclass, field
|
| 10 |
+
from shapely.geometry import Polygon, box, Point
|
| 11 |
+
from shapely.ops import unary_union
|
| 12 |
+
import logging
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class PlotConfig:
|
| 19 |
+
"""Plot configuration"""
|
| 20 |
+
x: float
|
| 21 |
+
y: float
|
| 22 |
+
width: float
|
| 23 |
+
height: float
|
| 24 |
+
|
| 25 |
+
@property
|
| 26 |
+
def area(self) -> float:
|
| 27 |
+
return self.width * self.height
|
| 28 |
+
|
| 29 |
+
@property
|
| 30 |
+
def geometry(self) -> Polygon:
|
| 31 |
+
return box(self.x, self.y, self.x + self.width, self.y + self.height)
|
| 32 |
+
|
| 33 |
+
def to_dict(self) -> Dict:
|
| 34 |
+
return {
|
| 35 |
+
"x": self.x,
|
| 36 |
+
"y": self.y,
|
| 37 |
+
"width": self.width,
|
| 38 |
+
"height": self.height,
|
| 39 |
+
"area": self.area,
|
| 40 |
+
"coords": list(self.geometry.exterior.coords)
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
@dataclass
|
| 45 |
+
class LayoutCandidate:
|
| 46 |
+
"""A layout candidate in the GA population"""
|
| 47 |
+
plots: List[PlotConfig] = field(default_factory=list)
|
| 48 |
+
fitness: float = 0.0
|
| 49 |
+
|
| 50 |
+
@property
|
| 51 |
+
def total_area(self) -> float:
|
| 52 |
+
return sum(p.area for p in self.plots)
|
| 53 |
+
|
| 54 |
+
@property
|
| 55 |
+
def avg_plot_size(self) -> float:
|
| 56 |
+
return self.total_area / len(self.plots) if self.plots else 0
|
| 57 |
+
|
| 58 |
+
def to_dict(self) -> Dict:
|
| 59 |
+
return {
|
| 60 |
+
"plots": [p.to_dict() for p in self.plots],
|
| 61 |
+
"total_plots": len(self.plots),
|
| 62 |
+
"total_area": self.total_area,
|
| 63 |
+
"avg_size": self.avg_plot_size,
|
| 64 |
+
"fitness": self.fitness
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class SimpleGAOptimizer:
|
| 69 |
+
"""
|
| 70 |
+
Simple Genetic Algorithm for layout optimization
|
| 71 |
+
|
| 72 |
+
Per MVP-24h.md:
|
| 73 |
+
- Population: 10 layouts
|
| 74 |
+
- Generations: 20
|
| 75 |
+
- Elite: 3 best
|
| 76 |
+
- Mutation rate: 30%
|
| 77 |
+
- Output: 3 diverse options
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
def __init__(
|
| 81 |
+
self,
|
| 82 |
+
population_size: int = 10,
|
| 83 |
+
n_generations: int = 20,
|
| 84 |
+
elite_size: int = 3,
|
| 85 |
+
mutation_rate: float = 0.3,
|
| 86 |
+
setback: float = 50.0,
|
| 87 |
+
target_plots: int = 8
|
| 88 |
+
):
|
| 89 |
+
self.population_size = population_size
|
| 90 |
+
self.n_generations = n_generations
|
| 91 |
+
self.elite_size = elite_size
|
| 92 |
+
self.mutation_rate = mutation_rate
|
| 93 |
+
self.setback = setback
|
| 94 |
+
self.target_plots = target_plots
|
| 95 |
+
|
| 96 |
+
# Plot size ranges
|
| 97 |
+
self.min_plot_width = 30
|
| 98 |
+
self.max_plot_width = 80
|
| 99 |
+
self.min_plot_height = 40
|
| 100 |
+
self.max_plot_height = 100
|
| 101 |
+
|
| 102 |
+
def optimize(self, boundary_coords: List[List[float]]) -> List[Dict]:
|
| 103 |
+
"""
|
| 104 |
+
Run GA optimization and return 3 diverse layout options
|
| 105 |
+
|
| 106 |
+
Args:
|
| 107 |
+
boundary_coords: List of [x, y] coordinate pairs
|
| 108 |
+
|
| 109 |
+
Returns:
|
| 110 |
+
List of 3 layout options with different strategies
|
| 111 |
+
"""
|
| 112 |
+
logger.info("Starting GA optimization")
|
| 113 |
+
|
| 114 |
+
# Create boundary polygon
|
| 115 |
+
boundary = Polygon(boundary_coords)
|
| 116 |
+
if not boundary.is_valid:
|
| 117 |
+
boundary = boundary.buffer(0)
|
| 118 |
+
|
| 119 |
+
# Get buildable area (after setback)
|
| 120 |
+
buildable = boundary.buffer(-self.setback)
|
| 121 |
+
if buildable.is_empty or not buildable.is_valid:
|
| 122 |
+
logger.warning("Buildable area too small, reducing setback")
|
| 123 |
+
buildable = boundary.buffer(-self.setback / 2)
|
| 124 |
+
|
| 125 |
+
bounds = buildable.bounds # (minx, miny, maxx, maxy)
|
| 126 |
+
|
| 127 |
+
# Initialize population
|
| 128 |
+
population = self._initialize_population(buildable, bounds)
|
| 129 |
+
|
| 130 |
+
# Evolution loop
|
| 131 |
+
for gen in range(self.n_generations):
|
| 132 |
+
# Evaluate fitness
|
| 133 |
+
for candidate in population:
|
| 134 |
+
candidate.fitness = self._evaluate_fitness(candidate, buildable, boundary)
|
| 135 |
+
|
| 136 |
+
# Sort by fitness
|
| 137 |
+
population.sort(key=lambda x: x.fitness, reverse=True)
|
| 138 |
+
|
| 139 |
+
# Keep elite
|
| 140 |
+
elite = population[:self.elite_size]
|
| 141 |
+
|
| 142 |
+
# Create new population from elite
|
| 143 |
+
new_population = elite.copy()
|
| 144 |
+
|
| 145 |
+
while len(new_population) < self.population_size:
|
| 146 |
+
parent = random.choice(elite)
|
| 147 |
+
child = self._mutate(parent, bounds, buildable)
|
| 148 |
+
new_population.append(child)
|
| 149 |
+
|
| 150 |
+
population = new_population
|
| 151 |
+
|
| 152 |
+
# Final sort
|
| 153 |
+
for candidate in population:
|
| 154 |
+
candidate.fitness = self._evaluate_fitness(candidate, buildable, boundary)
|
| 155 |
+
population.sort(key=lambda x: x.fitness, reverse=True)
|
| 156 |
+
|
| 157 |
+
# Create 3 diverse options
|
| 158 |
+
options = self._create_diverse_options(population, buildable, bounds, boundary)
|
| 159 |
+
|
| 160 |
+
logger.info(f"GA complete: {len(options)} options generated")
|
| 161 |
+
return options
|
| 162 |
+
|
| 163 |
+
def _initialize_population(self, buildable: Polygon, bounds: Tuple) -> List[LayoutCandidate]:
|
| 164 |
+
"""Create initial random population"""
|
| 165 |
+
population = []
|
| 166 |
+
minx, miny, maxx, maxy = bounds
|
| 167 |
+
|
| 168 |
+
for _ in range(self.population_size):
|
| 169 |
+
candidate = LayoutCandidate()
|
| 170 |
+
placed = []
|
| 171 |
+
|
| 172 |
+
for _ in range(self.target_plots):
|
| 173 |
+
# Random plot dimensions
|
| 174 |
+
width = random.uniform(self.min_plot_width, self.max_plot_width)
|
| 175 |
+
height = random.uniform(self.min_plot_height, self.max_plot_height)
|
| 176 |
+
|
| 177 |
+
# Random position
|
| 178 |
+
for attempt in range(20):
|
| 179 |
+
x = random.uniform(minx, maxx - width)
|
| 180 |
+
y = random.uniform(miny, maxy - height)
|
| 181 |
+
|
| 182 |
+
plot = PlotConfig(x=x, y=y, width=width, height=height)
|
| 183 |
+
|
| 184 |
+
# Check if within buildable and no overlap
|
| 185 |
+
if buildable.contains(plot.geometry):
|
| 186 |
+
overlaps = False
|
| 187 |
+
for existing in placed:
|
| 188 |
+
if plot.geometry.intersects(existing.geometry):
|
| 189 |
+
overlaps = True
|
| 190 |
+
break
|
| 191 |
+
|
| 192 |
+
if not overlaps:
|
| 193 |
+
placed.append(plot)
|
| 194 |
+
break
|
| 195 |
+
|
| 196 |
+
candidate.plots = placed
|
| 197 |
+
population.append(candidate)
|
| 198 |
+
|
| 199 |
+
return population
|
| 200 |
+
|
| 201 |
+
def _evaluate_fitness(self, candidate: LayoutCandidate, buildable: Polygon, boundary: Polygon) -> float:
|
| 202 |
+
"""
|
| 203 |
+
Evaluate fitness of a layout candidate
|
| 204 |
+
|
| 205 |
+
Fitness = (Profit × 0.5) + (Compliance × 0.3) + (Efficiency × 0.2)
|
| 206 |
+
"""
|
| 207 |
+
if not candidate.plots:
|
| 208 |
+
return 0.0
|
| 209 |
+
|
| 210 |
+
# Profit score (normalized total area)
|
| 211 |
+
max_area = buildable.area * 0.6 # Max 60% coverage
|
| 212 |
+
profit = min(candidate.total_area / max_area, 1.0)
|
| 213 |
+
|
| 214 |
+
# Compliance score (all plots within setback)
|
| 215 |
+
compliant = sum(1 for p in candidate.plots if buildable.contains(p.geometry))
|
| 216 |
+
compliance = compliant / len(candidate.plots)
|
| 217 |
+
|
| 218 |
+
# Efficiency score (plot count vs target)
|
| 219 |
+
efficiency = min(len(candidate.plots) / self.target_plots, 1.0)
|
| 220 |
+
|
| 221 |
+
fitness = (profit * 0.5) + (compliance * 0.3) + (efficiency * 0.2)
|
| 222 |
+
return round(fitness, 4)
|
| 223 |
+
|
| 224 |
+
def _mutate(self, parent: LayoutCandidate, bounds: Tuple, buildable: Polygon) -> LayoutCandidate:
|
| 225 |
+
"""Create mutated child from parent"""
|
| 226 |
+
child = LayoutCandidate()
|
| 227 |
+
minx, miny, maxx, maxy = bounds
|
| 228 |
+
|
| 229 |
+
for plot in parent.plots:
|
| 230 |
+
if random.random() < self.mutation_rate:
|
| 231 |
+
# Mutate position (±30m)
|
| 232 |
+
new_x = plot.x + random.uniform(-30, 30)
|
| 233 |
+
new_y = plot.y + random.uniform(-30, 30)
|
| 234 |
+
|
| 235 |
+
# Keep within bounds
|
| 236 |
+
new_x = max(minx, min(new_x, maxx - plot.width))
|
| 237 |
+
new_y = max(miny, min(new_y, maxy - plot.height))
|
| 238 |
+
|
| 239 |
+
new_plot = PlotConfig(x=new_x, y=new_y, width=plot.width, height=plot.height)
|
| 240 |
+
|
| 241 |
+
if buildable.contains(new_plot.geometry):
|
| 242 |
+
child.plots.append(new_plot)
|
| 243 |
+
else:
|
| 244 |
+
child.plots.append(plot)
|
| 245 |
+
else:
|
| 246 |
+
child.plots.append(plot)
|
| 247 |
+
|
| 248 |
+
return child
|
| 249 |
+
|
| 250 |
+
def _create_diverse_options(
|
| 251 |
+
self,
|
| 252 |
+
population: List[LayoutCandidate],
|
| 253 |
+
buildable: Polygon,
|
| 254 |
+
bounds: Tuple,
|
| 255 |
+
boundary: Polygon
|
| 256 |
+
) -> List[Dict]:
|
| 257 |
+
"""
|
| 258 |
+
Create 3 diverse layout options:
|
| 259 |
+
1. Maximum Profit (most plots)
|
| 260 |
+
2. Balanced (medium density)
|
| 261 |
+
3. Premium (fewer, larger plots)
|
| 262 |
+
"""
|
| 263 |
+
options = []
|
| 264 |
+
|
| 265 |
+
# Option 1: Maximum Profit (best fitness from GA)
|
| 266 |
+
if population:
|
| 267 |
+
best = population[0]
|
| 268 |
+
options.append({
|
| 269 |
+
"id": 1,
|
| 270 |
+
"name": "Maximum Profit",
|
| 271 |
+
"icon": "💰",
|
| 272 |
+
"description": "Maximizes sellable area with more plots",
|
| 273 |
+
"plots": [p.to_dict() for p in best.plots],
|
| 274 |
+
"metrics": {
|
| 275 |
+
"total_plots": len(best.plots),
|
| 276 |
+
"total_area": round(best.total_area, 2),
|
| 277 |
+
"avg_size": round(best.avg_plot_size, 2),
|
| 278 |
+
"fitness": best.fitness,
|
| 279 |
+
"compliance": "PASS"
|
| 280 |
+
}
|
| 281 |
+
})
|
| 282 |
+
|
| 283 |
+
# Option 2: Balanced - Generate with medium density
|
| 284 |
+
balanced = self._generate_balanced_layout(buildable, bounds)
|
| 285 |
+
options.append({
|
| 286 |
+
"id": 2,
|
| 287 |
+
"name": "Balanced",
|
| 288 |
+
"icon": "⚖️",
|
| 289 |
+
"description": "Balanced approach with medium-sized plots",
|
| 290 |
+
"plots": [p.to_dict() for p in balanced.plots],
|
| 291 |
+
"metrics": {
|
| 292 |
+
"total_plots": len(balanced.plots),
|
| 293 |
+
"total_area": round(balanced.total_area, 2),
|
| 294 |
+
"avg_size": round(balanced.avg_plot_size, 2),
|
| 295 |
+
"fitness": self._evaluate_fitness(balanced, buildable, boundary),
|
| 296 |
+
"compliance": "PASS"
|
| 297 |
+
}
|
| 298 |
+
})
|
| 299 |
+
|
| 300 |
+
# Option 3: Premium - Fewer, larger plots
|
| 301 |
+
premium = self._generate_premium_layout(buildable, bounds)
|
| 302 |
+
options.append({
|
| 303 |
+
"id": 3,
|
| 304 |
+
"name": "Premium",
|
| 305 |
+
"icon": "🏢",
|
| 306 |
+
"description": "Premium layout with fewer, larger plots",
|
| 307 |
+
"plots": [p.to_dict() for p in premium.plots],
|
| 308 |
+
"metrics": {
|
| 309 |
+
"total_plots": len(premium.plots),
|
| 310 |
+
"total_area": round(premium.total_area, 2),
|
| 311 |
+
"avg_size": round(premium.avg_plot_size, 2),
|
| 312 |
+
"fitness": self._evaluate_fitness(premium, buildable, boundary),
|
| 313 |
+
"compliance": "PASS"
|
| 314 |
+
}
|
| 315 |
+
})
|
| 316 |
+
|
| 317 |
+
return options
|
| 318 |
+
|
| 319 |
+
def _generate_balanced_layout(self, buildable: Polygon, bounds: Tuple) -> LayoutCandidate:
|
| 320 |
+
"""Generate balanced layout with medium plot sizes"""
|
| 321 |
+
candidate = LayoutCandidate()
|
| 322 |
+
minx, miny, maxx, maxy = bounds
|
| 323 |
+
|
| 324 |
+
# Medium plot size
|
| 325 |
+
plot_width = 50
|
| 326 |
+
plot_height = 70
|
| 327 |
+
spacing = 20
|
| 328 |
+
|
| 329 |
+
placed = []
|
| 330 |
+
y = miny + spacing
|
| 331 |
+
|
| 332 |
+
while y + plot_height < maxy:
|
| 333 |
+
x = minx + spacing
|
| 334 |
+
while x + plot_width < maxx:
|
| 335 |
+
plot = PlotConfig(x=x, y=y, width=plot_width, height=plot_height)
|
| 336 |
+
|
| 337 |
+
if buildable.contains(plot.geometry):
|
| 338 |
+
overlaps = False
|
| 339 |
+
for existing in placed:
|
| 340 |
+
if plot.geometry.intersects(existing.geometry):
|
| 341 |
+
overlaps = True
|
| 342 |
+
break
|
| 343 |
+
|
| 344 |
+
if not overlaps:
|
| 345 |
+
placed.append(plot)
|
| 346 |
+
|
| 347 |
+
x += plot_width + spacing
|
| 348 |
+
y += plot_height + spacing
|
| 349 |
+
|
| 350 |
+
candidate.plots = placed[:8] # Limit to 8 plots
|
| 351 |
+
return candidate
|
| 352 |
+
|
| 353 |
+
def _generate_premium_layout(self, buildable: Polygon, bounds: Tuple) -> LayoutCandidate:
|
| 354 |
+
"""Generate premium layout with fewer, larger plots"""
|
| 355 |
+
candidate = LayoutCandidate()
|
| 356 |
+
minx, miny, maxx, maxy = bounds
|
| 357 |
+
|
| 358 |
+
# Large plot size
|
| 359 |
+
plot_width = 80
|
| 360 |
+
plot_height = 100
|
| 361 |
+
spacing = 30
|
| 362 |
+
|
| 363 |
+
placed = []
|
| 364 |
+
y = miny + spacing
|
| 365 |
+
|
| 366 |
+
while y + plot_height < maxy and len(placed) < 4:
|
| 367 |
+
x = minx + spacing
|
| 368 |
+
while x + plot_width < maxx and len(placed) < 4:
|
| 369 |
+
plot = PlotConfig(x=x, y=y, width=plot_width, height=plot_height)
|
| 370 |
+
|
| 371 |
+
if buildable.contains(plot.geometry):
|
| 372 |
+
overlaps = False
|
| 373 |
+
for existing in placed:
|
| 374 |
+
if plot.geometry.intersects(existing.geometry):
|
| 375 |
+
overlaps = True
|
| 376 |
+
break
|
| 377 |
+
|
| 378 |
+
if not overlaps:
|
| 379 |
+
placed.append(plot)
|
| 380 |
+
|
| 381 |
+
x += plot_width + spacing
|
| 382 |
+
y += plot_height + spacing
|
| 383 |
+
|
| 384 |
+
candidate.plots = placed
|
| 385 |
+
return candidate
|
| 386 |
+
|
| 387 |
+
|
| 388 |
+
# Example usage
|
| 389 |
+
if __name__ == "__main__":
|
| 390 |
+
logging.basicConfig(level=logging.INFO)
|
| 391 |
+
|
| 392 |
+
# Sample boundary (simple rectangle)
|
| 393 |
+
boundary = [
|
| 394 |
+
[0, 0], [500, 0], [500, 400], [0, 400], [0, 0]
|
| 395 |
+
]
|
| 396 |
+
|
| 397 |
+
optimizer = SimpleGAOptimizer()
|
| 398 |
+
options = optimizer.optimize(boundary)
|
| 399 |
+
|
| 400 |
+
for opt in options:
|
| 401 |
+
print(f"\n{opt['icon']} {opt['name']}")
|
| 402 |
+
print(f" Plots: {opt['metrics']['total_plots']}")
|
| 403 |
+
print(f" Area: {opt['metrics']['total_area']} m²")
|
| 404 |
+
print(f" Avg: {opt['metrics']['avg_size']} m²")
|
| 405 |
+
print(f" Fitness: {opt['metrics']['fitness']}")
|
src/algorithms/milp_solver.py
ADDED
|
@@ -0,0 +1,659 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MILP Solver - Module B: The Engineer (CP Module)
|
| 3 |
+
Mixed-Integer Linear Programming solver for precision constraints using OR-Tools
|
| 4 |
+
Đảm bảo tính hợp lệ toán học tuyệt đối - loại bỏ "hallucination"
|
| 5 |
+
"""
|
| 6 |
+
import numpy as np
|
| 7 |
+
from ortools.linear_solver import pywraplp
|
| 8 |
+
from ortools.sat.python import cp_model
|
| 9 |
+
from typing import List, Tuple, Dict, Optional, Any
|
| 10 |
+
from dataclasses import dataclass, field
|
| 11 |
+
import json
|
| 12 |
+
import logging
|
| 13 |
+
|
| 14 |
+
from src.models.domain import Layout, Plot, PlotType, SiteBoundary, RoadNetwork
|
| 15 |
+
from shapely.geometry import Polygon, box, LineString, MultiLineString
|
| 16 |
+
from shapely.ops import unary_union
|
| 17 |
+
|
| 18 |
+
logger = logging.getLogger(__name__)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass
|
| 22 |
+
class MILPResult:
|
| 23 |
+
"""Result from MILP solver"""
|
| 24 |
+
status: str # 'OPTIMAL', 'FEASIBLE', 'INFEASIBLE', 'TIMEOUT'
|
| 25 |
+
objective_value: float = 0.0
|
| 26 |
+
solve_time_seconds: float = 0.0
|
| 27 |
+
plots: List[Dict[str, Any]] = field(default_factory=list)
|
| 28 |
+
error_message: Optional[str] = None
|
| 29 |
+
|
| 30 |
+
def is_success(self) -> bool:
|
| 31 |
+
return self.status in ['OPTIMAL', 'FEASIBLE']
|
| 32 |
+
|
| 33 |
+
def to_json(self) -> str:
|
| 34 |
+
"""Export result as JSON for LLM interpretation"""
|
| 35 |
+
return json.dumps({
|
| 36 |
+
'status': self.status,
|
| 37 |
+
'objective_value': self.objective_value,
|
| 38 |
+
'solve_time_seconds': self.solve_time_seconds,
|
| 39 |
+
'num_plots': len(self.plots),
|
| 40 |
+
'plots': self.plots,
|
| 41 |
+
'error_message': self.error_message
|
| 42 |
+
}, indent=2)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class MILPSolver:
|
| 46 |
+
"""
|
| 47 |
+
Mixed-Integer Linear Programming Solver - The "Muscle/Kỹ sư"
|
| 48 |
+
|
| 49 |
+
Responsibilities:
|
| 50 |
+
- Enforce non-overlapping constraints mathematically
|
| 51 |
+
- Ensure road connectivity for all plots
|
| 52 |
+
- Guarantee geometric closure and snapping
|
| 53 |
+
- Provide exact numerical solutions (no hallucination)
|
| 54 |
+
|
| 55 |
+
This is a "black-box" that receives JSON parameters and returns raw results.
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
def __init__(self, time_limit_seconds: int = 3600, solver_type: str = "SCIP"):
|
| 59 |
+
"""
|
| 60 |
+
Initialize MILP Solver
|
| 61 |
+
|
| 62 |
+
Args:
|
| 63 |
+
time_limit_seconds: Maximum solve time
|
| 64 |
+
solver_type: Solver backend ('SCIP', 'CBC', 'GLOP', 'SAT')
|
| 65 |
+
"""
|
| 66 |
+
self.time_limit_seconds = time_limit_seconds
|
| 67 |
+
self.solver_type = solver_type
|
| 68 |
+
self.logger = logging.getLogger(__name__)
|
| 69 |
+
|
| 70 |
+
# Try to find an available solver
|
| 71 |
+
self._available_solver = self._find_available_solver()
|
| 72 |
+
|
| 73 |
+
def _find_available_solver(self) -> str:
|
| 74 |
+
"""Find an available solver"""
|
| 75 |
+
solvers_to_try = [self.solver_type, 'SCIP', 'CBC', 'GLOP', 'SAT']
|
| 76 |
+
|
| 77 |
+
for solver_name in solvers_to_try:
|
| 78 |
+
solver = pywraplp.Solver.CreateSolver(solver_name)
|
| 79 |
+
if solver:
|
| 80 |
+
self.logger.info(f"Using solver: {solver_name}")
|
| 81 |
+
return solver_name
|
| 82 |
+
|
| 83 |
+
self.logger.warning("No LP solver available, will use CP-SAT only")
|
| 84 |
+
return None
|
| 85 |
+
|
| 86 |
+
def validate_and_refine(self, layout: Layout) -> Tuple[Layout, MILPResult]:
|
| 87 |
+
"""
|
| 88 |
+
Validate and refine a layout using MILP constraints
|
| 89 |
+
|
| 90 |
+
This is the main entry point - receives layout, returns refined layout with
|
| 91 |
+
mathematically guaranteed validity.
|
| 92 |
+
|
| 93 |
+
Args:
|
| 94 |
+
layout: Layout to validate and refine
|
| 95 |
+
|
| 96 |
+
Returns:
|
| 97 |
+
Tuple of (refined_layout, result)
|
| 98 |
+
"""
|
| 99 |
+
self.logger.info(f"MILP validation for layout {layout.id}")
|
| 100 |
+
import time
|
| 101 |
+
start_time = time.time()
|
| 102 |
+
|
| 103 |
+
# Step 1: Check for overlaps and fix
|
| 104 |
+
overlap_result = self._resolve_overlaps(layout)
|
| 105 |
+
if not overlap_result.is_success():
|
| 106 |
+
return layout, overlap_result
|
| 107 |
+
|
| 108 |
+
# Step 2: Ensure road connectivity
|
| 109 |
+
connectivity_result = self._ensure_road_connectivity(layout)
|
| 110 |
+
if not connectivity_result.is_success():
|
| 111 |
+
return layout, connectivity_result
|
| 112 |
+
|
| 113 |
+
# Step 3: Snap geometries to grid
|
| 114 |
+
self._snap_geometries(layout)
|
| 115 |
+
|
| 116 |
+
# Step 4: Validate final geometry closure
|
| 117 |
+
closure_result = self._validate_geometry_closure(layout)
|
| 118 |
+
|
| 119 |
+
solve_time = time.time() - start_time
|
| 120 |
+
|
| 121 |
+
result = MILPResult(
|
| 122 |
+
status='OPTIMAL' if closure_result else 'FEASIBLE',
|
| 123 |
+
objective_value=layout.metrics.sellable_area_sqm,
|
| 124 |
+
solve_time_seconds=solve_time,
|
| 125 |
+
plots=[{
|
| 126 |
+
'id': p.id,
|
| 127 |
+
'area_sqm': p.area_sqm,
|
| 128 |
+
'type': p.type.value,
|
| 129 |
+
'has_road_access': p.has_road_access
|
| 130 |
+
} for p in layout.plots]
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
self.logger.info(f"MILP validation complete: {result.status} in {solve_time:.2f}s")
|
| 134 |
+
|
| 135 |
+
return layout, result
|
| 136 |
+
|
| 137 |
+
def _resolve_overlaps(self, layout: Layout) -> MILPResult:
|
| 138 |
+
"""
|
| 139 |
+
Resolve plot overlaps using constraint programming
|
| 140 |
+
|
| 141 |
+
Uses OR-Tools CP-SAT solver to find non-overlapping placement
|
| 142 |
+
"""
|
| 143 |
+
industrial_plots = [p for p in layout.plots if p.type == PlotType.INDUSTRIAL]
|
| 144 |
+
|
| 145 |
+
if len(industrial_plots) < 2:
|
| 146 |
+
return MILPResult(status='OPTIMAL')
|
| 147 |
+
|
| 148 |
+
# Check for overlaps
|
| 149 |
+
overlaps_found = []
|
| 150 |
+
for i, p1 in enumerate(industrial_plots):
|
| 151 |
+
for p2 in industrial_plots[i+1:]:
|
| 152 |
+
if p1.geometry and p2.geometry:
|
| 153 |
+
if p1.geometry.intersects(p2.geometry):
|
| 154 |
+
intersection = p1.geometry.intersection(p2.geometry)
|
| 155 |
+
if intersection.area > 1.0: # 1 sqm tolerance
|
| 156 |
+
overlaps_found.append((p1.id, p2.id, intersection.area))
|
| 157 |
+
|
| 158 |
+
if not overlaps_found:
|
| 159 |
+
return MILPResult(status='OPTIMAL')
|
| 160 |
+
|
| 161 |
+
self.logger.warning(f"Found {len(overlaps_found)} overlapping plot pairs")
|
| 162 |
+
|
| 163 |
+
# Use CP-SAT to resolve overlaps
|
| 164 |
+
model = cp_model.CpModel()
|
| 165 |
+
|
| 166 |
+
# Get site bounds
|
| 167 |
+
minx, miny, maxx, maxy = layout.site_boundary.geometry.bounds
|
| 168 |
+
site_width = int(maxx - minx)
|
| 169 |
+
site_height = int(maxy - miny)
|
| 170 |
+
|
| 171 |
+
# Decision variables: position of each plot
|
| 172 |
+
plot_vars = {}
|
| 173 |
+
for plot in industrial_plots:
|
| 174 |
+
width = int(plot.width_m) if plot.width_m > 0 else 50
|
| 175 |
+
height = int(plot.depth_m) if plot.depth_m > 0 else 50
|
| 176 |
+
|
| 177 |
+
# X and Y positions
|
| 178 |
+
x = model.NewIntVar(0, site_width - width, f'x_{plot.id}')
|
| 179 |
+
y = model.NewIntVar(0, site_height - height, f'y_{plot.id}')
|
| 180 |
+
|
| 181 |
+
plot_vars[plot.id] = {
|
| 182 |
+
'x': x,
|
| 183 |
+
'y': y,
|
| 184 |
+
'width': width,
|
| 185 |
+
'height': height,
|
| 186 |
+
'plot': plot
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
# Non-overlap constraints (using interval variables)
|
| 190 |
+
x_intervals = []
|
| 191 |
+
y_intervals = []
|
| 192 |
+
|
| 193 |
+
for plot_id, vars in plot_vars.items():
|
| 194 |
+
x_interval = model.NewIntervalVar(
|
| 195 |
+
vars['x'],
|
| 196 |
+
vars['width'],
|
| 197 |
+
vars['x'] + vars['width'],
|
| 198 |
+
f'x_interval_{plot_id}'
|
| 199 |
+
)
|
| 200 |
+
y_interval = model.NewIntervalVar(
|
| 201 |
+
vars['y'],
|
| 202 |
+
vars['height'],
|
| 203 |
+
vars['y'] + vars['height'],
|
| 204 |
+
f'y_interval_{plot_id}'
|
| 205 |
+
)
|
| 206 |
+
x_intervals.append(x_interval)
|
| 207 |
+
y_intervals.append(y_interval)
|
| 208 |
+
|
| 209 |
+
# Add 2D no-overlap constraint
|
| 210 |
+
model.AddNoOverlap2D(x_intervals, y_intervals)
|
| 211 |
+
|
| 212 |
+
# Solve
|
| 213 |
+
solver = cp_model.CpSolver()
|
| 214 |
+
solver.parameters.max_time_in_seconds = min(60, self.time_limit_seconds)
|
| 215 |
+
|
| 216 |
+
status = solver.Solve(model)
|
| 217 |
+
|
| 218 |
+
if status in [cp_model.OPTIMAL, cp_model.FEASIBLE]:
|
| 219 |
+
# Update plot positions
|
| 220 |
+
for plot_id, vars in plot_vars.items():
|
| 221 |
+
new_x = solver.Value(vars['x']) + minx
|
| 222 |
+
new_y = solver.Value(vars['y']) + miny
|
| 223 |
+
width = vars['width']
|
| 224 |
+
height = vars['height']
|
| 225 |
+
|
| 226 |
+
# Update plot geometry
|
| 227 |
+
plot = vars['plot']
|
| 228 |
+
plot.geometry = box(new_x, new_y, new_x + width, new_y + height)
|
| 229 |
+
plot.area_sqm = plot.geometry.area
|
| 230 |
+
|
| 231 |
+
return MILPResult(
|
| 232 |
+
status='OPTIMAL' if status == cp_model.OPTIMAL else 'FEASIBLE',
|
| 233 |
+
solve_time_seconds=solver.WallTime()
|
| 234 |
+
)
|
| 235 |
+
else:
|
| 236 |
+
return MILPResult(
|
| 237 |
+
status='INFEASIBLE',
|
| 238 |
+
error_message='Cannot resolve overlaps - site may be too constrained'
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
def _ensure_road_connectivity(self, layout: Layout) -> MILPResult:
|
| 242 |
+
"""
|
| 243 |
+
Ensure all industrial plots have road access
|
| 244 |
+
|
| 245 |
+
Uses simple distance-based connectivity check
|
| 246 |
+
"""
|
| 247 |
+
max_distance = 200 # meters (from regulations)
|
| 248 |
+
|
| 249 |
+
# If no road network, create a simple grid
|
| 250 |
+
if not layout.road_network or not layout.road_network.primary_roads:
|
| 251 |
+
self._generate_simple_road_network(layout)
|
| 252 |
+
|
| 253 |
+
# Check connectivity for each industrial plot
|
| 254 |
+
disconnected_plots = []
|
| 255 |
+
all_roads = []
|
| 256 |
+
|
| 257 |
+
if layout.road_network:
|
| 258 |
+
if layout.road_network.primary_roads:
|
| 259 |
+
all_roads.extend(layout.road_network.primary_roads.geoms if hasattr(layout.road_network.primary_roads, 'geoms') else [layout.road_network.primary_roads])
|
| 260 |
+
if layout.road_network.secondary_roads:
|
| 261 |
+
all_roads.extend(layout.road_network.secondary_roads.geoms if hasattr(layout.road_network.secondary_roads, 'geoms') else [layout.road_network.secondary_roads])
|
| 262 |
+
|
| 263 |
+
for plot in layout.plots:
|
| 264 |
+
if plot.type == PlotType.INDUSTRIAL and plot.geometry:
|
| 265 |
+
# Find minimum distance to any road
|
| 266 |
+
min_distance = float('inf')
|
| 267 |
+
for road in all_roads:
|
| 268 |
+
dist = plot.geometry.distance(road)
|
| 269 |
+
min_distance = min(min_distance, dist)
|
| 270 |
+
|
| 271 |
+
if min_distance <= max_distance:
|
| 272 |
+
plot.has_road_access = True
|
| 273 |
+
else:
|
| 274 |
+
plot.has_road_access = False
|
| 275 |
+
disconnected_plots.append(plot.id)
|
| 276 |
+
|
| 277 |
+
if disconnected_plots:
|
| 278 |
+
self.logger.warning(f"Plots without road access: {disconnected_plots}")
|
| 279 |
+
return MILPResult(
|
| 280 |
+
status='FEASIBLE',
|
| 281 |
+
error_message=f'Plots {disconnected_plots} exceed {max_distance}m from road'
|
| 282 |
+
)
|
| 283 |
+
|
| 284 |
+
return MILPResult(status='OPTIMAL')
|
| 285 |
+
|
| 286 |
+
def _generate_simple_road_network(self, layout: Layout):
|
| 287 |
+
"""Generate a simple grid road network"""
|
| 288 |
+
bounds = layout.site_boundary.geometry.bounds
|
| 289 |
+
minx, miny, maxx, maxy = bounds
|
| 290 |
+
|
| 291 |
+
# Create primary roads (cross pattern)
|
| 292 |
+
center_x = (minx + maxx) / 2
|
| 293 |
+
center_y = (miny + maxy) / 2
|
| 294 |
+
|
| 295 |
+
horizontal = LineString([(minx, center_y), (maxx, center_y)])
|
| 296 |
+
vertical = LineString([(center_x, miny), (center_x, maxy)])
|
| 297 |
+
|
| 298 |
+
layout.road_network = RoadNetwork(
|
| 299 |
+
primary_roads=MultiLineString([horizontal, vertical]),
|
| 300 |
+
total_length_m=horizontal.length + vertical.length
|
| 301 |
+
)
|
| 302 |
+
|
| 303 |
+
# Calculate road area (assume 24m width for primary roads)
|
| 304 |
+
road_width = 24
|
| 305 |
+
road_area = layout.road_network.total_length_m * road_width
|
| 306 |
+
layout.road_network.total_area_sqm = road_area
|
| 307 |
+
|
| 308 |
+
def _snap_geometries(self, layout: Layout, grid_size: float = 1.0):
|
| 309 |
+
"""
|
| 310 |
+
Snap all geometries to a grid for clean coordinates
|
| 311 |
+
|
| 312 |
+
Args:
|
| 313 |
+
layout: Layout to snap
|
| 314 |
+
grid_size: Grid size in meters (default 1m)
|
| 315 |
+
"""
|
| 316 |
+
for plot in layout.plots:
|
| 317 |
+
if plot.geometry:
|
| 318 |
+
coords = list(plot.geometry.exterior.coords)
|
| 319 |
+
snapped_coords = [
|
| 320 |
+
(round(x / grid_size) * grid_size, round(y / grid_size) * grid_size)
|
| 321 |
+
for x, y in coords
|
| 322 |
+
]
|
| 323 |
+
try:
|
| 324 |
+
plot.geometry = Polygon(snapped_coords)
|
| 325 |
+
plot.area_sqm = plot.geometry.area
|
| 326 |
+
except Exception as e:
|
| 327 |
+
self.logger.warning(f"Failed to snap plot {plot.id}: {e}")
|
| 328 |
+
|
| 329 |
+
def _validate_geometry_closure(self, layout: Layout) -> bool:
|
| 330 |
+
"""
|
| 331 |
+
Validate that all geometries are properly closed
|
| 332 |
+
|
| 333 |
+
Returns:
|
| 334 |
+
True if all geometries are valid
|
| 335 |
+
"""
|
| 336 |
+
all_valid = True
|
| 337 |
+
|
| 338 |
+
for plot in layout.plots:
|
| 339 |
+
if plot.geometry:
|
| 340 |
+
if not plot.geometry.is_valid:
|
| 341 |
+
self.logger.warning(f"Plot {plot.id} has invalid geometry")
|
| 342 |
+
# Try to fix
|
| 343 |
+
plot.geometry = plot.geometry.buffer(0)
|
| 344 |
+
if not plot.geometry.is_valid:
|
| 345 |
+
all_valid = False
|
| 346 |
+
|
| 347 |
+
return all_valid
|
| 348 |
+
|
| 349 |
+
def solve_plot_placement(
|
| 350 |
+
self,
|
| 351 |
+
site_boundary: SiteBoundary,
|
| 352 |
+
num_plots: int,
|
| 353 |
+
min_plot_size: float = 1000,
|
| 354 |
+
max_plot_size: float = 10000,
|
| 355 |
+
setback: float = 50
|
| 356 |
+
) -> MILPResult:
|
| 357 |
+
"""
|
| 358 |
+
Solve optimal plot placement from scratch using MILP
|
| 359 |
+
|
| 360 |
+
This is the "black-box" function that LLM can call via Function Calling.
|
| 361 |
+
Receives JSON-like parameters, returns raw numerical results.
|
| 362 |
+
|
| 363 |
+
Args:
|
| 364 |
+
site_boundary: Site boundary polygon
|
| 365 |
+
num_plots: Target number of plots
|
| 366 |
+
min_plot_size: Minimum plot area in sqm
|
| 367 |
+
max_plot_size: Maximum plot area in sqm
|
| 368 |
+
setback: Boundary setback in meters
|
| 369 |
+
|
| 370 |
+
Returns:
|
| 371 |
+
MILPResult with plot placements
|
| 372 |
+
"""
|
| 373 |
+
self.logger.info(f"MILP solving for {num_plots} plots")
|
| 374 |
+
import time
|
| 375 |
+
start_time = time.time()
|
| 376 |
+
|
| 377 |
+
# Create solver - use available solver or fallback to CP-SAT
|
| 378 |
+
solver_to_use = self._available_solver or 'SAT'
|
| 379 |
+
solver = pywraplp.Solver.CreateSolver(solver_to_use)
|
| 380 |
+
if not solver:
|
| 381 |
+
# Fallback: Use CP-SAT based approach
|
| 382 |
+
return self._solve_with_cpsat(site_boundary, num_plots, min_plot_size, max_plot_size, setback)
|
| 383 |
+
|
| 384 |
+
solver.SetTimeLimit(self.time_limit_seconds * 1000) # Convert to ms
|
| 385 |
+
|
| 386 |
+
# Get buildable area (after setback)
|
| 387 |
+
buildable = site_boundary.geometry.buffer(-setback)
|
| 388 |
+
if buildable.is_empty:
|
| 389 |
+
return MILPResult(
|
| 390 |
+
status='INFEASIBLE',
|
| 391 |
+
error_message=f'Site too small for {setback}m setback'
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
bounds = buildable.bounds
|
| 395 |
+
minx, miny, maxx, maxy = bounds
|
| 396 |
+
width = maxx - minx
|
| 397 |
+
height = maxy - miny
|
| 398 |
+
|
| 399 |
+
# Decision variables
|
| 400 |
+
infinity = solver.infinity()
|
| 401 |
+
|
| 402 |
+
# For each plot: x, y, w, h (continuous), active (binary)
|
| 403 |
+
plots_vars = []
|
| 404 |
+
for i in range(num_plots):
|
| 405 |
+
x = solver.NumVar(0, width, f'x_{i}')
|
| 406 |
+
y = solver.NumVar(0, height, f'y_{i}')
|
| 407 |
+
w = solver.NumVar(20, 200, f'w_{i}') # 20-200m width
|
| 408 |
+
h = solver.NumVar(20, 200, f'h_{i}') # 20-200m height
|
| 409 |
+
active = solver.IntVar(0, 1, f'active_{i}')
|
| 410 |
+
|
| 411 |
+
plots_vars.append({
|
| 412 |
+
'x': x, 'y': y, 'w': w, 'h': h,
|
| 413 |
+
'active': active, 'index': i
|
| 414 |
+
})
|
| 415 |
+
|
| 416 |
+
# Boundary constraints
|
| 417 |
+
solver.Add(x + w <= width)
|
| 418 |
+
solver.Add(y + h <= height)
|
| 419 |
+
|
| 420 |
+
# Size constraints
|
| 421 |
+
solver.Add(w * h >= min_plot_size * active)
|
| 422 |
+
solver.Add(w * h <= max_plot_size)
|
| 423 |
+
|
| 424 |
+
# Objective: Maximize total active plot area
|
| 425 |
+
objective = solver.Objective()
|
| 426 |
+
for pv in plots_vars:
|
| 427 |
+
# Approximate area (linearization)
|
| 428 |
+
objective.SetCoefficient(pv['w'], 100)
|
| 429 |
+
objective.SetCoefficient(pv['h'], 100)
|
| 430 |
+
objective.SetCoefficient(pv['active'], min_plot_size)
|
| 431 |
+
objective.SetMaximization()
|
| 432 |
+
|
| 433 |
+
# Solve
|
| 434 |
+
status = solver.Solve()
|
| 435 |
+
|
| 436 |
+
solve_time = time.time() - start_time
|
| 437 |
+
|
| 438 |
+
# Parse results
|
| 439 |
+
if status in [pywraplp.Solver.OPTIMAL, pywraplp.Solver.FEASIBLE]:
|
| 440 |
+
result_plots = []
|
| 441 |
+
for pv in plots_vars:
|
| 442 |
+
if pv['active'].solution_value() > 0.5:
|
| 443 |
+
x = pv['x'].solution_value() + minx + setback
|
| 444 |
+
y = pv['y'].solution_value() + miny + setback
|
| 445 |
+
w = pv['w'].solution_value()
|
| 446 |
+
h = pv['h'].solution_value()
|
| 447 |
+
|
| 448 |
+
result_plots.append({
|
| 449 |
+
'id': f'plot_{pv["index"]}',
|
| 450 |
+
'x': x,
|
| 451 |
+
'y': y,
|
| 452 |
+
'width': w,
|
| 453 |
+
'height': h,
|
| 454 |
+
'area_sqm': w * h,
|
| 455 |
+
'type': 'industrial'
|
| 456 |
+
})
|
| 457 |
+
|
| 458 |
+
return MILPResult(
|
| 459 |
+
status='OPTIMAL' if status == pywraplp.Solver.OPTIMAL else 'FEASIBLE',
|
| 460 |
+
objective_value=solver.Objective().Value(),
|
| 461 |
+
solve_time_seconds=solve_time,
|
| 462 |
+
plots=result_plots
|
| 463 |
+
)
|
| 464 |
+
else:
|
| 465 |
+
status_map = {
|
| 466 |
+
pywraplp.Solver.INFEASIBLE: 'INFEASIBLE',
|
| 467 |
+
pywraplp.Solver.UNBOUNDED: 'UNBOUNDED',
|
| 468 |
+
pywraplp.Solver.NOT_SOLVED: 'TIMEOUT'
|
| 469 |
+
}
|
| 470 |
+
return MILPResult(
|
| 471 |
+
status=status_map.get(status, 'ERROR'),
|
| 472 |
+
solve_time_seconds=solve_time,
|
| 473 |
+
error_message='Could not find valid plot placement'
|
| 474 |
+
)
|
| 475 |
+
|
| 476 |
+
def _solve_with_cpsat(
|
| 477 |
+
self,
|
| 478 |
+
site_boundary: SiteBoundary,
|
| 479 |
+
num_plots: int,
|
| 480 |
+
min_plot_size: float,
|
| 481 |
+
max_plot_size: float,
|
| 482 |
+
setback: float
|
| 483 |
+
) -> MILPResult:
|
| 484 |
+
"""
|
| 485 |
+
Fallback solver using CP-SAT for plot placement
|
| 486 |
+
"""
|
| 487 |
+
import time
|
| 488 |
+
start_time = time.time()
|
| 489 |
+
|
| 490 |
+
# Get buildable area
|
| 491 |
+
buildable = site_boundary.geometry.buffer(-setback)
|
| 492 |
+
if buildable.is_empty:
|
| 493 |
+
return MILPResult(
|
| 494 |
+
status='INFEASIBLE',
|
| 495 |
+
error_message=f'Site too small for {setback}m setback'
|
| 496 |
+
)
|
| 497 |
+
|
| 498 |
+
minx, miny, maxx, maxy = buildable.bounds
|
| 499 |
+
width = int(maxx - minx)
|
| 500 |
+
height = int(maxy - miny)
|
| 501 |
+
|
| 502 |
+
# Use CP-SAT
|
| 503 |
+
model = cp_model.CpModel()
|
| 504 |
+
|
| 505 |
+
# Fixed plot dimensions for simplicity
|
| 506 |
+
plot_width = int(min_plot_size ** 0.5) # Square plots
|
| 507 |
+
plot_height = plot_width
|
| 508 |
+
|
| 509 |
+
# Create plot variables
|
| 510 |
+
plot_vars = []
|
| 511 |
+
x_intervals = []
|
| 512 |
+
y_intervals = []
|
| 513 |
+
|
| 514 |
+
for i in range(num_plots):
|
| 515 |
+
x = model.NewIntVar(0, max(0, width - plot_width), f'x_{i}')
|
| 516 |
+
y = model.NewIntVar(0, max(0, height - plot_height), f'y_{i}')
|
| 517 |
+
|
| 518 |
+
x_interval = model.NewIntervalVar(x, plot_width, x + plot_width, f'x_int_{i}')
|
| 519 |
+
y_interval = model.NewIntervalVar(y, plot_height, y + plot_height, f'y_int_{i}')
|
| 520 |
+
|
| 521 |
+
plot_vars.append({'x': x, 'y': y, 'width': plot_width, 'height': plot_height})
|
| 522 |
+
x_intervals.append(x_interval)
|
| 523 |
+
y_intervals.append(y_interval)
|
| 524 |
+
|
| 525 |
+
# No overlap constraint
|
| 526 |
+
model.AddNoOverlap2D(x_intervals, y_intervals)
|
| 527 |
+
|
| 528 |
+
# Solve
|
| 529 |
+
solver = cp_model.CpSolver()
|
| 530 |
+
solver.parameters.max_time_in_seconds = min(30, self.time_limit_seconds)
|
| 531 |
+
status = solver.Solve(model)
|
| 532 |
+
|
| 533 |
+
solve_time = time.time() - start_time
|
| 534 |
+
|
| 535 |
+
if status in [cp_model.OPTIMAL, cp_model.FEASIBLE]:
|
| 536 |
+
result_plots = []
|
| 537 |
+
for i, pv in enumerate(plot_vars):
|
| 538 |
+
x = solver.Value(pv['x']) + minx + setback
|
| 539 |
+
y = solver.Value(pv['y']) + miny + setback
|
| 540 |
+
|
| 541 |
+
result_plots.append({
|
| 542 |
+
'id': f'plot_{i}',
|
| 543 |
+
'x': x,
|
| 544 |
+
'y': y,
|
| 545 |
+
'width': pv['width'],
|
| 546 |
+
'height': pv['height'],
|
| 547 |
+
'area_sqm': pv['width'] * pv['height'],
|
| 548 |
+
'type': 'industrial'
|
| 549 |
+
})
|
| 550 |
+
|
| 551 |
+
return MILPResult(
|
| 552 |
+
status='OPTIMAL' if status == cp_model.OPTIMAL else 'FEASIBLE',
|
| 553 |
+
objective_value=len(result_plots) * plot_width * plot_height,
|
| 554 |
+
solve_time_seconds=solve_time,
|
| 555 |
+
plots=result_plots
|
| 556 |
+
)
|
| 557 |
+
else:
|
| 558 |
+
return MILPResult(
|
| 559 |
+
status='INFEASIBLE',
|
| 560 |
+
solve_time_seconds=solve_time,
|
| 561 |
+
error_message='Could not place plots without overlap'
|
| 562 |
+
)
|
| 563 |
+
|
| 564 |
+
def to_json_interface(self, request: Dict[str, Any]) -> str:
|
| 565 |
+
"""
|
| 566 |
+
JSON interface for LLM Function Calling
|
| 567 |
+
|
| 568 |
+
This is the standardized interface that LLM uses to call the CP Module.
|
| 569 |
+
|
| 570 |
+
Input format:
|
| 571 |
+
{
|
| 572 |
+
"action": "solve_placement" | "validate_layout",
|
| 573 |
+
"parameters": {...}
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
Output format:
|
| 577 |
+
{
|
| 578 |
+
"status": "OPTIMAL" | "FEASIBLE" | "INFEASIBLE" | "ERROR",
|
| 579 |
+
"result": {...}
|
| 580 |
+
}
|
| 581 |
+
"""
|
| 582 |
+
action = request.get('action')
|
| 583 |
+
params = request.get('parameters', {})
|
| 584 |
+
|
| 585 |
+
try:
|
| 586 |
+
if action == 'solve_placement':
|
| 587 |
+
# Create site boundary from params
|
| 588 |
+
bounds = params.get('bounds', [0, 0, 500, 500])
|
| 589 |
+
site_geom = box(*bounds)
|
| 590 |
+
site = SiteBoundary(geometry=site_geom, area_sqm=site_geom.area)
|
| 591 |
+
|
| 592 |
+
result = self.solve_plot_placement(
|
| 593 |
+
site_boundary=site,
|
| 594 |
+
num_plots=params.get('num_plots', 10),
|
| 595 |
+
min_plot_size=params.get('min_plot_size', 1000),
|
| 596 |
+
max_plot_size=params.get('max_plot_size', 10000),
|
| 597 |
+
setback=params.get('setback', 50)
|
| 598 |
+
)
|
| 599 |
+
|
| 600 |
+
elif action == 'validate_layout':
|
| 601 |
+
# Would need to deserialize layout from JSON
|
| 602 |
+
return json.dumps({
|
| 603 |
+
'status': 'ERROR',
|
| 604 |
+
'error_message': 'validate_layout requires Layout object'
|
| 605 |
+
})
|
| 606 |
+
else:
|
| 607 |
+
return json.dumps({
|
| 608 |
+
'status': 'ERROR',
|
| 609 |
+
'error_message': f'Unknown action: {action}'
|
| 610 |
+
})
|
| 611 |
+
|
| 612 |
+
return result.to_json()
|
| 613 |
+
|
| 614 |
+
except Exception as e:
|
| 615 |
+
return json.dumps({
|
| 616 |
+
'status': 'ERROR',
|
| 617 |
+
'error_message': str(e)
|
| 618 |
+
})
|
| 619 |
+
|
| 620 |
+
|
| 621 |
+
# Example usage
|
| 622 |
+
if __name__ == "__main__":
|
| 623 |
+
from shapely.geometry import box as shapely_box
|
| 624 |
+
|
| 625 |
+
# Create test site
|
| 626 |
+
site_geom = shapely_box(0, 0, 500, 500)
|
| 627 |
+
site = SiteBoundary(geometry=site_geom, area_sqm=site_geom.area)
|
| 628 |
+
site.buildable_area_sqm = site.area_sqm
|
| 629 |
+
|
| 630 |
+
# Test MILP solver
|
| 631 |
+
solver = MILPSolver(time_limit_seconds=60)
|
| 632 |
+
|
| 633 |
+
# Test plot placement
|
| 634 |
+
result = solver.solve_plot_placement(
|
| 635 |
+
site_boundary=site,
|
| 636 |
+
num_plots=10,
|
| 637 |
+
min_plot_size=1000,
|
| 638 |
+
max_plot_size=5000,
|
| 639 |
+
setback=50
|
| 640 |
+
)
|
| 641 |
+
|
| 642 |
+
print(f"Status: {result.status}")
|
| 643 |
+
print(f"Solve time: {result.solve_time_seconds:.2f}s")
|
| 644 |
+
print(f"Number of plots: {len(result.plots)}")
|
| 645 |
+
|
| 646 |
+
# Test JSON interface
|
| 647 |
+
json_request = {
|
| 648 |
+
"action": "solve_placement",
|
| 649 |
+
"parameters": {
|
| 650 |
+
"bounds": [0, 0, 500, 500],
|
| 651 |
+
"num_plots": 10,
|
| 652 |
+
"min_plot_size": 1000,
|
| 653 |
+
"setback": 50
|
| 654 |
+
}
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
json_response = solver.to_json_interface(json_request)
|
| 658 |
+
print("\nJSON Response:")
|
| 659 |
+
print(json_response)
|
src/algorithms/nsga2_optimizer.py
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
NSGA-II Optimizer - Module A: The Architect
|
| 3 |
+
Multi-objective genetic algorithm for industrial estate layout optimization
|
| 4 |
+
"""
|
| 5 |
+
import numpy as np
|
| 6 |
+
from pymoo.algorithms.moo.nsga2 import NSGA2
|
| 7 |
+
from pymoo.core.problem import Problem
|
| 8 |
+
from pymoo.optimize import minimize
|
| 9 |
+
from pymoo.operators.crossover.sbx import SBX
|
| 10 |
+
from pymoo.operators.mutation.pm import PM
|
| 11 |
+
from pymoo.operators.sampling.rnd import FloatRandomSampling
|
| 12 |
+
from typing import List, Tuple
|
| 13 |
+
import yaml
|
| 14 |
+
from pathlib import Path
|
| 15 |
+
|
| 16 |
+
from src.models.domain import Layout, SiteBoundary, Plot, PlotType, ParetoFront, RoadNetwork
|
| 17 |
+
from shapely.geometry import Polygon, box
|
| 18 |
+
import logging
|
| 19 |
+
|
| 20 |
+
logger = logging.getLogger(__name__)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class IndustrialEstateProblem(Problem):
|
| 24 |
+
"""
|
| 25 |
+
Multi-objective optimization problem for industrial estate layout
|
| 26 |
+
|
| 27 |
+
Objectives:
|
| 28 |
+
1. Maximize sellable area
|
| 29 |
+
2. Maximize green space
|
| 30 |
+
3. Minimize road network length
|
| 31 |
+
4. Maximize regulatory compliance score
|
| 32 |
+
"""
|
| 33 |
+
|
| 34 |
+
def __init__(self, site_boundary: SiteBoundary, regulations: dict, n_plots: int = 20):
|
| 35 |
+
"""
|
| 36 |
+
Initialize optimization problem
|
| 37 |
+
|
| 38 |
+
Args:
|
| 39 |
+
site_boundary: Site boundary with constraints
|
| 40 |
+
regulations: Regulatory requirements from YAML
|
| 41 |
+
n_plots: Target number of industrial plots
|
| 42 |
+
"""
|
| 43 |
+
self.site_boundary = site_boundary
|
| 44 |
+
self.regulations = regulations
|
| 45 |
+
self.n_plots = n_plots
|
| 46 |
+
|
| 47 |
+
# Decision variables: [x1, y1, width1, height1, orientation1, ..., xN, yN, widthN, heightN, orientationN]
|
| 48 |
+
# 5 variables per plot: x, y position (normalized), width, height (meters), orientation (0-360)
|
| 49 |
+
n_var = n_plots * 5
|
| 50 |
+
|
| 51 |
+
# Variable bounds
|
| 52 |
+
xl = np.array([0, 0, 20, 20, 0] * n_plots) # Lower bounds
|
| 53 |
+
xu = np.array([1, 1, 200, 200, 360] * n_plots) # Upper bounds
|
| 54 |
+
|
| 55 |
+
super().__init__(
|
| 56 |
+
n_var=n_var,
|
| 57 |
+
n_obj=4, # 4 objectives
|
| 58 |
+
n_constr=0, # Constraints handled via penalties
|
| 59 |
+
xl=xl,
|
| 60 |
+
xu=xu
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
def _evaluate(self, X, out, *args, **kwargs):
|
| 64 |
+
"""
|
| 65 |
+
Evaluate population
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
X: Population matrix (n_individuals x n_variables)
|
| 69 |
+
out: Output dictionary
|
| 70 |
+
"""
|
| 71 |
+
n_individuals = X.shape[0]
|
| 72 |
+
|
| 73 |
+
# Initialize objective arrays
|
| 74 |
+
f1_sellable = np.zeros(n_individuals) # Maximize (will negate)
|
| 75 |
+
f2_green = np.zeros(n_individuals) # Maximize (will negate)
|
| 76 |
+
f3_road_length = np.zeros(n_individuals) # Minimize
|
| 77 |
+
f4_compliance = np.zeros(n_individuals) # Maximize (will negate)
|
| 78 |
+
|
| 79 |
+
for i in range(n_individuals):
|
| 80 |
+
layout = self._decode_solution(X[i])
|
| 81 |
+
|
| 82 |
+
# Calculate objectives
|
| 83 |
+
metrics = layout.calculate_metrics()
|
| 84 |
+
|
| 85 |
+
# F1: Maximize sellable area (negate for minimization)
|
| 86 |
+
f1_sellable[i] = -metrics.sellable_area_sqm
|
| 87 |
+
|
| 88 |
+
# F2: Maximize green space (negate for minimization)
|
| 89 |
+
f2_green[i] = -metrics.green_space_area_sqm
|
| 90 |
+
|
| 91 |
+
# F3: Minimize road network length
|
| 92 |
+
if layout.road_network:
|
| 93 |
+
f3_road_length[i] = layout.road_network.total_length_m
|
| 94 |
+
else:
|
| 95 |
+
f3_road_length[i] = 1e6 # Penalty for no road
|
| 96 |
+
|
| 97 |
+
# F4: Regulatory compliance score (0-1, higher is better)
|
| 98 |
+
compliance_score = self._calculate_compliance_score(layout)
|
| 99 |
+
f4_compliance[i] = -compliance_score # Negate for minimization
|
| 100 |
+
|
| 101 |
+
# Set objectives
|
| 102 |
+
out["F"] = np.column_stack([f1_sellable, f2_green, f3_road_length, f4_compliance])
|
| 103 |
+
|
| 104 |
+
def _decode_solution(self, x: np.ndarray) -> Layout:
|
| 105 |
+
"""
|
| 106 |
+
Decode decision variables into a Layout
|
| 107 |
+
|
| 108 |
+
Args:
|
| 109 |
+
x: Decision variables array
|
| 110 |
+
|
| 111 |
+
Returns:
|
| 112 |
+
Layout object
|
| 113 |
+
"""
|
| 114 |
+
layout = Layout(site_boundary=self.site_boundary)
|
| 115 |
+
|
| 116 |
+
# Get site bounds for denormalization
|
| 117 |
+
minx, miny, maxx, maxy = self.site_boundary.geometry.bounds
|
| 118 |
+
site_width = maxx - minx
|
| 119 |
+
site_height = maxy - miny
|
| 120 |
+
|
| 121 |
+
plots = []
|
| 122 |
+
for i in range(self.n_plots):
|
| 123 |
+
idx = i * 5
|
| 124 |
+
|
| 125 |
+
# Denormalize position
|
| 126 |
+
x_norm, y_norm = x[idx], x[idx + 1]
|
| 127 |
+
x_pos = minx + x_norm * site_width
|
| 128 |
+
y_pos = miny + y_norm * site_height
|
| 129 |
+
|
| 130 |
+
width, height = x[idx + 2], x[idx + 3]
|
| 131 |
+
orientation = x[idx + 4]
|
| 132 |
+
|
| 133 |
+
# Create simple rectangular plot
|
| 134 |
+
plot_geom = box(x_pos, y_pos, x_pos + width, y_pos + height)
|
| 135 |
+
|
| 136 |
+
# Check if plot is within buildable area
|
| 137 |
+
if not self.site_boundary.geometry.contains(plot_geom):
|
| 138 |
+
continue # Skip invalid plots
|
| 139 |
+
|
| 140 |
+
plot = Plot(
|
| 141 |
+
geometry=plot_geom,
|
| 142 |
+
area_sqm=plot_geom.area,
|
| 143 |
+
type=PlotType.INDUSTRIAL,
|
| 144 |
+
width_m=width,
|
| 145 |
+
depth_m=height,
|
| 146 |
+
orientation_degrees=orientation
|
| 147 |
+
)
|
| 148 |
+
plots.append(plot)
|
| 149 |
+
|
| 150 |
+
# Add green space (simplified: use remaining area)
|
| 151 |
+
# In practice, this would be more sophisticated
|
| 152 |
+
green_area_target = self.site_boundary.buildable_area_sqm * 0.15 # 15% minimum
|
| 153 |
+
|
| 154 |
+
layout.plots = plots
|
| 155 |
+
layout.road_network = RoadNetwork() # Simplified
|
| 156 |
+
|
| 157 |
+
return layout
|
| 158 |
+
|
| 159 |
+
def _calculate_compliance_score(self, layout: Layout) -> float:
|
| 160 |
+
"""
|
| 161 |
+
Calculate regulatory compliance score (0-1)
|
| 162 |
+
|
| 163 |
+
Args:
|
| 164 |
+
layout: Layout to evaluate
|
| 165 |
+
|
| 166 |
+
Returns:
|
| 167 |
+
Compliance score (1.0 = fully compliant)
|
| 168 |
+
"""
|
| 169 |
+
score = 1.0
|
| 170 |
+
penalties = 0
|
| 171 |
+
|
| 172 |
+
metrics = layout.metrics
|
| 173 |
+
|
| 174 |
+
# Check green space requirement
|
| 175 |
+
min_green = self.regulations.get('green_space', {}).get('minimum_percentage', 0.15)
|
| 176 |
+
if metrics.green_space_ratio < min_green:
|
| 177 |
+
penalties += 0.3
|
| 178 |
+
|
| 179 |
+
# Check FAR
|
| 180 |
+
max_far = self.regulations.get('far', {}).get('maximum', 0.7)
|
| 181 |
+
if metrics.far_value > max_far:
|
| 182 |
+
penalties += 0.3
|
| 183 |
+
|
| 184 |
+
# Check plot sizes
|
| 185 |
+
min_plot_size = self.regulations.get('plot', {}).get('minimum_area_sqm', 1000)
|
| 186 |
+
for plot in layout.plots:
|
| 187 |
+
if plot.type == PlotType.INDUSTRIAL and plot.area_sqm < min_plot_size:
|
| 188 |
+
penalties += 0.1
|
| 189 |
+
break
|
| 190 |
+
|
| 191 |
+
score = max(0.0, score - penalties)
|
| 192 |
+
return score
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
class NSGA2Optimizer:
|
| 196 |
+
"""
|
| 197 |
+
NSGA-II based multi-objective optimizer for industrial estate layouts
|
| 198 |
+
"""
|
| 199 |
+
|
| 200 |
+
def __init__(self, config_path: str = "config/regulations.yaml"):
|
| 201 |
+
"""
|
| 202 |
+
Initialize optimizer
|
| 203 |
+
|
| 204 |
+
Args:
|
| 205 |
+
config_path: Path to regulations YAML file
|
| 206 |
+
"""
|
| 207 |
+
self.config_path = Path(config_path)
|
| 208 |
+
self.regulations = self._load_regulations()
|
| 209 |
+
self.logger = logging.getLogger(__name__)
|
| 210 |
+
|
| 211 |
+
def _load_regulations(self) -> dict:
|
| 212 |
+
"""Load regulations from YAML file"""
|
| 213 |
+
if not self.config_path.exists():
|
| 214 |
+
self.logger.warning(f"Regulations file not found: {self.config_path}")
|
| 215 |
+
return {}
|
| 216 |
+
|
| 217 |
+
with open(self.config_path, 'r', encoding='utf-8') as f:
|
| 218 |
+
return yaml.safe_load(f)
|
| 219 |
+
|
| 220 |
+
def optimize(
|
| 221 |
+
self,
|
| 222 |
+
site_boundary: SiteBoundary,
|
| 223 |
+
population_size: int = 100,
|
| 224 |
+
n_generations: int = 200,
|
| 225 |
+
n_plots: int = 20
|
| 226 |
+
) -> ParetoFront:
|
| 227 |
+
"""
|
| 228 |
+
Run NSGA-II optimization
|
| 229 |
+
|
| 230 |
+
Args:
|
| 231 |
+
site_boundary: Site boundary with constraints
|
| 232 |
+
population_size: NSGA-II population size
|
| 233 |
+
n_generations: Number of generations
|
| 234 |
+
n_plots: Target number of plots
|
| 235 |
+
|
| 236 |
+
Returns:
|
| 237 |
+
ParetoFront with optimal solutions
|
| 238 |
+
"""
|
| 239 |
+
import time
|
| 240 |
+
start_time = time.time()
|
| 241 |
+
|
| 242 |
+
self.logger.info(f"Starting NSGA-II optimization: pop={population_size}, gen={n_generations}")
|
| 243 |
+
|
| 244 |
+
# Define problem
|
| 245 |
+
problem = IndustrialEstateProblem(
|
| 246 |
+
site_boundary=site_boundary,
|
| 247 |
+
regulations=self.regulations,
|
| 248 |
+
n_plots=n_plots
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
# Define algorithm
|
| 252 |
+
algorithm = NSGA2(
|
| 253 |
+
pop_size=population_size,
|
| 254 |
+
sampling=FloatRandomSampling(),
|
| 255 |
+
crossover=SBX(prob=0.9, eta=15),
|
| 256 |
+
mutation=PM(eta=20),
|
| 257 |
+
eliminate_duplicates=True
|
| 258 |
+
)
|
| 259 |
+
|
| 260 |
+
# Run optimization
|
| 261 |
+
result = minimize(
|
| 262 |
+
problem,
|
| 263 |
+
algorithm,
|
| 264 |
+
('n_gen', n_generations),
|
| 265 |
+
seed=42,
|
| 266 |
+
verbose=True
|
| 267 |
+
)
|
| 268 |
+
|
| 269 |
+
# Extract Pareto front
|
| 270 |
+
pareto_front = ParetoFront()
|
| 271 |
+
|
| 272 |
+
if result.X is not None:
|
| 273 |
+
# result.X can be 1D or 2D depending on number of solutions
|
| 274 |
+
if len(result.X.shape) == 1:
|
| 275 |
+
solutions = [result.X]
|
| 276 |
+
else:
|
| 277 |
+
solutions = result.X
|
| 278 |
+
|
| 279 |
+
for i, x in enumerate(solutions):
|
| 280 |
+
layout = problem._decode_solution(x)
|
| 281 |
+
layout.pareto_rank = i
|
| 282 |
+
layout.calculate_metrics()
|
| 283 |
+
|
| 284 |
+
# Store fitness scores
|
| 285 |
+
if len(result.F.shape) == 1:
|
| 286 |
+
f = result.F
|
| 287 |
+
else:
|
| 288 |
+
f = result.F[i]
|
| 289 |
+
|
| 290 |
+
layout.fitness_scores = {
|
| 291 |
+
'sellable_area': -f[0], # Negate back
|
| 292 |
+
'green_space': -f[1],
|
| 293 |
+
'road_length': f[2],
|
| 294 |
+
'compliance': -f[3]
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
pareto_front.layouts.append(layout)
|
| 298 |
+
|
| 299 |
+
pareto_front.generation_time_seconds = time.time() - start_time
|
| 300 |
+
|
| 301 |
+
self.logger.info(f"Optimization complete: {len(pareto_front.layouts)} solutions in {pareto_front.generation_time_seconds:.2f}s")
|
| 302 |
+
|
| 303 |
+
return pareto_front
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
# Example usage
|
| 307 |
+
if __name__ == "__main__":
|
| 308 |
+
# Example: Create a simple rectangular site
|
| 309 |
+
from shapely.geometry import box
|
| 310 |
+
|
| 311 |
+
site_geom = box(0, 0, 500, 500) # 500m x 500m site
|
| 312 |
+
site = SiteBoundary(
|
| 313 |
+
geometry=site_geom,
|
| 314 |
+
area_sqm=site_geom.area
|
| 315 |
+
)
|
| 316 |
+
site.buildable_area_sqm = site.area_sqm
|
| 317 |
+
|
| 318 |
+
# Run optimization
|
| 319 |
+
optimizer = NSGA2Optimizer()
|
| 320 |
+
pareto_front = optimizer.optimize(
|
| 321 |
+
site_boundary=site,
|
| 322 |
+
population_size=50,
|
| 323 |
+
n_generations=100,
|
| 324 |
+
n_plots=15
|
| 325 |
+
)
|
| 326 |
+
|
| 327 |
+
print(f"Generated {len(pareto_front.layouts)} Pareto-optimal layouts")
|
| 328 |
+
|
| 329 |
+
# Show best layouts
|
| 330 |
+
max_sellable = pareto_front.get_max_sellable_layout()
|
| 331 |
+
if max_sellable:
|
| 332 |
+
print(f"Max sellable area: {max_sellable.metrics.sellable_area_sqm:.2f} m²")
|
| 333 |
+
|
| 334 |
+
max_green = pareto_front.get_max_green_layout()
|
| 335 |
+
if max_green:
|
| 336 |
+
print(f"Max green space: {max_green.metrics.green_space_area_sqm:.2f} m²")
|
src/algorithms/regulation_checker.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Regulation Checker - Module C: The Inspector
|
| 3 |
+
Rule-based expert system for Vietnamese industrial estate regulatory compliance
|
| 4 |
+
"""
|
| 5 |
+
import yaml
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from typing import List, Dict
|
| 8 |
+
import logging
|
| 9 |
+
from shapely.geometry import Polygon, MultiPolygon, Point
|
| 10 |
+
from shapely.ops import unary_union
|
| 11 |
+
import geopandas as gpd
|
| 12 |
+
|
| 13 |
+
from src.models.domain import Layout, Plot, PlotType, ComplianceReport, Constraint, ConstraintType
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class RegulationChecker:
|
| 19 |
+
"""
|
| 20 |
+
Automated regulatory compliance checker for industrial estate layouts
|
| 21 |
+
Based on Vietnamese industrial estate regulations
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(self, regulations_path: str = "config/regulations.yaml"):
|
| 25 |
+
"""
|
| 26 |
+
Initialize regulation checker
|
| 27 |
+
|
| 28 |
+
Args:
|
| 29 |
+
regulations_path: Path to regulations YAML configuration
|
| 30 |
+
"""
|
| 31 |
+
self.regulations_path = Path(regulations_path)
|
| 32 |
+
self.regulations = self._load_regulations()
|
| 33 |
+
self.logger = logging.getLogger(__name__)
|
| 34 |
+
|
| 35 |
+
def _load_regulations(self) -> dict:
|
| 36 |
+
"""Load regulations from YAML file"""
|
| 37 |
+
if not self.regulations_path.exists():
|
| 38 |
+
self.logger.warning(f"Regulations file not found: {self.regulations_path}")
|
| 39 |
+
return self._get_default_regulations()
|
| 40 |
+
|
| 41 |
+
with open(self.regulations_path, 'r', encoding='utf-8') as f:
|
| 42 |
+
return yaml.safe_load(f)
|
| 43 |
+
|
| 44 |
+
def _get_default_regulations(self) -> dict:
|
| 45 |
+
"""Get default regulations if file not found"""
|
| 46 |
+
return {
|
| 47 |
+
'setbacks': {
|
| 48 |
+
'boundary_minimum': 50,
|
| 49 |
+
'fire_safety_distance': 30,
|
| 50 |
+
'waterway_buffer': 100
|
| 51 |
+
},
|
| 52 |
+
'far': {
|
| 53 |
+
'maximum': 0.7,
|
| 54 |
+
'minimum': 0.3
|
| 55 |
+
},
|
| 56 |
+
'green_space': {
|
| 57 |
+
'minimum_percentage': 0.15
|
| 58 |
+
},
|
| 59 |
+
'plot': {
|
| 60 |
+
'minimum_area_sqm': 1000,
|
| 61 |
+
'minimum_width_m': 20
|
| 62 |
+
},
|
| 63 |
+
'roads': {
|
| 64 |
+
'maximum_distance_to_road_m': 200
|
| 65 |
+
}
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
def validate_compliance(self, layout: Layout) -> ComplianceReport:
|
| 69 |
+
"""
|
| 70 |
+
Comprehensive regulatory compliance validation
|
| 71 |
+
|
| 72 |
+
Args:
|
| 73 |
+
layout: Layout to validate
|
| 74 |
+
|
| 75 |
+
Returns:
|
| 76 |
+
ComplianceReport with violations and warnings
|
| 77 |
+
"""
|
| 78 |
+
report = ComplianceReport(
|
| 79 |
+
layout_id=layout.id,
|
| 80 |
+
is_compliant=True
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
self.logger.info(f"Validating layout {layout.id} against Vietnamese regulations")
|
| 84 |
+
|
| 85 |
+
# Run all checks
|
| 86 |
+
self._check_boundary_setbacks(layout, report)
|
| 87 |
+
self._check_far_compliance(layout, report)
|
| 88 |
+
self._check_green_space_requirements(layout, report)
|
| 89 |
+
self._check_plot_dimensions(layout, report)
|
| 90 |
+
self._check_road_accessibility(layout, report)
|
| 91 |
+
self._check_fire_safety_distances(layout, report)
|
| 92 |
+
self._check_no_overlaps(layout, report)
|
| 93 |
+
|
| 94 |
+
# Final determination
|
| 95 |
+
if len(report.violations) == 0:
|
| 96 |
+
report.is_compliant = True
|
| 97 |
+
self.logger.info(f"Layout {layout.id} is COMPLIANT")
|
| 98 |
+
else:
|
| 99 |
+
report.is_compliant = False
|
| 100 |
+
self.logger.warning(f"Layout {layout.id} has {len(report.violations)} violations")
|
| 101 |
+
|
| 102 |
+
return report
|
| 103 |
+
|
| 104 |
+
def _check_boundary_setbacks(self, layout: Layout, report: ComplianceReport):
|
| 105 |
+
"""Check minimum setback from site boundary"""
|
| 106 |
+
min_setback = self.regulations['setbacks']['boundary_minimum']
|
| 107 |
+
|
| 108 |
+
# Create buffer zone inside boundary
|
| 109 |
+
boundary = layout.site_boundary.geometry
|
| 110 |
+
setback_zone = boundary.buffer(-min_setback)
|
| 111 |
+
|
| 112 |
+
for plot in layout.plots:
|
| 113 |
+
if plot.type == PlotType.INDUSTRIAL:
|
| 114 |
+
if not setback_zone.contains(plot.geometry):
|
| 115 |
+
report.add_violation(
|
| 116 |
+
f"Plot {plot.id} violates {min_setback}m boundary setback requirement"
|
| 117 |
+
)
|
| 118 |
+
return
|
| 119 |
+
|
| 120 |
+
report.add_pass("Boundary setback compliance")
|
| 121 |
+
|
| 122 |
+
def _check_far_compliance(self, layout: Layout, report: ComplianceReport):
|
| 123 |
+
"""Check Floor Area Ratio (FAR) compliance"""
|
| 124 |
+
max_far = self.regulations['far']['maximum']
|
| 125 |
+
min_far = self.regulations['far'].get('minimum', 0.0)
|
| 126 |
+
|
| 127 |
+
metrics = layout.metrics
|
| 128 |
+
|
| 129 |
+
# FAR = Total floor area / Land area
|
| 130 |
+
# Simplified: assuming single story, FAR = Building area / Land area
|
| 131 |
+
if metrics.sellable_ratio > max_far:
|
| 132 |
+
report.add_violation(
|
| 133 |
+
f"FAR {metrics.sellable_ratio:.2f} exceeds maximum {max_far}"
|
| 134 |
+
)
|
| 135 |
+
elif metrics.sellable_ratio < min_far:
|
| 136 |
+
report.add_warning(
|
| 137 |
+
f"FAR {metrics.sellable_ratio:.2f} below recommended minimum {min_far}"
|
| 138 |
+
)
|
| 139 |
+
else:
|
| 140 |
+
report.add_pass(f"FAR compliance ({metrics.sellable_ratio:.2f})")
|
| 141 |
+
|
| 142 |
+
def _check_green_space_requirements(self, layout: Layout, report: ComplianceReport):
|
| 143 |
+
"""Check minimum green space requirement"""
|
| 144 |
+
min_green = self.regulations['green_space']['minimum_percentage']
|
| 145 |
+
|
| 146 |
+
metrics = layout.metrics
|
| 147 |
+
|
| 148 |
+
if metrics.green_space_ratio < min_green:
|
| 149 |
+
deficit = (min_green - metrics.green_space_ratio) * 100
|
| 150 |
+
report.add_violation(
|
| 151 |
+
f"Green space {metrics.green_space_ratio*100:.1f}% is below minimum {min_green*100}% "
|
| 152 |
+
f"(deficit: {deficit:.1f}%)"
|
| 153 |
+
)
|
| 154 |
+
else:
|
| 155 |
+
report.add_pass(f"Green space compliance ({metrics.green_space_ratio*100:.1f}%)")
|
| 156 |
+
|
| 157 |
+
def _check_plot_dimensions(self, layout: Layout, report: ComplianceReport):
|
| 158 |
+
"""Check plot minimum dimensions"""
|
| 159 |
+
min_area = self.regulations['plot']['minimum_area_sqm']
|
| 160 |
+
min_width = self.regulations['plot']['minimum_width_m']
|
| 161 |
+
|
| 162 |
+
violations = []
|
| 163 |
+
for plot in layout.plots:
|
| 164 |
+
if plot.type == PlotType.INDUSTRIAL:
|
| 165 |
+
if plot.area_sqm < min_area:
|
| 166 |
+
violations.append(
|
| 167 |
+
f"Plot {plot.id} area {plot.area_sqm:.0f}m² below minimum {min_area}m²"
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
if plot.width_m < min_width:
|
| 171 |
+
violations.append(
|
| 172 |
+
f"Plot {plot.id} width {plot.width_m:.1f}m below minimum {min_width}m"
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
if violations:
|
| 176 |
+
for v in violations:
|
| 177 |
+
report.add_violation(v)
|
| 178 |
+
else:
|
| 179 |
+
report.add_pass("Plot dimension compliance")
|
| 180 |
+
|
| 181 |
+
def _check_road_accessibility(self, layout: Layout, report: ComplianceReport):
|
| 182 |
+
"""Check that all plots have road access within maximum distance"""
|
| 183 |
+
max_distance = self.regulations['roads']['maximum_distance_to_road_m']
|
| 184 |
+
|
| 185 |
+
if not layout.road_network or not layout.road_network.primary_roads:
|
| 186 |
+
report.add_warning("No road network defined for accessibility check")
|
| 187 |
+
return
|
| 188 |
+
|
| 189 |
+
# Simplified check: ensure plots are within max_distance of roads
|
| 190 |
+
# In practice, would check actual road connectivity
|
| 191 |
+
|
| 192 |
+
violations = []
|
| 193 |
+
for plot in layout.plots:
|
| 194 |
+
if plot.type == PlotType.INDUSTRIAL:
|
| 195 |
+
if not plot.has_road_access:
|
| 196 |
+
violations.append(f"Plot {plot.id} lacks road access")
|
| 197 |
+
|
| 198 |
+
if violations:
|
| 199 |
+
for v in violations:
|
| 200 |
+
report.add_violation(v)
|
| 201 |
+
else:
|
| 202 |
+
report.add_pass("Road accessibility compliance")
|
| 203 |
+
|
| 204 |
+
def _check_fire_safety_distances(self, layout: Layout, report: ComplianceReport):
|
| 205 |
+
"""Check fire safety distance requirements"""
|
| 206 |
+
fire_distance = self.regulations['setbacks']['fire_safety_distance']
|
| 207 |
+
|
| 208 |
+
# Check spacing between industrial plots
|
| 209 |
+
industrial_plots = [p for p in layout.plots if p.type == PlotType.INDUSTRIAL]
|
| 210 |
+
|
| 211 |
+
for i, plot1 in enumerate(industrial_plots):
|
| 212 |
+
for plot2 in industrial_plots[i+1:]:
|
| 213 |
+
distance = plot1.geometry.distance(plot2.geometry)
|
| 214 |
+
if distance < fire_distance:
|
| 215 |
+
report.add_violation(
|
| 216 |
+
f"Plots {plot1.id} and {plot2.id} violate {fire_distance}m fire safety distance "
|
| 217 |
+
f"(actual: {distance:.1f}m)"
|
| 218 |
+
)
|
| 219 |
+
return
|
| 220 |
+
|
| 221 |
+
report.add_pass("Fire safety distance compliance")
|
| 222 |
+
|
| 223 |
+
def _check_no_overlaps(self, layout: Layout, report: ComplianceReport):
|
| 224 |
+
"""Check that no plots overlap"""
|
| 225 |
+
plots = layout.plots
|
| 226 |
+
|
| 227 |
+
for i, plot1 in enumerate(plots):
|
| 228 |
+
for plot2 in plots[i+1:]:
|
| 229 |
+
if plot1.geometry.intersects(plot2.geometry):
|
| 230 |
+
intersection = plot1.geometry.intersection(plot2.geometry)
|
| 231 |
+
if intersection.area > 0.01: # Small tolerance for numerical errors
|
| 232 |
+
report.add_violation(
|
| 233 |
+
f"Plots {plot1.id} and {plot2.id} overlap by {intersection.area:.2f}m²"
|
| 234 |
+
)
|
| 235 |
+
return
|
| 236 |
+
|
| 237 |
+
report.add_pass("No plot overlaps")
|
| 238 |
+
|
| 239 |
+
def check_constraint_compliance(self, layout: Layout, constraints: List[Constraint]) -> ComplianceReport:
|
| 240 |
+
"""
|
| 241 |
+
Check compliance against specific spatial constraints
|
| 242 |
+
|
| 243 |
+
Args:
|
| 244 |
+
layout: Layout to check
|
| 245 |
+
constraints: List of spatial constraints
|
| 246 |
+
|
| 247 |
+
Returns:
|
| 248 |
+
ComplianceReport
|
| 249 |
+
"""
|
| 250 |
+
report = ComplianceReport(
|
| 251 |
+
layout_id=layout.id,
|
| 252 |
+
is_compliant=True
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
for constraint in constraints:
|
| 256 |
+
violations = self._check_single_constraint(layout, constraint)
|
| 257 |
+
for v in violations:
|
| 258 |
+
report.add_violation(v)
|
| 259 |
+
|
| 260 |
+
return report
|
| 261 |
+
|
| 262 |
+
def _check_single_constraint(self, layout: Layout, constraint: Constraint) -> List[str]:
|
| 263 |
+
"""Check a single constraint"""
|
| 264 |
+
violations = []
|
| 265 |
+
|
| 266 |
+
for plot in layout.plots:
|
| 267 |
+
if plot.type == PlotType.INDUSTRIAL:
|
| 268 |
+
if plot.geometry.intersects(constraint.geometry):
|
| 269 |
+
violations.append(
|
| 270 |
+
f"Plot {plot.id} violates {constraint.type.value} constraint: {constraint.description}"
|
| 271 |
+
)
|
| 272 |
+
|
| 273 |
+
return violations
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
# Example usage
|
| 277 |
+
if __name__ == "__main__":
|
| 278 |
+
from shapely.geometry import box
|
| 279 |
+
from src.models.domain import SiteBoundary
|
| 280 |
+
|
| 281 |
+
# Create example site and layout
|
| 282 |
+
site_geom = box(0, 0, 500, 500)
|
| 283 |
+
site = SiteBoundary(geometry=site_geom, area_sqm=site_geom.area)
|
| 284 |
+
site.buildable_area_sqm = site.area_sqm
|
| 285 |
+
|
| 286 |
+
layout = Layout(site_boundary=site)
|
| 287 |
+
|
| 288 |
+
# Add some example plots
|
| 289 |
+
plot1 = Plot(
|
| 290 |
+
geometry=box(60, 60, 150, 150),
|
| 291 |
+
area_sqm=8100,
|
| 292 |
+
type=PlotType.INDUSTRIAL,
|
| 293 |
+
width_m=90,
|
| 294 |
+
depth_m=90,
|
| 295 |
+
has_road_access=True
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
plot2 = Plot(
|
| 299 |
+
geometry=box(200, 60, 290, 150),
|
| 300 |
+
area_sqm=8100,
|
| 301 |
+
type=PlotType.INDUSTRIAL,
|
| 302 |
+
width_m=90,
|
| 303 |
+
depth_m=90,
|
| 304 |
+
has_road_access=True
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
green_plot = Plot(
|
| 308 |
+
geometry=box(60, 200, 150, 290),
|
| 309 |
+
area_sqm=8100,
|
| 310 |
+
type=PlotType.GREEN_SPACE
|
| 311 |
+
)
|
| 312 |
+
|
| 313 |
+
layout.plots = [plot1, plot2, green_plot]
|
| 314 |
+
layout.calculate_metrics()
|
| 315 |
+
|
| 316 |
+
# Check compliance
|
| 317 |
+
checker = RegulationChecker()
|
| 318 |
+
report = checker.validate_compliance(layout)
|
| 319 |
+
|
| 320 |
+
print(f"Compliance: {report.is_compliant}")
|
| 321 |
+
print(f"Violations: {len(report.violations)}")
|
| 322 |
+
for v in report.violations:
|
| 323 |
+
print(f" - {v}")
|
| 324 |
+
print(f"Warnings: {len(report.warnings)}")
|
| 325 |
+
for w in report.warnings:
|
| 326 |
+
print(f" - {w}")
|
| 327 |
+
print(f"Checks passed: {len(report.checks_passed)}")
|
| 328 |
+
for c in report.checks_passed:
|
| 329 |
+
print(f" ✓ {c}")
|
src/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""API package"""
|
src/api/main.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastAPI Main Application - REMB Optimization Engine
|
| 3 |
+
"""
|
| 4 |
+
from fastapi import FastAPI, UploadFile, File, HTTPException
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from pydantic import BaseModel
|
| 7 |
+
from typing import Optional, List
|
| 8 |
+
import logging
|
| 9 |
+
|
| 10 |
+
from config.settings import settings
|
| 11 |
+
|
| 12 |
+
# Configure logging
|
| 13 |
+
logging.basicConfig(level=logging.INFO)
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
# Create FastAPI app
|
| 17 |
+
app = FastAPI(
|
| 18 |
+
title=settings.PROJECT_NAME,
|
| 19 |
+
version=settings.VERSION,
|
| 20 |
+
description="AI-Powered Industrial Estate Master Planning Optimization Engine"
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
# CORS middleware
|
| 24 |
+
app.add_middleware(
|
| 25 |
+
CORSMiddleware,
|
| 26 |
+
allow_origins=["*"],
|
| 27 |
+
allow_credentials=True,
|
| 28 |
+
allow_methods=["*"],
|
| 29 |
+
allow_headers=["*"],
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# Pydantic models for API
|
| 34 |
+
class OptimizationRequest(BaseModel):
|
| 35 |
+
"""Request model for optimization"""
|
| 36 |
+
site_id: str
|
| 37 |
+
population_size: int = 100
|
| 38 |
+
n_generations: int = 200
|
| 39 |
+
n_plots: int = 20
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class OptimizationResponse(BaseModel):
|
| 43 |
+
"""Response model for optimization"""
|
| 44 |
+
optimization_id: str
|
| 45 |
+
status: str
|
| 46 |
+
n_solutions: int
|
| 47 |
+
generation_time_seconds: float
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class LayoutMetricsResponse(BaseModel):
|
| 51 |
+
"""Layout metrics response"""
|
| 52 |
+
layout_id: str
|
| 53 |
+
total_area_sqm: float
|
| 54 |
+
sellable_area_sqm: float
|
| 55 |
+
green_space_area_sqm: float
|
| 56 |
+
sellable_ratio: float
|
| 57 |
+
green_space_ratio: float
|
| 58 |
+
is_compliant: bool
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
@app.get("/")
|
| 62 |
+
async def root():
|
| 63 |
+
"""Root endpoint"""
|
| 64 |
+
return {
|
| 65 |
+
"message": "REMB Industrial Estate Master Planning Optimization Engine",
|
| 66 |
+
"version": settings.VERSION,
|
| 67 |
+
"status": "operational"
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
@app.get("/api/v1/health")
|
| 72 |
+
async def health_check():
|
| 73 |
+
"""Health check endpoint"""
|
| 74 |
+
return {
|
| 75 |
+
"status": "healthy",
|
| 76 |
+
"version": settings.VERSION
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
@app.post("/api/v1/sites/upload")
|
| 81 |
+
async def upload_site_boundary(file: UploadFile = File(...)):
|
| 82 |
+
"""
|
| 83 |
+
Upload site boundary file (Shapefile or DXF)
|
| 84 |
+
|
| 85 |
+
Args:
|
| 86 |
+
file: Shapefile (.shp) or DXF file
|
| 87 |
+
|
| 88 |
+
Returns:
|
| 89 |
+
Site ID and metadata
|
| 90 |
+
"""
|
| 91 |
+
if not file.filename:
|
| 92 |
+
raise HTTPException(status_code=400, detail="No file provided")
|
| 93 |
+
|
| 94 |
+
# Check file extension
|
| 95 |
+
extension = file.filename.split('.')[-1].lower()
|
| 96 |
+
if f".{extension}" not in settings.ALLOWED_EXTENSIONS:
|
| 97 |
+
raise HTTPException(
|
| 98 |
+
status_code=400,
|
| 99 |
+
detail=f"File type .{extension} not allowed. Allowed: {settings.ALLOWED_EXTENSIONS}"
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
# TODO: Implement actual file processing
|
| 103 |
+
site_id = "site_123" # Placeholder
|
| 104 |
+
|
| 105 |
+
return {
|
| 106 |
+
"site_id": site_id,
|
| 107 |
+
"filename": file.filename,
|
| 108 |
+
"status": "uploaded",
|
| 109 |
+
"message": "Site boundary uploaded successfully"
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
@app.post("/api/v1/sites/{site_id}/optimize", response_model=OptimizationResponse)
|
| 114 |
+
async def optimize_site(site_id: str, request: OptimizationRequest):
|
| 115 |
+
"""
|
| 116 |
+
Run optimization for a site
|
| 117 |
+
|
| 118 |
+
Args:
|
| 119 |
+
site_id: Site identifier
|
| 120 |
+
request: Optimization parameters
|
| 121 |
+
|
| 122 |
+
Returns:
|
| 123 |
+
Optimization results with Pareto front
|
| 124 |
+
"""
|
| 125 |
+
logger.info(f"Starting optimization for site {site_id}")
|
| 126 |
+
|
| 127 |
+
# TODO: Implement actual optimization
|
| 128 |
+
# This would call NSGA2Optimizer and return results
|
| 129 |
+
|
| 130 |
+
return OptimizationResponse(
|
| 131 |
+
optimization_id="opt_123",
|
| 132 |
+
status="completed",
|
| 133 |
+
n_solutions=8,
|
| 134 |
+
generation_time_seconds=120.5
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
@app.get("/api/v1/layouts/{layout_id}/metrics", response_model=LayoutMetricsResponse)
|
| 139 |
+
async def get_layout_metrics(layout_id: str):
|
| 140 |
+
"""
|
| 141 |
+
Get metrics for a specific layout
|
| 142 |
+
|
| 143 |
+
Args:
|
| 144 |
+
layout_id: Layout identifier
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
Layout metrics
|
| 148 |
+
"""
|
| 149 |
+
# TODO: Retrieve from database
|
| 150 |
+
return LayoutMetricsResponse(
|
| 151 |
+
layout_id=layout_id,
|
| 152 |
+
total_area_sqm=250000.0,
|
| 153 |
+
sellable_area_sqm=162500.0,
|
| 154 |
+
green_space_area_sqm=37500.0,
|
| 155 |
+
sellable_ratio=0.65,
|
| 156 |
+
green_space_ratio=0.15,
|
| 157 |
+
is_compliant=True
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
|
| 161 |
+
@app.get("/api/v1/layouts/{layout_id}/export")
|
| 162 |
+
async def export_layout_dxf(layout_id: str):
|
| 163 |
+
"""
|
| 164 |
+
Export layout as DXF file
|
| 165 |
+
|
| 166 |
+
Args:
|
| 167 |
+
layout_id: Layout identifier
|
| 168 |
+
|
| 169 |
+
Returns:
|
| 170 |
+
DXF file download
|
| 171 |
+
"""
|
| 172 |
+
# TODO: Implement DXF export
|
| 173 |
+
return {
|
| 174 |
+
"layout_id": layout_id,
|
| 175 |
+
"status": "exported",
|
| 176 |
+
"download_url": f"/downloads/{layout_id}.dxf"
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
if __name__ == "__main__":
|
| 181 |
+
import uvicorn
|
| 182 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
src/api/mvp_api.py
ADDED
|
@@ -0,0 +1,677 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
AIOptimize™ MVP API
|
| 3 |
+
FastAPI backend for industrial estate planning optimization
|
| 4 |
+
Per MVP-24h.md specification
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
import io
|
| 8 |
+
import json
|
| 9 |
+
import zipfile
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
from typing import Optional, List, Dict, Any
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
from fastapi import FastAPI, HTTPException, UploadFile, File, Form
|
| 15 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 16 |
+
from fastapi.responses import StreamingResponse, JSONResponse
|
| 17 |
+
from pydantic import BaseModel
|
| 18 |
+
from shapely.geometry import Polygon, shape
|
| 19 |
+
|
| 20 |
+
# Import services
|
| 21 |
+
from src.services.session_manager import session_manager
|
| 22 |
+
from src.services.gemini_service import gemini_service
|
| 23 |
+
from src.algorithms.ga_optimizer import SimpleGAOptimizer
|
| 24 |
+
from src.export.dxf_exporter import DXFExporter
|
| 25 |
+
from src.models.domain import Layout, Plot, PlotType, SiteBoundary, LayoutMetrics
|
| 26 |
+
|
| 27 |
+
# Sample data
|
| 28 |
+
SAMPLE_BOUNDARY = {
|
| 29 |
+
"type": "Feature",
|
| 30 |
+
"geometry": {
|
| 31 |
+
"type": "Polygon",
|
| 32 |
+
"coordinates": [[
|
| 33 |
+
[0, 0], [500, 0], [500, 400], [0, 400], [0, 0]
|
| 34 |
+
]]
|
| 35 |
+
},
|
| 36 |
+
"properties": {"name": "Sample Industrial Site"}
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# === Pydantic Models ===
|
| 41 |
+
|
| 42 |
+
class UploadResponse(BaseModel):
|
| 43 |
+
session_id: str
|
| 44 |
+
boundary: Dict[str, Any]
|
| 45 |
+
metadata: Dict[str, Any]
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class GenerateRequest(BaseModel):
|
| 49 |
+
session_id: str
|
| 50 |
+
target_plots: int = 8
|
| 51 |
+
setback: float = 50.0
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class ChatRequest(BaseModel):
|
| 55 |
+
session_id: str
|
| 56 |
+
message: str
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class ChatResponse(BaseModel):
|
| 60 |
+
message: str
|
| 61 |
+
model: str
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
class ExportRequest(BaseModel):
|
| 65 |
+
session_id: str
|
| 66 |
+
option_id: int
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
class HealthResponse(BaseModel):
|
| 70 |
+
status: str
|
| 71 |
+
version: str
|
| 72 |
+
gemini_available: bool
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# === FastAPI App ===
|
| 76 |
+
|
| 77 |
+
app = FastAPI(
|
| 78 |
+
title="AIOptimize™ API",
|
| 79 |
+
description="AI-Powered Industrial Estate Planning Engine",
|
| 80 |
+
version="1.0.0"
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
# CORS for frontend
|
| 84 |
+
app.add_middleware(
|
| 85 |
+
CORSMiddleware,
|
| 86 |
+
allow_origins=["*"], # Allow all for development
|
| 87 |
+
allow_credentials=True,
|
| 88 |
+
allow_methods=["*"],
|
| 89 |
+
allow_headers=["*"],
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
# Initialize optimizer and exporter
|
| 93 |
+
ga_optimizer = SimpleGAOptimizer()
|
| 94 |
+
dxf_exporter = DXFExporter()
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
# === API Endpoints ===
|
| 98 |
+
|
| 99 |
+
@app.get("/api/health", response_model=HealthResponse)
|
| 100 |
+
async def health_check():
|
| 101 |
+
"""Health check endpoint"""
|
| 102 |
+
return HealthResponse(
|
| 103 |
+
status="healthy",
|
| 104 |
+
version="1.0.0",
|
| 105 |
+
gemini_available=gemini_service.is_available
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
@app.get("/api/sample-data")
|
| 110 |
+
async def get_sample_data():
|
| 111 |
+
"""Get sample GeoJSON boundary data"""
|
| 112 |
+
return SAMPLE_BOUNDARY
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
@app.post("/api/upload-boundary", response_model=UploadResponse)
|
| 116 |
+
async def upload_boundary(file: UploadFile = File(None), geojson: str = Form(None)):
|
| 117 |
+
"""
|
| 118 |
+
Upload site boundary (GeoJSON)
|
| 119 |
+
|
| 120 |
+
Accepts either file upload or JSON string
|
| 121 |
+
"""
|
| 122 |
+
try:
|
| 123 |
+
# Get GeoJSON data
|
| 124 |
+
if file and file.filename:
|
| 125 |
+
content = await file.read()
|
| 126 |
+
geojson_data = json.loads(content)
|
| 127 |
+
elif geojson:
|
| 128 |
+
geojson_data = json.loads(geojson)
|
| 129 |
+
else:
|
| 130 |
+
raise HTTPException(400, "No boundary data provided")
|
| 131 |
+
|
| 132 |
+
# Extract coordinates from GeoJSON
|
| 133 |
+
if geojson_data.get("type") == "Feature":
|
| 134 |
+
geometry = geojson_data.get("geometry", {})
|
| 135 |
+
elif geojson_data.get("type") == "FeatureCollection":
|
| 136 |
+
features = geojson_data.get("features", [])
|
| 137 |
+
if features:
|
| 138 |
+
geometry = features[0].get("geometry", {})
|
| 139 |
+
else:
|
| 140 |
+
raise HTTPException(400, "No features in FeatureCollection")
|
| 141 |
+
elif geojson_data.get("type") == "Polygon":
|
| 142 |
+
geometry = geojson_data
|
| 143 |
+
else:
|
| 144 |
+
raise HTTPException(400, "Invalid GeoJSON format")
|
| 145 |
+
|
| 146 |
+
# Get coordinates
|
| 147 |
+
coords = geometry.get("coordinates", [[]])[0]
|
| 148 |
+
if not coords:
|
| 149 |
+
raise HTTPException(400, "No coordinates found")
|
| 150 |
+
|
| 151 |
+
# Create Shapely polygon and validate
|
| 152 |
+
polygon = Polygon(coords)
|
| 153 |
+
if not polygon.is_valid:
|
| 154 |
+
polygon = polygon.buffer(0)
|
| 155 |
+
|
| 156 |
+
# Calculate metadata
|
| 157 |
+
metadata = {
|
| 158 |
+
"area": polygon.area,
|
| 159 |
+
"perimeter": polygon.length,
|
| 160 |
+
"bounds": list(polygon.bounds),
|
| 161 |
+
"centroid": [polygon.centroid.x, polygon.centroid.y]
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
# Create session and store data
|
| 165 |
+
session = session_manager.create_session()
|
| 166 |
+
session_manager.set_boundary(
|
| 167 |
+
session.id,
|
| 168 |
+
boundary=geojson_data,
|
| 169 |
+
coords=coords,
|
| 170 |
+
metadata=metadata
|
| 171 |
+
)
|
| 172 |
+
|
| 173 |
+
return UploadResponse(
|
| 174 |
+
session_id=session.id,
|
| 175 |
+
boundary=geojson_data,
|
| 176 |
+
metadata=metadata
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
except json.JSONDecodeError as e:
|
| 180 |
+
raise HTTPException(400, f"Invalid JSON format: {str(e)}")
|
| 181 |
+
except HTTPException:
|
| 182 |
+
raise
|
| 183 |
+
except Exception as e:
|
| 184 |
+
import traceback
|
| 185 |
+
traceback.print_exc()
|
| 186 |
+
raise HTTPException(500, f"Error processing boundary: {str(e)}")
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
# Alternative JSON endpoint for easier frontend integration
|
| 190 |
+
class UploadBoundaryRequest(BaseModel):
|
| 191 |
+
geojson: Dict[str, Any]
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
@app.post("/api/upload-boundary-json", response_model=UploadResponse)
|
| 195 |
+
async def upload_boundary_json(request: UploadBoundaryRequest):
|
| 196 |
+
"""Upload site boundary via JSON body"""
|
| 197 |
+
try:
|
| 198 |
+
geojson_data = request.geojson
|
| 199 |
+
|
| 200 |
+
# Extract coordinates from GeoJSON
|
| 201 |
+
if geojson_data.get("type") == "Feature":
|
| 202 |
+
geometry = geojson_data.get("geometry", {})
|
| 203 |
+
elif geojson_data.get("type") == "FeatureCollection":
|
| 204 |
+
features = geojson_data.get("features", [])
|
| 205 |
+
if features:
|
| 206 |
+
geometry = features[0].get("geometry", {})
|
| 207 |
+
else:
|
| 208 |
+
raise HTTPException(400, "No features in FeatureCollection")
|
| 209 |
+
elif geojson_data.get("type") == "Polygon":
|
| 210 |
+
geometry = geojson_data
|
| 211 |
+
else:
|
| 212 |
+
raise HTTPException(400, "Invalid GeoJSON format")
|
| 213 |
+
|
| 214 |
+
coords = geometry.get("coordinates", [[]])[0]
|
| 215 |
+
if not coords:
|
| 216 |
+
raise HTTPException(400, "No coordinates found")
|
| 217 |
+
|
| 218 |
+
polygon = Polygon(coords)
|
| 219 |
+
if not polygon.is_valid:
|
| 220 |
+
polygon = polygon.buffer(0)
|
| 221 |
+
|
| 222 |
+
metadata = {
|
| 223 |
+
"area": polygon.area,
|
| 224 |
+
"perimeter": polygon.length,
|
| 225 |
+
"bounds": list(polygon.bounds),
|
| 226 |
+
"centroid": [polygon.centroid.x, polygon.centroid.y]
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
session = session_manager.create_session()
|
| 230 |
+
session_manager.set_boundary(
|
| 231 |
+
session.id,
|
| 232 |
+
boundary=geojson_data,
|
| 233 |
+
coords=coords,
|
| 234 |
+
metadata=metadata
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
return UploadResponse(
|
| 238 |
+
session_id=session.id,
|
| 239 |
+
boundary=geojson_data,
|
| 240 |
+
metadata=metadata
|
| 241 |
+
)
|
| 242 |
+
|
| 243 |
+
except HTTPException:
|
| 244 |
+
raise
|
| 245 |
+
except Exception as e:
|
| 246 |
+
import traceback
|
| 247 |
+
traceback.print_exc()
|
| 248 |
+
raise HTTPException(500, f"Error: {str(e)}")
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
@app.post("/api/upload-dxf", response_model=UploadResponse)
|
| 252 |
+
async def upload_dxf(file: UploadFile = File(...)):
|
| 253 |
+
"""
|
| 254 |
+
Upload site boundary from DXF file
|
| 255 |
+
|
| 256 |
+
Parses LWPOLYLINE entities to extract site boundary polygon
|
| 257 |
+
"""
|
| 258 |
+
import ezdxf
|
| 259 |
+
import tempfile
|
| 260 |
+
|
| 261 |
+
if not file.filename or not file.filename.lower().endswith('.dxf'):
|
| 262 |
+
raise HTTPException(400, "Please upload a valid .dxf file")
|
| 263 |
+
|
| 264 |
+
try:
|
| 265 |
+
# Save to temp file for ezdxf to read
|
| 266 |
+
content = await file.read()
|
| 267 |
+
with tempfile.NamedTemporaryFile(suffix='.dxf', delete=False) as tmp:
|
| 268 |
+
tmp.write(content)
|
| 269 |
+
tmp_path = tmp.name
|
| 270 |
+
|
| 271 |
+
# Parse DXF
|
| 272 |
+
doc = ezdxf.readfile(tmp_path)
|
| 273 |
+
msp = doc.modelspace()
|
| 274 |
+
|
| 275 |
+
# Find closed polylines
|
| 276 |
+
polygons = []
|
| 277 |
+
for entity in msp:
|
| 278 |
+
if entity.dxftype() == 'LWPOLYLINE':
|
| 279 |
+
if entity.closed:
|
| 280 |
+
points = list(entity.get_points())
|
| 281 |
+
if len(points) >= 3:
|
| 282 |
+
coords = [(p[0], p[1]) for p in points]
|
| 283 |
+
coords.append(coords[0]) # Close polygon
|
| 284 |
+
poly = Polygon(coords)
|
| 285 |
+
if poly.is_valid:
|
| 286 |
+
polygons.append((poly, coords))
|
| 287 |
+
elif entity.dxftype() == 'POLYLINE':
|
| 288 |
+
if entity.is_closed:
|
| 289 |
+
points = list(entity.points())
|
| 290 |
+
if len(points) >= 3:
|
| 291 |
+
coords = [(p[0], p[1]) for p in points]
|
| 292 |
+
coords.append(coords[0])
|
| 293 |
+
poly = Polygon(coords)
|
| 294 |
+
if poly.is_valid:
|
| 295 |
+
polygons.append((poly, coords))
|
| 296 |
+
|
| 297 |
+
# Clean up temp file
|
| 298 |
+
os.unlink(tmp_path)
|
| 299 |
+
|
| 300 |
+
if not polygons:
|
| 301 |
+
raise HTTPException(400, "No closed polygons found in DXF file")
|
| 302 |
+
|
| 303 |
+
# Get largest polygon as site boundary
|
| 304 |
+
polygon, coords = max(polygons, key=lambda x: x[0].area)
|
| 305 |
+
|
| 306 |
+
# Create GeoJSON boundary
|
| 307 |
+
geojson_data = {
|
| 308 |
+
"type": "Feature",
|
| 309 |
+
"geometry": {
|
| 310 |
+
"type": "Polygon",
|
| 311 |
+
"coordinates": [coords]
|
| 312 |
+
},
|
| 313 |
+
"properties": {"source": file.filename}
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
# Calculate metadata
|
| 317 |
+
metadata = {
|
| 318 |
+
"area": polygon.area,
|
| 319 |
+
"perimeter": polygon.length,
|
| 320 |
+
"bounds": list(polygon.bounds),
|
| 321 |
+
"centroid": [polygon.centroid.x, polygon.centroid.y],
|
| 322 |
+
"dxf_source": file.filename
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
# Create session
|
| 326 |
+
session = session_manager.create_session()
|
| 327 |
+
session_manager.set_boundary(
|
| 328 |
+
session.id,
|
| 329 |
+
boundary=geojson_data,
|
| 330 |
+
coords=coords,
|
| 331 |
+
metadata=metadata
|
| 332 |
+
)
|
| 333 |
+
|
| 334 |
+
print(f"[DXF] Parsed {file.filename}: {len(coords)-1} vertices, area={polygon.area:.0f}m²")
|
| 335 |
+
|
| 336 |
+
return UploadResponse(
|
| 337 |
+
session_id=session.id,
|
| 338 |
+
boundary=geojson_data,
|
| 339 |
+
metadata=metadata
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
except HTTPException:
|
| 343 |
+
raise
|
| 344 |
+
except Exception as e:
|
| 345 |
+
import traceback
|
| 346 |
+
traceback.print_exc()
|
| 347 |
+
raise HTTPException(500, f"DXF parsing error: {str(e)}")
|
| 348 |
+
|
| 349 |
+
|
| 350 |
+
@app.post("/api/generate-layouts")
|
| 351 |
+
async def generate_layouts(request: GenerateRequest):
|
| 352 |
+
"""
|
| 353 |
+
Generate optimized layout options using Genetic Algorithm
|
| 354 |
+
|
| 355 |
+
Returns 3 diverse layout options:
|
| 356 |
+
1. Maximum Profit
|
| 357 |
+
2. Balanced
|
| 358 |
+
3. Premium
|
| 359 |
+
"""
|
| 360 |
+
# Get session
|
| 361 |
+
session = session_manager.get_session(request.session_id)
|
| 362 |
+
if not session:
|
| 363 |
+
raise HTTPException(404, "Session not found")
|
| 364 |
+
|
| 365 |
+
if not session.boundary_coords:
|
| 366 |
+
raise HTTPException(400, "No boundary uploaded for this session")
|
| 367 |
+
|
| 368 |
+
try:
|
| 369 |
+
# Configure and run optimizer
|
| 370 |
+
optimizer = SimpleGAOptimizer(
|
| 371 |
+
setback=request.setback,
|
| 372 |
+
target_plots=request.target_plots
|
| 373 |
+
)
|
| 374 |
+
|
| 375 |
+
options = optimizer.optimize(session.boundary_coords)
|
| 376 |
+
|
| 377 |
+
# Store in session
|
| 378 |
+
session_manager.set_layouts(request.session_id, options)
|
| 379 |
+
|
| 380 |
+
return {
|
| 381 |
+
"session_id": request.session_id,
|
| 382 |
+
"options": options,
|
| 383 |
+
"count": len(options)
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
except Exception as e:
|
| 387 |
+
raise HTTPException(500, f"Optimization failed: {str(e)}")
|
| 388 |
+
|
| 389 |
+
|
| 390 |
+
@app.post("/api/chat", response_model=ChatResponse)
|
| 391 |
+
async def chat(request: ChatRequest):
|
| 392 |
+
"""
|
| 393 |
+
Chat with AI about layout options
|
| 394 |
+
|
| 395 |
+
Uses Gemini Flash 2.0 if available, otherwise falls back to hardcoded responses
|
| 396 |
+
"""
|
| 397 |
+
# Get session
|
| 398 |
+
session = session_manager.get_session(request.session_id)
|
| 399 |
+
if not session:
|
| 400 |
+
raise HTTPException(404, "Session not found")
|
| 401 |
+
|
| 402 |
+
# Add user message to history
|
| 403 |
+
session_manager.add_chat_message(request.session_id, "user", request.message)
|
| 404 |
+
|
| 405 |
+
# Generate response
|
| 406 |
+
response = gemini_service.chat(
|
| 407 |
+
message=request.message,
|
| 408 |
+
layouts=session.layouts,
|
| 409 |
+
boundary_metadata=session.metadata
|
| 410 |
+
)
|
| 411 |
+
|
| 412 |
+
# Add assistant message to history
|
| 413 |
+
session_manager.add_chat_message(
|
| 414 |
+
request.session_id,
|
| 415 |
+
"assistant",
|
| 416 |
+
response["message"],
|
| 417 |
+
response["model"]
|
| 418 |
+
)
|
| 419 |
+
|
| 420 |
+
return ChatResponse(**response)
|
| 421 |
+
|
| 422 |
+
|
| 423 |
+
@app.post("/api/export-dxf")
|
| 424 |
+
async def export_dxf(request: ExportRequest):
|
| 425 |
+
"""
|
| 426 |
+
Export single layout option to DXF
|
| 427 |
+
|
| 428 |
+
Returns DXF file as download
|
| 429 |
+
"""
|
| 430 |
+
# Get session
|
| 431 |
+
session = session_manager.get_session(request.session_id)
|
| 432 |
+
if not session:
|
| 433 |
+
raise HTTPException(404, "Session not found")
|
| 434 |
+
|
| 435 |
+
if not session.layouts:
|
| 436 |
+
raise HTTPException(400, "No layouts generated")
|
| 437 |
+
|
| 438 |
+
# Find requested option
|
| 439 |
+
option = None
|
| 440 |
+
for layout in session.layouts:
|
| 441 |
+
if layout.get("id") == request.option_id:
|
| 442 |
+
option = layout
|
| 443 |
+
break
|
| 444 |
+
|
| 445 |
+
if not option:
|
| 446 |
+
raise HTTPException(404, f"Option {request.option_id} not found")
|
| 447 |
+
|
| 448 |
+
try:
|
| 449 |
+
# Create Layout object for exporter
|
| 450 |
+
layout_obj = _create_layout_from_option(option, session)
|
| 451 |
+
|
| 452 |
+
# Generate DXF to bytes
|
| 453 |
+
dxf_bytes = _export_layout_to_bytes(layout_obj, option)
|
| 454 |
+
|
| 455 |
+
# Create filename
|
| 456 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 457 |
+
filename = f"option_{request.option_id}_{timestamp}.dxf"
|
| 458 |
+
|
| 459 |
+
return StreamingResponse(
|
| 460 |
+
io.BytesIO(dxf_bytes),
|
| 461 |
+
media_type="application/x-autocad-dxf",
|
| 462 |
+
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
| 463 |
+
)
|
| 464 |
+
|
| 465 |
+
except Exception as e:
|
| 466 |
+
raise HTTPException(500, f"Export failed: {str(e)}")
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
@app.post("/api/export-all-dxf")
|
| 470 |
+
async def export_all_dxf(session_id: str = Form(...)):
|
| 471 |
+
"""
|
| 472 |
+
Export all layout options as ZIP file
|
| 473 |
+
|
| 474 |
+
Returns ZIP containing 3 DXF files
|
| 475 |
+
"""
|
| 476 |
+
# Get session
|
| 477 |
+
session = session_manager.get_session(session_id)
|
| 478 |
+
if not session:
|
| 479 |
+
raise HTTPException(404, "Session not found")
|
| 480 |
+
|
| 481 |
+
if not session.layouts:
|
| 482 |
+
raise HTTPException(400, "No layouts generated")
|
| 483 |
+
|
| 484 |
+
try:
|
| 485 |
+
# Create ZIP in memory
|
| 486 |
+
zip_buffer = io.BytesIO()
|
| 487 |
+
|
| 488 |
+
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zf:
|
| 489 |
+
for option in session.layouts:
|
| 490 |
+
# Create Layout object
|
| 491 |
+
layout_obj = _create_layout_from_option(option, session)
|
| 492 |
+
|
| 493 |
+
# Generate DXF
|
| 494 |
+
dxf_bytes = _export_layout_to_bytes(layout_obj, option)
|
| 495 |
+
|
| 496 |
+
# Add to ZIP
|
| 497 |
+
filename = f"option_{option.get('id', 0)}_{option.get('name', 'layout').replace(' ', '_')}.dxf"
|
| 498 |
+
zf.writestr(filename, dxf_bytes)
|
| 499 |
+
|
| 500 |
+
zip_buffer.seek(0)
|
| 501 |
+
|
| 502 |
+
# Create filename
|
| 503 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
| 504 |
+
filename = f"layouts_{timestamp}.zip"
|
| 505 |
+
|
| 506 |
+
return StreamingResponse(
|
| 507 |
+
zip_buffer,
|
| 508 |
+
media_type="application/zip",
|
| 509 |
+
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
| 510 |
+
)
|
| 511 |
+
|
| 512 |
+
except Exception as e:
|
| 513 |
+
raise HTTPException(500, f"Export failed: {str(e)}")
|
| 514 |
+
|
| 515 |
+
|
| 516 |
+
@app.get("/api/session/{session_id}")
|
| 517 |
+
async def get_session(session_id: str):
|
| 518 |
+
"""Get session info"""
|
| 519 |
+
session = session_manager.get_session(session_id)
|
| 520 |
+
if not session:
|
| 521 |
+
raise HTTPException(404, "Session not found")
|
| 522 |
+
|
| 523 |
+
return session.to_dict()
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
# === Helper Functions ===
|
| 527 |
+
|
| 528 |
+
def _create_layout_from_option(option: Dict, session) -> Layout:
|
| 529 |
+
"""Convert GA option to Layout object for DXF export"""
|
| 530 |
+
from shapely.geometry import box, Polygon
|
| 531 |
+
|
| 532 |
+
# Create site boundary
|
| 533 |
+
boundary_poly = Polygon(session.boundary_coords)
|
| 534 |
+
site = SiteBoundary(
|
| 535 |
+
geometry=boundary_poly,
|
| 536 |
+
area_sqm=boundary_poly.area
|
| 537 |
+
)
|
| 538 |
+
site.buildable_area_sqm = boundary_poly.buffer(-50).area
|
| 539 |
+
|
| 540 |
+
# Create layout
|
| 541 |
+
layout = Layout(site_boundary=site)
|
| 542 |
+
|
| 543 |
+
# Add plots
|
| 544 |
+
plots = []
|
| 545 |
+
for i, plot_data in enumerate(option.get("plots", [])):
|
| 546 |
+
coords = plot_data.get("coords", [])
|
| 547 |
+
if coords:
|
| 548 |
+
plot_geom = Polygon(coords)
|
| 549 |
+
else:
|
| 550 |
+
plot_geom = box(
|
| 551 |
+
plot_data["x"],
|
| 552 |
+
plot_data["y"],
|
| 553 |
+
plot_data["x"] + plot_data["width"],
|
| 554 |
+
plot_data["y"] + plot_data["height"]
|
| 555 |
+
)
|
| 556 |
+
|
| 557 |
+
plot = Plot(
|
| 558 |
+
id=f"P{i+1}",
|
| 559 |
+
geometry=plot_geom,
|
| 560 |
+
area_sqm=plot_data.get("area", plot_geom.area),
|
| 561 |
+
type=PlotType.INDUSTRIAL,
|
| 562 |
+
width_m=plot_data.get("width", 50),
|
| 563 |
+
depth_m=plot_data.get("height", 50)
|
| 564 |
+
)
|
| 565 |
+
plots.append(plot)
|
| 566 |
+
|
| 567 |
+
layout.plots = plots
|
| 568 |
+
|
| 569 |
+
# Set metrics
|
| 570 |
+
metrics = option.get("metrics", {})
|
| 571 |
+
layout.metrics = LayoutMetrics(
|
| 572 |
+
total_area_sqm=site.area_sqm,
|
| 573 |
+
sellable_area_sqm=metrics.get("total_area", 0),
|
| 574 |
+
green_space_area_sqm=0,
|
| 575 |
+
road_area_sqm=0,
|
| 576 |
+
num_plots=metrics.get("total_plots", len(plots)),
|
| 577 |
+
is_compliant=True
|
| 578 |
+
)
|
| 579 |
+
layout.metrics.sellable_ratio = layout.metrics.sellable_area_sqm / layout.metrics.total_area_sqm if layout.metrics.total_area_sqm > 0 else 0
|
| 580 |
+
|
| 581 |
+
return layout
|
| 582 |
+
|
| 583 |
+
|
| 584 |
+
def _export_layout_to_bytes(layout: Layout, option: Dict) -> bytes:
|
| 585 |
+
"""Export layout to DXF bytes"""
|
| 586 |
+
import ezdxf
|
| 587 |
+
from ezdxf.enums import TextEntityAlignment
|
| 588 |
+
|
| 589 |
+
# Create DXF document
|
| 590 |
+
doc = ezdxf.new(dxfversion="R2010")
|
| 591 |
+
msp = doc.modelspace()
|
| 592 |
+
|
| 593 |
+
# Setup layers
|
| 594 |
+
layers = {
|
| 595 |
+
'BOUNDARY': {'color': 7}, # White
|
| 596 |
+
'SETBACK': {'color': 1}, # Red
|
| 597 |
+
'PLOTS': {'color': 5}, # Blue
|
| 598 |
+
'LABELS': {'color': 7}, # White
|
| 599 |
+
'ANNOTATIONS': {'color': 2}, # Yellow
|
| 600 |
+
'TITLEBLOCK': {'color': 7} # White
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
for name, props in layers.items():
|
| 604 |
+
doc.layers.add(name, color=props['color'])
|
| 605 |
+
|
| 606 |
+
# Draw boundary
|
| 607 |
+
if layout.site_boundary and layout.site_boundary.geometry:
|
| 608 |
+
coords = list(layout.site_boundary.geometry.exterior.coords)
|
| 609 |
+
msp.add_lwpolyline(coords, dxfattribs={'layer': 'BOUNDARY', 'closed': True})
|
| 610 |
+
|
| 611 |
+
# Draw setback zone
|
| 612 |
+
setback = layout.site_boundary.geometry.buffer(-50)
|
| 613 |
+
if not setback.is_empty:
|
| 614 |
+
setback_coords = list(setback.exterior.coords)
|
| 615 |
+
msp.add_lwpolyline(setback_coords, dxfattribs={'layer': 'SETBACK', 'closed': True})
|
| 616 |
+
|
| 617 |
+
# Draw plots
|
| 618 |
+
for plot in layout.plots:
|
| 619 |
+
if plot.geometry:
|
| 620 |
+
coords = list(plot.geometry.exterior.coords)
|
| 621 |
+
msp.add_lwpolyline(coords, dxfattribs={'layer': 'PLOTS', 'closed': True})
|
| 622 |
+
|
| 623 |
+
# Add label
|
| 624 |
+
centroid = plot.geometry.centroid
|
| 625 |
+
msp.add_text(
|
| 626 |
+
plot.id,
|
| 627 |
+
dxfattribs={
|
| 628 |
+
'layer': 'LABELS',
|
| 629 |
+
'height': 5,
|
| 630 |
+
'insert': (centroid.x, centroid.y)
|
| 631 |
+
}
|
| 632 |
+
)
|
| 633 |
+
|
| 634 |
+
# Add area annotation
|
| 635 |
+
msp.add_text(
|
| 636 |
+
f"{plot.area_sqm:.0f}m²",
|
| 637 |
+
dxfattribs={
|
| 638 |
+
'layer': 'ANNOTATIONS',
|
| 639 |
+
'height': 3,
|
| 640 |
+
'insert': (centroid.x, centroid.y - 8)
|
| 641 |
+
}
|
| 642 |
+
)
|
| 643 |
+
|
| 644 |
+
# Add title block
|
| 645 |
+
if layout.site_boundary:
|
| 646 |
+
bounds = layout.site_boundary.geometry.bounds
|
| 647 |
+
minx, miny = bounds[0], bounds[1]
|
| 648 |
+
|
| 649 |
+
title_lines = [
|
| 650 |
+
f"AIOptimize™ - {option.get('name', 'Layout')}",
|
| 651 |
+
f"Plots: {option.get('metrics', {}).get('total_plots', 0)}",
|
| 652 |
+
f"Total Area: {option.get('metrics', {}).get('total_area', 0):.0f} m²",
|
| 653 |
+
f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}"
|
| 654 |
+
]
|
| 655 |
+
|
| 656 |
+
y = miny - 20
|
| 657 |
+
for line in title_lines:
|
| 658 |
+
msp.add_text(
|
| 659 |
+
line,
|
| 660 |
+
dxfattribs={
|
| 661 |
+
'layer': 'TITLEBLOCK',
|
| 662 |
+
'height': 4,
|
| 663 |
+
'insert': (minx, y)
|
| 664 |
+
}
|
| 665 |
+
)
|
| 666 |
+
y -= 8
|
| 667 |
+
|
| 668 |
+
# Save to bytes
|
| 669 |
+
stream = io.StringIO()
|
| 670 |
+
doc.write(stream)
|
| 671 |
+
return stream.getvalue().encode('utf-8')
|
| 672 |
+
|
| 673 |
+
|
| 674 |
+
# Run with: uvicorn src.api.mvp_api:app --reload --port 8000
|
| 675 |
+
if __name__ == "__main__":
|
| 676 |
+
import uvicorn
|
| 677 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
src/core/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Core package - Orchestrator and central coordination"""
|
| 2 |
+
from src.core.orchestrator import CoreOrchestrator, OrchestrationResult, OrchestrationStatus
|
src/core/orchestrator.py
ADDED
|
@@ -0,0 +1,662 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Core Orchestrator - The Brain/Nhạc trưởng
|
| 3 |
+
Coordinates between LLM understanding and CP Module execution
|
| 4 |
+
Following the "Handshake Loop" pattern from Core_document.md
|
| 5 |
+
"""
|
| 6 |
+
import json
|
| 7 |
+
from typing import Dict, Any, Optional, List, Tuple
|
| 8 |
+
from dataclasses import dataclass, field
|
| 9 |
+
from enum import Enum
|
| 10 |
+
import logging
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
from src.models.domain import Layout, SiteBoundary, Plot, ParetoFront, ComplianceReport
|
| 14 |
+
from src.algorithms.nsga2_optimizer import NSGA2Optimizer
|
| 15 |
+
from src.algorithms.milp_solver import MILPSolver, MILPResult
|
| 16 |
+
from src.algorithms.regulation_checker import RegulationChecker
|
| 17 |
+
from src.geometry.site_processor import SiteProcessor
|
| 18 |
+
from src.geometry.road_network import RoadNetworkGenerator
|
| 19 |
+
from src.geometry.plot_generator import PlotGenerator
|
| 20 |
+
from src.export.dxf_exporter import DXFExporter
|
| 21 |
+
|
| 22 |
+
logger = logging.getLogger(__name__)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class OrchestrationStatus(str, Enum):
|
| 26 |
+
"""Status of orchestration step"""
|
| 27 |
+
SUCCESS = "success"
|
| 28 |
+
FAILURE = "failure"
|
| 29 |
+
CONFLICT = "conflict"
|
| 30 |
+
PENDING = "pending"
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@dataclass
|
| 34 |
+
class OrchestrationResult:
|
| 35 |
+
"""Result from orchestration step"""
|
| 36 |
+
status: OrchestrationStatus
|
| 37 |
+
message: str
|
| 38 |
+
data: Dict[str, Any] = field(default_factory=dict)
|
| 39 |
+
suggestions: List[str] = field(default_factory=list) # For conflict resolution
|
| 40 |
+
|
| 41 |
+
def to_json(self) -> str:
|
| 42 |
+
"""Convert to JSON for LLM interpretation"""
|
| 43 |
+
return json.dumps({
|
| 44 |
+
'status': self.status.value,
|
| 45 |
+
'message': self.message,
|
| 46 |
+
'data': self.data,
|
| 47 |
+
'suggestions': self.suggestions
|
| 48 |
+
}, indent=2, default=str)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
class CoreOrchestrator:
|
| 52 |
+
"""
|
| 53 |
+
Core Orchestrator - The central coordinator
|
| 54 |
+
|
| 55 |
+
Implements the "Handshake Loop" pattern:
|
| 56 |
+
1. Dịch (Translation): Convert natural language to technical parameters
|
| 57 |
+
2. Giải (Execution): Run CP Module algorithms
|
| 58 |
+
3. Hiểu (Interpretation): Evaluate results against requirements
|
| 59 |
+
4. Quyết định (Reasoning & Action): Report success or propose alternatives
|
| 60 |
+
|
| 61 |
+
Key principle: LLM handles semantics, CP handles math
|
| 62 |
+
- All numbers come from CP (no hallucination)
|
| 63 |
+
- LLM provides flexibility in input/output
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
def __init__(self, regulations_path: str = "config/regulations.yaml"):
|
| 67 |
+
"""
|
| 68 |
+
Initialize orchestrator with all modules
|
| 69 |
+
|
| 70 |
+
Args:
|
| 71 |
+
regulations_path: Path to regulations YAML
|
| 72 |
+
"""
|
| 73 |
+
self.regulations_path = regulations_path
|
| 74 |
+
|
| 75 |
+
# Initialize all modules
|
| 76 |
+
self.site_processor = SiteProcessor(regulations_path)
|
| 77 |
+
self.road_generator = RoadNetworkGenerator(regulations_path)
|
| 78 |
+
self.plot_generator = PlotGenerator(regulations_path)
|
| 79 |
+
self.nsga2_optimizer = NSGA2Optimizer(regulations_path)
|
| 80 |
+
self.milp_solver = MILPSolver()
|
| 81 |
+
self.regulation_checker = RegulationChecker(regulations_path)
|
| 82 |
+
self.dxf_exporter = DXFExporter()
|
| 83 |
+
|
| 84 |
+
self.logger = logging.getLogger(__name__)
|
| 85 |
+
|
| 86 |
+
# Current session state
|
| 87 |
+
self.current_site: Optional[SiteBoundary] = None
|
| 88 |
+
self.current_layouts: List[Layout] = []
|
| 89 |
+
self.current_pareto: Optional[ParetoFront] = None
|
| 90 |
+
|
| 91 |
+
# =========================================================================
|
| 92 |
+
# STAGE 1: Digital Twin Initialization (Dịch + Giải)
|
| 93 |
+
# =========================================================================
|
| 94 |
+
|
| 95 |
+
def initialize_site(
|
| 96 |
+
self,
|
| 97 |
+
source: str,
|
| 98 |
+
source_type: str = "coordinates"
|
| 99 |
+
) -> OrchestrationResult:
|
| 100 |
+
"""
|
| 101 |
+
Initialize site from various sources
|
| 102 |
+
|
| 103 |
+
Args:
|
| 104 |
+
source: File path or coordinate string
|
| 105 |
+
source_type: 'shapefile', 'geojson', 'dxf', 'coordinates'
|
| 106 |
+
|
| 107 |
+
Returns:
|
| 108 |
+
OrchestrationResult
|
| 109 |
+
"""
|
| 110 |
+
self.logger.info(f"Initializing site from {source_type}")
|
| 111 |
+
|
| 112 |
+
try:
|
| 113 |
+
# Dịch (Translation): Parse input based on type
|
| 114 |
+
if source_type == "shapefile":
|
| 115 |
+
self.current_site = self.site_processor.import_from_shapefile(source)
|
| 116 |
+
elif source_type == "geojson":
|
| 117 |
+
self.current_site = self.site_processor.import_from_geojson(source)
|
| 118 |
+
elif source_type == "dxf":
|
| 119 |
+
self.current_site = self.site_processor.import_from_dxf(source)
|
| 120 |
+
elif source_type == "coordinates":
|
| 121 |
+
# Parse coordinates from string or list
|
| 122 |
+
if isinstance(source, str):
|
| 123 |
+
coords = json.loads(source)
|
| 124 |
+
else:
|
| 125 |
+
coords = source
|
| 126 |
+
self.current_site = self.site_processor.import_from_coordinates(coords)
|
| 127 |
+
else:
|
| 128 |
+
return OrchestrationResult(
|
| 129 |
+
status=OrchestrationStatus.FAILURE,
|
| 130 |
+
message=f"Unknown source type: {source_type}"
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
# Giải (Execution): Site processor has calculated buildable area
|
| 134 |
+
|
| 135 |
+
# Hiểu (Interpretation): Check if site is valid
|
| 136 |
+
if self.current_site.buildable_area_sqm <= 0:
|
| 137 |
+
return OrchestrationResult(
|
| 138 |
+
status=OrchestrationStatus.CONFLICT,
|
| 139 |
+
message="Site too small after applying setbacks",
|
| 140 |
+
suggestions=[
|
| 141 |
+
"Reduce boundary setback requirement",
|
| 142 |
+
"Choose a larger site",
|
| 143 |
+
"Apply for variance permit"
|
| 144 |
+
]
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
# Quyết định: Success
|
| 148 |
+
return OrchestrationResult(
|
| 149 |
+
status=OrchestrationStatus.SUCCESS,
|
| 150 |
+
message="Site initialized successfully",
|
| 151 |
+
data={
|
| 152 |
+
'site_id': self.current_site.id,
|
| 153 |
+
'total_area_sqm': self.current_site.area_sqm,
|
| 154 |
+
'buildable_area_sqm': self.current_site.buildable_area_sqm,
|
| 155 |
+
'buildable_ratio': self.current_site.buildable_area_sqm / self.current_site.area_sqm,
|
| 156 |
+
'num_constraints': len(self.current_site.constraints)
|
| 157 |
+
}
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
except Exception as e:
|
| 161 |
+
self.logger.error(f"Site initialization failed: {e}")
|
| 162 |
+
return OrchestrationResult(
|
| 163 |
+
status=OrchestrationStatus.FAILURE,
|
| 164 |
+
message=f"Failed to initialize site: {str(e)}"
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
# =========================================================================
|
| 168 |
+
# STAGE 2: Infrastructure Skeleton (Dịch + Giải)
|
| 169 |
+
# =========================================================================
|
| 170 |
+
|
| 171 |
+
def generate_road_network(
|
| 172 |
+
self,
|
| 173 |
+
pattern: str = "grid",
|
| 174 |
+
primary_spacing: float = 200,
|
| 175 |
+
secondary_spacing: float = 100
|
| 176 |
+
) -> OrchestrationResult:
|
| 177 |
+
"""
|
| 178 |
+
Generate road network
|
| 179 |
+
|
| 180 |
+
Args:
|
| 181 |
+
pattern: 'grid' or 'spine'
|
| 182 |
+
primary_spacing: Primary road spacing
|
| 183 |
+
secondary_spacing: Secondary road spacing
|
| 184 |
+
|
| 185 |
+
Returns:
|
| 186 |
+
OrchestrationResult
|
| 187 |
+
"""
|
| 188 |
+
if not self.current_site:
|
| 189 |
+
return OrchestrationResult(
|
| 190 |
+
status=OrchestrationStatus.FAILURE,
|
| 191 |
+
message="No site initialized. Call initialize_site first."
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
self.logger.info(f"Generating {pattern} road network")
|
| 195 |
+
|
| 196 |
+
try:
|
| 197 |
+
# Giải (Execution): Generate road network
|
| 198 |
+
if pattern == "spine":
|
| 199 |
+
road_network = self.road_generator.generate_spine_network(self.current_site)
|
| 200 |
+
else:
|
| 201 |
+
road_network = self.road_generator.generate_grid_network(
|
| 202 |
+
self.current_site,
|
| 203 |
+
primary_spacing=primary_spacing,
|
| 204 |
+
secondary_spacing=secondary_spacing
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
# Check for dead zones
|
| 208 |
+
dead_zones = self.road_generator.identify_dead_zones(
|
| 209 |
+
self.current_site, road_network
|
| 210 |
+
)
|
| 211 |
+
|
| 212 |
+
# Hiểu (Interpretation)
|
| 213 |
+
if dead_zones:
|
| 214 |
+
dead_area = sum(z.area for z in dead_zones)
|
| 215 |
+
if dead_area > self.current_site.buildable_area_sqm * 0.1:
|
| 216 |
+
return OrchestrationResult(
|
| 217 |
+
status=OrchestrationStatus.CONFLICT,
|
| 218 |
+
message=f"Road network leaves {len(dead_zones)} dead zones ({dead_area:.0f}m²)",
|
| 219 |
+
data={'road_network': road_network, 'dead_zones': len(dead_zones)},
|
| 220 |
+
suggestions=[
|
| 221 |
+
f"Try pattern='grid' with smaller spacing",
|
| 222 |
+
f"Add more secondary roads",
|
| 223 |
+
f"Accept dead zones as green space"
|
| 224 |
+
]
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
# Store road network in site for later use
|
| 228 |
+
self._current_road_network = road_network
|
| 229 |
+
|
| 230 |
+
return OrchestrationResult(
|
| 231 |
+
status=OrchestrationStatus.SUCCESS,
|
| 232 |
+
message="Road network generated successfully",
|
| 233 |
+
data={
|
| 234 |
+
'total_length_m': road_network.total_length_m,
|
| 235 |
+
'total_area_sqm': road_network.total_area_sqm,
|
| 236 |
+
'dead_zones': len(dead_zones) if dead_zones else 0,
|
| 237 |
+
'pattern': pattern
|
| 238 |
+
}
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
except Exception as e:
|
| 242 |
+
self.logger.error(f"Road network generation failed: {e}")
|
| 243 |
+
return OrchestrationResult(
|
| 244 |
+
status=OrchestrationStatus.FAILURE,
|
| 245 |
+
message=f"Failed to generate road network: {str(e)}"
|
| 246 |
+
)
|
| 247 |
+
|
| 248 |
+
# =========================================================================
|
| 249 |
+
# STAGE 3: Constraint Mapping (Dịch + Giải)
|
| 250 |
+
# =========================================================================
|
| 251 |
+
|
| 252 |
+
def add_constraint(
|
| 253 |
+
self,
|
| 254 |
+
constraint_type: str,
|
| 255 |
+
description: str,
|
| 256 |
+
geometry: Any,
|
| 257 |
+
buffer_m: float = 0
|
| 258 |
+
) -> OrchestrationResult:
|
| 259 |
+
"""
|
| 260 |
+
Add a constraint to the site
|
| 261 |
+
|
| 262 |
+
Natural language examples that LLM would translate:
|
| 263 |
+
- "Tránh kho xăng 200m" -> constraint_type="hazard", buffer_m=200
|
| 264 |
+
- "Cách sông 100m" -> constraint_type="waterway", buffer_m=100
|
| 265 |
+
|
| 266 |
+
Args:
|
| 267 |
+
constraint_type: Type of constraint
|
| 268 |
+
description: Human-readable description
|
| 269 |
+
geometry: Constraint geometry (coordinates or polygon)
|
| 270 |
+
buffer_m: Buffer distance in meters
|
| 271 |
+
|
| 272 |
+
Returns:
|
| 273 |
+
OrchestrationResult
|
| 274 |
+
"""
|
| 275 |
+
if not self.current_site:
|
| 276 |
+
return OrchestrationResult(
|
| 277 |
+
status=OrchestrationStatus.FAILURE,
|
| 278 |
+
message="No site initialized"
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
try:
|
| 282 |
+
from src.models.domain import ConstraintType
|
| 283 |
+
|
| 284 |
+
# Dịch (Translation): Map string to enum
|
| 285 |
+
type_map = {
|
| 286 |
+
'setback': ConstraintType.SETBACK,
|
| 287 |
+
'fire_safety': ConstraintType.FIRE_SAFETY,
|
| 288 |
+
'waterway': ConstraintType.WATERWAY,
|
| 289 |
+
'hazard': ConstraintType.HAZARD_ZONE,
|
| 290 |
+
'no_build': ConstraintType.NO_BUILD
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
constraint_enum = type_map.get(constraint_type.lower(), ConstraintType.NO_BUILD)
|
| 294 |
+
|
| 295 |
+
# Giải (Execution)
|
| 296 |
+
constraint = self.site_processor.add_constraint(
|
| 297 |
+
self.current_site,
|
| 298 |
+
constraint_enum,
|
| 299 |
+
geometry,
|
| 300 |
+
buffer_distance=buffer_m,
|
| 301 |
+
description=description
|
| 302 |
+
)
|
| 303 |
+
|
| 304 |
+
# Hiểu (Interpretation)
|
| 305 |
+
if self.current_site.buildable_area_sqm <= 0:
|
| 306 |
+
return OrchestrationResult(
|
| 307 |
+
status=OrchestrationStatus.CONFLICT,
|
| 308 |
+
message="Site no longer has buildable area after constraint",
|
| 309 |
+
suggestions=[
|
| 310 |
+
"Reduce buffer distance",
|
| 311 |
+
"Move constraint location",
|
| 312 |
+
"Request exemption"
|
| 313 |
+
]
|
| 314 |
+
)
|
| 315 |
+
|
| 316 |
+
return OrchestrationResult(
|
| 317 |
+
status=OrchestrationStatus.SUCCESS,
|
| 318 |
+
message=f"Constraint added: {description}",
|
| 319 |
+
data={
|
| 320 |
+
'constraint_type': constraint_type,
|
| 321 |
+
'buffer_m': buffer_m,
|
| 322 |
+
'remaining_buildable_sqm': self.current_site.buildable_area_sqm
|
| 323 |
+
}
|
| 324 |
+
)
|
| 325 |
+
|
| 326 |
+
except Exception as e:
|
| 327 |
+
return OrchestrationResult(
|
| 328 |
+
status=OrchestrationStatus.FAILURE,
|
| 329 |
+
message=f"Failed to add constraint: {str(e)}"
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
# =========================================================================
|
| 333 |
+
# STAGE 4: Automated Optimization (Hybrid AI: GA + MILP)
|
| 334 |
+
# =========================================================================
|
| 335 |
+
|
| 336 |
+
def run_optimization(
|
| 337 |
+
self,
|
| 338 |
+
population_size: int = 100,
|
| 339 |
+
n_generations: int = 200,
|
| 340 |
+
n_plots: int = 20
|
| 341 |
+
) -> OrchestrationResult:
|
| 342 |
+
"""
|
| 343 |
+
Run full optimization pipeline (NSGA-II + MILP + Compliance)
|
| 344 |
+
|
| 345 |
+
This is the core of the "Handshake Loop" - multiple iterations
|
| 346 |
+
between GA exploration and MILP validation.
|
| 347 |
+
|
| 348 |
+
Args:
|
| 349 |
+
population_size: NSGA-II population size
|
| 350 |
+
n_generations: Number of GA generations
|
| 351 |
+
n_plots: Target number of plots
|
| 352 |
+
|
| 353 |
+
Returns:
|
| 354 |
+
OrchestrationResult with Pareto front
|
| 355 |
+
"""
|
| 356 |
+
if not self.current_site:
|
| 357 |
+
return OrchestrationResult(
|
| 358 |
+
status=OrchestrationStatus.FAILURE,
|
| 359 |
+
message="No site initialized"
|
| 360 |
+
)
|
| 361 |
+
|
| 362 |
+
self.logger.info("Starting optimization pipeline")
|
| 363 |
+
|
| 364 |
+
try:
|
| 365 |
+
# STEP 1: NSGA-II Exploration (Giải)
|
| 366 |
+
self.logger.info("Step 1: NSGA-II multi-objective optimization")
|
| 367 |
+
pareto_front = self.nsga2_optimizer.optimize(
|
| 368 |
+
site_boundary=self.current_site,
|
| 369 |
+
population_size=population_size,
|
| 370 |
+
n_generations=n_generations,
|
| 371 |
+
n_plots=n_plots
|
| 372 |
+
)
|
| 373 |
+
|
| 374 |
+
if not pareto_front.layouts:
|
| 375 |
+
return OrchestrationResult(
|
| 376 |
+
status=OrchestrationStatus.FAILURE,
|
| 377 |
+
message="NSGA-II failed to generate valid layouts",
|
| 378 |
+
suggestions=[
|
| 379 |
+
"Reduce number of plots",
|
| 380 |
+
"Relax constraints",
|
| 381 |
+
"Check site configuration"
|
| 382 |
+
]
|
| 383 |
+
)
|
| 384 |
+
|
| 385 |
+
# STEP 2: MILP Validation for each layout (Giải)
|
| 386 |
+
self.logger.info("Step 2: MILP validation")
|
| 387 |
+
validated_layouts = []
|
| 388 |
+
|
| 389 |
+
for layout in pareto_front.layouts:
|
| 390 |
+
refined_layout, milp_result = self.milp_solver.validate_and_refine(layout)
|
| 391 |
+
|
| 392 |
+
if milp_result.is_success():
|
| 393 |
+
validated_layouts.append(refined_layout)
|
| 394 |
+
|
| 395 |
+
if not validated_layouts:
|
| 396 |
+
return OrchestrationResult(
|
| 397 |
+
status=OrchestrationStatus.CONFLICT,
|
| 398 |
+
message="No layouts passed MILP validation",
|
| 399 |
+
data={'original_count': len(pareto_front.layouts)},
|
| 400 |
+
suggestions=[
|
| 401 |
+
"Reduce plot density",
|
| 402 |
+
"Increase road network",
|
| 403 |
+
"Review constraint compatibility"
|
| 404 |
+
]
|
| 405 |
+
)
|
| 406 |
+
|
| 407 |
+
# STEP 3: Regulatory Compliance Check (Giải)
|
| 408 |
+
self.logger.info("Step 3: Regulatory compliance check")
|
| 409 |
+
compliant_layouts = []
|
| 410 |
+
violations_summary = []
|
| 411 |
+
|
| 412 |
+
for layout in validated_layouts:
|
| 413 |
+
report = self.regulation_checker.validate_compliance(layout)
|
| 414 |
+
layout.metrics.is_compliant = report.is_compliant
|
| 415 |
+
layout.metrics.compliance_violations = report.violations
|
| 416 |
+
|
| 417 |
+
if report.is_compliant:
|
| 418 |
+
compliant_layouts.append(layout)
|
| 419 |
+
else:
|
| 420 |
+
violations_summary.extend(report.violations[:2]) # Top 2 violations
|
| 421 |
+
|
| 422 |
+
# Hiểu (Interpretation) + Quyết định (Reasoning)
|
| 423 |
+
if not compliant_layouts:
|
| 424 |
+
return OrchestrationResult(
|
| 425 |
+
status=OrchestrationStatus.CONFLICT,
|
| 426 |
+
message="No layouts meet regulatory compliance",
|
| 427 |
+
data={
|
| 428 |
+
'validated_count': len(validated_layouts),
|
| 429 |
+
'sample_violations': violations_summary[:5]
|
| 430 |
+
},
|
| 431 |
+
suggestions=[
|
| 432 |
+
"Increase green space ratio",
|
| 433 |
+
"Review boundary setbacks",
|
| 434 |
+
"Reduce plot density"
|
| 435 |
+
]
|
| 436 |
+
)
|
| 437 |
+
|
| 438 |
+
# Success - store Pareto front
|
| 439 |
+
self.current_pareto = ParetoFront(
|
| 440 |
+
layouts=compliant_layouts,
|
| 441 |
+
generation_time_seconds=pareto_front.generation_time_seconds
|
| 442 |
+
)
|
| 443 |
+
self.current_layouts = compliant_layouts
|
| 444 |
+
|
| 445 |
+
# Generate summary
|
| 446 |
+
max_sellable = self.current_pareto.get_max_sellable_layout()
|
| 447 |
+
max_green = self.current_pareto.get_max_green_layout()
|
| 448 |
+
balanced = self.current_pareto.get_balanced_layout()
|
| 449 |
+
|
| 450 |
+
return OrchestrationResult(
|
| 451 |
+
status=OrchestrationStatus.SUCCESS,
|
| 452 |
+
message=f"Generated {len(compliant_layouts)} compliant layouts",
|
| 453 |
+
data={
|
| 454 |
+
'num_layouts': len(compliant_layouts),
|
| 455 |
+
'generation_time_seconds': pareto_front.generation_time_seconds,
|
| 456 |
+
'max_sellable_area': max_sellable.metrics.sellable_area_sqm if max_sellable else 0,
|
| 457 |
+
'max_green_ratio': max_green.metrics.green_space_ratio if max_green else 0,
|
| 458 |
+
'balanced_sellable': balanced.metrics.sellable_area_sqm if balanced else 0,
|
| 459 |
+
'scenarios': [
|
| 460 |
+
{
|
| 461 |
+
'name': 'Max Sellable',
|
| 462 |
+
'id': max_sellable.id if max_sellable else None,
|
| 463 |
+
'sellable_sqm': max_sellable.metrics.sellable_area_sqm if max_sellable else 0
|
| 464 |
+
},
|
| 465 |
+
{
|
| 466 |
+
'name': 'Max Green',
|
| 467 |
+
'id': max_green.id if max_green else None,
|
| 468 |
+
'green_ratio': max_green.metrics.green_space_ratio if max_green else 0
|
| 469 |
+
},
|
| 470 |
+
{
|
| 471 |
+
'name': 'Balanced',
|
| 472 |
+
'id': balanced.id if balanced else None
|
| 473 |
+
}
|
| 474 |
+
]
|
| 475 |
+
}
|
| 476 |
+
)
|
| 477 |
+
|
| 478 |
+
except Exception as e:
|
| 479 |
+
self.logger.error(f"Optimization failed: {e}")
|
| 480 |
+
return OrchestrationResult(
|
| 481 |
+
status=OrchestrationStatus.FAILURE,
|
| 482 |
+
message=f"Optimization pipeline failed: {str(e)}"
|
| 483 |
+
)
|
| 484 |
+
|
| 485 |
+
# =========================================================================
|
| 486 |
+
# STAGE 5: Engineering Delivery (Giải + Output)
|
| 487 |
+
# =========================================================================
|
| 488 |
+
|
| 489 |
+
def export_layout(
|
| 490 |
+
self,
|
| 491 |
+
layout_id: str,
|
| 492 |
+
output_path: str,
|
| 493 |
+
format: str = "dxf"
|
| 494 |
+
) -> OrchestrationResult:
|
| 495 |
+
"""
|
| 496 |
+
Export a specific layout to file
|
| 497 |
+
|
| 498 |
+
Args:
|
| 499 |
+
layout_id: Layout ID to export
|
| 500 |
+
output_path: Output file path
|
| 501 |
+
format: Export format ('dxf')
|
| 502 |
+
|
| 503 |
+
Returns:
|
| 504 |
+
OrchestrationResult
|
| 505 |
+
"""
|
| 506 |
+
# Find layout
|
| 507 |
+
layout = None
|
| 508 |
+
for l in self.current_layouts:
|
| 509 |
+
if l.id == layout_id:
|
| 510 |
+
layout = l
|
| 511 |
+
break
|
| 512 |
+
|
| 513 |
+
if not layout:
|
| 514 |
+
return OrchestrationResult(
|
| 515 |
+
status=OrchestrationStatus.FAILURE,
|
| 516 |
+
message=f"Layout not found: {layout_id}"
|
| 517 |
+
)
|
| 518 |
+
|
| 519 |
+
try:
|
| 520 |
+
if format.lower() == "dxf":
|
| 521 |
+
filepath = self.dxf_exporter.export(layout, output_path)
|
| 522 |
+
return OrchestrationResult(
|
| 523 |
+
status=OrchestrationStatus.SUCCESS,
|
| 524 |
+
message=f"Layout exported to {filepath}",
|
| 525 |
+
data={'filepath': filepath, 'format': 'DXF'}
|
| 526 |
+
)
|
| 527 |
+
else:
|
| 528 |
+
return OrchestrationResult(
|
| 529 |
+
status=OrchestrationStatus.FAILURE,
|
| 530 |
+
message=f"Unsupported format: {format}"
|
| 531 |
+
)
|
| 532 |
+
|
| 533 |
+
except Exception as e:
|
| 534 |
+
return OrchestrationResult(
|
| 535 |
+
status=OrchestrationStatus.FAILURE,
|
| 536 |
+
message=f"Export failed: {str(e)}"
|
| 537 |
+
)
|
| 538 |
+
|
| 539 |
+
def export_all_layouts(self, output_dir: str) -> OrchestrationResult:
|
| 540 |
+
"""
|
| 541 |
+
Export all layouts in Pareto front
|
| 542 |
+
|
| 543 |
+
Args:
|
| 544 |
+
output_dir: Output directory
|
| 545 |
+
|
| 546 |
+
Returns:
|
| 547 |
+
OrchestrationResult
|
| 548 |
+
"""
|
| 549 |
+
if not self.current_pareto:
|
| 550 |
+
return OrchestrationResult(
|
| 551 |
+
status=OrchestrationStatus.FAILURE,
|
| 552 |
+
message="No optimization results available"
|
| 553 |
+
)
|
| 554 |
+
|
| 555 |
+
try:
|
| 556 |
+
files = self.dxf_exporter.export_pareto_front(
|
| 557 |
+
self.current_pareto,
|
| 558 |
+
output_dir,
|
| 559 |
+
prefix="layout"
|
| 560 |
+
)
|
| 561 |
+
|
| 562 |
+
return OrchestrationResult(
|
| 563 |
+
status=OrchestrationStatus.SUCCESS,
|
| 564 |
+
message=f"Exported {len(files)} layouts",
|
| 565 |
+
data={'files': files}
|
| 566 |
+
)
|
| 567 |
+
|
| 568 |
+
except Exception as e:
|
| 569 |
+
return OrchestrationResult(
|
| 570 |
+
status=OrchestrationStatus.FAILURE,
|
| 571 |
+
message=f"Export failed: {str(e)}"
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
# =========================================================================
|
| 575 |
+
# JSON Interface for LLM Function Calling
|
| 576 |
+
# =========================================================================
|
| 577 |
+
|
| 578 |
+
def execute_command(self, command_json: str) -> str:
|
| 579 |
+
"""
|
| 580 |
+
Execute a command from LLM via JSON
|
| 581 |
+
|
| 582 |
+
This is the standardized interface for LLM → Orchestrator communication.
|
| 583 |
+
|
| 584 |
+
Input format:
|
| 585 |
+
{
|
| 586 |
+
"action": "initialize_site" | "generate_roads" | "add_constraint" | "optimize" | "export",
|
| 587 |
+
"parameters": {...}
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
Args:
|
| 591 |
+
command_json: JSON command string
|
| 592 |
+
|
| 593 |
+
Returns:
|
| 594 |
+
JSON response string
|
| 595 |
+
"""
|
| 596 |
+
try:
|
| 597 |
+
command = json.loads(command_json)
|
| 598 |
+
action = command.get('action')
|
| 599 |
+
params = command.get('parameters', {})
|
| 600 |
+
|
| 601 |
+
if action == 'initialize_site':
|
| 602 |
+
result = self.initialize_site(**params)
|
| 603 |
+
elif action == 'generate_roads':
|
| 604 |
+
result = self.generate_road_network(**params)
|
| 605 |
+
elif action == 'add_constraint':
|
| 606 |
+
result = self.add_constraint(**params)
|
| 607 |
+
elif action == 'optimize':
|
| 608 |
+
result = self.run_optimization(**params)
|
| 609 |
+
elif action == 'export':
|
| 610 |
+
result = self.export_layout(**params)
|
| 611 |
+
elif action == 'export_all':
|
| 612 |
+
result = self.export_all_layouts(**params)
|
| 613 |
+
else:
|
| 614 |
+
result = OrchestrationResult(
|
| 615 |
+
status=OrchestrationStatus.FAILURE,
|
| 616 |
+
message=f"Unknown action: {action}"
|
| 617 |
+
)
|
| 618 |
+
|
| 619 |
+
return result.to_json()
|
| 620 |
+
|
| 621 |
+
except json.JSONDecodeError as e:
|
| 622 |
+
return json.dumps({
|
| 623 |
+
'status': 'failure',
|
| 624 |
+
'message': f'Invalid JSON: {str(e)}'
|
| 625 |
+
})
|
| 626 |
+
except Exception as e:
|
| 627 |
+
return json.dumps({
|
| 628 |
+
'status': 'failure',
|
| 629 |
+
'message': f'Execution error: {str(e)}'
|
| 630 |
+
})
|
| 631 |
+
|
| 632 |
+
|
| 633 |
+
# Example usage
|
| 634 |
+
if __name__ == "__main__":
|
| 635 |
+
logging.basicConfig(level=logging.INFO)
|
| 636 |
+
|
| 637 |
+
# Initialize orchestrator
|
| 638 |
+
orchestrator = CoreOrchestrator()
|
| 639 |
+
|
| 640 |
+
# Stage 1: Initialize site
|
| 641 |
+
coords = [(0, 0), (500, 0), (500, 500), (0, 500), (0, 0)]
|
| 642 |
+
result = orchestrator.initialize_site(coords, source_type="coordinates")
|
| 643 |
+
print(f"Site init: {result.status.value}")
|
| 644 |
+
print(result.to_json())
|
| 645 |
+
|
| 646 |
+
# Stage 2: Generate roads
|
| 647 |
+
result = orchestrator.generate_road_network(pattern="grid", primary_spacing=150)
|
| 648 |
+
print(f"\nRoad gen: {result.status.value}")
|
| 649 |
+
|
| 650 |
+
# Stage 4: Run optimization
|
| 651 |
+
result = orchestrator.run_optimization(
|
| 652 |
+
population_size=50,
|
| 653 |
+
n_generations=50,
|
| 654 |
+
n_plots=10
|
| 655 |
+
)
|
| 656 |
+
print(f"\nOptimization: {result.status.value}")
|
| 657 |
+
print(result.to_json())
|
| 658 |
+
|
| 659 |
+
# Stage 5: Export
|
| 660 |
+
if result.status == OrchestrationStatus.SUCCESS:
|
| 661 |
+
result = orchestrator.export_all_layouts("output/")
|
| 662 |
+
print(f"\nExport: {result.status.value}")
|
src/export/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Export package"""
|
| 2 |
+
from src.export.dxf_exporter import DXFExporter
|
src/export/dxf_exporter.py
ADDED
|
@@ -0,0 +1,444 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
DXF Exporter - Engineering Delivery
|
| 3 |
+
Export layouts to AutoCAD DXF format with proper layering
|
| 4 |
+
"""
|
| 5 |
+
import ezdxf
|
| 6 |
+
from ezdxf.enums import TextEntityAlignment
|
| 7 |
+
from shapely.geometry import Polygon, MultiPolygon, LineString, MultiLineString
|
| 8 |
+
from typing import List, Optional, Dict, Tuple
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
import logging
|
| 11 |
+
from datetime import datetime
|
| 12 |
+
|
| 13 |
+
from src.models.domain import Layout, Plot, PlotType, SiteBoundary, RoadNetwork
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class DXFExporter:
|
| 19 |
+
"""
|
| 20 |
+
AutoCAD DXF file exporter
|
| 21 |
+
|
| 22 |
+
Exports industrial estate layouts with proper layering:
|
| 23 |
+
- SITE_BOUNDARY: Site boundary polygon
|
| 24 |
+
- ROADS_PRIMARY: Primary road network
|
| 25 |
+
- ROADS_SECONDARY: Secondary road network
|
| 26 |
+
- PLOTS_INDUSTRIAL: Industrial plots
|
| 27 |
+
- PLOTS_GREEN: Green space plots
|
| 28 |
+
- UTILITIES: Utility corridors
|
| 29 |
+
- ANNOTATIONS: Text annotations and dimensions
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
# Layer configuration
|
| 33 |
+
LAYERS = {
|
| 34 |
+
'SITE_BOUNDARY': {'color': 7, 'linetype': 'CONTINUOUS'}, # White
|
| 35 |
+
'ROADS_PRIMARY': {'color': 1, 'linetype': 'CONTINUOUS'}, # Red
|
| 36 |
+
'ROADS_SECONDARY': {'color': 3, 'linetype': 'DASHED'}, # Green
|
| 37 |
+
'ROADS_TERTIARY': {'color': 4, 'linetype': 'DASHED'}, # Cyan
|
| 38 |
+
'PLOTS_INDUSTRIAL': {'color': 5, 'linetype': 'CONTINUOUS'}, # Blue
|
| 39 |
+
'PLOTS_GREEN': {'color': 3, 'linetype': 'CONTINUOUS'}, # Green
|
| 40 |
+
'PLOTS_UTILITY': {'color': 6, 'linetype': 'CONTINUOUS'}, # Magenta
|
| 41 |
+
'CONSTRAINTS': {'color': 1, 'linetype': 'PHANTOM'}, # Red dashed
|
| 42 |
+
'ANNOTATIONS': {'color': 7, 'linetype': 'CONTINUOUS'}, # White
|
| 43 |
+
'DIMENSIONS': {'color': 2, 'linetype': 'CONTINUOUS'}, # Yellow
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
def __init__(self, version: str = "R2010"):
|
| 47 |
+
"""
|
| 48 |
+
Initialize DXF exporter
|
| 49 |
+
|
| 50 |
+
Args:
|
| 51 |
+
version: DXF version ('R12', 'R2000', 'R2004', 'R2007', 'R2010', 'R2013', 'R2018')
|
| 52 |
+
"""
|
| 53 |
+
self.version = version
|
| 54 |
+
self.logger = logging.getLogger(__name__)
|
| 55 |
+
|
| 56 |
+
def export(
|
| 57 |
+
self,
|
| 58 |
+
layout: Layout,
|
| 59 |
+
filepath: str,
|
| 60 |
+
include_annotations: bool = True,
|
| 61 |
+
include_dimensions: bool = True
|
| 62 |
+
) -> str:
|
| 63 |
+
"""
|
| 64 |
+
Export layout to DXF file
|
| 65 |
+
|
| 66 |
+
Args:
|
| 67 |
+
layout: Layout to export
|
| 68 |
+
filepath: Output file path
|
| 69 |
+
include_annotations: Include text annotations
|
| 70 |
+
include_dimensions: Include dimensions
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
Path to created file
|
| 74 |
+
"""
|
| 75 |
+
self.logger.info(f"Exporting layout {layout.id} to DXF: {filepath}")
|
| 76 |
+
|
| 77 |
+
# Create new DXF document with setup=True to include standard linetypes (DASHED, PHANTOM, etc.)
|
| 78 |
+
doc = ezdxf.new(dxfversion=self.version, setup=True)
|
| 79 |
+
msp = doc.modelspace()
|
| 80 |
+
|
| 81 |
+
# Setup layers and dimension style
|
| 82 |
+
self._setup_layers(doc)
|
| 83 |
+
self._setup_dimension_style(doc)
|
| 84 |
+
|
| 85 |
+
# Export site boundary
|
| 86 |
+
if layout.site_boundary and layout.site_boundary.geometry:
|
| 87 |
+
self._export_site_boundary(msp, layout.site_boundary)
|
| 88 |
+
|
| 89 |
+
# Export road network
|
| 90 |
+
if layout.road_network:
|
| 91 |
+
self._export_road_network(msp, layout.road_network)
|
| 92 |
+
|
| 93 |
+
# Export plots
|
| 94 |
+
for plot in layout.plots:
|
| 95 |
+
self._export_plot(msp, plot)
|
| 96 |
+
|
| 97 |
+
# Export constraints
|
| 98 |
+
if layout.site_boundary:
|
| 99 |
+
for constraint in layout.site_boundary.constraints:
|
| 100 |
+
self._export_constraint(msp, constraint)
|
| 101 |
+
|
| 102 |
+
# Add annotations
|
| 103 |
+
if include_annotations:
|
| 104 |
+
self._add_annotations(msp, layout)
|
| 105 |
+
|
| 106 |
+
# Add dimensions
|
| 107 |
+
if include_dimensions:
|
| 108 |
+
self._add_dimensions(msp, layout)
|
| 109 |
+
|
| 110 |
+
# Add title block
|
| 111 |
+
self._add_title_block(msp, layout)
|
| 112 |
+
|
| 113 |
+
# Save file with error handling for file locking
|
| 114 |
+
output_path = Path(filepath)
|
| 115 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 116 |
+
|
| 117 |
+
try:
|
| 118 |
+
doc.saveas(str(filepath))
|
| 119 |
+
except PermissionError:
|
| 120 |
+
# File is locked (e.g., open in AutoCAD)
|
| 121 |
+
self.logger.warning(f"File is locked: {filepath}, trying alternate name")
|
| 122 |
+
timestamp = datetime.now().strftime('%H%M%S')
|
| 123 |
+
alt_path = output_path.with_stem(f"{output_path.stem}_{timestamp}")
|
| 124 |
+
doc.saveas(str(alt_path))
|
| 125 |
+
self.logger.info(f"DXF exported to alternate path: {alt_path}")
|
| 126 |
+
return str(alt_path)
|
| 127 |
+
|
| 128 |
+
self.logger.info(f"DXF exported successfully: {filepath}")
|
| 129 |
+
return str(filepath)
|
| 130 |
+
|
| 131 |
+
def _setup_layers(self, doc):
|
| 132 |
+
"""Setup DXF layers with colors and linetypes"""
|
| 133 |
+
for layer_name, config in self.LAYERS.items():
|
| 134 |
+
linetype = config.get('linetype', 'CONTINUOUS')
|
| 135 |
+
# Ensure linetype exists (setup=True should provide DASHED, PHANTOM, etc.)
|
| 136 |
+
if linetype not in doc.linetypes:
|
| 137 |
+
linetype = 'CONTINUOUS'
|
| 138 |
+
doc.layers.add(
|
| 139 |
+
layer_name,
|
| 140 |
+
color=config['color'],
|
| 141 |
+
linetype=linetype
|
| 142 |
+
)
|
| 143 |
+
|
| 144 |
+
def _setup_dimension_style(self, doc):
|
| 145 |
+
"""
|
| 146 |
+
Setup custom dimension style for stability
|
| 147 |
+
Prevents crashes from malformed default dimension styles
|
| 148 |
+
"""
|
| 149 |
+
# Create custom engineering dimension style
|
| 150 |
+
if 'ENG_DIM' not in doc.dimstyles:
|
| 151 |
+
dim_style = doc.dimstyles.new('ENG_DIM')
|
| 152 |
+
dim_style.dxf.dimtxt = 2.5 # Text height
|
| 153 |
+
dim_style.dxf.dimasz = 2.5 # Arrow size
|
| 154 |
+
dim_style.dxf.dimexe = 1.5 # Extension line extension
|
| 155 |
+
dim_style.dxf.dimexo = 0.625 # Extension line offset
|
| 156 |
+
dim_style.dxf.dimgap = 0.625 # Gap from dimension line
|
| 157 |
+
dim_style.dxf.dimdec = 2 # Decimal places
|
| 158 |
+
dim_style.dxf.dimtad = 1 # Text above dimension line
|
| 159 |
+
dim_style.dxf.dimclrd = 2 # Dimension line color (yellow)
|
| 160 |
+
dim_style.dxf.dimclre = 2 # Extension line color (yellow)
|
| 161 |
+
dim_style.dxf.dimclrt = 7 # Text color (white)
|
| 162 |
+
|
| 163 |
+
def _export_site_boundary(self, msp, site: SiteBoundary):
|
| 164 |
+
"""Export site boundary polygon"""
|
| 165 |
+
if site.geometry:
|
| 166 |
+
coords = self._polygon_to_coords(site.geometry)
|
| 167 |
+
msp.add_lwpolyline(
|
| 168 |
+
coords,
|
| 169 |
+
dxfattribs={'layer': 'SITE_BOUNDARY', 'closed': True}
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
def _export_road_network(self, msp, road_network: RoadNetwork):
|
| 173 |
+
"""Export road network lines"""
|
| 174 |
+
# Primary roads
|
| 175 |
+
if road_network.primary_roads:
|
| 176 |
+
self._export_multilinestring(
|
| 177 |
+
msp, road_network.primary_roads, 'ROADS_PRIMARY'
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
# Secondary roads
|
| 181 |
+
if road_network.secondary_roads:
|
| 182 |
+
self._export_multilinestring(
|
| 183 |
+
msp, road_network.secondary_roads, 'ROADS_SECONDARY'
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
# Tertiary roads
|
| 187 |
+
if road_network.tertiary_roads:
|
| 188 |
+
self._export_multilinestring(
|
| 189 |
+
msp, road_network.tertiary_roads, 'ROADS_TERTIARY'
|
| 190 |
+
)
|
| 191 |
+
|
| 192 |
+
def _export_multilinestring(self, msp, geometry, layer: str):
|
| 193 |
+
"""Export MultiLineString or LineString to DXF"""
|
| 194 |
+
if hasattr(geometry, 'geoms'):
|
| 195 |
+
lines = geometry.geoms
|
| 196 |
+
else:
|
| 197 |
+
lines = [geometry]
|
| 198 |
+
|
| 199 |
+
for line in lines:
|
| 200 |
+
if isinstance(line, LineString):
|
| 201 |
+
points = [(p[0], p[1]) for p in line.coords]
|
| 202 |
+
msp.add_lwpolyline(
|
| 203 |
+
points,
|
| 204 |
+
dxfattribs={'layer': layer}
|
| 205 |
+
)
|
| 206 |
+
|
| 207 |
+
def _export_plot(self, msp, plot: Plot):
|
| 208 |
+
"""Export a plot polygon"""
|
| 209 |
+
if not plot.geometry:
|
| 210 |
+
return
|
| 211 |
+
|
| 212 |
+
# Determine layer based on plot type
|
| 213 |
+
layer_map = {
|
| 214 |
+
PlotType.INDUSTRIAL: 'PLOTS_INDUSTRIAL',
|
| 215 |
+
PlotType.GREEN_SPACE: 'PLOTS_GREEN',
|
| 216 |
+
PlotType.UTILITY: 'PLOTS_UTILITY',
|
| 217 |
+
PlotType.ROAD: 'ROADS_TERTIARY',
|
| 218 |
+
PlotType.BUFFER: 'CONSTRAINTS'
|
| 219 |
+
}
|
| 220 |
+
layer = layer_map.get(plot.type, 'PLOTS_INDUSTRIAL')
|
| 221 |
+
|
| 222 |
+
# Export polygon
|
| 223 |
+
coords = self._polygon_to_coords(plot.geometry)
|
| 224 |
+
msp.add_lwpolyline(
|
| 225 |
+
coords,
|
| 226 |
+
dxfattribs={'layer': layer, 'closed': True}
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
# Add plot ID label at centroid
|
| 230 |
+
centroid = plot.geometry.centroid
|
| 231 |
+
msp.add_text(
|
| 232 |
+
plot.id,
|
| 233 |
+
dxfattribs={
|
| 234 |
+
'layer': 'ANNOTATIONS',
|
| 235 |
+
'height': 2,
|
| 236 |
+
'insert': (centroid.x, centroid.y)
|
| 237 |
+
}
|
| 238 |
+
)
|
| 239 |
+
|
| 240 |
+
def _export_constraint(self, msp, constraint):
|
| 241 |
+
"""Export constraint zone"""
|
| 242 |
+
if not constraint.geometry:
|
| 243 |
+
return
|
| 244 |
+
|
| 245 |
+
if isinstance(constraint.geometry, Polygon):
|
| 246 |
+
coords = self._polygon_to_coords(constraint.geometry)
|
| 247 |
+
msp.add_lwpolyline(
|
| 248 |
+
coords,
|
| 249 |
+
dxfattribs={'layer': 'CONSTRAINTS', 'closed': True}
|
| 250 |
+
)
|
| 251 |
+
|
| 252 |
+
def _add_annotations(self, msp, layout: Layout):
|
| 253 |
+
"""Add text annotations"""
|
| 254 |
+
# Summary annotation at top-right
|
| 255 |
+
if layout.site_boundary:
|
| 256 |
+
bounds = layout.site_boundary.geometry.bounds
|
| 257 |
+
maxx, maxy = bounds[2], bounds[3]
|
| 258 |
+
|
| 259 |
+
# Create summary text
|
| 260 |
+
metrics = layout.metrics
|
| 261 |
+
summary_lines = [
|
| 262 |
+
f"LAYOUT SUMMARY",
|
| 263 |
+
f"----------------",
|
| 264 |
+
f"Total Area: {metrics.total_area_sqm:.0f} m²",
|
| 265 |
+
f"Sellable: {metrics.sellable_area_sqm:.0f} m² ({metrics.sellable_ratio*100:.1f}%)",
|
| 266 |
+
f"Green: {metrics.green_space_area_sqm:.0f} m² ({metrics.green_space_ratio*100:.1f}%)",
|
| 267 |
+
f"Roads: {metrics.road_area_sqm:.0f} m²",
|
| 268 |
+
f"Num Plots: {metrics.num_plots}",
|
| 269 |
+
f"Compliant: {'Yes' if metrics.is_compliant else 'No'}"
|
| 270 |
+
]
|
| 271 |
+
|
| 272 |
+
y_offset = maxy + 20
|
| 273 |
+
for line in summary_lines:
|
| 274 |
+
msp.add_text(
|
| 275 |
+
line,
|
| 276 |
+
dxfattribs={
|
| 277 |
+
'layer': 'ANNOTATIONS',
|
| 278 |
+
'height': 3,
|
| 279 |
+
'insert': (maxx + 20, y_offset)
|
| 280 |
+
}
|
| 281 |
+
)
|
| 282 |
+
y_offset -= 5
|
| 283 |
+
|
| 284 |
+
def _add_dimensions(self, msp, layout: Layout):
|
| 285 |
+
"""Add dimension annotations"""
|
| 286 |
+
if layout.site_boundary and layout.site_boundary.geometry:
|
| 287 |
+
bounds = layout.site_boundary.geometry.bounds
|
| 288 |
+
minx, miny, maxx, maxy = bounds
|
| 289 |
+
|
| 290 |
+
# Add site dimensions using custom dimension style
|
| 291 |
+
# Width dimension
|
| 292 |
+
msp.add_linear_dim(
|
| 293 |
+
base=(minx, miny - 10),
|
| 294 |
+
p1=(minx, miny),
|
| 295 |
+
p2=(maxx, miny),
|
| 296 |
+
dimstyle='ENG_DIM',
|
| 297 |
+
dxfattribs={'layer': 'DIMENSIONS'}
|
| 298 |
+
).render()
|
| 299 |
+
|
| 300 |
+
# Height dimension
|
| 301 |
+
msp.add_linear_dim(
|
| 302 |
+
base=(maxx + 10, miny),
|
| 303 |
+
p1=(maxx, miny),
|
| 304 |
+
p2=(maxx, maxy),
|
| 305 |
+
angle=90,
|
| 306 |
+
dimstyle='ENG_DIM',
|
| 307 |
+
dxfattribs={'layer': 'DIMENSIONS'}
|
| 308 |
+
).render()
|
| 309 |
+
|
| 310 |
+
def _add_title_block(self, msp, layout: Layout):
|
| 311 |
+
"""Add title block with project info"""
|
| 312 |
+
if not layout.site_boundary:
|
| 313 |
+
return
|
| 314 |
+
|
| 315 |
+
bounds = layout.site_boundary.geometry.bounds
|
| 316 |
+
minx, miny = bounds[0], bounds[1]
|
| 317 |
+
|
| 318 |
+
# Title block at bottom-left
|
| 319 |
+
title_lines = [
|
| 320 |
+
"INDUSTRIAL ESTATE MASTER PLAN",
|
| 321 |
+
f"Layout ID: {layout.id}",
|
| 322 |
+
f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}",
|
| 323 |
+
"REMB Optimization Engine v0.1.0",
|
| 324 |
+
"PiXerse.AI"
|
| 325 |
+
]
|
| 326 |
+
|
| 327 |
+
y_offset = miny - 30
|
| 328 |
+
for i, line in enumerate(title_lines):
|
| 329 |
+
msp.add_text(
|
| 330 |
+
line,
|
| 331 |
+
dxfattribs={
|
| 332 |
+
'layer': 'ANNOTATIONS',
|
| 333 |
+
'height': 4 if i == 0 else 2.5,
|
| 334 |
+
'insert': (minx, y_offset)
|
| 335 |
+
}
|
| 336 |
+
)
|
| 337 |
+
y_offset -= 6
|
| 338 |
+
|
| 339 |
+
def _polygon_to_coords(self, polygon: Polygon) -> List[Tuple[float, float]]:
|
| 340 |
+
"""Convert Shapely polygon to coordinate list"""
|
| 341 |
+
return [(p[0], p[1]) for p in polygon.exterior.coords]
|
| 342 |
+
|
| 343 |
+
def export_pareto_front(
|
| 344 |
+
self,
|
| 345 |
+
pareto_front,
|
| 346 |
+
output_dir: str,
|
| 347 |
+
prefix: str = "layout"
|
| 348 |
+
) -> List[str]:
|
| 349 |
+
"""
|
| 350 |
+
Export all layouts in a Pareto front
|
| 351 |
+
|
| 352 |
+
Args:
|
| 353 |
+
pareto_front: ParetoFront object
|
| 354 |
+
output_dir: Output directory
|
| 355 |
+
prefix: Filename prefix
|
| 356 |
+
|
| 357 |
+
Returns:
|
| 358 |
+
List of created file paths
|
| 359 |
+
"""
|
| 360 |
+
output_path = Path(output_dir)
|
| 361 |
+
output_path.mkdir(parents=True, exist_ok=True)
|
| 362 |
+
|
| 363 |
+
files = []
|
| 364 |
+
for i, layout in enumerate(pareto_front.layouts):
|
| 365 |
+
filename = f"{prefix}_{i:02d}_{layout.id[:8]}.dxf"
|
| 366 |
+
filepath = output_path / filename
|
| 367 |
+
self.export(layout, str(filepath))
|
| 368 |
+
files.append(str(filepath))
|
| 369 |
+
|
| 370 |
+
return files
|
| 371 |
+
|
| 372 |
+
|
| 373 |
+
# Example usage
|
| 374 |
+
if __name__ == "__main__":
|
| 375 |
+
from shapely.geometry import box
|
| 376 |
+
from src.models.domain import Layout, Plot, PlotType, SiteBoundary, RoadNetwork, LayoutMetrics
|
| 377 |
+
from shapely.geometry import LineString, MultiLineString
|
| 378 |
+
|
| 379 |
+
# Create example layout
|
| 380 |
+
site_geom = box(0, 0, 500, 500)
|
| 381 |
+
site = SiteBoundary(geometry=site_geom, area_sqm=site_geom.area)
|
| 382 |
+
site.buildable_area_sqm = site.area_sqm
|
| 383 |
+
|
| 384 |
+
layout = Layout(site_boundary=site)
|
| 385 |
+
|
| 386 |
+
# Add some plots
|
| 387 |
+
layout.plots = [
|
| 388 |
+
Plot(
|
| 389 |
+
id="plot_001",
|
| 390 |
+
geometry=box(60, 60, 160, 160),
|
| 391 |
+
area_sqm=10000,
|
| 392 |
+
type=PlotType.INDUSTRIAL,
|
| 393 |
+
width_m=100,
|
| 394 |
+
depth_m=100
|
| 395 |
+
),
|
| 396 |
+
Plot(
|
| 397 |
+
id="plot_002",
|
| 398 |
+
geometry=box(200, 60, 300, 160),
|
| 399 |
+
area_sqm=10000,
|
| 400 |
+
type=PlotType.INDUSTRIAL,
|
| 401 |
+
width_m=100,
|
| 402 |
+
depth_m=100
|
| 403 |
+
),
|
| 404 |
+
Plot(
|
| 405 |
+
id="green_001",
|
| 406 |
+
geometry=box(60, 200, 160, 300),
|
| 407 |
+
area_sqm=10000,
|
| 408 |
+
type=PlotType.GREEN_SPACE,
|
| 409 |
+
width_m=100,
|
| 410 |
+
depth_m=100
|
| 411 |
+
)
|
| 412 |
+
]
|
| 413 |
+
|
| 414 |
+
# Add road network
|
| 415 |
+
layout.road_network = RoadNetwork(
|
| 416 |
+
primary_roads=MultiLineString([
|
| 417 |
+
LineString([(0, 250), (500, 250)]),
|
| 418 |
+
LineString([(250, 0), (250, 500)])
|
| 419 |
+
]),
|
| 420 |
+
total_length_m=1000
|
| 421 |
+
)
|
| 422 |
+
|
| 423 |
+
# Calculate metrics
|
| 424 |
+
layout.metrics = LayoutMetrics(
|
| 425 |
+
total_area_sqm=250000,
|
| 426 |
+
sellable_area_sqm=20000,
|
| 427 |
+
green_space_area_sqm=10000,
|
| 428 |
+
road_area_sqm=24000,
|
| 429 |
+
sellable_ratio=0.65,
|
| 430 |
+
green_space_ratio=0.15,
|
| 431 |
+
num_plots=2,
|
| 432 |
+
is_compliant=True
|
| 433 |
+
)
|
| 434 |
+
|
| 435 |
+
# Export
|
| 436 |
+
exporter = DXFExporter()
|
| 437 |
+
output_file = exporter.export(
|
| 438 |
+
layout,
|
| 439 |
+
"output/test_layout.dxf",
|
| 440 |
+
include_annotations=True,
|
| 441 |
+
include_dimensions=True
|
| 442 |
+
)
|
| 443 |
+
|
| 444 |
+
print(f"Exported to: {output_file}")
|