Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Huy working branch last sprint #269

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions es/translator/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export default {
"Length": "Length",
"Lines": "Lines",
"Load project": "Load project",
"Fetch and generate": "Fetch and generate",
"Mouse X Coordinate": "Mouse X Coordinate",
"Mouse Y Coordinate": "Mouse Y Coordinate",
"Name": "Name",
Expand Down
1 change: 1 addition & 0 deletions lib/translator/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ exports.default = {
"Length": "Length",
"Lines": "Lines",
"Load project": "Load project",
"Fetch and generate": "Fetch and generate",
"Mouse X Coordinate": "Mouse X Coordinate",
"Mouse Y Coordinate": "Mouse Y Coordinate",
"Name": "Name",
Expand Down
9,820 changes: 9,820 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
"immutable": "3.8.2",
"immutablediff": "0.4.4",
"immutablepatch": "0.5.0",
"multer": "^1.4.5-lts.1",
"polylabel": "1.0.2",
"prop-types": "15.7.2",
"react-icons": "3.5.0",
Expand Down
211 changes: 211 additions & 0 deletions server/floor_plan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import cv2
import os
import numpy as np
import pyautogui
import time
from selenium import webdriver
from selenium.webdriver import Firefox, FirefoxOptions
from selenium.webdriver.common.keys import Keys
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By

# Get the current directory
current_dir = os.path.dirname(os.path.abspath(__file__))

# Image file name
image_file = "uploads/floor_plan.png" # Include the 'uploads' directory in the path
output_file = "uploads/output.png" # Include the 'uploads' directory in the path

# Image file path
image_path = os.path.join(current_dir, image_file)
output_path = os.path.join(current_dir, output_file)

# Check if the image file exists
if not os.path.isfile(image_path):
raise FileNotFoundError(f"Image file '{image_file}' not found.")

def getWalls(image_path, threshold_area, canny_threshold1, canny_threshold2):
# Load the image
image = cv2.imread(image_path)

# Convert the image to grayscale
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

# Apply thresholding to segment the walls
_, thresholded = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)

# Apply Canny edge detection to detect lines
edges = cv2.Canny(thresholded, canny_threshold1, canny_threshold2)

# Remove small components or noise from the detected lines
num_labels, labels, stats, _ = cv2.connectedComponentsWithStats(edges, connectivity=8)

# Create a mask to filter out small components
mask = np.zeros_like(labels, dtype=np.uint8)
for label in range(1, num_labels):
area = stats[label, cv2.CC_STAT_AREA]
if area > threshold_area:
mask[labels == label] = 255

# Apply the mask to retain only the lines along the walls
result = cv2.bitwise_and(edges, edges, mask=mask)

# Perform morphological dilation to connect adjacent line segments
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 15))
dilated = cv2.dilate(result, kernel, iterations=1)

# Display the resulting image [UNDO TO SEE MID RESULT]
## cv2.imshow('Original Image', image)
## cv2.imshow('Lines along Walls', dilated)
cv2.imwrite(output_path, dilated)

cv2.waitKey(0)
cv2.destroyAllWindows()

def reduction(pathToOutput):
img = cv2.imread(pathToOutput, cv2.IMREAD_GRAYSCALE)

# Set the desired thickness reduction factor
thickness_reduction_factor = 9

# Define the structuring element for erosion
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))

# Perform erosion
eroded_image = cv2.erode(img, kernel, iterations=thickness_reduction_factor)

# Display the eroded image [UNDO TO SEE MID RESULT]
## cv2.imshow("Eroded Image", eroded_image)
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite(output_path, eroded_image)

def detect_line_segments(image_path):
# Load the image in grayscale
image = cv2.imread(image_path, 0)

# Apply Canny edge detection
edges = cv2.Canny(image, 50, 150, apertureSize=3)

# Perform Hough Line Transform
lines = cv2.HoughLinesP(edges, rho=1, theta=np.pi / 180, threshold=100, minLineLength=20, maxLineGap=10)

# Process the detected lines
dimensions = []
if lines is not None:
for line in lines:
x1, y1, x2, y2 = line[0]
length = np.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2)
angle = np.arctan2(y2 - y1, x2 - x1) * 180 / np.pi
dimensions.append((x1, y1, x2, y2, length, angle))

return dimensions

# Call the function to reduce the image to lines and label them
threshold_area = 90
canny_threshold1 = 10
canny_threshold2 = 10
thickness_reduction_factor = 1

# Call the function to remove background elements and retain lines along walls
getWalls(image_path, threshold_area, canny_threshold1, canny_threshold2)

reduction(output_path)

line_segments = detect_line_segments(output_path)

list_of_xcor = []
list_of_ycor = []

for segment in line_segments:
x1, y1, x2, y2, length, angle = segment
print(f"Start Point: ({x1}, {y1}), End Point: ({x2}, {y2}), Length: {length}, Angle: {angle}")
list_of_xcor.append(x1)
list_of_ycor.append(y1)
list_of_xcor.append(x2)
list_of_ycor.append(y2)

list_of_xcor = [int(x) for x in list_of_xcor]
list_of_ycor = [int(y) for y in list_of_ycor]

# Set the downloads directory path
downloads_dir = os.path.join(current_dir, "jsons")
if not os.path.exists(downloads_dir):
os.makedirs(downloads_dir)

# Set up Firefox options
opts = FirefoxOptions()
opts.add_argument("--width=4000")
opts.add_argument("--height=4000")
opts.add_argument('--headless')

# Set Firefox Preferences to specify the custom download directory
opts.set_preference("browser.download.folderList", 2) # Use custom download path
opts.set_preference("browser.download.manager.showWhenStarting", False)
opts.set_preference("browser.download.dir", downloads_dir)
opts.set_preference("browser.helperApps.neverAsk.saveToDisk", "application/octet-stream") # MIME type

# Create the driver with the custom options
driver = Firefox(options=opts)
driver.get('https://cvdlab.github.io/react-planner/')
actionChains = ActionChains(driver)

def selectWall(i):
cssSelector_OpenMenu = ".toolbar > div:nth-child(4) > div:nth-child(1) > svg:nth-child(1)"
## cssSelector = "#app > div:nth-child(1) > div:nth-child(2) > div:nth-child(4) > svg:nth-child(1) > g:nth-child(2) > g:nth-child(2) > g:nth-child(2) > g:nth-child(1) > rect:nth-child(1)"

button = driver.find_element(By.CSS_SELECTOR, cssSelector_OpenMenu)
actionChains.move_to_element(button).click().perform()
time.sleep(0.20)
if i != 0:
cssSelector_Wall = "#app > div:nth-child(1) > div:nth-child(2) > div:nth-child(4) > div:nth-child(3)"
else:
cssSelector_Wall = "#app > div:nth-child(1) > div:nth-child(2) > div:nth-child(3) > div:nth-child(3)"
button = driver.find_element(By.CSS_SELECTOR, cssSelector_Wall)
actionChains.move_to_element(button).click().perform()


def drawWall(x1, y1, x2, y2):
## CSS Properties of the Grid
cssSelector_Grid = "#app > div:nth-child(1) > div:nth-child(2) > div:nth-child(4) > svg:nth-child(1) > g:nth-child(2) > g:nth-child(2) > g:nth-child(2) > g:nth-child(1) > rect:nth-child(1)"

## Grid Size = (3000, 2000)
## Center = (1500,1000)
Offset_X1 = x1 - 1500
Offset_Y1 = y1 - 1000

Offset_X2 = x2 - 1500
Offset_Y2 = y2 - 1000

button = driver.find_element(By.CSS_SELECTOR, cssSelector_Grid)
actionChains.move_to_element_with_offset(button, Offset_X1, Offset_Y1)
actionChains.click()
actionChains.move_to_element_with_offset(button, Offset_X2, Offset_Y2)
actionChains.click()
actionChains.send_keys(Keys.ESCAPE).perform()

coords = list(map(list, zip(list_of_xcor, list_of_ycor)))

for i in range(0,len(coords),2):
selectWall(i)
time.sleep(0.20)
drawWall(coords[i][0], coords[i][1], coords[i+1][0], coords[i+1][1])

def saveProject():
time.sleep(0.20)
## Properties of the Save Button
cssSelector_Save = ".toolbar > div:nth-child(2) > div:nth-child(1) > svg:nth-child(1) > path:nth-child(1)"
## Find the Save Button
button = driver.find_element(By.CSS_SELECTOR, cssSelector_Save)
## Move to and click the save Button
actionChains.move_to_element(button).click().perform()

## Wait for it to load properly in case
time.sleep(0.20)
## Accept the alert pop-up
driver.switch_to.alert.accept()

saveProject()
time.sleep(1)
print("clicked")
driver.quit()
86 changes: 86 additions & 0 deletions server/server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const express = require("express");
const cors = require("cors");
const multer = require("multer");
const fs = require("fs");
const path = require("path");
const port = 4000;
const { exec } = require("child_process");

// Initialize express application.
const app = express();

// Enable CORS with specific options for allowed origins and a success status.
app.use(
cors({
origin: ["http://localhost:9000", "http://localhost:44463"], // Allowed domains for cross-origin requests
optionsSuccessStatus: 200 // Status to send on successful OPTIONS request
})
);

// Configuration for multer storage: how files are named and where they are stored.
const storage = multer.diskStorage({
destination: "./uploads/", // Directory where files will be uploaded
filename: function (req, file, cb) {
const newFilename = "floor_plan.png"; // New static name for all uploaded files
cb(null, newFilename);
},
});

// Initialize multer with storage configuration and file filter for PNG files only.
const upload = multer({
storage: storage,
fileFilter: function (req, file, cb) {
checkFileType(file, cb);
},
}).single("file"); // Accepts only one file, with the form field named 'file'.

// Helper function to check if the uploaded file is a PNG.
function checkFileType(file, cb) {
if (file.mimetype === "image/png") {
cb(null, true); // Accept file if it is PNG
} else {
cb("Error: Only PNG files are allowed!"); // Reject file if not PNG
}
}

// Serve static files from the 'uploads' directory.
app.use("/uploads", express.static("uploads"));

// Route to handle POST requests to upload PNG files.
app.post("/upload-png", (req, res) => {
upload(req, res, (err) => {
if (err) {
res.send({
message: err, // Send error message if there's an error during upload
});
} else {
if (req.file == undefined) {
res.send({
message: "Error: No File Selected!", // Send error if no file is selected
});
} else {
res.send({
message: "File Uploaded!", // Confirm file upload success
fileInfo: {
filename: req.file.filename, // Return filename
path: req.file.path, // Return file path
},
});
}
}
});
});

// Route to execute a Python script and return its output.
app.get("/process-projects", (req, res) => {
exec("python floor_plan.py", (error, stdout, stderr) => {
if (error) {
console.error(`exec error: ${error}`);
return res.status(500).send("Failed to execute Python script."); // Send error response if script fails
}
res.send(stdout); // Send script output if successful
});
});

// Start the server on the specified port, confirming with a console message.
app.listen(port, () => console.log(`Server started on port ${port}`));
Binary file added server/uploads/floor_plan.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added server/uploads/output.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/components/toolbar/export.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import ToolbarButton from './toolbar-button';
import ToolbarSaveButton from './toolbar-save-button';
import ToolbarLoadButton from './toolbar-load-button';
import ToolbarFetchButton from "./toolbar-fetch-button"
import Toolbar from './toolbar';

export {
ToolbarButton,
ToolbarSaveButton,
ToolbarLoadButton,
ToolbarFetchButton,
Toolbar
};

export default {
ToolbarButton,
ToolbarSaveButton,
ToolbarLoadButton,
ToolbarFetchButton,
Toolbar
};
43 changes: 43 additions & 0 deletions src/components/toolbar/toolbar-fetch-button.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FaCloudDownloadAlt as IconFetch } from 'react-icons/fa';
import ToolbarButton from './toolbar-button';

export default function ToolbarFetchButton({ state }, { translator, projectActions }) {

let fetchAndLoadProject = () => {
// Use fetch to get project data from the server
fetch('http://localhost:4000/process-projects')
.then(response => {
// Check if the fetch request was successful
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json(); // Parse JSON response body
})
.then(data => {
// Assuming 'data' contains the generated project data
// and matches the structure expected by loadProject
projectActions.loadProject(data);
})
.catch(error => {
// Log or handle errors in fetching or processing the data
console.error('Error fetching and loading project:', error);
});
};

return (
<ToolbarButton active={false} tooltip={translator.t("Fetch and generate")} onClick={fetchAndLoadProject}>
<IconFetch />
</ToolbarButton>
);
}

ToolbarFetchButton.propTypes = {
state: PropTypes.object.isRequired,
};

ToolbarFetchButton.contextTypes = {
projectActions: PropTypes.object.isRequired,
translator: PropTypes.object.isRequired,
};
Loading