diff --git a/__tests__/render_home_page.test.js b/__tests__/render_home_page.test.js index 663eb50..68ad71b 100644 --- a/__tests__/render_home_page.test.js +++ b/__tests__/render_home_page.test.js @@ -1,22 +1,31 @@ -import { render, screen, fireEvent, waitFor } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; -import Home from "../src/app/page"; +import Home from '../src/app/page'; import { customLog, customLogClear } from '../src/app/scripts'; +// Mock customLog and customLogClear jest.mock('../src/app/scripts', () => ({ customLog: jest.fn(), customLogClear: jest.fn(), })); +// Mock next/navigation instead of next/router +jest.mock('next/navigation', () => ({ + useRouter: jest.fn().mockReturnValue({ + push: jest.fn(), + }), +})); + +// Mock GLPKAPI to avoid issues with undefined LPF_ECOND jest.mock('../src/solver/glpk.min.js', () => ({ LPF_ECOND: 2, })); test('render home page', () => { - // render website + // Render Home component render(); - // check if text is in document - const headingElement = screen.getByText(/OR-Tool/i); // text search in document + // Check if the heading text "OR-Tool" is present in the document + const headingElement = screen.getByText(/OR-Tool/i); // Match text that contains "OR-Tool" expect(headingElement).toBeInTheDocument(); }); diff --git a/__tests__/scripts.test.js b/__tests__/scripts.test.js index 5df2024..b1e9280 100644 --- a/__tests__/scripts.test.js +++ b/__tests__/scripts.test.js @@ -20,6 +20,13 @@ jest.mock('../src/solver/glpk.min.js', () => ({ // Mocking console.log const consoleLogMock = jest.spyOn(console, 'log').mockImplementation(() => {}); +// Mock useRouter to avoid invariant error +jest.mock('next/navigation', () => ({ + useRouter: jest.fn(() => ({ + push: jest.fn(), // Mock the 'push' function + })), +})); + beforeEach(() => { document.body.innerHTML = `
@@ -90,7 +97,7 @@ test('calculate_click should display "Calculating" in the output box', () => { document.getElementById('bounds').value = 'x <= 5'; document.getElementById('vars').value = 'x\ny'; - // Simuliere den Button-Klick, der die Berechnung startet + // Simulate the button click to trigger calculation fireEvent.click(screen.getByText('Calculate')); // Check the contents of out box @@ -101,4 +108,3 @@ test('calculate_click should display "Calculating" in the output box', () => { mockClear.mockRestore(); mockLog.mockRestore(); }); - diff --git a/src/app/context/LanguageContext.tsx b/src/app/context/LanguageContext.tsx new file mode 100644 index 0000000..96e9a7c --- /dev/null +++ b/src/app/context/LanguageContext.tsx @@ -0,0 +1,21 @@ +'use client'; + +import React, { createContext, useState, ReactNode } from 'react'; + +export const LanguageContext = createContext<{ + language: string; + setLanguage: (lang: string) => void; +}>({ + language: 'eng', + setLanguage: () => {}, +}); + +export const LanguageProvider = ({ children }: { children: ReactNode }) => { + const [language, setLanguage] = useState('eng'); + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css index 790cffc..faf52dd 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -61,6 +61,14 @@ body { padding: 10px; } +.button_spec { + border: 2px solid #5353535c; + background-color: #1010105c; + border-radius: 20px; + margin: 10px; + padding: 20px; +} + .button:hover { border: 2px solid #5353535c; background-color: #5353535c; @@ -186,3 +194,163 @@ body { font-size: 16px; cursor: pointer; } + +.containerGmpl { + display: flex; + flex-direction: column; + margin: 20px; + padding: 20px; + background-color: #2a2a2a; + border-radius: 10px; + border: 1px solid #8d8d8d; + max-width: 90%; + margin-left: auto; + margin-right: auto; +} + +.headerGmpl { + font-size: 24px; + color: #ffffff; + text-align: center; + margin-bottom: 20px; +} + +.fileInput { + margin-bottom: 20px; +} + +.buttoncontainerGmpl { + display: flex; + justify-content: flex-end; +} + +.buttonGmpl { + padding: 10px 20px; + font-size: 16px; + background-color: #28a745; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.togglebuttonGmpl { + margin: 10px 0; + padding: 10px; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.fileContentBox { + background-color: #3c3c3c; + color: #ffffff; + padding: 15px; + margin-bottom: 20px; + border-radius: 5px; + font-family: 'Courier New', Courier, monospace; + white-space: pre-line; +} + +.textarea { + width: 100%; + background-color: #202020; + color: #ffffff; + border: 1px solid #8d8d8d; + border-radius: 5px; + padding: 10px; + font-family: 'Courier New', Courier, monospace; + font-size: 16px; + margin-bottom: 10px; + overflow: scroll; + max-height: 800px; + min-height: 400px; +} + +.downloadbuttonGmpl { + padding: 10px 20px; + font-size: 16px; + background-color: #28a745; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.loadingSpinner { + color: #fff; + font-size: 16px; + text-align: center; + margin-top: 20px; +} + +.pre { + margin: 0; +} + +.subheaderGmpl { + color: #ffffff; + margin-bottom: 10px; +} + +.msgZone { + margin-top: 20px; + color: #ffcc00; + background-color: #333; + padding: 10px; + border-radius: 5px; +} + +.syntaxErrorBox { + margin-top: 20px; + padding: 10px; + background-color: #ffcc00; + border-radius: 5px; + color: #333; +} + +.errorPopup { + position: fixed; + bottom: 20px; + left: 20px; + padding: 10px; + background-color: #ffcc00; + color: #333; + border-radius: 5px; + box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.2); +} + +.popup { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.7); + display: flex; + justify-content: center; + align-items: center; +} + +.popupContent { + background-color: #333; + padding: 20px; + border-radius: 10px; + color: #fff; + text-align: center; + width: 80%; + max-width: 600px; +} + +.popupResult { + max-height: 300px; + overflow-y: auto; + text-align: left; + padding: 10px; + border: 1px solid #8d8d8d; + background-color: #202020; + color: #ffffff; + border-radius: 5px; +} \ No newline at end of file diff --git a/src/app/glp/page.tsx b/src/app/glp/page.tsx new file mode 100644 index 0000000..d0a8deb --- /dev/null +++ b/src/app/glp/page.tsx @@ -0,0 +1,359 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import React, { useState, useContext, useRef, useEffect } from 'react'; +import text from "../lang"; +import { LanguageContext } from '../context/LanguageContext'; +import * as GLPKAPI from "../../solver/glpk.min.js" + + + +const GlpPage = () => { + const router = useRouter(); + + const { language, setLanguage } = useContext(LanguageContext); + const [model, setModel] = useState('gen'); + + const tr_hTitle = text(language, 'header_title'); + const tr_hSubtitle = text(language, 'header_subtitle'); + const tr_calcButton = text(language, "buttonCalc"); + const tr_GmplTitle = text(language, 'GmplHeader'); + const tr_GenProblems = text(language, 'GenProblem'); + const tr_SpecProblems = text(language, 'SpecProblem'); + const tr_fileUpload = text(language, 'FileUpload'); + const tr_fileName = text(language, 'FileName'); + + + + + const [fileContent, setFileContent] = useState(''); + const [isFileUploaded, setIsFileUploaded] = useState(false); + const [showFileContent, setShowFileContent] = useState(true); + const [isLoading, setIsLoading] = useState(false); + const [solverTime, setSolverTime] = useState(''); + const [showPopup, setShowPopup] = useState(false); + const [resultContent, setResultContent] = useState(''); + //const [syntaxErrors, setSyntaxErrors] = useState([]); + const [showErrorPopup, setShowErrorPopup] = useState(false); + const solverTimeoutRef = useRef(null); + const solverAbortController = useRef(null); + //const [highlightedContent, setHighlightedContent] = useState(''); + const [fileName, setFileName] = useState(""); + + + const handleLanguageChange = (event: React.ChangeEvent) => { + setLanguage(event.target.value); + }; + + const changeModel = (event: React.ChangeEvent) => { + const selectedModel = event.target.value; + setModel(selectedModel); + + if (selectedModel === 'spec') { + router.push('/'); + } + }; + + //useEffect(() => { + // if (syntaxErrors.length > 0) { + // setShowErrorPopup(true); + // } + //}, [syntaxErrors]); + + const addMessage = (message: string) => { + const msgZone = document.getElementById("msgZone"); + if (msgZone) { + msgZone.innerHTML += `
${message}
`; + } + }; + + // const updateHighlightedContent = (content: string) => { + // const highlighted = highlightErrors(content); + // setHighlightedContent(highlighted); + // }; + + + const handleFileUpload = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + setFileName(file.name); + } + if (!file) return; + + const reader = new FileReader(); + reader.onload = (e) => { + const content = e.target?.result as string; + setFileContent(content); + // updateHighlightedContent(content); + addMessage("File successfully uploaded and read."); + setIsFileUploaded(true); + + // Perform syntax check + //const errors = checkSyntax(content); + //setSyntaxErrors(errors); + // updateHighlightedContent(content); + }; + reader.readAsText(file); + }; + + //const checkSyntax = (content: string): string[] => { + // const errors: string[] = []; + // const lines = content.split("\n"); + // + // const ignorePattern = /^(#|\/\/|printf|\/\*|\*).*$/; + // + // lines.forEach((line, index) => { + // if (ignorePattern.test(line) || line.trim() === "") { + // return; // Ignore this line + // } + // + // const validGmplLinePattern = /^(var|param|set|maximize|minimize|s\.t\.|subject to|for|in|if|then|else|end|:=|<=|>=|=|\+|\-|\*|\/|[a-zA-Z_][a-zA-Z0-9_]*\s*[=<>+\-*/]*\s*[0-9a-zA-Z_]+.*;)/; + // + // if (!validGmplLinePattern.test(line)) { + // errors.push(`Syntax error on line ${index + 1}: ${line}`); + // } + // }); + // + // return errors; + //}; + + // Highlight syntax errors in the file content + //const highlightErrors = (content: string) => { + // const lines = content.split("\n"); + // return lines.map((line, index) => { + // const error = syntaxErrors.find((error) => error.includes(`line ${index + 1}`)); + // if (error) { + // return `${line}`; + // } + // return line; + // }).join("\n"); + //}; + + // Function to dynamically set the height of the textarea + const getTextAreaHeight = (value: any) => { + const stringValue = String(value); + const lineCount = stringValue.split("\n").length; + if (lineCount <= 3) return lineCount; + return 3; + }; + + // Function to abort solving after a certain time + const abortSolving = () => { + if (solverAbortController.current) { + solverAbortController.current.abort(); + setIsLoading(false); + addMessage("Solving was aborted after the timeout."); + } + }; + + const solve = () => { + addMessage("Starting the solver..."); + setIsLoading(true); + const startTime = performance.now(); + + // Set a timeout to abort the solver after 5 seconds + solverTimeoutRef.current = setTimeout(abortSolving, 5000); + + solverAbortController.current = new AbortController(); + const { signal } = solverAbortController.current; + + try { + let model = fileContent; + let lp = GLPKAPI.glp_create_prob(); + let tran = GLPKAPI.glp_mpl_alloc_wksp(); + GLPKAPI._glp_mpl_init_rand(tran, 1); + + GLPKAPI.glp_mpl_read_model_from_string(tran, "model", model, 0); + GLPKAPI.glp_mpl_generate(tran, null, function () { }); + addMessage("Model successfully converted to solver format."); + + GLPKAPI.glp_mpl_build_prob(tran, lp); + let smcp = new GLPKAPI.SMCP({ presolve: GLPKAPI.GLP_ON }); + GLPKAPI.glp_simplex(lp, smcp); + + let iocp = new GLPKAPI.IOCP({ presolve: GLPKAPI.GLP_ON }); + GLPKAPI.glp_intopt(lp, iocp); + GLPKAPI.glp_mpl_postsolve(tran, lp, GLPKAPI.GLP_MIP); + + addMessage("Model solved successfully."); + setIsLoading(false); + + const endTime = performance.now(); + const solverDuration = ((endTime - startTime) / 1000).toFixed(2); + setSolverTime(`Solver time: ${solverDuration} seconds`); + addMessage(`Solver time: ${solverDuration} seconds`); + + let status; + switch (GLPKAPI.glp_mip_status(lp)) { + case GLPKAPI.GLP_OPT: + status = "OPTIMAL"; + break; + case GLPKAPI.GLP_UNDEF: + status = "UNDEFINED SOLUTION"; + break; + case GLPKAPI.GLP_INFEAS: + status = "INFEASIBLE SOLUTION"; + break; + case GLPKAPI.GLP_NOFEAS: + status = "NO FEASIBLE SOLUTION"; + break; + case GLPKAPI.GLP_FEAS: + status = "FEASIBLE SOLUTION"; + break; + case GLPKAPI.GLP_UNBND: + status = "UNBOUNDED SOLUTION"; + break; + } + + const result = `Solution status: ${status}`; + let variables = "Variable results:\n"; + for (let i = 1; i <= GLPKAPI.glp_get_num_cols(lp); i++) { + variables += `${GLPKAPI.glp_get_col_name(lp, i)} = ${GLPKAPI.glp_mip_col_val(lp, i)}\n`; + } + if (solverTimeoutRef.current) clearTimeout(solverTimeoutRef.current); + setResultContent(`${result}\n\n${variables}\n\nSolver time: ${solverDuration} seconds`); + setShowPopup(true); + + } catch (err) { + setIsLoading(false); + addMessage("
" + (err as Error).toString() + "
"); + console.log(err); + } + }; + + const downloadFile = () => { + const element = document.createElement("a"); + const file = new Blob([fileContent], { type: 'text/plain' }); + element.href = URL.createObjectURL(file); + element.download = "problem.gmpl"; + document.body.appendChild(element); + element.click(); + document.body.removeChild(element); + }; + + return ( +
+
+
+
+ {tr_hTitle} +
+ + {tr_hSubtitle} + +
+ + +
+
+
+ +
+

{tr_GmplTitle}

+ + {/* File Upload */} +
+ + + + {fileName ? fileName : tr_fileName} + +
+ + {isFileUploaded && ( + <> +
+ +
+ + )} + + {fileContent && ( +
+ {showFileContent && ( +
+

File Content

+ {/* Use a textarea for editable text */} +