diff --git a/package.json b/package.json
index 0fdb08b..b609806 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"antd": "^3.21.3",
"axios": "^0.19.0",
"babel-plugin-import": "^1.12.0",
+ "color-interpolate": "^1.0.5",
"connected-react-router": "^6.5.2",
"deck.gl": "^7.2.2",
"electron-updater": "^4.1.2",
diff --git a/src/renderer/actions/inputEditor.js b/src/renderer/actions/inputEditor.js
index 747c0b6..8f31c61 100644
--- a/src/renderer/actions/inputEditor.js
+++ b/src/renderer/actions/inputEditor.js
@@ -15,6 +15,8 @@ export const REQUEST_MAPDATA = 'REQUEST_MAPDATA';
export const RECEIVE_MAPDATA = 'RECEIVE_MAPDATA';
export const SET_SELECTED = 'SET_SELECTED';
export const UPDATE_INPUTDATA = 'UPDATE_INPUTDATA';
+export const UPDATE_YEARSCHEDULE = 'UPDATE_YEARSCHEDULE';
+export const UPDATE_DAYSCHEDULE = 'UPDATE_DAYSCHEDULE';
export const DELETE_BUILDINGS = 'DELETE_BUILDINGS';
export const SAVE_INPUTDATA = 'SAVE_INPUTDATA';
export const SAVE_INPUTDATA_SUCCESS = 'SAVE_INPUTDATA_SUCCESS';
@@ -38,69 +40,69 @@ export const fetchInputData = () =>
export const saveChanges = () => (dispatch, getState) =>
// eslint-disable-next-line no-undef
new Promise((resolve, reject) => {
- const { tables, geojsons, crs } = getState().inputData;
+ const { tables, geojsons, crs, schedules } = getState().inputData;
dispatch(
httpAction({
url: '/inputs/all-inputs',
method: 'PUT',
type: SAVE_INPUTDATA,
- data: { tables, geojsons, crs },
+ data: { tables, geojsons, crs, schedules },
onSuccess: data => resolve(data),
onFailure: error => reject(error)
})
);
});
-export const fetchBuildingSchedule = buildings => (dispatch, getState) => {
- const toFetch = buildings.filter(
- building => !Object.keys(getState().inputData.schedules).includes(building)
+export const fetchBuildingSchedule = buildings => dispatch => {
+ dispatch({ type: REQUEST_BUILDINGSCHEDULE });
+ let errors = {};
+ const promises = buildings.map(building =>
+ axios
+ .get(`http://localhost:5050/api/inputs/building-schedule/${building}`)
+ .then(resp => {
+ return { [building]: resp.data };
+ })
+ .catch(error => {
+ errors[building] = error.response.data;
+ })
);
- if (toFetch.length) {
- dispatch({ type: REQUEST_BUILDINGSCHEDULE });
- let errors = {};
- const promises = buildings.map(building =>
- axios
- .get(`http://localhost:5050/api/inputs/building-schedule/${building}`)
- .then(resp => {
- return { [building]: resp.data };
- })
- .catch(error => {
- errors[building] = error.response.data;
- })
- );
- // eslint-disable-next-line no-undef
- return Promise.all(promises).then(values => {
- if (Object.keys(errors).length) {
- throw errors;
- } else {
- let out = {};
- for (const schedule of values) {
- const building = Object.keys(schedule)[0];
- out[building] = schedule[building];
- }
- dispatch({
- type: REQUEST_BUILDINGSCHEDULE_SUCCESS,
- payload: out
- });
- }
- });
- }
// eslint-disable-next-line no-undef
- return Promise.resolve();
+ return Promise.all(promises).then(values => {
+ if (Object.keys(errors).length) {
+ throw errors;
+ } else {
+ let out = {};
+ for (const schedule of values) {
+ const building = Object.keys(schedule)[0];
+ out[building] = schedule[building];
+ }
+ dispatch({
+ type: REQUEST_BUILDINGSCHEDULE_SUCCESS,
+ payload: out
+ });
+ }
+ });
};
-export const discardChanges = () => dispatch =>
+export const discardChanges = () => (dispatch, getState) =>
// eslint-disable-next-line no-undef
- new Promise((resolve, reject) => {
- dispatch(
- httpAction({
- url: '/inputs/all-inputs',
- type: DISCARD_INPUTDATA_CHANGES,
- onSuccess: data => resolve(data),
- onFailure: error => reject(error)
- })
- );
- });
+ Promise.all([
+ // eslint-disable-next-line no-undef
+ new Promise((resolve, reject) => {
+ dispatch(
+ httpAction({
+ url: '/inputs/all-inputs',
+ type: DISCARD_INPUTDATA_CHANGES,
+ onSuccess: data => resolve(data),
+ onFailure: error => reject(error)
+ })
+ );
+ }),
+ fetchBuildingSchedule(Object.keys(getState().inputData.schedules))(
+ dispatch,
+ getState
+ )
+ ]);
export const updateInputData = (
table = '',
@@ -111,6 +113,22 @@ export const updateInputData = (
payload: { table, buildings, properties }
});
+export const updateYearSchedule = (buildings = [], month = '', value = 0) => ({
+ type: UPDATE_YEARSCHEDULE,
+ payload: { buildings, month, value }
+});
+
+export const updateDaySchedule = (
+ buildings = [],
+ tab = '',
+ day = '',
+ hour = 0,
+ value = ''
+) => ({
+ type: UPDATE_DAYSCHEDULE,
+ payload: { buildings, tab, day, hour, value }
+});
+
export const deleteBuildings = (buildings = []) => ({
type: DELETE_BUILDINGS,
payload: { buildings }
diff --git a/src/renderer/components/InputEditor/InputEditor.js b/src/renderer/components/InputEditor/InputEditor.js
index d3b2a14..f67db0b 100644
--- a/src/renderer/components/InputEditor/InputEditor.js
+++ b/src/renderer/components/InputEditor/InputEditor.js
@@ -8,7 +8,6 @@ import CenterSpinner from '../HomePage/CenterSpinner';
import NavigationPrompt from './NavigationPrompt';
import { withErrorBoundary } from '../../utils/ErrorBoundary';
import './InputEditor.css';
-import ScheduleEditor from './ScheduleEditor';
const MAP_STYLE = {
height: '500px',
@@ -78,7 +77,7 @@ const InputTable = () => {
{TabPanes}
-
-
-
- {buildings.includes(selected[0]) ? (
- Object.keys(errors).length ? (
-
- ERRORS FOUND:
- {Object.keys(errors).map(building => (
-
{`${building}: ${errors[building].message}`}
- ))}
-
- ) : (
-
-
-
-
-
-
-
-
-
-
-
- )
- ) : (
-
- {selected.length
- ? 'Selected buildings do not have a schedule'
- : 'No building selected'}
-
- )}
-
-
+
+
-
+
+
+ {buildings.includes(selected[0]) ? (
+ Object.keys(errors).length ? (
+
+ ERRORS FOUND:
+ {Object.keys(errors).map(building => (
+
{`${building}: ${errors[building].message}`}
+ ))}
+
+ ) : (
+
+
+
+
+
+
+
+
+
+
+
+ )
+ ) : (
+
+ {selected.length
+ ? 'Selected buildings do not have a schedule'
+ : 'No building selected'}
+
+ )}
+
+
+
);
};
-const DataTable = ({ selected, tab }) => {
- const { schedules } = useSelector(state => state.inputData);
+const DataTable = ({ selected, tab, schedules, loading }) => {
const tabulator = useRef(null);
const divRef = useRef(null);
-
- const parseData = (building, tab) => {
- const buildingSchedule = schedules[building];
- if (!buildingSchedule || !tab) return [];
- const days = buildingSchedule.SCHEDULES[tab];
- return Object.keys(days).map(day => ({ DAY: day, ...days[day] }));
- };
+ const tooltipsRef = useRef({ selected, schedules, tab });
+ const dispatch = useDispatch();
useEffect(() => {
tabulator.current = new Tabulator(divRef.current, {
@@ -140,42 +167,119 @@ const DataTable = ({ selected, tab }) => {
columns: [
{ title: 'DAY \\ HOUR', field: 'DAY', width: 100, headerSort: false },
...[...Array(24).keys()].map(i => ({
- title: i.toString(),
+ title: (i + 1).toString(),
field: i.toString(),
- headerSort: false
+ headerSort: false,
+ editor: 'input',
+ // Hack to allow editing when double clicking
+ cellDblClick: () => {},
+ formatter: cell => {
+ formatCellStyle(cell);
+ return cell.getValue();
+ }
}))
],
+ validationFailed: cell => {
+ cell.cancelEdit();
+ },
+ cellEdited: cell => {
+ formatCellStyle(cell);
+ dispatch(
+ updateDaySchedule(
+ tooltipsRef.current.selected,
+ tooltipsRef.current.tab,
+ cell.getData().DAY,
+ Number(cell.getField()),
+ cell.getValue()
+ )
+ );
+ },
layoutColumnsOnNewData: true,
- layout: 'fitDataFill'
+ layout: 'fitDataFill',
+ tooltips: cell => {
+ if (cell.getValue() == 'DIFF') {
+ let out = '';
+ for (const building of tooltipsRef.current.selected.sort()) {
+ out += `${building} - ${
+ tooltipsRef.current.schedules[building].SCHEDULES[
+ tooltipsRef.current.tab
+ ][cell.getData().DAY][cell.getField()]
+ }\n`;
+ }
+ return out;
+ }
+ }
});
}, []);
useEffect(() => {
- tabulator.current && tabulator.current.setData(parseData(selected[0], tab));
+ tooltipsRef.current = { selected, schedules, tab };
+ tab &&
+ selected.every(building => Object.keys(schedules).includes(building)) &&
+ tabulator.current.updateOrAddData(parseData(schedules, selected, tab));
}, [selected, schedules, tab]);
+ useEffect(() => {
+ selected.every(building => Object.keys(schedules).includes(building)) &&
+ tabulator.current.redraw(true);
+ }, [selected, tab, loading]);
+
return
;
};
-const YearTable = ({ selected }) => {
- const { schedules } = useSelector(state => state.inputData);
+const YearTable = ({ selected, schedules, loading }) => {
const tabulator = useRef(null);
const divRef = useRef(null);
+ const tooltipsRef = useRef({ selected, schedules });
+ const dispatch = useDispatch();
useEffect(() => {
- console.log('render year');
tabulator.current = new Tabulator(divRef.current, {
- data: parseYearData(schedules, selected),
+ data: [],
index: 'name',
columns: [
{ title: '', field: 'name', headerSort: false },
...[...Array(12).keys()].map(i => ({
title: months_short[i],
field: i.toString(),
- headerSort: false
+ headerSort: false,
+ editor: 'input',
+ validator: ['max:1', 'min:0'],
+ // Hack to allow editing when double clicking
+ cellDblClick: () => {},
+ formatter: cell => {
+ formatCellStyle(cell);
+ return cell.getValue();
+ }
}))
],
- layout: 'fitDataFill'
+ validationFailed: cell => {
+ cell.cancelEdit();
+ },
+ cellEdited: cell => {
+ formatCellStyle(cell);
+ dispatch(
+ updateYearSchedule(
+ tooltipsRef.current.selected,
+ cell.getField(),
+ cell.getValue()
+ )
+ );
+ },
+ layout: 'fitDataFill',
+ tooltips: cell => {
+ if (cell.getValue() == 'DIFF') {
+ let out = '';
+ for (const building of tooltipsRef.current.selected.sort()) {
+ out += `${building} - ${
+ tooltipsRef.current.schedules[building].MONTHLY_MULTIPLIER[
+ cell.getField()
+ ]
+ }\n`;
+ }
+ return out;
+ }
+ }
});
const redrawTable = () => {
@@ -189,15 +293,20 @@ const YearTable = ({ selected }) => {
}, []);
useEffect(() => {
- tabulator.current &&
- tabulator.current.setData(parseYearData(schedules, selected));
+ tooltipsRef.current = { selected, schedules };
+ selected.every(building => Object.keys(schedules).includes(building)) &&
+ tabulator.current.updateOrAddData(parseYearData(schedules, selected));
}, [schedules, selected]);
+ useEffect(() => {
+ selected.every(building => Object.keys(schedules).includes(building)) &&
+ tabulator.current.redraw(true);
+ }, [selected, loading]);
+
return
;
};
-const ScheduleTab = ({ tab, setTab }) => {
- const { schedules } = useSelector(state => state.inputData);
+const ScheduleTab = ({ tab, setTab, schedules }) => {
const TabPanes = getScheduleTypes(schedules).map(schedule => (
));
@@ -214,57 +323,52 @@ const ScheduleTab = ({ tab, setTab }) => {
);
};
-const TableButtons = ({ selected, tabulator }) => {
- const [filterToggle, setFilterToggle] = useState(false);
- const dispatch = useDispatch();
-
- const selectAll = () => {
- dispatch(setSelected(tabulator.current.getData().map(data => data.Name)));
- };
-
- const filterSelected = () => {
- if (filterToggle) {
- tabulator.current.clearFilter();
- } else {
- tabulator.current.setFilter('Name', 'in', selected);
+const formatCellStyle = cell => {
+ const states = ['OFF', 'SETBACK', 'SETPOINT'];
+ const value = cell.getValue();
+ if (value == 'DIFF') {
+ cell.getElement().style.fontWeight = 'bold';
+ cell.getElement().style.fontStyle = 'italic';
+ } else {
+ if (!isNaN(value)) {
+ cell.getElement().style.backgroundColor = addRGBAlpha(
+ colormap(value),
+ 0.5
+ );
+ } else if (states.includes(value)) {
+ cell.getElement().style.backgroundColor = addRGBAlpha(
+ colormap(states.indexOf(value) / (states.length - 1)),
+ 0.5
+ );
}
- tabulator.current.redraw();
- setFilterToggle(oldValue => !oldValue);
- };
-
- const clearSelected = () => {
- dispatch(setSelected([]));
- };
+ cell.getElement().style.fontWeight = 'normal';
+ cell.getElement().style.fontStyle = 'normal';
+ }
+};
- return (
-
-
-
- {selected.length ? (
-
- ) : null}
-
- );
+const addRGBAlpha = (color, opacity) => {
+ const rgb = color.replace(/[^\d,]/g, '').split(',');
+ return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${opacity}`;
};
const parseYearData = (schedules, selected) => {
- if (
- !selected.length ||
- !selected.every(building => Object.keys(schedules).includes(building))
- )
- return [];
+ let out = [];
for (const building of selected) {
const buildingSchedule = schedules[building].MONTHLY_MULTIPLIER;
+ out = diffArray(out, buildingSchedule);
}
+ return [{ name: 'MONTHLY_MULTIPLIER', ...out }];
+};
- return [
- { name: 'MONTHLY_MULTIPLIER', ...schedules[selected[0]].MONTHLY_MULTIPLIER }
- ];
+const parseData = (schedules, selected, tab) => {
+ let out = {};
+ for (const building of selected) {
+ const days = schedules[building].SCHEDULES[tab];
+ for (const day of Object.keys(days)) {
+ out[day] = diffArray(out[day], days[day]);
+ }
+ }
+ return Object.keys(out).map(day => ({ DAY: day, ...out[day] }));
};
const getScheduleTypes = schedules => {
@@ -278,4 +382,12 @@ const getScheduleTypes = schedules => {
}
};
+const diffArray = (first, second) => {
+ if (typeof first == 'undefined' || !first.length) return second;
+ return first.map((value, index) => {
+ if (value == second[index]) return value;
+ return 'DIFF';
+ });
+};
+
export default ScheduleEditor;
diff --git a/src/renderer/components/InputEditor/Table.js b/src/renderer/components/InputEditor/Table.js
index ec919a8..f7100bb 100644
--- a/src/renderer/components/InputEditor/Table.js
+++ b/src/renderer/components/InputEditor/Table.js
@@ -1,7 +1,8 @@
import React, { useEffect, useRef, useState } from 'react';
+import ReactDOM from 'react-dom';
import { useSelector, useDispatch } from 'react-redux';
import { Link } from 'react-router-dom';
-import { Card, Button, Modal, message } from 'antd';
+import { Card, Button, Modal, message, Tooltip, Icon } from 'antd';
import {
setSelected,
updateInputData,
@@ -13,130 +14,50 @@ import EditSelectedModal from './EditSelectedModal';
import routes from '../../constants/routes';
import Tabulator from 'tabulator-tables';
import 'tabulator-tables/dist/css/tabulator.min.css';
+import ScheduleEditor from './ScheduleEditor';
-const useTableData = tab => {
- const tables = useSelector(state => state.inputData.tables);
- const columns = useSelector(state => state.inputData.columns);
-
- const [data, setData] = useState([]);
- const [columnDef, setColumnDef] = useState([]);
-
- const dispatch = useDispatch();
-
- const selectRow = (e, cell) => {
- const row = cell.getRow();
- const selectedRows = cell
- .getTable()
- .getSelectedData()
- .map(data => data.Name);
- if (cell.getRow().isSelected()) {
- dispatch(
- setSelected(selectedRows.filter(name => name !== row.getIndex()))
- );
- } else {
- dispatch(setSelected([...selectedRows, row.getIndex()]));
- }
- };
-
- const getData = () =>
- Object.keys(tables[tab])
- .sort((a, b) => (a > b ? 1 : -1))
- .map(row => ({
- Name: row,
- ...tables[tab][row]
- }));
-
- const getColumnDef = () =>
- Object.keys(columns[tab]).map(column => {
- let columnDef = { title: column, field: column };
- if (column === 'Name') {
- columnDef.frozen = true;
- columnDef.cellClick = selectRow;
- } else if (column !== 'REFERENCE') {
- columnDef.editor = 'input';
- columnDef.validator =
- columns[tab][column].type === 'str' ? 'string' : 'numeric';
- columnDef.minWidth = 100;
- // Hack to allow editing when double clicking
- columnDef.cellDblClick = () => {};
- }
- return columnDef;
- });
-
- useEffect(() => {
- setColumnDef(getColumnDef());
- setData(getData());
- }, [tab]);
-
- useEffect(() => {
- setData(getData());
- }, [tables[tab]]);
+const Table = ({ tab }) => {
+ const { selected, changes, schedules } = useSelector(
+ state => state.inputData
+ );
+ const tabulator = useRef(null);
- return [data, columnDef];
+ return (
+
+
+
+
+ }
+ extra={
+
+ }
+ >
+ {tab == 'schedules' ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
};
-const Table = ({ tab }) => {
- const [data, columnDef] = useTableData(tab);
- const { selected, changes } = useSelector(state => state.inputData);
+const InputEditorButtons = ({ changes }) => {
+ const dispatch = useDispatch();
const noChanges =
!Object.keys(changes.update).length && !Object.keys(changes.delete).length;
- const dispatch = useDispatch();
- const tableRef = useRef(tab);
- const tabulator = useRef(null);
- const divRef = useRef(null);
-
- useEffect(() => {
- tabulator.current = new Tabulator(divRef.current, {
- data: data,
- index: 'Name',
- columns: columnDef,
- layout: 'fitDataFill',
- height: '300px',
- validationFailed: cell => {
- cell.cancelEdit();
- },
- cellEdited: cell => {
- dispatch(
- updateInputData(
- tableRef.current,
- [cell.getData()['Name']],
- [{ property: cell.getField(), value: cell.getValue() }]
- )
- );
- },
- placeholder: '
No matching records found.
'
- });
- }, []);
-
- // Keep reference of current table name
- useEffect(() => {
- tableRef.current = tab;
- }, [tab]);
-
- useEffect(() => {
- if (tabulator.current) {
- tabulator.current.setData([]);
- tabulator.current.setColumns(columnDef);
- }
- }, [columnDef]);
-
- useEffect(() => {
- if (tabulator.current) {
- if (!tabulator.current.getData().length) {
- tabulator.current.setData(data);
- tabulator.current.selectRow(selected);
- } else tabulator.current.updateData(data);
- }
- }, [data]);
-
- useEffect(() => {
- if (tabulator.current) {
- tabulator.current.deselectRow();
- tabulator.current.selectRow(selected);
- tabulator.current.getFilters().length &&
- tabulator.current.setFilter('Name', 'in', selected);
- }
- }, [selected]);
const _saveChanges = () => {
Modal.confirm({
@@ -188,6 +109,9 @@ const Table = ({ tab }) => {
await dispatch(discardChanges())
.then(data => {
console.log(data);
+ message.config({
+ top: 120
+ });
message.info('Unsaved changes have been discarded.');
})
.catch(error => {
@@ -200,28 +124,6 @@ const Table = ({ tab }) => {
return (
-
- }
- >
-
- {!data.length ? (
-
- Input file could not be found. You can create the file using
- {tab == 'surroundings' ? (
-
- {' surroundings-helper '}
-
- ) : (
- {' data-helper '}
- )}
- tool.
-
- ) : null}
-
@@ -273,7 +175,7 @@ const ChangesSummary = ({ changes }) => {
property => (
{property}
- {`: ${changes.update[table][building][property].oldValue}
+ {` : ${changes.update[table][building][property].oldValue}
→
${changes.update[table][building][property].newValue}`}
@@ -290,7 +192,7 @@ const ChangesSummary = ({ changes }) => {
);
};
-const TableButtons = ({ selected, tabulator, table }) => {
+const TableButtons = ({ selected, tabulator, tab }) => {
const [filterToggle, setFilterToggle] = useState(false);
const [selectedInTable, setSelectedInTable] = useState(true);
const [modalVisible, setModalVisible] = useState(false);
@@ -298,9 +200,9 @@ const TableButtons = ({ selected, tabulator, table }) => {
const dispatch = useDispatch();
useEffect(() => {
- const tableData = data[table] || {};
+ const tableData = data[tab] || tab == 'schedules' ? data['zone'] : {};
setSelectedInTable(Object.keys(tableData).includes(selected[0]));
- }, [table, selected]);
+ }, [tab, selected]);
const selectAll = () => {
dispatch(setSelected(tabulator.current.getData().map(data => data.Name)));
@@ -312,6 +214,7 @@ const TableButtons = ({ selected, tabulator, table }) => {
} else {
tabulator.current.setFilter('Name', 'in', selected);
}
+ tabulator.current.redraw();
setFilterToggle(oldValue => !oldValue);
};
@@ -357,7 +260,9 @@ const TableButtons = ({ selected, tabulator, table }) => {
{selectedInTable ? (
-
+ {tab != 'schedules' && (
+
+ )}
);
};
+const TableEditor = ({ tab, selected, tabulator }) => {
+ const [data, columnDef] = useTableData(tab);
+ const dispatch = useDispatch();
+ const divRef = useRef(null);
+ const tableRef = useRef(tab);
+
+ useEffect(() => {
+ const filtered = tabulator.current && tabulator.current.getFilters().length;
+ tabulator.current = new Tabulator(divRef.current, {
+ data: data,
+ index: 'Name',
+ columns: columnDef.columns,
+ layout: 'fitDataFill',
+ height: '300px',
+ validationFailed: cell => {
+ cell.cancelEdit();
+ },
+ cellEdited: cell => {
+ dispatch(
+ updateInputData(
+ tableRef.current,
+ [cell.getData()['Name']],
+ [{ property: cell.getField(), value: cell.getValue() }]
+ )
+ );
+ },
+ placeholder: 'No matching records found.
'
+ });
+ filtered && tabulator.current.setFilter('Name', 'in', selected);
+ }, []);
+
+ // Keep reference of current table name
+ useEffect(() => {
+ tableRef.current = tab;
+ }, [tab]);
+
+ useEffect(() => {
+ if (tabulator.current) {
+ tabulator.current.setData([]);
+ tabulator.current.setColumns(columnDef.columns);
+ }
+ }, [columnDef]);
+
+ useEffect(() => {
+ if (tabulator.current) {
+ if (!tabulator.current.getData().length) {
+ tabulator.current.setData(data);
+ tabulator.current.selectRow(selected);
+ // Add tooltips to column headers on new data
+ document
+ .querySelectorAll('.tabulator-col-content')
+ .forEach((col, index) => {
+ const { description, unit } = columnDef.description[index];
+ ReactDOM.render(
+