Merge gmpl main2 #54

Closed
SinusFox wants to merge 6 commits from merge_gmpl_main2 into main
7 changed files with 606 additions and 4 deletions
+8
View File
@@ -2,12 +2,20 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import Home from "../src/app/page"; import Home from "../src/app/page";
import { customLog, customLogClear } from '../src/app/scripts'; import { customLog, customLogClear } from '../src/app/scripts';
import { useRouter } from 'next/router';
jest.mock('../src/app/scripts', () => ({ jest.mock('../src/app/scripts', () => ({
customLog: jest.fn(), customLog: jest.fn(),
customLogClear: jest.fn(), customLogClear: jest.fn(),
})); }));
jest.mock('next/router', () => ({
useRouter: jest.fn().mockReturnValue({
push: jest.fn(),
}),
}));
jest.mock('../src/solver/glpk.min.js', () => ({ jest.mock('../src/solver/glpk.min.js', () => ({
LPF_ECOND: 2, LPF_ECOND: 2,
})); }));
+21
View File
@@ -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 (
<LanguageContext.Provider value={{ language, setLanguage }}>
{children}
</LanguageContext.Provider>
);
};
+168
View File
@@ -61,6 +61,14 @@ body {
padding: 10px; padding: 10px;
} }
.button_spec {
border: 2px solid #5353535c;
background-color: #1010105c;
border-radius: 20px;
margin: 10px;
padding: 20px;
}
.button:hover { .button:hover {
border: 2px solid #5353535c; border: 2px solid #5353535c;
background-color: #5353535c; background-color: #5353535c;
@@ -186,3 +194,163 @@ body {
font-size: 16px; font-size: 16px;
cursor: pointer; 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;
}
+359
View File
@@ -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<string>('');
const [isFileUploaded, setIsFileUploaded] = useState(false);
const [showFileContent, setShowFileContent] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [solverTime, setSolverTime] = useState<string>('');
const [showPopup, setShowPopup] = useState(false);
const [resultContent, setResultContent] = useState<string>('');
//const [syntaxErrors, setSyntaxErrors] = useState<string[]>([]);
const [showErrorPopup, setShowErrorPopup] = useState(false);
const solverTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const solverAbortController = useRef<AbortController | null>(null);
//const [highlightedContent, setHighlightedContent] = useState<string>('');
const [fileName, setFileName] = useState<string>("");
const handleLanguageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setLanguage(event.target.value);
};
const changeModel = (event: React.ChangeEvent<HTMLSelectElement>) => {
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 += `<div>${message}</div>`;
}
};
// const updateHighlightedContent = (content: string) => {
// const highlighted = highlightErrors(content);
// setHighlightedContent(highlighted);
// };
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
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 `<span style="background-color: yellow; color: black;" title="${error}">${line}</span>`;
// }
// 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 {
var model = fileContent;
var lp = GLPKAPI.glp_create_prob();
var 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);
var smcp = new GLPKAPI.SMCP({ presolve: GLPKAPI.GLP_ON });
GLPKAPI.glp_simplex(lp, smcp);
var 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`);
var 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 (var 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("<div class='alert alert-danger'>" + (err as Error).toString() + "</div>");
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 (
<div>
<header className="header">
<div className="title">
<main className="header_box">
{tr_hTitle}
<br />
<span className="header_copyright">
<i>{tr_hSubtitle}</i>
</span>
<br />
<select id="language_current" value={language} onChange={handleLanguageChange} className="dropdown-custom">
<option value="ger">Deutsch</option>
<option value="eng">English</option>
</select>
<select id="language_current" value={model} onChange={changeModel} className="dropdown-custom">
<option value="gen">{tr_GenProblems}</option>
<option value="spec">{tr_SpecProblems}</option>
</select>
</main>
</div>
</header>
<div className="containerGmpl">
<h1 className="headerGmpl">{tr_GmplTitle}</h1>
{/* File Upload */}
<div className="button_spec">
<label htmlFor="fileUpload" className="button_green">
{tr_fileUpload}
</label>
<input
id="fileUpload"
type="file"
accept=".mod,.dat,.txt,.gmpl"
onChange={handleFileUpload}
style={{ display: "none" }}
/>
<span className="fileName">
{fileName ? fileName : tr_fileName}
</span>
</div>
{isFileUploaded && (
<>
<div className="buttoncontainerGmpl">
<button className="button_green" onClick={solve} disabled={isLoading}>
{tr_calcButton}
</button>
</div>
</>
)}
{fileContent && (
<div>
{showFileContent && (
<div className="fileContentBox">
<h2 className="subheaderGmpl">File Content</h2>
{/* Use a textarea for editable text */}
<textarea
className="textarea"
value={fileContent}
onChange={(e) => {
setFileContent(e.target.value);
//updateHighlightedContent(e.target.value);
}}
rows={getTextAreaHeight(fileContent)}
/>
<button className="button" onClick={downloadFile}>
Download GMPL File
</button>
</div>
)}
</div>
)}
{/* Loading Spinner */}
{isLoading && (
<div className="loadingSpinner">Loading...</div>
)}
<div id="msgZone" className="msgZone"></div>
<div>{solverTime}</div> {/* Display Solver Time */}
{/* Syntax Errors
{syntaxErrors.length > 0 && (
<div className="syntaxErrorBox">
<h3>Syntax Errors</h3>
<ul>
{syntaxErrors.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
</div>
)}
*/}
{/* Syntax Error Popup
{showErrorPopup && (
<div className="errorPopup">
<p>There are syntax errors in the file.</p>
</div>
)}
*/}
{/* Result Popup */}
{showPopup && (
<div className="popup">
<div className="popupContent">
<h2>Solver Results</h2>
<div className="popupResult">
<pre className="pre">{resultContent}</pre>
</div>
<button className="button_green" onClick={() => setShowPopup(false)}>Close</button>
</div>
</div>
)}
</div>
</div>
);
};
export default GlpPage;
+22 -2
View File
@@ -8,6 +8,16 @@ export default function text(lang: string, input: string): string {
return "von Spaceholder Programming"; return "von Spaceholder Programming";
case "boxObjTitle": case "boxObjTitle":
return "Ziel"; 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": 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"; 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": case "boxSubjTitle":
@@ -109,6 +119,16 @@ export default function text(lang: string, input: string): string {
// English translation // English translation
if (lang === "eng") { if (lang === "eng") {
switch (input) { 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": case "header_title":
return "OR-Tool"; return "OR-Tool";
case "header_subtitle": case "header_subtitle":
@@ -116,8 +136,8 @@ export default function text(lang: string, input: string): string {
case "boxObjTitle": case "boxObjTitle":
return "Objective"; return "Objective";
case "boxObjDesc": 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"; 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": case "boxSubjTitle":
return "Subject"; return "Subject";
case "boxSubjDesc": 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"; 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";
+6
View File
@@ -1,6 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import Image from "next/image"; import Image from "next/image";
import localFont from "next/font/local"; import localFont from "next/font/local";
import { LanguageProvider } from './context/LanguageContext';
import "./globals.css"; import "./globals.css";
const geistSans = localFont({ const geistSans = localFont({
@@ -19,16 +20,19 @@ export const metadata: Metadata = {
description: "OR-Tool by Spaceholder Programming", description: "OR-Tool by Spaceholder Programming",
}; };
export default function RootLayout({ export default function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
return ( return (
<html lang="en"> <html lang="en">
<body <body
className={`${geistSans.variable} ${geistMono.variable} antialiased`} className={`${geistSans.variable} ${geistMono.variable} antialiased`}
> >
<LanguageProvider>
{children} {children}
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]"> <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<footer className=" flex gap-6 flex-wrap items-center justify-center"> <footer className=" flex gap-6 flex-wrap items-center justify-center">
@@ -79,6 +83,8 @@ export default function RootLayout({
</a> </a>
</footer> </footer>
</div> </div>
</LanguageProvider>
</body> </body>
</html> </html>
); );
+22 -2
View File
@@ -1,13 +1,19 @@
'use client' 'use client'
import React, { useState } from 'react'; import React, { useState, useContext } from 'react';
import { Box, Button, Output } from "./modules"; import { Box, Button, Output } from "./modules";
import { calculate_click, downloadLP, downloadMPS } from "./scripts"; import { calculate_click, downloadLP, downloadMPS } from "./scripts";
import text from "./lang"; import text from "./lang";
import { spec } from 'node:test/reporters';
import { useRouter } from 'next/navigation';
import { LanguageContext } from './context/LanguageContext';
export default function Home() { export default function Home() {
const [language, setLanguage] = useState('eng'); const { language, setLanguage } = useContext(LanguageContext);
const [maxminOption, setMaxminOption] = useState('maximize'); const [maxminOption, setMaxminOption] = useState('maximize');
const [model] = useState('spec');
const router = useRouter();
const tr_hTitle = text(language, 'header_title'); const tr_hTitle = text(language, 'header_title');
const tr_hSubtitle = text(language, 'header_subtitle'); const tr_hSubtitle = text(language, 'header_subtitle');
@@ -25,11 +31,21 @@ export default function Home() {
const tr_calc_min = text(language, "minimize"); const tr_calc_min = text(language, "minimize");
const tr_calcButton = text(language, "buttonCalc"); const tr_calcButton = text(language, "buttonCalc");
const tr_boxExportMPS = text(language, "boxExportMPS"); const tr_boxExportMPS = text(language, "boxExportMPS");
const tr_GenProblems = text(language, 'GenProblem');
const tr_SpecProblems = text(language, 'SpecProblem');
const handleLanguageChange = (event: React.ChangeEvent<HTMLSelectElement>) => { const handleLanguageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setLanguage(event.target.value); setLanguage(event.target.value);
}; };
const changeModel = (event: React.ChangeEvent<HTMLSelectElement>) => {
const selectedModel = event.target.value;
if (selectedModel === 'gen') {
router.push('./glp');
}
};
const handleMaxMinChange = (event: React.ChangeEvent<HTMLSelectElement>) => { const handleMaxMinChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setMaxminOption(event.target.value); setMaxminOption(event.target.value);
}; };
@@ -49,6 +65,10 @@ export default function Home() {
<option value="ger">Deutsch</option> <option value="ger">Deutsch</option>
<option value="eng">English</option> <option value="eng">English</option>
</select> </select>
<select id="language_current" value={model} onChange={changeModel} className="dropdown-custom">
<option value="gen">{tr_GenProblems}</option>
<option value="spec">{tr_SpecProblems}</option>
</select>
</main> </main>
</div> </div>
</header> </header>