├── src ├── util │ ├── GeojsonToEpanet │ │ ├── testData │ │ │ └── .gitkeep │ │ ├── controls.ts │ │ └── test.ts │ ├── LiveDataReader │ │ ├── testData │ │ │ └── .gitkeep │ │ ├── index.ts │ │ └── test.ts │ ├── SubnetworkTrace │ │ ├── testData │ │ │ └── .gitkeep │ │ ├── test.ts │ │ └── index.ts │ ├── WebWorker │ │ ├── worker-loader.d.ts │ │ ├── EPANetWorker.ts │ │ └── index.ts │ ├── EpanetBinary │ │ ├── test.ts │ │ ├── testData │ │ │ └── out.js │ │ └── index.ts │ └── reproject │ │ ├── index.ts │ │ └── test.ts ├── interfaces │ ├── CalibrationActions.ts │ └── ModelFeatureCollection.ts ├── mapstyles │ ├── water │ │ ├── selectedMain.ts │ │ ├── main.ts │ │ ├── meter.ts │ │ ├── calibrationActionMain.ts │ │ ├── hydrant.ts │ │ ├── calibrationAction.ts │ │ ├── fixedhead.ts │ │ ├── transferNode.ts │ │ ├── liveData.ts │ │ ├── calibrationActionLabel.ts │ │ ├── valve.ts │ │ └── waterIcons.ts │ ├── base │ │ └── blank.json │ └── index.ts ├── index.css ├── components │ ├── MapView │ │ ├── index.css │ │ └── index.tsx │ ├── CalibrateTab │ │ └── index.tsx │ ├── SubModelTab │ │ └── index.tsx │ ├── ErrorBoundary │ │ └── index.tsx │ ├── Landing │ │ ├── index.css │ │ └── index.tsx │ ├── CalibrationActions │ │ └── index.tsx │ ├── ModelInfo │ │ ├── index.tsx │ │ └── index.css │ ├── AboutTab │ │ └── index.tsx │ ├── CenteredTabs │ │ └── index.tsx │ ├── TimeSeriesChart │ │ └── index.tsx │ ├── DropDownSelect │ │ └── index.tsx │ ├── ModelDropZone │ │ └── index.tsx │ ├── AppMaterialUi │ │ └── index.tsx │ ├── App │ │ └── index.tsx │ ├── CalibrationAction │ │ └── index.tsx │ ├── CalibrationActionsV2 │ │ └── index.tsx │ ├── ExportTab │ │ └── index.tsx │ ├── CalibrationGraphs │ │ └── index.tsx │ ├── AddCalibrationAction │ │ └── index.tsx │ ├── ExtractionGuide │ │ └── index.tsx │ ├── ResultsProvider │ │ └── index.tsx │ ├── CalibrationActionPrv │ │ └── index.tsx │ ├── CalibrationActionRoughness │ │ └── index.tsx │ ├── CalibrationActionThv │ │ └── index.tsx │ └── UpdateScriptNotification │ │ └── index.tsx ├── index.tsx ├── react-app-env.d.ts ├── logo.svg └── serviceWorker.ts ├── .env.production ├── .env.local ├── img └── app.png ├── public ├── favicon.ico ├── output.wasm ├── imgs │ ├── background.png │ ├── undraw_bug_fixing.png │ └── extractguide │ │ ├── 01-FixedHead.png │ │ ├── 02-ExportToCSV.png │ │ ├── 03-AllSelected.png │ │ ├── 04-RunRubyScript.png │ │ └── 05-DragAndDrop.png ├── manifest.json └── index.html ├── config ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── env.js └── webpackDevServer.config.js ├── .gitignore ├── tsconfig.json ├── README.md ├── scripts ├── test.js ├── start.js └── build.js └── package.json /src/util/GeojsonToEpanet/testData/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/util/LiveDataReader/testData/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/util/SubnetworkTrace/testData/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | GENERATE_SOURCEMAP=false 2 | -------------------------------------------------------------------------------- /.env.local: -------------------------------------------------------------------------------- 1 | REACT_APP_MAPBOX_ACCESS_TOKEN=INSERT_TOKEN_HERE -------------------------------------------------------------------------------- /img/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcreate/model-calibrate/HEAD/img/app.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcreate/model-calibrate/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/output.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcreate/model-calibrate/HEAD/public/output.wasm -------------------------------------------------------------------------------- /public/imgs/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcreate/model-calibrate/HEAD/public/imgs/background.png -------------------------------------------------------------------------------- /public/imgs/undraw_bug_fixing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcreate/model-calibrate/HEAD/public/imgs/undraw_bug_fixing.png -------------------------------------------------------------------------------- /public/imgs/extractguide/01-FixedHead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcreate/model-calibrate/HEAD/public/imgs/extractguide/01-FixedHead.png -------------------------------------------------------------------------------- /public/imgs/extractguide/02-ExportToCSV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcreate/model-calibrate/HEAD/public/imgs/extractguide/02-ExportToCSV.png -------------------------------------------------------------------------------- /public/imgs/extractguide/03-AllSelected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcreate/model-calibrate/HEAD/public/imgs/extractguide/03-AllSelected.png -------------------------------------------------------------------------------- /public/imgs/extractguide/04-RunRubyScript.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcreate/model-calibrate/HEAD/public/imgs/extractguide/04-RunRubyScript.png -------------------------------------------------------------------------------- /public/imgs/extractguide/05-DragAndDrop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/modelcreate/model-calibrate/HEAD/public/imgs/extractguide/05-DragAndDrop.png -------------------------------------------------------------------------------- /src/interfaces/CalibrationActions.ts: -------------------------------------------------------------------------------- 1 | export default interface CalibrationActions { 2 | [id: string]: { 3 | [property: string]: number; 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /src/util/WebWorker/worker-loader.d.ts: -------------------------------------------------------------------------------- 1 | 2 | declare module "worker-loader!*" { 3 | class WebpackWorker extends Worker { 4 | constructor(); 5 | } 6 | 7 | export default WebpackWorker; 8 | } -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/en/webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/mapstyles/water/selectedMain.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from "immutable"; 2 | 3 | const layout = { visibility: "visible" }; 4 | 5 | const paint = { 6 | "line-color": "#e31a1c", 7 | "line-width": 3 8 | }; 9 | 10 | const SelectedMainStyle = fromJS({ 11 | id: "selected-mains-geojson", 12 | source: "selected_mains", 13 | type: "line", 14 | paint, 15 | layout 16 | }); 17 | 18 | export default SelectedMainStyle; 19 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 5 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 6 | sans-serif; 7 | -webkit-font-smoothing: antialiased; 8 | -moz-osx-font-smoothing: grayscale; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 13 | monospace; 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | #binary files 15 | *.bin 16 | 17 | # misc 18 | .DS_Store 19 | #.env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | #firebase 29 | /.firebase 30 | -------------------------------------------------------------------------------- /src/components/MapView/index.css: -------------------------------------------------------------------------------- 1 | 2 | .App-header { 3 | min-height: 100vh; 4 | 5 | font-size: calc(10px + 2vmin); 6 | color: white; 7 | } 8 | 9 | .tooltip { 10 | position: absolute; 11 | margin: 8px; 12 | padding: 4px; 13 | background: rgba(0, 0, 0, 0.8); 14 | color: #fff; 15 | max-width: 300px; 16 | font-size: 10px; 17 | z-index: 9; 18 | pointer-events: none; 19 | } 20 | 21 | .mapboxgl-ctrl-attrib { 22 | padding: 0 5px; 23 | background-color: rgba(0, 0, 0, 0.15)!important; 24 | margin: 0; 25 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "preserve" 17 | }, 18 | "exclude": ["src/util/EpanetEngine/output.js"], 19 | "include": ["src"] 20 | } 21 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./components/App"; 5 | import ErrorBoundary from "./components/ErrorBoundary"; 6 | import * as serviceWorker from "./serviceWorker"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById("root") 13 | ); 14 | 15 | // If you want your app to work offline and load faster, you can change 16 | // unregister() to register() below. Note this comes with some pitfalls. 17 | // Learn more about service workers: https://bit.ly/CRA-PWA 18 | serviceWorker.unregister(); 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Title Image 4 | 5 |

6 | 7 | ## Model Calibrate 8 | Extract subsections of your InfoWorks WS Pro models and run them in your browser. As you make calibration changes such as modifying roughness or restriction valves the application runs an epanet model and compares the simulated results to those observered in the field. 9 | 10 | Extract a model with the provided Ruby scripts and then drag into the app 11 | 12 | ## Learn More 13 | View the presentation I gave at CwMAG [Calibrating a Model in Your Browser](https://cwmagblog.files.wordpress.com/2019/10/calibrating-a-model-in-your-browser-v4.pdf) 14 | 15 | -------------------------------------------------------------------------------- /src/components/CalibrateTab/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, FunctionComponent } from "react"; 2 | import Typography from "@material-ui/core/Typography"; 3 | import Grid from "@material-ui/core/Grid"; 4 | import Paper from "@material-ui/core/Paper"; 5 | 6 | import TimeSeriesChart from "../TimeSeriesChart"; 7 | import CalibrationActions from "../CalibrationActions"; 8 | import CalibrationActionsV2 from "../CalibrationActionsV2"; 9 | import CalibrationGraphs from "../CalibrationGraphs"; 10 | import { ResultsContext } from "../ResultsProvider"; 11 | 12 | const CalibrateTab: FunctionComponent<{}> = () => { 13 | console.log("Calibrate Tab Rendered"); 14 | return ( 15 | <> 16 | {} 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default CalibrateTab; 23 | -------------------------------------------------------------------------------- /src/mapstyles/water/main.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | const layout = { visibility: 'visible' }; 4 | 5 | const paint = { 6 | 'line-color': [ 7 | 'case', 8 | ["==", ['get', 'operationa'], 'Abandoned'], '#7af500', 9 | ["==", ['get', 'operationa'], 'Removed'], '#7af500', 10 | ["==", ['get', 'operationa'], 'Isolated'], '#5e9294', 11 | ["==", ['get', 'operationa'], 'Proposed'], '#ff7f00', 12 | ["==", ['get', 'type'], 'Fire'], '#00ffff', 13 | ["==", ['get', 'type'], 'Distributi'], '#1528f7', 14 | ["==", ['get', 'type'], 'Trunk'], '#e31a1c', 15 | /* other */ '#1528f7' 16 | ], 17 | 'line-width': 2 18 | }; 19 | 20 | 21 | const MainStyle = fromJS({ 22 | id: 'main-geojson', 23 | source: 'mains', 24 | type: 'line', 25 | paint, 26 | layout 27 | }); 28 | 29 | export default MainStyle 30 | -------------------------------------------------------------------------------- /src/mapstyles/base/blank.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 8, 3 | "name": "Blank", 4 | "metadata": { 5 | "mapbox:autocomposite": true, 6 | "mapbox:type": "template", 7 | "mapbox:sdk-support": { 8 | "js": "0.50.0", 9 | "android": "6.7.0", 10 | "ios": "4.6.0" 11 | } 12 | }, 13 | "center": [ 14 | -1.464858786792547, 15 | 50.939150779110975 16 | ], 17 | "zoom": 13.12365211904204, 18 | "bearing": -0.44200633613297663, 19 | "pitch": 0, 20 | "light": { 21 | "intensity": 0.25, 22 | "color": "hsl(0, 0%, 100%)" 23 | }, 24 | "sprite": "mapbox://sprites/mapbox/basic-v8", 25 | "glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf", 26 | "layers": [ 27 | { 28 | "id": "background", 29 | "type": "background", 30 | "paint": { 31 | "background-color": "#e6e5e1" 32 | } 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /src/mapstyles/water/meter.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | import WaterIcons from './waterIcons' 3 | 4 | 5 | const layout = { 6 | 'visibility': 'visible', 7 | 'symbol-placement': 'line-center', 8 | 'icon-image': 'meter', 9 | 'icon-size': { 10 | 'base': 1.75, 11 | 'stops': [[10, 0.4], [22, 1]] 12 | }, 13 | 'icon-allow-overlap': true, 14 | 'icon-ignore-placement': true 15 | }; 16 | 17 | 18 | const icons = { 19 | 'meter': WaterIcons.meter, 20 | }; 21 | 22 | const images = []; 23 | for (const key in icons) { 24 | const iconImage = new Image(); 25 | iconImage.src = 'data:image/svg+xml;charset=utf-8;base64,' + btoa(WaterIcons.meter); 26 | images.push([key, iconImage]) 27 | } 28 | 29 | 30 | const MeterStyle = fromJS({ 31 | id: 'meter-geojson', 32 | source: 'meters', 33 | type: 'symbol', 34 | images, 35 | layout, 36 | minZoom: 1 37 | }); 38 | 39 | export default MeterStyle 40 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/en/webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | const assetFilename = JSON.stringify(path.basename(filename)); 11 | 12 | if (filename.match(/\.svg$/)) { 13 | return `const React = require('react'); 14 | module.exports = { 15 | __esModule: true, 16 | default: ${assetFilename}, 17 | ReactComponent: React.forwardRef((props, ref) => ({ 18 | $$typeof: Symbol.for('react.element'), 19 | type: 'svg', 20 | ref: ref, 21 | key: null, 22 | props: Object.assign({}, props, { 23 | children: ${assetFilename} 24 | }) 25 | })), 26 | };`; 27 | } 28 | 29 | return `module.exports = ${assetFilename};`; 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/mapstyles/water/calibrationActionMain.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from "immutable"; 2 | 3 | const layout = { visibility: "visible" }; 4 | 5 | const paint = { 6 | "line-color": [ 7 | "case", 8 | ["==", ["get", "operationa"], "Abandoned"], 9 | "#7af500", 10 | ["==", ["get", "operationa"], "Removed"], 11 | "#7af500", 12 | ["==", ["get", "operationa"], "Isolated"], 13 | "#5e9294", 14 | ["==", ["get", "operationa"], "Proposed"], 15 | "#ff7f00", 16 | ["==", ["get", "type"], "Fire"], 17 | "#00ffff", 18 | ["==", ["get", "type"], "Distributi"], 19 | "#1528f7", 20 | ["==", ["get", "type"], "Trunk"], 21 | "#e31a1c", 22 | /* other */ "#ff7f00" 23 | ], 24 | "line-width": 2 25 | }; 26 | 27 | const CalibrationActionMainStyle = fromJS({ 28 | id: "ca-mains-geojson", 29 | source: "calibration_action_mains", 30 | type: "line", 31 | paint, 32 | layout 33 | }); 34 | 35 | export default CalibrationActionMainStyle; 36 | -------------------------------------------------------------------------------- /src/mapstyles/index.ts: -------------------------------------------------------------------------------- 1 | import BlankStyle from "./base/blank.json"; 2 | import OsBaseStyle from "./base/open-zoom-stack-light.json"; 3 | import HydrantStyle from "./water/hydrant"; 4 | import MainStyle from "./water/main"; 5 | import MeterStyle from "./water/meter"; 6 | import FixedHeadStyle from "./water/fixedhead"; 7 | import ValveStyle from "./water/valve"; 8 | import LiveDataStyle from "./water/liveData"; 9 | import TransferNodeStyle from "./water/transferNode"; 10 | import CalibrationActionStyle from "./water/calibrationAction"; 11 | import CalibrationActionLabelStyle from "./water/calibrationActionLabel"; 12 | import CalibrationActionMainStyle from "./water/calibrationActionMain"; 13 | import SelectedMainStyle from "./water/selectedMain"; 14 | 15 | export { 16 | BlankStyle, 17 | OsBaseStyle, 18 | HydrantStyle, 19 | MainStyle, 20 | MeterStyle, 21 | ValveStyle, 22 | FixedHeadStyle, 23 | LiveDataStyle, 24 | TransferNodeStyle, 25 | CalibrationActionStyle, 26 | CalibrationActionLabelStyle, 27 | CalibrationActionMainStyle, 28 | SelectedMainStyle 29 | }; 30 | -------------------------------------------------------------------------------- /src/mapstyles/water/hydrant.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from 'immutable'; 2 | 3 | const layout = { visibility: 'visible' }; 4 | const paint = { 5 | "circle-opacity": { 6 | stops: [[11.5, 0], [12, 1]] 7 | }, 8 | "circle-stroke-opacity": { 9 | stops: [[11.5, 0], [12, 1]] 10 | }, 11 | 12 | 'circle-color': [ 13 | 'case', 14 | ["==", ['get', 'operational'], 'Abandoned'], '#33d935', 15 | ["==", ['get', 'type'], 'Fire'], '#b300ff', 16 | ["==", ['get', 'type'], 'Washout'], '#fff', 17 | /* other */ '#ccc' 18 | ], 19 | 'circle-radius': { 20 | 'base': 1, 21 | 'stops': [[17, 2], [22, 10]] 22 | }, 23 | 'circle-stroke-color': [ 24 | 'case', 25 | ["==", ['get', 'operational'], 'Abandoned'], '#33d935', 26 | 27 | /* other */ '#b300ff' 28 | ], 29 | 'circle-stroke-width': { 30 | 'base': 0.5, 31 | 'stops': [[15, 1.25], [22, 4]] 32 | }, 33 | 34 | }; 35 | 36 | 37 | const HydrantStyle = fromJS({ 38 | id: 'hydrants-geojson', 39 | source: 'hydrants', 40 | type: 'circle', 41 | paint, 42 | layout 43 | }); 44 | 45 | export default HydrantStyle -------------------------------------------------------------------------------- /src/components/SubModelTab/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from "react"; 2 | import { ResultsContext } from "../ResultsProvider"; 3 | import Typography from "@material-ui/core/Typography"; 4 | import Button from "@material-ui/core/Button"; 5 | 6 | const SubModelTab: FunctionComponent<{}> = () => { 7 | return ( 8 |
9 | 10 | Move the fixed head to downstream log points. This feature is an early 11 | preview and may occasionally fail to work while it is being tested and 12 | improved. 13 | 14 | 15 | 16 | {// 17 | //@ts-ignore 18 | results => ( 19 | <> 20 | {results.subModels.map((sub: React.ReactNode, i: number) => ( 21 | 29 | ))} 30 | 31 | )} 32 | 33 |
34 | ); 35 | }; 36 | 37 | export default SubModelTab; 38 | -------------------------------------------------------------------------------- /src/util/GeojsonToEpanet/controls.ts: -------------------------------------------------------------------------------- 1 | import ModelFeatureCollection from "../../interfaces/ModelFeatureCollection"; 2 | 3 | export function createControls(model: ModelFeatureCollection): string { 4 | const modelFilterPrv = model.features.filter(f => { 5 | return ( 6 | f.properties && 7 | f.properties.profiles && 8 | f.properties.table === "wn_valve" && 9 | f.properties.mode === "PRV" 10 | ); 11 | }); 12 | 13 | const controls = modelFilterPrv 14 | .map(f => { 15 | if (f.properties) { 16 | const id = f.properties.i; 17 | const linear = f.properties.linear_profile === "1" ? true : false; 18 | return timePrvControls(f.properties.profiles, id, linear); 19 | } 20 | }) 21 | .join("\n"); 22 | 23 | return `[CONTROLS]\n${controls}`; 24 | } 25 | 26 | function timePrvControls( 27 | settings: string[][], 28 | id: string, 29 | linearProfile: boolean 30 | ): string { 31 | const reduceSettings = settings.reduce((settingString, prof, i, arr) => { 32 | const setting = parseFloat(prof[8]); 33 | const time = i === 0 ? "00:00" : prof[0].slice(-8, -3); 34 | return `${settingString}VALVE ${id} ${setting.toFixed( 35 | 2 36 | )} AT CLOCKTIME ${time}\n`; 37 | }, ""); 38 | 39 | return reduceSettings; 40 | } 41 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type ErrorBoundaryProps = {}; 4 | interface ErrorBoundaryState { 5 | hasError: boolean; 6 | } 7 | 8 | class ErrorBoundary extends React.Component< 9 | ErrorBoundaryProps, 10 | ErrorBoundaryState 11 | > { 12 | state: Readonly = { 13 | hasError: false 14 | }; 15 | 16 | componentDidCatch(error: Error, info: React.ErrorInfo) { 17 | // Display fallback UI 18 | this.setState({ hasError: true }); 19 | // You can also log the error to an error reporting service 20 | //logErrorToMyService(error, info); 21 | console.log(error); 22 | console.log(info); 23 | } 24 | 25 | render() { 26 | if (this.state.hasError) { 27 | // You can render any custom fallback UI 28 | return ( 29 |
30 | 31 |

Oh no... Something went wrong.

32 |

33 | Please double check you have extracted the model correctly. If 34 | problems continue, please forward me a copy your model and I can 35 | look into the issue (luke@matrado.ca). 36 |

37 |
38 | ); 39 | } 40 | return this.props.children; 41 | } 42 | } 43 | 44 | export default ErrorBoundary; 45 | -------------------------------------------------------------------------------- /src/mapstyles/water/calibrationAction.ts: -------------------------------------------------------------------------------- 1 | import WaterIcons from './waterIcons' 2 | import { fromJS } from 'immutable'; 3 | 4 | 5 | const layout = { 6 | 'visibility': 'visible', 7 | 'symbol-placement': 'line-center', 8 | 'icon-image': 'ca-valve', 9 | 'icon-size': { 10 | 'base': 1.75, 11 | 'stops': [[10, 0.4], [22, 1]] 12 | }, 13 | 'icon-rotate': ["*", ['get', 'geom_orien'], -1], 14 | 'text-field': '{description}', 15 | 'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'], 16 | 'text-offset': [0, 0.6], 17 | 'text-anchor': 'top', 18 | 'text-size': 8, 19 | 'icon-allow-overlap': true, 20 | 'icon-ignore-placement': true 21 | }; 22 | 23 | 24 | 25 | 26 | const paint = { 27 | "text-color": "black", 28 | "text-halo-color": "white", 29 | "text-halo-width": 2 30 | }; 31 | 32 | 33 | const icons = { 34 | 'defaultValve': WaterIcons.defaultValve("#b300ff") 35 | }; 36 | 37 | let images = []; 38 | for (const key in icons) { 39 | const iconImage = new Image(); 40 | iconImage.src = 'data:image/svg+xml;charset=utf-8;base64,' + btoa(WaterIcons.defaultValve("#ff7f00")); 41 | images.push([key, iconImage]) 42 | } 43 | 44 | const CalibrationActionStyle = fromJS({ 45 | id: 'ca-geojson', 46 | source: 'calibration_action', 47 | type: 'symbol', 48 | images, 49 | layout, 50 | paint, 51 | minZoom: 10 52 | }); 53 | 54 | export default CalibrationActionStyle -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | declare namespace NodeJS { 6 | interface ProcessEnv { 7 | NODE_ENV: 'development' | 'production' | 'test'; 8 | PUBLIC_URL: string; 9 | } 10 | } 11 | 12 | declare module '*.bmp' { 13 | const src: string; 14 | export default src; 15 | } 16 | 17 | declare module '*.gif' { 18 | const src: string; 19 | export default src; 20 | } 21 | 22 | declare module '*.jpg' { 23 | const src: string; 24 | export default src; 25 | } 26 | 27 | declare module '*.jpeg' { 28 | const src: string; 29 | export default src; 30 | } 31 | 32 | declare module '*.png' { 33 | const src: string; 34 | export default src; 35 | } 36 | 37 | declare module '*.webp' { 38 | const src: string; 39 | export default src; 40 | } 41 | 42 | declare module '*.svg' { 43 | import * as React from 'react'; 44 | 45 | export const ReactComponent: React.SFC>; 46 | 47 | const src: string; 48 | export default src; 49 | } 50 | 51 | declare module '*.module.css' { 52 | const classes: { [key: string]: string }; 53 | export default classes; 54 | } 55 | 56 | declare module '*.module.scss' { 57 | const classes: { [key: string]: string }; 58 | export default classes; 59 | } 60 | 61 | declare module '*.module.sass' { 62 | const classes: { [key: string]: string }; 63 | export default classes; 64 | } 65 | -------------------------------------------------------------------------------- /src/mapstyles/water/fixedhead.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from "immutable"; 2 | import WaterIcons from "./waterIcons"; 3 | 4 | const layout = { 5 | visibility: "visible", 6 | //"symbol-placement": "line-center", 7 | "icon-image": "triangleSolid", 8 | "icon-size": { 9 | base: 2.75, 10 | stops: [[10, 0.4], [22, 1]] 11 | }, 12 | "text-field": "{id}", 13 | //'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'], 14 | "text-offset": [0.4, 0], 15 | "text-anchor": "left", 16 | //"text-max-width": 3, 17 | "text-size": { 18 | base: 2, 19 | stops: [[8, 8], [12, 8], [12, 8], [13, 12]] 20 | }, 21 | "text-rotate": 0, 22 | "icon-allow-overlap": true, 23 | "text-allow-overlap": true, 24 | "text-ignore-placement": false, 25 | "icon-ignore-placement": true 26 | }; 27 | 28 | const paint = { 29 | "text-color": "black", 30 | "text-halo-color": "white", 31 | "text-halo-width": 2 32 | }; 33 | 34 | const icons = { 35 | triangleSolid: WaterIcons.triangleSolid 36 | }; 37 | 38 | const images = []; 39 | for (const key in icons) { 40 | const iconImage = new Image(); 41 | iconImage.src = 42 | "data:image/svg+xml;charset=utf-8;base64," + btoa(WaterIcons.triangleSolid); 43 | images.push([key, iconImage]); 44 | } 45 | 46 | const FixedHeadStyle = fromJS({ 47 | id: "fixedhead-geojson", 48 | source: "fixedhead", 49 | type: "symbol", 50 | images, 51 | paint, 52 | layout, 53 | minZoom: 1 54 | }); 55 | 56 | export default FixedHeadStyle; 57 | -------------------------------------------------------------------------------- /src/mapstyles/water/transferNode.ts: -------------------------------------------------------------------------------- 1 | import { fromJS } from "immutable"; 2 | import WaterIcons from "./waterIcons"; 3 | 4 | const layout = { 5 | visibility: "visible", 6 | //"symbol-placement": "line-center", 7 | "icon-image": "squareSolid", 8 | "icon-size": { 9 | base: 2.75, 10 | stops: [[10, 0.4], [22, 1]] 11 | }, 12 | "text-field": "{id}", 13 | //'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'], 14 | "text-offset": [0.4, 0], 15 | "text-anchor": "left", 16 | //"text-max-width": 3, 17 | "text-size": { 18 | base: 2, 19 | stops: [[8, 8], [12, 8], [12, 8], [13, 12]] 20 | }, 21 | "text-rotate": 0, 22 | "icon-allow-overlap": true, 23 | "text-allow-overlap": true, 24 | "text-ignore-placement": false, 25 | "icon-ignore-placement": true 26 | }; 27 | 28 | const paint = { 29 | "text-color": "black", 30 | "text-halo-color": "white", 31 | "text-halo-width": 2 32 | }; 33 | 34 | const icons = { 35 | squareSolid: WaterIcons.squareSolid 36 | }; 37 | 38 | const images = []; 39 | for (const key in icons) { 40 | const iconImage = new Image(); 41 | iconImage.src = 42 | "data:image/svg+xml;charset=utf-8;base64," + btoa(WaterIcons.squareSolid); 43 | images.push([key, iconImage]); 44 | } 45 | 46 | const TransferNodeStyle = fromJS({ 47 | id: "transfernode-geojson", 48 | source: "transfernode", 49 | type: "symbol", 50 | images, 51 | paint, 52 | layout, 53 | minZoom: 1 54 | }); 55 | 56 | export default TransferNodeStyle; 57 | -------------------------------------------------------------------------------- /src/mapstyles/water/liveData.ts: -------------------------------------------------------------------------------- 1 | import WaterIcons from "./waterIcons"; 2 | import { fromJS } from "immutable"; 3 | 4 | const layout = { 5 | visibility: "visible", 6 | //'symbol-placement': 'line-center', 7 | "icon-image": "live-data", 8 | "icon-size": { 9 | base: 1.75, 10 | stops: [[1, 0.4], [22, 1]] 11 | }, 12 | ////'icon-rotate': ["*", ['get', 'geom_orien'], -1], 13 | "text-field": "{live_data_point_id}", 14 | //'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'], 15 | "text-offset": [0.4, 0], 16 | "text-anchor": "left", 17 | "text-max-width": 3, 18 | "text-size": { 19 | base: 1, 20 | stops: [[8, 8], [12, 8], [12, 8], [13, 12]] 21 | }, 22 | "text-rotate": 0, 23 | "icon-allow-overlap": true, 24 | "text-allow-overlap": true, 25 | "text-ignore-placement": false, 26 | "icon-ignore-placement": false 27 | }; 28 | 29 | const paint = { 30 | "text-color": "black", 31 | "text-halo-color": "white", 32 | "text-halo-width": 2 33 | }; 34 | 35 | const icons = { 36 | defaultValve: WaterIcons.defaultValve("#b300ff") 37 | }; 38 | 39 | let images = []; 40 | for (const key in icons) { 41 | const iconImage = new Image(); 42 | iconImage.src = 43 | "data:image/svg+xml;charset=utf-8;base64," + btoa(WaterIcons.circleSolid); 44 | images.push([key, iconImage]); 45 | } 46 | 47 | const LiveDataStyle = fromJS({ 48 | id: "livedata-geojson", 49 | source: "live_data", 50 | type: "symbol", 51 | images, 52 | layout, 53 | paint, 54 | minZoom: 10 55 | }); 56 | 57 | export default LiveDataStyle; 58 | -------------------------------------------------------------------------------- /src/util/GeojsonToEpanet/test.ts: -------------------------------------------------------------------------------- 1 | import geojsonToEpanet from "."; 2 | 3 | import { createControls } from "./controls"; 4 | 5 | const fs = require("fs"); 6 | const inModel = fs.readFileSync(__dirname + "/testData/in.json", "utf8"); 7 | const prvModel = fs.readFileSync(__dirname + "/testData/timedPrv.json", "utf8"); 8 | const prvModelout = fs.readFileSync( 9 | __dirname + "/testData/timedPrv.inp", 10 | "utf8" 11 | ); 12 | const prvModelLinear = fs.readFileSync( 13 | __dirname + "/testData/timedPrv-linear.json", 14 | "utf8" 15 | ); 16 | const prvModelLinearout = fs.readFileSync( 17 | __dirname + "/testData/timedPrv-linear.inp", 18 | "utf8" 19 | ); 20 | const outInp = fs.readFileSync(__dirname + "/testData/out.inp", "utf8"); 21 | const outCalbibrationInp = fs.readFileSync( 22 | __dirname + "/testData/out-calibration.inp", 23 | "utf8" 24 | ); 25 | 26 | it("create network without calibration actions", () => { 27 | const modelJson = JSON.parse(inModel); 28 | const calibration = {}; 29 | expect(geojsonToEpanet(modelJson, calibration)).toEqual(outInp); 30 | }); 31 | 32 | it("creates a network with calibration actions", () => { 33 | const modelJson = JSON.parse(inModel); 34 | const calibration = { 35 | "03714480779558.03714470779557.1": { opening: 10 }, 36 | "03714500779561.03714490779561.1": { opening: 20 }, 37 | "03714510779563.03714530779562.1": { k: 50 } 38 | }; 39 | expect(geojsonToEpanet(modelJson, calibration)).toEqual(outCalbibrationInp); 40 | }); 41 | 42 | it("creates a network with a timed PRV", () => { 43 | const modelJson = JSON.parse(prvModel); 44 | expect(geojsonToEpanet(modelJson, {})).toEqual(prvModelout); 45 | }); 46 | -------------------------------------------------------------------------------- /src/util/SubnetworkTrace/test.ts: -------------------------------------------------------------------------------- 1 | import SubnetworkTrace from "."; 2 | 3 | const fs = require("fs"); 4 | const inModel = fs.readFileSync(__dirname + "/testData/in.json", "utf8"); 5 | 6 | it("traces a network", () => { 7 | const output = [ 8 | "1", 9 | "2", 10 | "3", 11 | "4", 12 | "5", 13 | "6", 14 | "7", 15 | "8", 16 | "9", 17 | "10", 18 | "1.2.1", 19 | "2.3.1", 20 | "3.4.1", 21 | "2.5.1", 22 | "5.4.1", 23 | "4.6.1", 24 | "6.7.1", 25 | "7.8.1", 26 | "3.9.1", 27 | "9.10.1" 28 | ].sort(); 29 | 30 | //var modelJson = require("../../data/Example.json"); 31 | 32 | const modelJson = JSON.parse(inModel); 33 | const tracer = new SubnetworkTrace(modelJson); 34 | const items = tracer.listAllItems(); 35 | 36 | //@ts-ignore 37 | const flatten = a => (Array.isArray(a) ? [].concat(...a.map(flatten)) : a); 38 | 39 | const flattened = flatten(items); 40 | 41 | expect(flattened.sort()).toEqual(output); 42 | }); 43 | 44 | it("lists subnetworks", () => { 45 | const modelJson = require("../../data/Example.json"); 46 | const tracer = new SubnetworkTrace(modelJson); 47 | const subModels = tracer.listSubModels(); 48 | 49 | const expectedSubModels = ["Tank01", "Log01", "Log02", "Log04", "Log05"]; 50 | 51 | expect(subModels).toEqual(expectedSubModels); 52 | }); 53 | 54 | it("gets a subnetwork", () => { 55 | const modelJson = require("../../data/Example.json"); 56 | const modelJson2 = require("./testData/out_test_log4.json"); 57 | const tracer = new SubnetworkTrace(modelJson); 58 | const subModel = tracer.getSubModel("Log04"); 59 | 60 | expect(subModel).toEqual(modelJson2); 61 | }); 62 | -------------------------------------------------------------------------------- /src/mapstyles/water/calibrationActionLabel.ts: -------------------------------------------------------------------------------- 1 | import WaterIcons from './waterIcons' 2 | import { fromJS } from 'immutable'; 3 | 4 | const layout = { 5 | 'visibility': 'visible', 6 | //'symbol-placement': 'line-center', 7 | 'icon-image': 'ca-point', 8 | 'icon-size': { 9 | 'base': 1.75, 10 | 'stops': [[1, 0.4], [22, 1]] 11 | }, 12 | //////'icon-rotate': ["*", ['get', 'geom_orien'], -1], 13 | 'text-field': 'V{id}', 14 | //'text-font': ['Open Sans Semibold', 'Arial Unicode MS Bold'], 15 | 'text-offset': [0.5, 0], 16 | 'text-anchor': 'left', 17 | 'text-max-width': 3, 18 | 'text-size': { 19 | 'base': 1, 20 | 'stops': [ 21 | [8, 8], [12, 8], 22 | [12, 8], [13, 12], 23 | ] 24 | }, 25 | 'text-rotate': 0, 26 | 'icon-allow-overlap': true, 27 | 'text-allow-overlap': true, 28 | 'text-ignore-placement': false, 29 | 'icon-ignore-placement': false 30 | }; 31 | 32 | 33 | 34 | const paint = { 35 | "icon-opacity": { 36 | stops: [[13.5, 1], [14, 0]] 37 | }, 38 | "text-color": "black", 39 | "text-halo-color": "white", 40 | "text-halo-width": 2 41 | }; 42 | 43 | 44 | const icons = { 45 | 'defaultValve': WaterIcons.circleSolidOrange 46 | }; 47 | 48 | let images = []; 49 | for (const key in icons) { 50 | const iconImage = new Image(); 51 | iconImage.src = 'data:image/svg+xml;charset=utf-8;base64,' + btoa(WaterIcons.circleSolidOrange); 52 | images.push([key, iconImage]) 53 | } 54 | 55 | const CalibrationActionLabelStyle = fromJS({ 56 | id: 'calibration_action_centregeojson', 57 | source: 'calibration_action_centre', 58 | type: 'symbol', 59 | images, 60 | layout, 61 | paint, 62 | minZoom: 10 63 | }); 64 | 65 | export default CalibrationActionLabelStyle -------------------------------------------------------------------------------- /src/interfaces/ModelFeatureCollection.ts: -------------------------------------------------------------------------------- 1 | import { FeatureCollection, Geometries, Properties } from "@turf/helpers"; 2 | 3 | import { Calibration } from "../components/ResultsProvider"; 4 | 5 | export interface LiveData { 6 | live_data_point_id: string; 7 | pressure_offset: string; 8 | time_offset: string; 9 | pressure_factor: number; 10 | flow_factor: number; 11 | flow_offset: number; 12 | channel_type: string; 13 | sensor_level: number; 14 | } 15 | 16 | export interface LiveDataRaw extends LiveData { 17 | live_data: { 18 | date: string; 19 | time: string; 20 | interval: string; 21 | time_unit: string; 22 | row_count: string; 23 | values: number[]; 24 | }; 25 | } 26 | 27 | export interface SensorData { 28 | [id: string]: number[]; 29 | } 30 | 31 | export interface ModelLiveData { 32 | liveDataPoints: LiveDataPoint[]; 33 | sensorData: SensorData; 34 | } 35 | 36 | export interface LiveDataPoint { 37 | nodeId: string; 38 | liveDataId: string; 39 | epanetId: number; 40 | } 41 | 42 | interface RunTimeSettings { 43 | start_date_time: string; 44 | } 45 | 46 | export default interface ModelFeatureCollection 47 | extends FeatureCollection { 48 | model: { 49 | ca?: Calibration[]; 50 | demands: NodeDemand; 51 | live_data: { 52 | [id: string]: LiveDataRaw; 53 | }; 54 | demand_profiles: { 55 | [name: string]: number[]; 56 | }; 57 | timesteps: string[]; 58 | run_time: RunTimeSettings; 59 | [name: string]: any; 60 | }; 61 | } 62 | 63 | export interface NodeDemand { 64 | [name: string]: Demand[]; 65 | } 66 | 67 | export interface Demand { 68 | category_id: string; 69 | spec_consumption: number; 70 | no_of_properties: number; 71 | } 72 | -------------------------------------------------------------------------------- /src/components/Landing/index.css: -------------------------------------------------------------------------------- 1 | .react-select-container { 2 | font-size: 12px; 3 | margin-bottom: 10px; 4 | margin-right: 15px; 5 | } 6 | 7 | .model-proj-subtitle{ 8 | margin: 25px 5px 5px; 9 | font-size: 14px; 10 | } 11 | 12 | 13 | 14 | .flex-grid { 15 | display: flex; 16 | min-height: 100vh; 17 | font-family: 'Roboto', sans-serif; 18 | background: linear-gradient(rgba(0, 0, 0, 0.1),rgba(0, 0, 0, 0.1)), url(/imgs/background.png) no-repeat center right; 19 | background-size: cover; 20 | color:rgba(0,0,0,.77); 21 | } 22 | 23 | .flex-grid .col1 { 24 | padding: 20px; 25 | background-color: #fafafa; 26 | box-shadow: 1px; 27 | box-shadow: 0px 2px 4px -1px rgba(0,0,0,0.2), 0px 4px 5px 0px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12); 28 | } 29 | 30 | .flex-grid h3 { 31 | margin: 0; 32 | padding-top: 40px; 33 | font-size: 1em; 34 | margin-left: 3px; 35 | margin-bottom: -5px; 36 | } 37 | 38 | .flex-grid h1 { 39 | margin: 0; 40 | font-size: 2.8em; 41 | } 42 | 43 | .flex-grid .subtitle { 44 | margin-top: -4px; 45 | margin-left: 4px; 46 | font-size: calc(8px + 1.4vmin); 47 | } 48 | .flex-grid .droparea { 49 | text-align: center; 50 | padding: 60px; 51 | border: 2px dashed rgb(145, 145, 145); 52 | border-radius: 15px; 53 | margin: 30px 50px; 54 | color: rgb(145, 145, 145) 55 | } 56 | 57 | .droparea p { 58 | font-size: 0.6em; 59 | color: rgb(145, 145, 145); 60 | } 61 | 62 | .blurb { 63 | font-size: 0.5em; 64 | color: rgb(145, 145, 145); 65 | margin-left: 5px; 66 | } 67 | 68 | 69 | .col1 { 70 | flex: 3; 71 | } 72 | .col2 { 73 | flex: 4; 74 | } 75 | 76 | .btns-float-left { 77 | float:right; 78 | margin-right: 15px; 79 | } 80 | 81 | 82 | @media (max-width: 400px) { 83 | .flex-grid { 84 | display: block; 85 | } 86 | } -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Do this as the first thing so that any code reading it knows the right env. 4 | process.env.BABEL_ENV = 'test'; 5 | process.env.NODE_ENV = 'test'; 6 | process.env.PUBLIC_URL = ''; 7 | 8 | // Makes the script crash on unhandled rejections instead of silently 9 | // ignoring them. In the future, promise rejections that are not handled will 10 | // terminate the Node.js process with a non-zero exit code. 11 | process.on('unhandledRejection', err => { 12 | throw err; 13 | }); 14 | 15 | // Ensure environment variables are read. 16 | require('../config/env'); 17 | 18 | 19 | const jest = require('jest'); 20 | const execSync = require('child_process').execSync; 21 | let argv = process.argv.slice(2); 22 | 23 | function isInGitRepository() { 24 | try { 25 | execSync('git rev-parse --is-inside-work-tree', { stdio: 'ignore' }); 26 | return true; 27 | } catch (e) { 28 | return false; 29 | } 30 | } 31 | 32 | function isInMercurialRepository() { 33 | try { 34 | execSync('hg --cwd . root', { stdio: 'ignore' }); 35 | return true; 36 | } catch (e) { 37 | return false; 38 | } 39 | } 40 | 41 | // Watch unless on CI, in coverage mode, explicitly adding `--no-watch`, 42 | // or explicitly running all tests 43 | if ( 44 | !process.env.CI && 45 | argv.indexOf('--coverage') === -1 && 46 | argv.indexOf('--no-watch') === -1 && 47 | argv.indexOf('--watchAll') === -1 48 | ) { 49 | // https://github.com/facebook/create-react-app/issues/5210 50 | const hasSourceControl = isInGitRepository() || isInMercurialRepository(); 51 | argv.push(hasSourceControl ? '--watch' : '--watchAll'); 52 | } 53 | 54 | // Jest doesn't have this option so we'll remove it 55 | if (argv.indexOf('--no-watch') !== -1) { 56 | argv = argv.filter(arg => arg !== '--no-watch'); 57 | } 58 | 59 | 60 | jest.run(argv); 61 | -------------------------------------------------------------------------------- /src/util/WebWorker/EPANetWorker.ts: -------------------------------------------------------------------------------- 1 | import { readBinary, EpanetResults } from "../../util/EpanetBinary"; 2 | import Module from "../../util/EpanetEngine/output.js"; 3 | import { LiveDataPoint } from "../../interfaces/ModelFeatureCollection"; 4 | //@ts-ignore 5 | declare function postMessage(message: any); 6 | 7 | interface SimulationResults { 8 | [key: string]: number[]; 9 | } 10 | 11 | const doWork = (work: MessageEvent) => { 12 | //const valveOpening = work.data 13 | const epanetInp = work.data.inp; 14 | const epaNetEngine = Module(); 15 | const FS = epaNetEngine.fs; 16 | //@ts-ignore 17 | epaNetEngine.onRuntimeInitialized = _ => { 18 | //const data = epaNet(valveOpening) 19 | 20 | FS.writeFile("/net1.inp", epanetInp); 21 | 22 | epaNetEngine._epanet_run(); 23 | 24 | const resultView = FS.readFile("/net1.bin"); 25 | 26 | const results = readBinary(resultView); 27 | //setEpaNetResults({ ...epaNetResults, timeseriesData: results.results.nodes[272].pressure }) 28 | //console.log(results.results.nodes[272].pressure) 29 | 30 | const liveData: LiveDataPoint[] = work.data.ld; 31 | 32 | const filteredResults = liveData.reduce( 33 | (prev, curr) => { 34 | const result = results.results.nodes[curr.epanetId].pressure; 35 | prev[curr.liveDataId] = result; 36 | return prev; 37 | }, 38 | {} 39 | ); 40 | 41 | postMessage(filteredResults); 42 | 43 | //postMessage({ 44 | // Log01: results.results.nodes[307].pressure, 45 | // Log02: results.results.nodes[289].pressure, 46 | // Log03: results.results.nodes[299].pressure, 47 | // Log04: results.results.nodes[280].pressure, 48 | // Log05: results.results.nodes[272].pressure 49 | // //Log06: results.results.nodes[325].pressure 50 | //}); 51 | }; 52 | }; 53 | 54 | self.addEventListener("message", message => { 55 | doWork(message); 56 | }); 57 | -------------------------------------------------------------------------------- /src/components/CalibrationActions/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | FunctionComponent, 3 | ChangeEvent, 4 | useState, 5 | useContext 6 | } from "react"; 7 | import { makeStyles } from "@material-ui/styles"; 8 | import Grid from "@material-ui/core/Grid"; 9 | import Typography from "@material-ui/core/Typography"; 10 | 11 | import { ResultsContext } from "../ResultsProvider"; 12 | import CalibrationAction from "../CalibrationAction"; 13 | 14 | const useStyles = makeStyles(theme => ({ 15 | button: { 16 | margin: 16 17 | }, 18 | input: { 19 | display: "none" 20 | }, 21 | root: { 22 | width: 300 23 | }, 24 | slider: { 25 | padding: "22px 0px" 26 | } 27 | })); 28 | 29 | interface ThrottleValve { 30 | id: number; 31 | thvSetting: number; 32 | } 33 | 34 | interface CalibrationActionProperties {} 35 | 36 | const CalibrationActions: FunctionComponent< 37 | CalibrationActionProperties 38 | > = () => { 39 | const classes = useStyles(); 40 | const [setting, setSetting] = useState(100); 41 | 42 | const handleChange = (event: ChangeEvent<{}>, value: number) => { 43 | //updateCalibration({ id: 0, thvSetting: value }) 44 | setSetting(value); 45 | }; 46 | 47 | return ( 48 | 49 | {// 50 | //@ts-ignore 51 | results => ( 52 | 53 | {// 54 | //@ts-ignore 55 | results.calibrationActions.map(ca => { 56 | return ( 57 | 58 | 63 | 64 | ); 65 | })} 66 | 67 | )} 68 | 69 | ); 70 | }; 71 | 72 | export default CalibrationActions; 73 | -------------------------------------------------------------------------------- /src/components/ModelInfo/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, FunctionComponent } from 'react'; 2 | import { Properties } from '@turf/helpers'; 3 | //import FeatureProperties from '../FeatureProperties'; 4 | import format from 'date-fns/format' 5 | import './index.css'; 6 | 7 | 8 | type DefaultContainer = {} 9 | 10 | const DefaultContainer: FunctionComponent = ({ children }) =>
{children}
; 11 | 12 | export interface ModelInfoSetting { 13 | modeName: string, 14 | currentTimestep: number, 15 | timesteps: Date[], 16 | selectedFeature: Properties 17 | } 18 | 19 | 20 | type ModelInfoProps = { 21 | settings: ModelInfoSetting, 22 | onChange: (value: string) => void, 23 | onClearSelected: () => void 24 | } 25 | 26 | 27 | const ModelInfo: FunctionComponent = ({ settings, onChange, onClearSelected }) => { 28 | 29 | return ( 30 | 31 |
32 |

{format( 33 | settings.timesteps[settings.currentTimestep], 34 | 'Do MMMM YY' 35 | )}

36 |

{format( 37 | settings.timesteps[settings.currentTimestep], 38 | 'HH:mm' 39 | )}

40 |
41 | onChange(evt.target.value)} 44 | /> 45 |
46 |
47 | 48 | {//settings.selectedFeature && 49 | // settings.selectedFeature && settings.selectedFeature[key].constructor === Array)} 55 | // /> 56 | } 57 | 58 | 59 |
60 | ); 61 | } 62 | 63 | 64 | export default ModelInfo 65 | -------------------------------------------------------------------------------- /src/mapstyles/water/valve.ts: -------------------------------------------------------------------------------- 1 | import WaterIcons from "./waterIcons"; 2 | import { fromJS } from "immutable"; 3 | 4 | const layout = { 5 | visibility: "visible", 6 | "symbol-placement": "line-center", 7 | "icon-image": [ 8 | "case", 9 | ["==", ["get", "pipe_closed"], "1"], 10 | "closedvalve", 11 | ["==", ["get", "mode"], "PRV"], 12 | "prv", 13 | ["==", ["get", "table"], "wn_non_return_valve"], 14 | "nrv", 15 | /* other */ "valve" 16 | ], 17 | "icon-size": { 18 | base: 1.75, 19 | stops: [[10, 0.4], [22, 1]] 20 | }, 21 | "icon-rotate": ["*", ["get", "geom_orien"], -1], 22 | "text-field": "{description}", 23 | "text-font": ["Open Sans Semibold", "Arial Unicode MS Bold"], 24 | "text-offset": [0, 0.6], 25 | "text-anchor": "top", 26 | "text-size": 8, 27 | "icon-allow-overlap": true, 28 | "icon-ignore-placement": true 29 | }; 30 | 31 | const paint = { 32 | "text-color": "black", 33 | "text-halo-color": "white", 34 | "text-halo-width": 2 35 | }; 36 | 37 | const icons = { 38 | defaultValve: WaterIcons.defaultValve("#b300ff"), 39 | sensitiveValve: WaterIcons.defaultValve("#ff7f00"), 40 | washoutValve: WaterIcons.washoutValve, 41 | closedValve: WaterIcons.defaultClosedValve, 42 | closedValvePCCPRAPSA: WaterIcons.closedValve("#FFF"), 43 | closedValveDMA: WaterIcons.closedValve("#33a02c"), 44 | closedValveWSZ: WaterIcons.closedValve("#ff7f00"), 45 | closedValveWOA: WaterIcons.closedValve("#e31a1c"), 46 | pressureReducing: WaterIcons.pressureReducing, 47 | pressureRelief: WaterIcons.pressureRelief, 48 | pressureSustaining: WaterIcons.pressureSustaining, 49 | refluxValve: WaterIcons.refluxValve 50 | }; 51 | 52 | let images = []; 53 | for (const key in icons) { 54 | const iconImage = new Image(); 55 | iconImage.src = 56 | "data:image/svg+xml;charset=utf-8;base64," + 57 | //@ts-ignore 58 | btoa(icons[key]); 59 | //btoa(WaterIcons.defaultValve("#b300ff")); 60 | images.push([key, iconImage]); 61 | } 62 | 63 | const ValveStyle = fromJS({ 64 | id: "valve-geojson", 65 | source: "valves", 66 | type: "symbol", 67 | images, 68 | layout, 69 | minZoom: 1 70 | }); 71 | 72 | export default ValveStyle; 73 | -------------------------------------------------------------------------------- /src/util/EpanetBinary/test.ts: -------------------------------------------------------------------------------- 1 | import { modelResult } from "./testData/out"; 2 | import { EpanetProlog, readBinary } from "."; 3 | 4 | const fs = require("fs"); 5 | const data = fs.readFileSync(__dirname + "/testData/in.bin"); 6 | 7 | it("is a dummy test", () => { 8 | expect(1).toEqual(1); 9 | }); 10 | 11 | //it('get results from binary', () => { 12 | // 13 | // const results = readBinary(data) 14 | // expect(results).toEqual(modelResult) 15 | // 16 | //}) 17 | 18 | //it('getResultByteOffSet', () => { 19 | // 20 | // const prolog: EpanetProlog = modelResult.prolog 21 | // 22 | // const result = getResultByteOffSet(prolog, 0, NodeResultTypes.Demand) 23 | // 24 | // const expected = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24] 25 | // 26 | // expect(result).toEqual(expected) 27 | // 28 | //}) 29 | 30 | //it('size is correct', () => { 31 | // 32 | // 33 | // const view1 = new DataView(data.buffer); 34 | // 35 | // const nodeCount = view1.getInt32(8, true) // 11 36 | // const resAndTankCount = view1.getInt32(12, true) // 2 37 | // const linkCount = view1.getInt32(16, true) // 13 38 | // const pumpCount = view1.getInt32(20, true) // 1 39 | // const valveCount = view1.getInt32(24, true) // 0 40 | // const reportingPeriods = view1.getInt32(data.byteLength - 12, true) // 25 41 | // 42 | // 43 | // const prologByteSize = 852 + (20 * nodeCount) + (36 * linkCount) + (8 * resAndTankCount) //1556 44 | // const energyUseByteSize = (28 * pumpCount) + 4 //32 45 | // const dynamicResultsByteSize = ((16 * nodeCount) + (32 * linkCount)) * (reportingPeriods) //14800 46 | // const EpilogByteSize = 28 //28 47 | // 48 | // const offsetNodeIDs = 884 49 | // const offsetLinkIDs = offsetNodeIDs + (32 * nodeCount) 50 | // const offsetNodeResults = offsetNodeIDs + (36 * nodeCount) + (52 * linkCount) + (8 * resAndTankCount) + (28 * pumpCount) + 4 51 | // const offsetLinkResults = 16 * nodeCount + offsetNodeResults 52 | // 53 | // const resultSize = 16 * nodeCount + 32 * linkCount 54 | // 55 | // const resultPeriod = 0 56 | // const nodeIndex = 0 57 | // 58 | // const demand1 = view1.getFloat32(offsetNodeResults + (resultSize * resultPeriod) + (4 * nodeIndex), true) 59 | // 60 | // expect(demand1).toEqual(0) 61 | // 62 | // 63 | //}) 64 | -------------------------------------------------------------------------------- /src/util/WebWorker/index.ts: -------------------------------------------------------------------------------- 1 | import Worker from "worker-loader!./EPANetWorker"; 2 | import { Calibration } from "../../components/ResultsProvider"; 3 | import ModelFeatureCollection, { 4 | LiveDataPoint 5 | } from "../../interfaces/ModelFeatureCollection"; 6 | import CalibrationActions from "../../interfaces/CalibrationActions"; 7 | import geojsonToEpanet from "../../util/GeojsonToEpanet"; 8 | import { EpanetResults } from "../../util/EpanetBinary"; 9 | 10 | const worker = new Worker(); 11 | 12 | interface LogResults { 13 | [key: string]: number[]; 14 | } 15 | 16 | interface WorkerQueue { 17 | working: boolean; 18 | queue?: { inp: string; ld: LiveDataPoint[] }; 19 | } 20 | const workerQueue: WorkerQueue = { 21 | working: false 22 | }; 23 | 24 | let _getResults: (n: LogResults) => void = n => {}; 25 | 26 | worker.addEventListener("message", message => { 27 | _getResults(message.data); 28 | if (workerQueue.queue) { 29 | const data = workerQueue.queue; 30 | workerQueue.queue = undefined; 31 | worker.postMessage(data); 32 | } else { 33 | workerQueue.working = false; 34 | } 35 | }); 36 | 37 | const epaWebWorker = { 38 | getResults: (setValue: (n: LogResults) => void) => { 39 | _getResults = setValue; 40 | }, 41 | requestWork: ( 42 | model: ModelFeatureCollection, 43 | data: Calibration[], 44 | liveDataPoints: LiveDataPoint[] 45 | ) => { 46 | const ca = calibrationActions(data); 47 | 48 | const epanetInp = geojsonToEpanet(model, ca); 49 | if (!workerQueue.working) { 50 | workerQueue.working = true; 51 | worker.postMessage({ inp: epanetInp, ld: liveDataPoints }); 52 | } else { 53 | workerQueue.queue = { inp: epanetInp, ld: liveDataPoints }; 54 | } 55 | } 56 | }; 57 | 58 | function calibrationActions(data: Calibration[]): CalibrationActions { 59 | return data.reduce( 60 | (prev, curr) => { 61 | const test = curr.actions.reduce( 62 | (prev, curr) => { 63 | return { 64 | ...prev, 65 | [curr.id]: { ...curr.action } 66 | }; 67 | }, 68 | {} 69 | ); 70 | 71 | return { 72 | ...prev, 73 | ...test 74 | }; 75 | }, 76 | {} 77 | ); 78 | } 79 | 80 | export default epaWebWorker; 81 | -------------------------------------------------------------------------------- /src/components/AboutTab/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from "react"; 2 | import Typography from "@material-ui/core/Typography"; 3 | 4 | const AboutTab: FunctionComponent<{}> = () => { 5 | return ( 6 | <> 7 | 12 | Calibrating The Model 13 | 14 | 15 | 16 | Current you can calibrate your model in two ways, either by using 17 | throttle valves or changing the roughness of pipes. 18 | 19 | 20 | 21 | To throttle a valve, zoom in on the map until you find a valve you wish 22 | to restrict. Clicking on the valve will add it to the list calibration 23 | actions on the left hand side, use the slider to change the restriction 24 | at this valve. 25 | 26 | 27 | 28 | To change the roughness on pipes, click 'Add Roughness Change 29 | Calibration' to expand the panel and find a table of all pipes within 30 | the model. Filter the table by material, year and size to the selection 31 | of pipes you would like to modifiy the roughness and then press create 32 | to make a new calibration action for these pipes. 33 | 34 | 35 | 40 | Contact Me 41 | 42 | 43 | 44 | Model Calibrate was created by Luke Butler of Matrado, a startup based 45 | in Toronto. 46 | 47 | 48 | Luke is a civil engineer, software developer and the co-founder of 49 | Matrado; he can be contacted on{" "} 50 | LinkedIn or by 51 | email at luke@matrado.ca 52 | 53 | 54 | ); 55 | }; 56 | 57 | export default AboutTab; 58 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | 20 | 21 | 25 | 26 | 30 | 31 | 40 | Model Calibrate 41 | 42 | 43 | 44 | 45 |
46 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/components/CenteredTabs/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent, FunctionComponent } from "react"; 2 | import { makeStyles } from "@material-ui/styles"; 3 | import Paper from "@material-ui/core/Paper"; 4 | import Tabs from "@material-ui/core/Tabs"; 5 | import Tab from "@material-ui/core/Tab"; 6 | import Toolbar from "@material-ui/core/Toolbar"; 7 | import Typography from "@material-ui/core/Typography"; 8 | import AppBar from "@material-ui/core/AppBar"; 9 | 10 | import CalibrateTab from "../CalibrateTab"; 11 | import SubModelTab from "../SubModelTab"; 12 | import AboutTab from "../AboutTab"; 13 | import ExportTab from "../ExportTab"; 14 | import ReactGA from "react-ga"; 15 | 16 | const TabContainer: FunctionComponent<{}> = ({ children }) => { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | const useStyles = makeStyles(theme => ({ 25 | root: { 26 | flexGrow: 1 27 | //backgroundColor: theme.palette.background.paper, 28 | } 29 | })); 30 | 31 | interface CenteredTabsProperties { 32 | isDemo: boolean; 33 | } 34 | 35 | const CenteredTabs: FunctionComponent = ({ 36 | isDemo 37 | }) => { 38 | const classes = useStyles(); 39 | const [value, setValue] = React.useState(0); 40 | 41 | function handleChange(vent: ChangeEvent<{}>, newValue: number) { 42 | setValue(newValue); 43 | const tabNames = ["Calibrate", "Sub Models", "About", "Export"]; 44 | ReactGA.event({ 45 | category: "Selected Tab", 46 | action: tabNames[newValue] 47 | }); 48 | } 49 | 50 | return ( 51 |
52 | 53 | 54 | 55 | Model Calibrate 56 | 57 | 58 | 59 | 60 | 61 | 62 | {!isDemo && } 63 | 64 | 65 | {value === 0 && } 66 | {value === 1 && } 67 | {value === 2 && } 68 | {value === 3 && } 69 |
70 | ); 71 | }; 72 | 73 | export default CenteredTabs; 74 | -------------------------------------------------------------------------------- /src/components/TimeSeriesChart/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent, useState } from "react"; 2 | import { VictoryChart, VictoryLine, VictoryLabel } from "victory"; 3 | import { ModelInfoSetting } from "../ModelInfo"; 4 | import { debug } from "util"; 5 | 6 | const calcRms = (tsv1: number[], tsv2: number[]): number => { 7 | const squaredDifference = tsv1.reduce((acc, value, index) => { 8 | return acc + Math.pow(value - tsv2[index], 2); 9 | }, 0); 10 | 11 | return Math.sqrt(squaredDifference / 96); 12 | }; 13 | 14 | type TimeSeriesChartProps = { 15 | title: string; 16 | timeseriesData: number[]; 17 | observered: number[]; 18 | timesteps: Date[]; 19 | }; 20 | 21 | const TimeSeriesChart: FunctionComponent = ({ 22 | title, 23 | timeseriesData, 24 | timesteps, 25 | observered 26 | }) => { 27 | const avgData = 28 | timeseriesData.reduce((p, c) => p + c, 0) / timeseriesData.length; 29 | const multipler = avgData >= 0 ? 1 : -1; 30 | 31 | const data = timesteps.map((timestep, i) => ({ 32 | x: timestep, 33 | y: timeseriesData[i] * multipler 34 | })); 35 | const observeredData = timesteps.map((timestep, i) => ({ 36 | x: timestep, 37 | y: observered[i] 38 | })); 39 | 40 | const max = Math.max(...timeseriesData, ...observered); 41 | const min = Math.min(...timeseriesData, ...observered); 42 | const domainMax = Math.max(Math.abs(max), Math.abs(min)); 43 | const domainMin = Math.min(Math.abs(max), Math.abs(min)); 44 | const diff = domainMax - domainMin; 45 | 46 | return ( 47 |
48 | 54 | 62 | 69 | 75 | 76 |
77 | ); 78 | }; 79 | 80 | export default TimeSeriesChart; 81 | -------------------------------------------------------------------------------- /src/util/reproject/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FeatureCollection, 3 | Geometries, 4 | Properties, 5 | Feature 6 | } from "@turf/helpers"; 7 | import { featureReduce, coordEach } from "@turf/meta"; 8 | import clone from "@turf/clone"; 9 | import { featureCollection } from "@turf/helpers"; 10 | import proj4 from "proj4"; 11 | 12 | export function reprojectFeatureCollection( 13 | geoJson: FeatureCollection, 14 | fromProject: string 15 | ): FeatureCollection { 16 | const initialValue: Array = []; 17 | 18 | const features = featureReduce( 19 | geoJson, 20 | function(previousValue, currentFeature, featureIndex) { 21 | const featureReproject = Object.assign( 22 | {}, 23 | currentFeature, 24 | reprojectFeature(currentFeature, fromProject) 25 | ); 26 | return previousValue.concat(featureReproject); 27 | }, 28 | initialValue 29 | ); 30 | 31 | return featureCollection(features); 32 | } 33 | 34 | export function reprojectFeature( 35 | feature: Feature, 36 | fromProject: string 37 | ): Feature { 38 | const newFeature = clone(feature); 39 | 40 | coordEach(newFeature, function(currentCoord) { 41 | const newCoord = reprojectCoord(currentCoord, fromProject); 42 | currentCoord[0] = newCoord[0]; 43 | currentCoord[1] = newCoord[1]; 44 | }); 45 | 46 | // TODO: Check again later, there is a bug in Mapbox GL JS where if the last two coords 47 | // are duplicates then it won't draw the line at high zooms. We will check here and remove 48 | // them if they exist 49 | // https://github.com/mapbox/mapbox-gl-js/issues/5171 50 | 51 | if ( 52 | newFeature.geometry && 53 | newFeature.geometry.type === "LineString" && 54 | newFeature.geometry.coordinates.length > 2 55 | ) { 56 | const totalCoords = newFeature.geometry.coordinates.length; 57 | const x1 = newFeature.geometry.coordinates[totalCoords - 1][0]; 58 | const x2 = newFeature.geometry.coordinates[totalCoords - 2][0]; 59 | const y1 = newFeature.geometry.coordinates[totalCoords - 1][1]; 60 | const y2 = newFeature.geometry.coordinates[totalCoords - 2][1]; 61 | if (x1 == x2 && y1 == y2) { 62 | newFeature.geometry.coordinates.pop(); 63 | } 64 | } 65 | 66 | return newFeature; 67 | } 68 | 69 | export function reprojectCoord(coord: number[], fromProject: string): number[] { 70 | //@ts-ignore 71 | return proj4(fromProject, proj4("EPSG:4326"), coord); 72 | } 73 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/Landing/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from "react"; 2 | import CircularProgress from "@material-ui/core/CircularProgress"; 3 | import Button from "@material-ui/core/Button"; 4 | import { 5 | WithStyles, 6 | withStyles, 7 | createStyles, 8 | Theme 9 | } from "@material-ui/core/styles"; 10 | import ExtractionGuide from "../ExtractionGuide"; 11 | import "./index.css"; 12 | 13 | const styles = (theme: Theme) => 14 | createStyles({ 15 | button: { 16 | //margin: theme.spacing() 17 | }, 18 | input: { 19 | display: "none" 20 | } 21 | }); 22 | 23 | interface LandingProperties extends WithStyles { 24 | isLoading: boolean; 25 | onLoadDemo: () => void; 26 | } 27 | 28 | const Landing: FunctionComponent = ({ 29 | isLoading, 30 | onLoadDemo, 31 | classes 32 | }) => { 33 | return ( 34 |
35 |
36 |

Matrado

37 |

Model Calibrate

38 | 39 | {isLoading ? ( 40 | 41 | ) : ( 42 | <> 43 |

44 | Share and calibrate models in the browser 45 |

46 |

47 | Model Calibrate is under active development, this version is an 48 | early preview. 49 |

50 | 51 |

52 | Feature requests and issues can be logged on{" "} 53 | 54 | Github 55 | 56 | , contact me on{" "} 57 | LinkedIn or 58 | email - luke@matrado.ca 59 |

60 | 61 |
62 |

Drop model extract here

63 |

64 | All data is proccessed client side, no model data sent to the 65 | server. 66 |

67 | 74 |
75 |
76 | 77 |
78 | 79 | )} 80 |
81 |
82 |
83 | ); 84 | }; 85 | 86 | export default withStyles(styles)(Landing); 87 | -------------------------------------------------------------------------------- /src/components/DropDownSelect/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from "react"; 2 | import { createStyles, Theme } from "@material-ui/core/styles"; 3 | import { makeStyles } from "@material-ui/styles"; 4 | import Input from "@material-ui/core/Input"; 5 | import InputLabel from "@material-ui/core/InputLabel"; 6 | import MenuItem from "@material-ui/core/MenuItem"; 7 | import FormControl from "@material-ui/core/FormControl"; 8 | import ListItemText from "@material-ui/core/ListItemText"; 9 | import Select from "@material-ui/core/Select"; 10 | import Checkbox from "@material-ui/core/Checkbox"; 11 | 12 | const useStyles = makeStyles((theme: Theme) => 13 | createStyles({ 14 | root: { 15 | display: "flex", 16 | flexWrap: "wrap" 17 | }, 18 | formControl: { 19 | minWidth: 80, 20 | width: "90%", 21 | maxWidth: 120 22 | }, 23 | chips: { 24 | display: "flex", 25 | flexWrap: "wrap" 26 | }, 27 | chip: { 28 | margin: 2 29 | }, 30 | noLabel: {} 31 | }) 32 | ); 33 | 34 | const ITEM_HEIGHT = 48; 35 | const ITEM_PADDING_TOP = 8; 36 | const MenuProps = { 37 | PaperProps: { 38 | style: { 39 | maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, 40 | width: 250 41 | } 42 | } 43 | }; 44 | 45 | 46 | type MultipleSelectProps = { 47 | list: string[]; 48 | filteredList: string[]; 49 | handleUpdate: (event: React.ChangeEvent<{ value: unknown }>) => void; 50 | isNumber: boolean; 51 | }; 52 | 53 | const MultipleSelect: FunctionComponent = ({ 54 | list, 55 | filteredList, 56 | handleUpdate, 57 | isNumber 58 | }) => { 59 | const classes = useStyles(); 60 | 61 | return ( 62 |
63 | 64 | 87 | 88 |
89 | ); 90 | }; 91 | 92 | export default MultipleSelect; 93 | -------------------------------------------------------------------------------- /src/components/ModelDropZone/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useCallback, FunctionComponent } from "react"; 2 | import { useDropzone } from "react-dropzone"; 3 | import { FeatureCollection, Geometries, Properties } from "@turf/helpers"; 4 | import ModelFeatureCollection from "../../interfaces/ModelFeatureCollection"; 5 | import { geojsonType } from "@turf/invariant"; 6 | 7 | const overlayStyle = { 8 | position: "absolute", 9 | top: 0, 10 | right: 0, 11 | bottom: 0, 12 | left: 0, 13 | padding: "2.5em 0", 14 | background: "rgba(0,0,0,0.5)", 15 | textAlign: "center", 16 | color: "#fff" 17 | } as React.CSSProperties; 18 | 19 | const baseStyle = { 20 | position: "relative" 21 | } as React.CSSProperties; 22 | 23 | const activeStyle = { 24 | borderStyle: "solid", 25 | borderColor: "#6c6", 26 | backgroundColor: "#eee" 27 | }; 28 | 29 | const acceptStyle = { 30 | borderStyle: "solid", 31 | borderColor: "#00e676" 32 | }; 33 | 34 | const rejectStyle = { 35 | borderStyle: "solid", 36 | borderColor: "#ff1744" 37 | }; 38 | 39 | type ModelDropZone = { 40 | onDroppedJson: (file: ModelFeatureCollection, filename: string) => void; 41 | }; 42 | 43 | const ModelDropZone: FunctionComponent = ({ 44 | onDroppedJson, 45 | children 46 | }) => { 47 | const onDrop = useCallback((acceptedFiles: File[]) => { 48 | if (acceptedFiles[0] !== undefined) { 49 | const reader = new FileReader(); 50 | reader.onload = () => { 51 | if (typeof reader.result === "string") { 52 | const geoJson: ModelFeatureCollection = JSON.parse(reader.result); 53 | try { 54 | geojsonType(geoJson, "FeatureCollection", "Drop Zone"); 55 | const filename = acceptedFiles[0].name.replace(/\.[^/.]+$/, ""); 56 | onDroppedJson(geoJson, filename); 57 | } catch (e) { 58 | console.log(e); 59 | // TODO: Handle if dropped bad JSON data 60 | } 61 | } 62 | }; 63 | 64 | reader.readAsText(acceptedFiles[0]); 65 | } 66 | }, []); 67 | 68 | const { 69 | acceptedFiles, 70 | getRootProps, 71 | isDragActive, 72 | isDragAccept, 73 | isDragReject 74 | } = useDropzone({ accept: "application/json", multiple: false, onDrop }); 75 | 76 | const style = useMemo( 77 | () => ({ 78 | ...baseStyle, 79 | ...(isDragActive ? activeStyle : {}), 80 | ...(isDragAccept ? acceptStyle : {}), 81 | ...(isDragReject ? rejectStyle : {}) 82 | }), 83 | [isDragActive, isDragReject] 84 | ); 85 | 86 | return ( 87 |
88 | {isDragActive &&
Drop files here
} 89 | {children} 90 |
91 | ); 92 | }; 93 | 94 | export default ModelDropZone; 95 | -------------------------------------------------------------------------------- /src/components/AppMaterialUi/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { FunctionComponent } from "react"; 2 | import { 3 | WithStyles, 4 | withStyles, 5 | createStyles, 6 | Theme 7 | } from "@material-ui/core/styles"; 8 | import Drawer from "@material-ui/core/Drawer"; 9 | import CssBaseline from "@material-ui/core/CssBaseline"; 10 | import AppBar from "@material-ui/core/AppBar"; 11 | import Toolbar from "@material-ui/core/Toolbar"; 12 | import List from "@material-ui/core/List"; 13 | import Typography from "@material-ui/core/Typography"; 14 | import Divider from "@material-ui/core/Divider"; 15 | import ListItem from "@material-ui/core/ListItem"; 16 | import ListItemText from "@material-ui/core/ListItemText"; 17 | import ModelRunWebWorkerButton from "../ModelRunWebWorkerButton"; 18 | 19 | import { ResultsContext } from "../ResultsProvider"; 20 | 21 | import MapView from "../MapView"; 22 | import CenteredTabs from "../CenteredTabs"; 23 | import UpdateScriptNotification from "../UpdateScriptNotification"; 24 | 25 | const drawerWidth = 700; 26 | 27 | const styles = (theme: Theme) => ({ 28 | root: { 29 | display: "flex" 30 | }, 31 | drawer: { 32 | width: drawerWidth, 33 | flexShrink: 0 34 | }, 35 | drawerPaper: { 36 | width: drawerWidth 37 | }, 38 | toolbar: theme.mixins.toolbar, 39 | content: { 40 | flexGrow: 1, 41 | backgroundColor: theme.palette.background.default, 42 | padding: theme.spacing(0) 43 | } 44 | }); 45 | 46 | interface AppMaterialUiProperties extends WithStyles { 47 | isDemo: boolean; 48 | version: number; 49 | } 50 | 51 | const AppMaterialUi: FunctionComponent = ({ 52 | isDemo, 53 | version, 54 | classes 55 | }) => { 56 | return ( 57 |
58 | {!isDemo && (version < 20191015 || typeof version === "undefined") && ( 59 | 60 | )} 61 | 62 | 70 | 71 | 72 |
73 | 74 | {( 75 | //@ts-ignore 76 | results 77 | ) => ( 78 | 84 | )} 85 | 86 |
87 |
88 | ); 89 | }; 90 | 91 | export default withStyles(styles)(AppMaterialUi); 92 | -------------------------------------------------------------------------------- /src/components/App/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, ChangeEvent, useState, useMemo } from "react"; 2 | import AppMaterial from "../AppMaterialUi"; 3 | import { ResultsProvider } from "../ResultsProvider"; 4 | import ModelDropZone from "../ModelDropZone"; 5 | import Landing from "../Landing"; 6 | import ModelFeatureCollection, { 7 | ModelLiveData 8 | } from "../../interfaces/ModelFeatureCollection"; 9 | import liveDataReader from "../../util/LiveDataReader"; 10 | import ReactGA from "react-ga"; 11 | 12 | ReactGA.initialize("UA-65873036-5"); 13 | ReactGA.pageview(window.location.pathname + window.location.search); 14 | 15 | var json = require("../../data/Example.json"); 16 | const model: ModelFeatureCollection = json as ModelFeatureCollection; 17 | 18 | const date = new Date(Date.UTC(2018, 0, 31)); 19 | const liveDataTemp = liveDataReader(model, 96); 20 | const blankLiveData: ModelLiveData = { liveDataPoints: [], sensorData: {} }; 21 | 22 | function App() { 23 | //const [modelResults, setModelResults] = useState(blankModel.logResults) 24 | const [modelJson, setModelJson] = useState(); 25 | const [fileName, setFileName] = useState("Demo"); 26 | const [liveData, setLiveData] = useState(blankLiveData); 27 | const [isDemo, setIsDemo] = useState(false); 28 | const [dragDropCount, setDragDropCount] = useState(0); 29 | 30 | const testModelJson = (model2: ModelFeatureCollection, filename: string) => { 31 | //pass along this function to ModelDropZone 32 | //which will then updae the state on this component 33 | //we will then pass this as a prop to the results provider 34 | //We could have a landing page here to load a model in the future 35 | 36 | ReactGA.event({ 37 | category: "Model", 38 | action: "Loaded User Model" 39 | }); 40 | 41 | setModelJson(model2); 42 | setFileName(filename); 43 | setLiveData(liveDataReader(model2, 96)); 44 | setDragDropCount(prevDragDropCount => prevDragDropCount + 1); 45 | }; 46 | 47 | const loadDemo = () => { 48 | ReactGA.event({ 49 | category: "Model", 50 | action: "Loaded Demo" 51 | }); 52 | 53 | setIsDemo(true); 54 | setModelJson(json); 55 | setLiveData(liveDataReader(json, 96)); 56 | setDragDropCount(prevDragDropCount => prevDragDropCount + 1); 57 | }; 58 | 59 | console.log("App Render"); 60 | 61 | return ( 62 | 63 | {modelJson ? ( 64 | 70 | 74 | 75 | ) : ( 76 | 77 | )} 78 | 79 | ); 80 | } 81 | 82 | export default App; 83 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebook/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(inputPath, needsSlash) { 15 | const hasSlash = inputPath.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return inputPath.substr(0, inputPath.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${inputPath}/`; 20 | } else { 21 | return inputPath; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right