├── 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 |
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 | {
24 | results.setSubModel(sub);
25 | }}
26 | >
27 | {sub}
28 |
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 | You need to enable JavaScript to run this app.
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 |
72 | Load Demo Model
73 |
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 | }
69 | renderValue={selected =>
70 | (selected as string[])
71 | .map(s => {
72 | return isNumber ? parseFloat(s).toFixed(1) : s;
73 | })
74 | .join(", ")
75 | }
76 | MenuProps={MenuProps}
77 | >
78 | {list.map(name => (
79 |
80 | -1} />
81 |
84 |
85 | ))}
86 |
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