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_GmplTitle}
+
+ {/* File Upload */}
+
+
+
+
+ {fileName ? fileName : tr_fileName}
+
+
+
+ {isFileUploaded && (
+ <>
+
+
+
+ >
+ )}
+
+ {fileContent && (
+
+ {showFileContent && (
+
+
File Content
+ {/* Use a textarea for editable text */}
+
+ )}
+
+ )}
+
+ {/* Loading Spinner */}
+ {isLoading && (
+
Loading...
+ )}
+
+
+
{solverTime}
{/* Display Solver Time */}
+
+ {/* Syntax Errors
+ {syntaxErrors.length > 0 && (
+
+
Syntax Errors
+
+ {syntaxErrors.map((error, index) => (
+ - {error}
+ ))}
+
+
+ )}
+ */}
+
+ {/* Syntax Error Popup
+ {showErrorPopup && (
+
+
There are syntax errors in the file.
+
+ )}
+ */}
+
+ {/* Result Popup */}
+ {showPopup && (
+
+
+
Solver Results
+
+
+
+
+ )}
+
+
+ );
+
+
+
+};
+
+export default GlpPage;
\ No newline at end of file
diff --git a/src/app/lang.ts b/src/app/lang.ts
index c61f566..b0157ca 100644
--- a/src/app/lang.ts
+++ b/src/app/lang.ts
@@ -8,6 +8,16 @@ export default function text(lang: string, input: string): string {
return "von Spaceholder Programming";
case "boxObjTitle":
return "Ziel";
+ case "GmplHeader":
+ return "Allgemeine Lineare Probleme";
+ case "FileUpload":
+ return "Datei hochladen";
+ case "FileName":
+ return "Kein File ausgewählt";
+ case "SpecProblem":
+ return "Spezifisches Problem";
+ case "GenProblem":
+ return "Allgemeines Lineares Problem";
case "boxObjDesc":
return "Geben Sie Ihr Ziel hier ein. Es ist nur ein Ziel erlaubt. Verwenden Sie eine Zeile dafür (kein 'Enter'!). Erlaubte Symbole sind 0-9, a-z, A-Z und <>=.\nBeispiel:\nx + y\n-786433 x1 + 655361 x2";
case "boxSubjTitle":
@@ -109,6 +119,16 @@ export default function text(lang: string, input: string): string {
// English translation
if (lang === "eng") {
switch (input) {
+ case "GmplHeader":
+ return "General Linear Problems";
+ case "SpecProblem":
+ return "Specific Problem";
+ case "FileUpload":
+ return "Upload File";
+ case "FileName":
+ return "No File selected";
+ case "GenProblem":
+ return "General Linear Problems";
case "header_title":
return "OR-Tool";
case "header_subtitle":
@@ -116,8 +136,8 @@ export default function text(lang: string, input: string): string {
case "boxObjTitle":
return "Objective";
case "boxObjDesc":
- return "Insert your objective here. One objective is allowed. Use one line for it (no \"return\"!) Allowed symbols are 0-9, a-z, A-Z and <>=.\nExample:\nx + y\n-786433 x1 + 655361 x2";
- case "boxSubjTitle":
+ return "Insert your objective here. One objective is allowed. Use one line for it (no \"return\"!) Allowed symbols are 0-9, a-z, A-Z and <>=.\nExample:\nx + y\n-786433 x1 + 655361 x2";
+ case "boxSubjTitle":
return "Subject";
case "boxSubjDesc":
return "Insert your subject here. One per line (divide by 'return' button). Allowed symbols are 0-9, a-z, A-Z and <>=.\nExample:\n+1 x + 2 y <= 15\n524321 x14 + 524305 x15 <= 4194303.5";
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 6f3f03b..a2d1618 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import Image from "next/image";
import localFont from "next/font/local";
+import { LanguageProvider } from './context/LanguageContext';
import "./globals.css";
const geistSans = localFont({
@@ -19,16 +20,19 @@ export const metadata: Metadata = {
description: "OR-Tool by Spaceholder Programming",
};
+
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
+
+
{children}
+
+
);
diff --git a/src/app/page.tsx b/src/app/page.tsx
index b321e2e..994b526 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,13 +1,19 @@
'use client'
-import React, { useState } from 'react';
+import React, { useState, useContext } from 'react';
import { Box, Button, Output } from "./modules";
import { calculate_click, downloadLP, downloadMPS } from "./scripts";
import text from "./lang";
+import { spec } from 'node:test/reporters';
+import { useRouter } from 'next/navigation';
+import { LanguageContext } from './context/LanguageContext';
export default function Home() {
- const [language, setLanguage] = useState('eng');
+ const { language, setLanguage } = useContext(LanguageContext);
const [maxminOption, setMaxminOption] = useState('maximize');
+ const [model, selectedModel] = useState('spec');
+ const router = useRouter();
+
const tr_hTitle = text(language, 'header_title');
const tr_hSubtitle = text(language, 'header_subtitle');
@@ -25,11 +31,21 @@ export default function Home() {
const tr_calc_min = text(language, "minimize");
const tr_calcButton = text(language, "buttonCalc");
const tr_boxExportMPS = text(language, "boxExportMPS");
+ const tr_GenProblems = text(language, 'GenProblem');
+ const tr_SpecProblems = text(language, 'SpecProblem');
const handleLanguageChange = (event: React.ChangeEvent) => {
setLanguage(event.target.value);
};
+ const changeModel = (event: React.ChangeEvent) => {
+ selectedModel(event.target.value);
+
+ if (event.target.value === 'gen') {
+ router.push('./glp');
+ }
+ };
+
const handleMaxMinChange = (event: React.ChangeEvent) => {
setMaxminOption(event.target.value);
};
@@ -49,6 +65,10 @@ export default function Home() {
+