diff --git a/package-lock.json b/package-lock.json index 3c5207ea..94b3eff6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,9 @@ "dependencies": { "@electron/remote": "^2.0.8", "babel-standalone": "^6.26.0", + "chart.js": "^4.3.0", + "dayjs": "^1.11.7", + "classnames": "^2.3.2", "decompress": "^4.2.1", "del": "^6.1.0", "electron-dl": "^3.3.1", @@ -19,6 +22,7 @@ "nugget": "^2.0.2", "python-shell": "^3.0.1", "react": "^18.1.0", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.1.0", "uuid": "^9.0.0", "vex-js": "^4.1.0" @@ -555,6 +559,11 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@malept/cross-spawn-promise": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", @@ -1437,6 +1446,17 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.3.0.tgz", + "integrity": "sha512-ynG0E79xGfMaV2xAHdbhwiPLczxnNNnasrmPEXriXsPJGjmhOBYzFVEsB65w2qMDz+CaBJJuJD0inE/ab/h36g==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=7" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -1457,6 +1477,11 @@ "resolved": "https://registry.npmjs.org/classlist-polyfill/-/classlist-polyfill-1.2.0.tgz", "integrity": "sha512-GzIjNdcEtH4ieA2S8NmrSxv7DfEV5fmixQeyTmqmRmRJPGpRBaSnA2a0VrCjyT8iW8JjEdMbKzDotAJf+ajgaQ==" }, + "node_modules/classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -1742,6 +1767,11 @@ "node": ">=0.10" } }, + "node_modules/dayjs": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" + }, "node_modules/debounce-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", @@ -3482,9 +3512,9 @@ "dev": true }, "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, "node_modules/http-proxy-agent": { "version": "5.0.0", @@ -5216,6 +5246,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -7152,6 +7191,11 @@ "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", "dev": true }, + "@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "@malept/cross-spawn-promise": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz", @@ -7481,9 +7525,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "requires": { - "ajv": "^8.0.0" - } + "requires": {} }, "ansi-escapes": { "version": "4.3.2", @@ -7842,6 +7884,14 @@ "supports-color": "^7.1.0" } }, + "chart.js": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.3.0.tgz", + "integrity": "sha512-ynG0E79xGfMaV2xAHdbhwiPLczxnNNnasrmPEXriXsPJGjmhOBYzFVEsB65w2qMDz+CaBJJuJD0inE/ab/h36g==", + "requires": { + "@kurkle/color": "^0.3.0" + } + }, "chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -7859,6 +7909,11 @@ "resolved": "https://registry.npmjs.org/classlist-polyfill/-/classlist-polyfill-1.2.0.tgz", "integrity": "sha512-GzIjNdcEtH4ieA2S8NmrSxv7DfEV5fmixQeyTmqmRmRJPGpRBaSnA2a0VrCjyT8iW8JjEdMbKzDotAJf+ajgaQ==" }, + "classnames": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz", + "integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==" + }, "clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -8046,6 +8101,11 @@ "assert-plus": "^1.0.0" } }, + "dayjs": { + "version": "1.11.7", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", + "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==" + }, "debounce-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", @@ -9393,9 +9453,9 @@ "dev": true }, "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, "http-proxy-agent": { "version": "5.0.0", @@ -10658,6 +10718,12 @@ "loose-envify": "^1.1.0" } }, + "react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "requires": {} + }, "react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", diff --git a/package.json b/package.json index be7c2490..a125ded4 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,9 @@ "dependencies": { "@electron/remote": "^2.0.8", "babel-standalone": "^6.26.0", + "chart.js": "^4.3.0", + "dayjs": "^1.11.7", + "classnames": "^2.3.2", "decompress": "^4.2.1", "del": "^6.1.0", "electron-dl": "^3.3.1", @@ -29,6 +32,7 @@ "nugget": "^2.0.2", "python-shell": "^3.0.1", "react": "^18.1.0", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.1.0", "uuid": "^9.0.0", "vex-js": "^4.1.0" diff --git a/src/enums.js b/src/enums.js new file mode 100644 index 00000000..07d632c9 --- /dev/null +++ b/src/enums.js @@ -0,0 +1,6 @@ +const SCENARIO_STATUS_STATE = { + STARTING: 'starting', + PREPARING: 'preparing', + RUNNING: 'running', + FINISHED: 'finished' +} \ No newline at end of file diff --git a/src/renderer/components/App.css b/src/renderer/components/App.css index 37f246e2..55b9ba43 100644 --- a/src/renderer/components/App.css +++ b/src/renderer/components/App.css @@ -88,6 +88,7 @@ hr { .App__header { display: flex; align-items: center; + justify-content: flex-start; height: 70px; background-color: #007AC9; background-image: url('pylvasdiagrammi_valkoinen.png'), url('hsl_logo_valkoinen.png'); @@ -110,3 +111,12 @@ hr { .App__body { } + +.header-documentation-link { + margin-top: .4rem; + margin-left: 2rem; +} + +.header-documentation-link:hover { + cursor: pointer; +} \ No newline at end of file diff --git a/src/renderer/components/App.jsx b/src/renderer/components/App.jsx index ecc6e441..5cd09c0e 100644 --- a/src/renderer/components/App.jsx +++ b/src/renderer/components/App.jsx @@ -3,7 +3,7 @@ import Store from 'electron-store'; import fs from "fs"; const homedir = require('os').homedir(); -const {ipcRenderer} = require('electron'); +const {ipcRenderer, shell} = require('electron'); const {execSync} = require('child_process'); const path = require('path'); @@ -180,6 +180,7 @@ const App = ({helmetUIVersion, versions, searchEMMEPython}) => { Helmet 4.1   {`UI ${helmetUIVersion}`} + shell.openExternal("https://hsldevcom.github.io/helmet-docs/")}> {/* HELMET Project -specific content, including runtime- & per-scenario-settings */} diff --git a/src/renderer/components/HelmetProject/HelmetProject.jsx b/src/renderer/components/HelmetProject/HelmetProject.jsx index beaaa130..d8e970b1 100644 --- a/src/renderer/components/HelmetProject/HelmetProject.jsx +++ b/src/renderer/components/HelmetProject/HelmetProject.jsx @@ -31,6 +31,9 @@ const HelmetProject = ({ const [statusState, setStatusState] = useState(null); const [statusLogfilePath, setStatusLogfilePath] = useState(null); const [statusReadyScenariosLogfiles, setStatusReadyScenariosLogfiles] = useState([]); // [{name: .., logfile: ..}] + const [statusRunStartTime, setStatusRunStartTime] = useState(null); //Updated when receiving "starting" message + const [statusRunFinishTime, setStatusRunFinishTime] = useState(null); //Updated when receiving "finished" message + const [demandConvergenceArray, setDemandConvergenceArray] = useState([]); // Add convergence values to array every iteration // Cost-Benefit Analysis (CBA) controls const [cbaOptions, setCbaOptions] = useState({}); @@ -127,6 +130,13 @@ const HelmetProject = ({ use_fixed_transit_cost: false, end_assignment_only: false, iterations: 15, + overriddenProjectSettings: { + emmePythonPath: null, + helmetScriptsPath: null, + projectPath: null, + basedataPath: null, + resultsPath: null, + }, }; // Create the new scenario in "scenarios" array first setScenarios(scenarios.concat(newScenario)); @@ -233,13 +243,14 @@ const HelmetProject = ({ ipcRenderer.send( 'message-from-ui-to-run-scenarios', scenariosToRun.map((s) => { - // Run parameters per each run (enrich with global settings' paths to EMME python & HELMET model system) + // Run parameters per each run (enrich with global settings' paths to EMME python & HELMET model system + return { ...s, - emme_python_path: emmePythonPath, - helmet_scripts_path: helmetScriptsPath, - base_data_folder_path: basedataPath, - results_data_folder_path: resultsPath, + emme_python_path: s.overriddenProjectSettings.emmePythonPath !== null && s.overriddenProjectSettings.emmePythonPath !== emmePythonPath ? s.overriddenProjectSettings.emmePythonPath : emmePythonPath, + helmet_scripts_path: s.overriddenProjectSettings.helmetScriptsPath !== null && s.overriddenProjectSettings.helmetScriptsPath !== helmetScriptsPath ? s.overriddenProjectSettings.helmetScriptsPath : helmetScriptsPath, + base_data_folder_path: s.overriddenProjectSettings.basedataPath !== null && s.overriddenProjectSettings.basedataPath !== basedataPath ? s.overriddenProjectSettings.basedataPath : basedataPath, + results_data_folder_path: s.overriddenProjectSettings.resultsPath !== null && s.overriddenProjectSettings.resultsPath !== resultsPath ? s.overriddenProjectSettings.resultsPath : resultsPath, log_level: 'DEBUG', } }) @@ -293,6 +304,11 @@ const HelmetProject = ({ }); }; + const parseDemandConvergenceLogMessage = (message) => { + const stringMsgArray = message.split(' '); + return { iteration: stringMsgArray[stringMsgArray.length - 3], value: stringMsgArray[stringMsgArray.length - 1]}; + }; + // Electron IPC event listeners const onLoggableEvent = (event, args) => { setLogContents(previousLog => [...previousLog, args]); @@ -304,11 +320,26 @@ const HelmetProject = ({ setStatusState(args.status['state']); setStatusLogfilePath(args.status['log']); - if (args.status.state === 'finished') { + if (args.status.state === SCENARIO_STATUS_STATE.FINISHED) { setStatusReadyScenariosLogfiles(statusReadyScenariosLogfiles.concat({ name: args.status.name, - logfile: args.status.log + logfile: args.status.log, + resultsPath: args.status.log.match(new RegExp('((?:[^/]*/)*)(.*)')) })) + setStatusRunFinishTime(args.time); + } + + if (args.status.state === SCENARIO_STATUS_STATE.STARTING) { + setStatusRunStartTime(args.time); + setStatusRunFinishTime(args.time); + setDemandConvergenceArray([]); + setStatusIterationsTotal(0); + } + } + if(args.level === 'INFO') { + if(args.message.includes('Demand model convergence in')) { + const currentDemandConvergenceValueAndIteration = parseDemandConvergenceLogMessage(args.message); + setDemandConvergenceArray(demandConvergenceArray => [...demandConvergenceArray, currentDemandConvergenceValueAndIteration ]); } } }; @@ -356,10 +387,14 @@ const HelmetProject = ({ handleClickScenarioToActive={_handleClickScenarioToActive} handleClickNewScenario={_handleClickNewScenario} handleClickStartStop={_handleClickStartStop} + duplicateScenario={duplicateScenario} statusIterationsTotal={statusIterationsTotal} statusIterationsCompleted={statusIterationsCompleted} statusReadyScenariosLogfiles={statusReadyScenariosLogfiles} - duplicateScenario={duplicateScenario} + statusRunStartTime={statusRunStartTime} + statusRunFinishTime={statusRunFinishTime} + statusState={statusState} + demandConvergenceArray={demandConvergenceArray} /> setOpenScenarioID(null)} existingOtherNames={scenarios.filter(s => s.id !== openScenarioID).map(s => s.name)} + inheritedGlobalProjectSettings={{ + emmePythonPath, + helmetScriptsPath, + projectPath, + basedataPath, + resultsPath + }} /> : "" @@ -394,4 +436,4 @@ const HelmetProject = ({ ) -}; +}; \ No newline at end of file diff --git a/src/renderer/components/HelmetProject/HelmetScenario/HelmetScenario.css b/src/renderer/components/HelmetProject/HelmetScenario/HelmetScenario.css index c4546f55..efb01774 100644 --- a/src/renderer/components/HelmetProject/HelmetScenario/HelmetScenario.css +++ b/src/renderer/components/HelmetProject/HelmetScenario/HelmetScenario.css @@ -137,4 +137,44 @@ span.Scenario__inline { .Scenario__radio-input { margin-right: 2em; +} + +.override-input { + visibility: hidden; +} + +.override-setting { + padding-left: 0px !important; +} + +.override-file-select-input { + width: 500px; +} + +.override-is-default { + font-style: italic; + color: gray; +} + +.inline-element { + display: inline !important; +} + +.override-reset-button { + width: fit-content; + height: fit-content; + border: none; + margin-left: 30px; +} + +.override-reset-icon { + margin-bottom: -15px; +} + +.override-setting-toggle { + margin-bottom: 20px; +} + +.override-setting-divider { + margin-top: 20px; } \ No newline at end of file diff --git a/src/renderer/components/HelmetProject/HelmetScenario/HelmetScenario.jsx b/src/renderer/components/HelmetProject/HelmetScenario/HelmetScenario.jsx index a06952ca..7e921fbe 100644 --- a/src/renderer/components/HelmetProject/HelmetScenario/HelmetScenario.jsx +++ b/src/renderer/components/HelmetProject/HelmetScenario/HelmetScenario.jsx @@ -1,9 +1,9 @@ import React, {useState} from 'react'; import path from 'path'; -import { isNull } from 'util'; const {dialog} = require('@electron/remote'); +const classNames = require('classnames'); -const HelmetScenario = ({projectPath, scenario, updateScenario, closeScenario, existingOtherNames}) => { +const HelmetScenario = ({projectPath, scenario, updateScenario, closeScenario, existingOtherNames, inheritedGlobalProjectSettings}) => { const [nameError, setNameError] = useState(""); @@ -225,6 +225,174 @@ const HelmetScenario = ({projectPath, scenario, updateScenario, closeScenario, e –{parseInt(scenario.first_matrix_id == null ? 100 : scenario.first_matrix_id) + 299} +
+
+

Skenaariokohtaiset yliajot

+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
) diff --git a/src/renderer/components/HelmetProject/RunLog/RunLog.css b/src/renderer/components/HelmetProject/RunLog/RunLog.css index 43177980..55cee8a1 100644 --- a/src/renderer/components/HelmetProject/RunLog/RunLog.css +++ b/src/renderer/components/HelmetProject/RunLog/RunLog.css @@ -39,12 +39,14 @@ .Log__header-controls { display: flex; - justify-content: space-between; + justify-content: space-around; + align-content: center; + width: 100%; } .Log__header-control { - margin-right: 10px; - width: 100%; + max-width: 25%; + margin: .25rem; } .Log__header-control--on { background-color: #007AC9; diff --git a/src/renderer/components/HelmetProject/RunLog/RunLog.jsx b/src/renderer/components/HelmetProject/RunLog/RunLog.jsx index 62bc80b7..e85e13d6 100644 --- a/src/renderer/components/HelmetProject/RunLog/RunLog.jsx +++ b/src/renderer/components/HelmetProject/RunLog/RunLog.jsx @@ -4,6 +4,7 @@ const RunLog = ({isScenarioRunning, entries, closeRunLog}) => { const [showUIEVENT, setShowUIEVENT] = useState(true); const [showINFO, setShowINFO] = useState(true); + const [showWARN, setShowWARN] = useState(true); const [showERROR, setShowERROR] = useState(true); const [showDEBUG, setShowDEBUG] = useState(false); @@ -33,6 +34,11 @@ const RunLog = ({isScenarioRunning, entries, closeRunLog}) => { > ERROR + +   + +   + Ajoaika: { dayjs.duration(dayjs(statusRunFinishTime).diff(dayjs(statusRunStartTime))).format('HH[h]:mm[m]:ss[s]') } +

+ ) })} ); diff --git a/src/renderer/components/HelmetProject/Runtime/Runtime.jsx b/src/renderer/components/HelmetProject/Runtime/Runtime.jsx index 2178b181..d7dd2a42 100644 --- a/src/renderer/components/HelmetProject/Runtime/Runtime.jsx +++ b/src/renderer/components/HelmetProject/Runtime/Runtime.jsx @@ -6,7 +6,7 @@ const Runtime = ({ reloadScenarios, handleClickScenarioToActive, handleClickNewScenario, statusIterationsTotal, statusIterationsCompleted, statusReadyScenariosLogfiles, - handleClickStartStop, duplicateScenario + handleClickStartStop, statusRunStartTime, statusRunFinishTime, statusState, demandConvergenceArray, duplicateScenario }) => { return (
@@ -94,6 +94,10 @@ const Runtime = ({ statusIterationsTotal={statusIterationsTotal} statusIterationsCompleted={statusIterationsCompleted} statusReadyScenariosLogfiles={statusReadyScenariosLogfiles} + statusRunStartTime={statusRunStartTime} + statusRunFinishTime={statusRunFinishTime} + statusState={statusState} + demandConvergenceArray={demandConvergenceArray} />