From 84d6700b7404d8576a39af7ff33c52d4362f3cab Mon Sep 17 00:00:00 2001 From: Zack Malkmus <112013308+zmalkmus@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:29:08 -0400 Subject: [PATCH] AGVis Update 4.0 (#72) * Legend.js * Window.js * Window.js * Updating Window.js comments * Updating CanvasLayer.js comments * Added CommunicationLayer.js header comment. * Overhauling developer comments * Updating ContMulti.js comments * Dev comments * Dev comments * Dev comments * LayerControl.js Comments * CanvasLayer comments * ContMulti.js comments * NDArray comments * Fixed header comment formatting * Fixed header comment formatting * PlaybackControl.js Comments * Updating PlaybackControl.js comments * PlayMulti.js comments * PlayMulti.js comments * SearcLayer.js Comments * TimeBox.js Comments * TimeBox.js Comments * TopMulti.js comments * TopMulti.js comments * Finishing touches * Finishing touches * Added windows support for the agvis run command * Updated usage documentation for Windows * Setting up tests folder * Runs server start command from the app.py directory * basic test setup * updated requirements * updating requirements * Initial restructure of AGVis backend * AGVis backend restructure * Finished backend restructure. Added dev mode. * fixed typo * fixed typo in go.sh * Updated agvis repository Docker uses * Fixed agvis pip install in Docker container * Added shell=False to subprocess * Changed subprocess command for testing * Disabled running from directory. Currently breaks Docker. * reenabled popen for subprocess * reenabled popen for subprocess * Fixed Docker issue. Added ability to build different branches using ./go.sh build * removed ! from go.sh * Removed all syntax errors relating to an extra ! at the end * Cleaned up object oriented implementation of flask app * Testing Web App * Writing tests * tests * conftest * dime testing * Added header comments. Finished initial dime_client test for agvis * Updated test name * disabled docker test, looking into js testing * adding tests for web * client tests * removed redundant test * Added header comment and removed unused imports * Created docker test file * debug ./go.sh file not being found * debug ./go.sh file not being found * debug ./go.sh file not being found * Added checking out of code * updated go.sh script to include docker testing * removed interactive tag * Disabling basic dime testing. Will keep the file in case it is needed in the future, but testing it here is has both project design and bug problems. * added PR unit test protection for master * Custom Flask Configurations for AGVis * Include pip upgrade in Dockerfile * Update citation * Update copyright * Updated max zoom * Rename test.yml to pythonapp.yml Conform with rest of LTB. * Update pythonapp.yml * Update dependency versions on pythonapp.yml * Added linting with flake8 * Remove flake 8 until its recommended fixes are corrected. * Publish Python to PyPI in pythonapp.yml * Fixed broken tests. Added agvis selftest functionality * Fixing last unittest * Removing problematic test working locally but not on github * Cleaning up code for codacy --------- Co-authored-by: Jinning Wang --- .github/workflows/pythonapp.yml | 61 ++ CITATION.bib | 10 + Dockerfile | 14 +- README.md | 6 +- agvis/__init__.py | 2 +- agvis/app.py | 45 -- agvis/cli.py | 4 +- agvis/flask_configurations.py | 21 + agvis/main.py | 35 +- agvis/static/js/CanvasLayer.js | 85 ++ agvis/static/js/CommunicationLayer.js | 70 ++ agvis/static/js/ConfigControl.js | 144 +++- agvis/static/js/ContMulti.js | 171 +++- agvis/static/js/ContourLayer.js | 152 +++- agvis/static/js/LayerControl.js | 799 ++++++++++--------- agvis/static/js/Legend.js | 52 +- agvis/static/js/NDArray.js | 85 +- agvis/static/js/PlayMulti.js | 107 ++- agvis/static/js/PlaybackControl.js | 77 +- agvis/static/js/SearchLayer.js | 55 +- agvis/static/js/TimeBox.js | 94 ++- agvis/static/js/TopMulti.js | 306 +++++-- agvis/static/js/TopologyLayer.js | 68 ++ agvis/static/js/Window.js | 182 ++++- agvis/static/js/ZoneLayer.js | 54 ++ agvis/web.py | 107 +++ docs/source/conf.py | 4 +- docs/source/getting_started/copyright.rst | 2 +- docs/source/getting_started/install.rst | 4 + docs/source/getting_started/tutorial/cli.rst | 5 + go.sh | 24 +- tests/__init__.py | 0 tests/_test_dime_client.py | 81 ++ tests/conftest.py | 11 + tests/test_cli.py | 74 ++ tests/test_web.py | 24 + 36 files changed, 2305 insertions(+), 730 deletions(-) create mode 100644 .github/workflows/pythonapp.yml create mode 100644 CITATION.bib delete mode 100644 agvis/app.py create mode 100644 agvis/flask_configurations.py create mode 100644 agvis/web.py create mode 100644 tests/__init__.py create mode 100644 tests/_test_dime_client.py create mode 100644 tests/conftest.py create mode 100644 tests/test_cli.py create mode 100644 tests/test_web.py diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml new file mode 100644 index 0000000..1863358 --- /dev/null +++ b/.github/workflows/pythonapp.yml @@ -0,0 +1,61 @@ +name: Python application + +on: [push, pull_request] + +jobs: + build: + name: Build distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.8" + - name: Install pypa/build + run: >- + python3 -m + pip install + build + --user + - name: Build a binary wheel and a source tarball + run: python3 -m build + - name: Store the distribution packages + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + test: + name: Run test suite + needs: + - build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Build the Docker image + run: ./go.sh build + - name: Run tests + run: ./go.sh run_tests + # Developer note: Fix flake8 errors before uncommenting here. + # - name: Lint with flake8 for pull requests + # if: github.event_name == 'pull_request' + # run: | + # pip install flake8 # specify flake8 to avoid unknown error + # # stop the build if there are Python syntax errors or undefined names + # flake8 . + + publish-to-pypi: + name: Publish Python distribution to PyPI + if: github.ref == 'refs/heads/master' && github.event_name == 'push' + needs: + - build + - test + runs-on: ubuntu-latest + steps: + - name: Publish Python dist to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_PASSWORD }} diff --git a/CITATION.bib b/CITATION.bib new file mode 100644 index 0000000..c74711e --- /dev/null +++ b/CITATION.bib @@ -0,0 +1,10 @@ +@INPROCEEDINGS{10318583, + author={Parsly, Nicholas and Wang, Jinning and West, Nick and Zhang, Qiwei and Cui, Hantao and Li, Fangxing}, + booktitle={2023 North American Power Symposium (NAPS)}, + title={DiME and AGVis: A Distributed Messaging Environment and Geographical Visualizer for Large-Scale Power System Simulation}, + year={2023}, + volume={}, + number={}, + pages={1-5}, + keywords={Power system simulation;Data visualization;Distributed databases;Nonhomogeneous media;Real-time systems;Power systems;North America;Power grid;Open-source software;Large-scale system;High-concurrency Data;Geovisualization;Digital twin}, + doi={10.1109/NAPS58826.2023.10318583}} diff --git a/Dockerfile b/Dockerfile index dc8a21d..68d47d7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ # Start with a base Python 3.10 image from the Debian Buster distribution FROM python:3.10-buster AS base +ARG BRANCH_NAME # Switch to the root user and set the working directory to /root USER root @@ -18,14 +19,12 @@ RUN apt update \ libev-dev \ && rm -rf /var/lib/apt/lists/* +RUN python3 -m pip install --upgrade pip + # Install the necessary Python packages RUN python3 -m pip install \ kvxopt \ git+https://github.com/cuihantao/andes.git@develop \ - --no-cache-dir \ - && python3 -m pip install \ - git+https://github.com/zmalkmus/agvisdev.git \ - # git+https://github.com/CURENT/agvis.git \ --no-cache-dir # Create a new user named 'cui' and a work directory @@ -50,6 +49,11 @@ RUN git clone https://github.com/CURENT/dime.git && \ rm -rf dime \ && rm -rf /tmp/dime +RUN git clone --single-branch --branch unittesting https://github.com/CURENT/agvis.git && \ + cd agvis && \ + python3 -m pip install -e . && \ + cd .. + # Switch to the 'cui' user and set the working directory to the new user's work directory USER cui WORKDIR /home/cui/work @@ -62,4 +66,4 @@ COPY . . # Set the entrypoint and command for the container ENTRYPOINT [] -CMD [] +CMD [] \ No newline at end of file diff --git a/README.md b/README.md index b0e17d6..f6b56b5 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,13 @@ AGVis is currently under active development. Use the following resources to get + Report bugs or issues by submitting a [GitHub issue][GitHub issues] + Submit contributions using [pull requests][GitHub pull requests] + Read release notes highlighted [here][release notes] -+ Check out and cite our [paper][arxiv paper] ++ Check out and cite our [paper][naps paper] # Citing AGVis If you use AGVis for research or consulting, please cite the following publications in your publication: -> Parsly, N., Wang, J., West, N., Zhang, Q., Cui, H., & Li, F. (2022). "DiME and AGVIS A Distributed Messaging Environment and Geographical Visualizer for Large-scale Power System Simulation". arXiv. https://doi.org/https://arxiv.org/abs/2211.11990v1 +> N. Parsly, J. Wang, N. West, Q. Zhang, H. Cui and F. Li, "DiME and AGVis: A Distributed Messaging Environment and Geographical Visualizer for Large-Scale Power System Simulation," 2023 North American Power Symposium (NAPS), Asheville, NC, USA, 2023, pp. 1-5, doi: 10.1109/NAPS58826.2023.10318583. Please refer as **LTB AGVis** for the first occurence and then refer as **AGVis**. @@ -70,7 +70,7 @@ AGVis is licensed under [GPL v3 License](./LICENSE) [readthedocs]: https://agvis.readthedocs.io [advanced usage]: https://agvis.readthedocs.io/en/latest/usage/index.html [release notes]: https://agvis.readthedocs.io/en/latest/release-notes.html -[arxiv paper]: https://arxiv.org/abs/2211.11990 +[naps paper]: https://ieeexplore.ieee.org/document/10318583 [tutorial]: https://agvis.readthedocs.io/en/latest/getting_started/tutorial/index.html [LTB Repository]: https://github.com/CURENT [Visualization Gallery]: https://ltb.readthedocs.io/projects/agvis/en/latest/getting_started/testcases.html#visualization-gallery diff --git a/agvis/__init__.py b/agvis/__init__.py index 7adf89a..b6620ae 100644 --- a/agvis/__init__.py +++ b/agvis/__init__.py @@ -2,7 +2,7 @@ __version__ = _version.get_versions()['version'] from agvis.main import config_logger # NOQA -from agvis.app import run_app +from agvis.web import AgvisWeb __author__ = 'Nicholas West, Nicholas Parsly, Zack Malkmus, and Jinning Wang' diff --git a/agvis/app.py b/agvis/app.py deleted file mode 100644 index c5cdd5b..0000000 --- a/agvis/app.py +++ /dev/null @@ -1,45 +0,0 @@ -import os -import subprocess -from flask import Flask, render_template, send_from_directory -import requests - -# Create the Flask application -app = Flask(__name__) -app.requests_session = requests.Session() - -def run_app(app_module, host='localhost', port=8810, workers=1): - try: - # Print out the URL to access the application - print(f"AGVis will serve static files from directory \"{os.path.join(os.getcwd(), 'agvis/static')}\"") - print(f"at the URL http://{host}:{port}. Open your web browser and navigate to the URL to access the application.") - print("\nStarting AGVis... Press Ctrl+C to stop.\n") - - # Build the command to start the application - command = [ - 'gunicorn', - '-b', f'{host}:{port}', - '-w', str(workers), - app_module - ] - - # Start the application - with app.requests_session as session: - subprocess.run(command, check=True) - except KeyboardInterrupt: - print('\nAGVis has been stopped. You may now close the browser.') - except Exception as e: - print(f'An unexpected error has occured while trying to start AGVis: {e}') - -# Serve index.html -@app.route('/') -def index(): - return render_template('index.html') - -# Serve static files -@app.route('/', methods=['GET']) -def static_proxy(path): - return send_from_directory('static', path) - -# Run the application -if __name__ == '__main__': - run_app() \ No newline at end of file diff --git a/agvis/cli.py b/agvis/cli.py index aabcc41..876744d 100644 --- a/agvis/cli.py +++ b/agvis/cli.py @@ -43,6 +43,7 @@ def create_parser(): run = sub_parsers.add_parser('run') run.add_argument('--host', default='127.0.0.1', help='Host to bind the server (default: 127.0.0.1)') run.add_argument('--port', default=8810, type=int, help='Port to bind the server (default: 8810)') + run.add_argument('--dev', default=False, type=bool, help='Run AGVis in development mode (default: False)') # run.add_argument('--static', default=None, help='Static path to serve (default: None)') misc = sub_parsers.add_parser('misc') @@ -54,8 +55,6 @@ def create_parser(): selftest = sub_parsers.add_parser('selftest', aliases=command_aliases['selftest']) - demo = sub_parsers.add_parser('demo') # NOQA - return parser @@ -107,7 +106,6 @@ def main(): # Run the command if args.command is None: parser.parse_args(sys.argv.append('--help')) - else: cmd = args.command for fullcmd, aliases in command_aliases.items(): diff --git a/agvis/flask_configurations.py b/agvis/flask_configurations.py new file mode 100644 index 0000000..d7742fe --- /dev/null +++ b/agvis/flask_configurations.py @@ -0,0 +1,21 @@ +# ================================================================================ +# File Name: flask_configurations.py +# Author: Zack Malkmus +# Date: 1/18/2024 (created) +# Description: Configuration file for FLASK APP +# ================================================================================ + +class DefaultConfig(object): + DEBUG = False + TESTING = False + CSRF_ENABLED = True + +class ProductionConfig(DefaultConfig): + DEBUG = False + +class DevelopmentConfig(DefaultConfig): + DEVELOPMENT = True + DEBUG = True + +class TestingConfig(DefaultConfig): + TESTING = True \ No newline at end of file diff --git a/agvis/main.py b/agvis/main.py index ba94bb4..9d3460f 100644 --- a/agvis/main.py +++ b/agvis/main.py @@ -8,14 +8,17 @@ import pprint import logging import tempfile - +import subprocess from ._version import get_versions - from andes.utils.misc import is_interactive import agvis +from agvis.web import AgvisWeb -logger = logging.getLogger(__name__) +agvis_web = AgvisWeb() +app = agvis_web.app + +logger = logging.getLogger(__name__) def config_logger(stream_level=logging.INFO, *, stream=True, @@ -171,8 +174,8 @@ def remove_output(recursive=False): def run(filename='', input_path='', verbose=20, - host='localhost', port=8810, socket_path=None, - static=None, + host='localhost', port=8810, dev=False, + socket_path=None, static=None, **kwargs): """ Entry point to run AGVis. @@ -209,7 +212,7 @@ def run(filename='', input_path='', verbose=20, cases = _find_cases(filename, input_path) #NOQA # Run the flask web app - agvis.app.run_app("agvis.app:app", host=host, port=port) + agvis_web.run(host=host, port=port, dev=dev) return True @@ -266,7 +269,7 @@ def print_license(): print(f""" AGVis version {agvis.__version__} - Copyright (c) 2020-2023 Nick West, Nicholas Parsly, Jinning Wang + Copyright (c) 2020-2024 Nick West, Nicholas Parsly, Jinning Wang This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -298,10 +301,6 @@ def misc(show_license=False, clean=True, recursive=False, remove_output(recursive) return - if demo is True: - demo(**kwargs) - return - if version is True: versioninfo() return @@ -313,15 +312,11 @@ def selftest(**kwargs): """ TODO: Run unit tests. """ - logger.warning("Tests have not been implemented") - - -def demo(**kwargs): - """ - TODO: show some demonstrations from CLI. - """ - logger.warning("Demos have not been implemented") - + # logger.warning("Tests have not been implemented") + logger.info('Running tests...') + path = os.path.dirname(os.path.abspath(__file__)) + test_path = os.path.join(path, '../tests') + subprocess.run(['pytest', '-v', test_path]) def versioninfo(): """ diff --git a/agvis/static/js/CanvasLayer.js b/agvis/static/js/CanvasLayer.js index 7e24ff2..8ff12db 100644 --- a/agvis/static/js/CanvasLayer.js +++ b/agvis/static/js/CanvasLayer.js @@ -1,3 +1,31 @@ +/* **************************************************************************************** + * File Name: CanvasLayer.js + * Authors: Nicholas West, Nicholas Parsley + * Date: 9/28/2023 (last modified) + * + * Description: CanvasLayer class. The CanvasLayer is an intermediary class, extending + * from Leaflet’s Layer class and being extended from by most of the other + * Layer-type classes in AGVis.Contains basic functions for rendering, + * removing, and adding layers to the map. + * + * API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/modeling/canvas.html + * ****************************************************************************************/ + +/** + * @class CanvasLayer + * @extends {L.Layer} + * + * @param {Object} options + * + * @var {Map} _map - The leaflet map passed from the Window. + * @var {HTML Canvas Element} _canvas - The canvas element that will be drawn on. + * @var {Boolean} _needsProjectionUpdate - Determines whether the Layer’s projection needs to be updated. + * @var {Point} size - Represents the current size of the map in pixels. + * @var {LatLngBounds} bounds - Represents the geographical bounds of the map. + * @var {Function} project - The latLngToContainerPoint function specifically for CanvasLayer._map. + * + * @returns {CanvasLayer} + */ L.CanvasLayer = L.Layer.extend({ options: { @@ -7,10 +35,25 @@ L.CanvasLayer = L.Layer.extend({ repeat: false, }, + /** + * Initializes the canvas layer with options + * + * @memberof CanvasLayer + * @param {Object} options + * @returns + */ initialize(options) { L.Util.setOptions(this, options); }, + /** + * Creates the canvas element and appends it to the map. + * Establishes how resizing works for the Layer. + * + * @memberof CanvasLayer + * @param {Object} map + * @returns + */ onAdd(map) { this._map = map; this._canvas = L.DomUtil.create('canvas', 'leaflet-canvas-layer'); @@ -30,6 +73,12 @@ L.CanvasLayer = L.Layer.extend({ this._reset(); }, + /** + * Redraws the canvas layer + * + * @memberof CanvasLayer + * @returns + */ redraw() { if (!this._frame) { this._frame = L.Util.requestAnimFrame(this._redraw, this); @@ -37,6 +86,13 @@ L.CanvasLayer = L.Layer.extend({ return this; }, + /** + * Clean up for the canvas layer + * + * @memberof CanvasLayer + * @param {Object} map + * @returns + */ onRemove(map) { this._map.getPane('overlayPane').removeChild(this._canvas); @@ -47,16 +103,37 @@ L.CanvasLayer = L.Layer.extend({ this._map = null; }, + /** + * Adds the canvas layer to the map + * + * @memberof CanvasLayer + * @param {Object} map + * @returns {Object} CanvasLayer object + */ addTo(map) { map.addLayer(this); return this; }, + /** + * Resizes the canvas layer from a resize event + * + * @memberof CanvasLayer + * @param {Object} resizeEvent + * @returns + */ _resize(resizeEvent) { this._canvas.width = this._map.getSize().x; this._canvas.height = this._map.getSize().y; }, + /** + * Initially starts drawing the Layer. Sets the position of the canvas + * and requests a projection update. + * + * @memberof CanvasLayer + * @returns + */ _reset() { var topLeft = this._map.containerPointToLayerPoint([0, 0]); L.DomUtil.setPosition(this._canvas, topLeft); @@ -64,6 +141,14 @@ L.CanvasLayer = L.Layer.extend({ this.redraw(); }, + /** + * Calls the rendering function for a given Layer if it has one. + * Sets up the variables mentioned above that are passed to the Topology-type + * and Contour-type Layers. + * + * @memberof CanvasLayer + * @returns + */ _redraw() { const size = this._map.getSize(); const bounds = this._map.getBounds(); diff --git a/agvis/static/js/CommunicationLayer.js b/agvis/static/js/CommunicationLayer.js index 3b89426..d161d6e 100644 --- a/agvis/static/js/CommunicationLayer.js +++ b/agvis/static/js/CommunicationLayer.js @@ -1,3 +1,47 @@ +/* **************************************************************************************** + * File Name: CommunicationLayer.js (Deprecated) + * Authors: Nicholas West + * Date: 9/15/2023 (last modified) + * + * Description: It appears that it was originally going to draw substantially more + * lines between points compared to the TopologyLayer. The color and + * curve of these lines would be determined by devices associated with + * the nodes and their capacities for transferring and receiving data. + * + * Warning: This file is not in use. It was used to draw the communication layer + * on the map. + * + * API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/modeling/communication.html + * ****************************************************************************************/ + +/** + * Renders for the CommunicationLayer. It primarily establishes lookup variables for + * device locations, device links, and data transfers. After setting up the variables, + * the Canvas Context draws lines between each set of linked devices. If then draws + * gradient lines between devices that transfer data, with the gradient indicating + * which node is the sending node and which is the receiving node. Lastly, it draws + * circles at the location of each device, with the color being determined by the + * device type. + * + * @param {HTML Canvas Element} canvas - The canvas that the layer will be drawn on. + * @param {Point} size - The size of the canvas. + * @param {LatLngBounds} bounds - The bounds of the map. + * @param {Function} project - The latLngToContainerPoint function specifically for CanvasLayer._map. + * @param {Boolean} needsProjectionUpdate - Determines whether the Layer’s projection needs to be updated. + * + * @var {Object} paramCache - Caches the locations for the various devices in the CommunciationLayer. + * @var {Object} varCache - Caches which devices send and which ones receive, along with how much data was transferred between them. + * @var {NDArray} pdcPixelCoords - Stores the location of each PDC device. + * @var {NDArray} pmuPixelCoords - Stores the location of each PMU device. + * @var {NDArray} switchPixelCoords - Stores the location of each Switch device. + * @var {NDArray} linkPixelCoords - Stores the connections between the various devices. + * @var {Object} transferBytesPerNode - Keeps track of the location and transfer amount of each node that sends data. + * @var {Object} receiveBytesPerNode - Keeps track of the location and transfer amount of each node that receives data. + * @var {NDArray} transferPixelCoords - Stores the data transfers between the various devices. + * @var {CanvasRenderingContext2D} ctx - The canvas context used to render the communication lines. + * + * @returns + */ function renderCommunication(canvas, { size, bounds, project, needsProjectionUpdate }) { const context = this._context; if (!context) return; @@ -301,12 +345,31 @@ function toggleRender () { this._render = !this._render; } +/** + * @class CommunicationLayer + * @extends {L.CanvasLayer} + * + * @param {Object} options + * + * @var {Object} _context - The Window’s workspace. + * @var {Object} _cache - Caches the data for different device types from the context. + * @var {Boolean} _render - Determines whether the CommunicationLayer will be displayed. + * + * @returns {CommunicationLayer} + */ L.CommunicationLayer = L.CanvasLayer.extend({ options: { render: renderCommunication, toggle: toggleRender }, + /** + * Sets the CommunicationLayer’s starting variables. + * + * @constructs CommunicationLayer + * @param {Object} options + * @returns + */ initialize(options) { this._context = null; this._cache = new WeakMap(); @@ -314,6 +377,13 @@ L.CommunicationLayer = L.CanvasLayer.extend({ L.CanvasLayer.prototype.initialize.call(this, options); }, + /** + * Updates the values for the devices and then re-renders the CommunicationLayer. + * + * @memberof CommunicationLayer + * @param {Object} context + * @returns + */ update(context) { this._context = context; this.redraw(); diff --git a/agvis/static/js/ConfigControl.js b/agvis/static/js/ConfigControl.js index 67230dc..65da5fc 100644 --- a/agvis/static/js/ConfigControl.js +++ b/agvis/static/js/ConfigControl.js @@ -1,3 +1,21 @@ +/* **************************************************************************************** + * File Name: ConfigControl.js + * Authors: Nicholas West, Nicholas Parsly + * Date: 9/16/2023 (last modified) + * + * Description: This file sets up the configuration control menu sidebar. The config menu + * is used to select the view state (what variable to show) of the contour + * layer. It creates custom Timestamps for the SimTimeBox, can change DiME + * server settings, change graphical settings, and save/load configurations + * and simulations. + * + * Note: Saving/loading configurations and simulations from the config control menu + * differs from the functionality of the Multilater and IDR. + * + * API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/modeling/config.html + * ****************************************************************************************/ + +// HTML for Configuration Control UI const table_html = ` @@ -88,6 +106,13 @@ const table_html = ` const SIDEBAR_CALLBACKS = []; +/** + * Contains the info for a DiME server address. Used when changing what DiME server AGVis is connected to. + * + * @param {Object} host + * @param {Object} port + * @returns + */ class DimeInfo { constructor(host, port) { this.host = host; @@ -97,15 +122,23 @@ class DimeInfo { } } +/** + * Sets up all of the event funciton for the UI elements in table_html. + * + * @param {Window} win - The current AGVis window + * @param {Object} options - Initial settings for several variables throughout AGVis. Presets input elements + * @param {Object} sidebar - The sidebar containing the configuration menu + * @returns {String} - A string containing the element id of the configuration panel + */ function addSidebarConfig(win, options, sidebar) { const table_id = "configpanel" + win.num; + // Add the configuration control menu to the sidebar sidebar.addPanel({ id: table_id, tab: '\u2699', pane: table_html, title: 'Configuration settings' - }); const opt_dimehost = document.querySelector(`#${table_id} input[name='opt_dimehost']`); @@ -128,57 +161,55 @@ function addSidebarConfig(win, options, sidebar) { const opt_flabel = document.querySelector(`#${table_id} span[name='opt_flabel']`); const ts_up = document.querySelector(`#${table_id} input[name='ts_up']`); - //Updating function for Timestamp + /** + * Verifies the custom Timestamp settings are properly formatted and updates the simTimeBox. + * + * @returns + */ ts_up.onclick = function() { let dval = document.getElementById("ts_date").value; let nval = Number(document.getElementById("ts_num").value); let yval = document.getElementById("ny").value; let tval = document.getElementById("ts_time").value; - //If the Timestamp isn't being used, don't bother checking for good inputs if (yval === "No") { - - //The Timestamp is updated based on where in the simulation the timer is on. if (win.workspace.Vargs) { - win.simTimeBox.update(win.workspace.Varvgs.t.toFixed(2)); } //If there is no simulation, set it to 0. else { - win.simTimeBox.update(0.00); } } else { - //Make sure all fully user provided inputs are proper. If not, throw an alert. if ((dval == "") || (nval < 0) || (!(Number.isFinite(nval))) || (tval == "")) { - alert("Please set all inputs properly before trying to update."); return; } if (win.workspace.Vargs) { - - win.simTimeBox.update(win.workspace.Varvgs.t.toFixed(2)); - } else { - win.simTimeBox.update(0.00); } } }; - + + /** + * Sets all of the values for the inputs based off their values in the options object + * + * @returns + */ function updateInputs() { if ("dimehost" in options) { opt_dimehost.value = options.dimehost; @@ -237,6 +268,12 @@ function addSidebarConfig(win, options, sidebar) { } }; + /** + * Handles setting up a new connection to a DiME server when a user changes the host or port number. + * + * @memberof Window + * @returns + */ win.dime_updated = function() { return new Promise(function(resolve, reject) { let callback = function() { @@ -263,6 +300,15 @@ function addSidebarConfig(win, options, sidebar) { }); }; + // =================================================== + // Update ContourLayer’s view state variable + // =================================================== + + /** + * Updates the minimum range for the ContourLayer’s voltage angle variable. + * + * @returns + */ opt_amin.oninput = function() { const val = Number(opt_amin.value); @@ -283,6 +329,11 @@ function addSidebarConfig(win, options, sidebar) { } } + /** + * Updates the maximum range for the ContourLayer’s voltage angle variable. + * + * @returns + */ opt_amax.oninput = function() { const val = Number(opt_amax.value); @@ -303,6 +354,11 @@ function addSidebarConfig(win, options, sidebar) { } }; + /** + * Updates the minimum range for the ContourLayer’s voltage magnitude variable. + * + * @returns + */ opt_vmin.oninput = function() { const val = Number(opt_vmin.value); @@ -323,6 +379,11 @@ function addSidebarConfig(win, options, sidebar) { } }; + /** + * Updates the maximum range for the ContourLayer’s voltage magnitude variable. + * + * @returns + */ opt_vmax.oninput = function() { const val = Number(opt_vmax.value); @@ -343,6 +404,11 @@ function addSidebarConfig(win, options, sidebar) { } } + /** + * Updates the minimum range for the ContourLayer’s voltage frequency variable. + * + * @returns + */ opt_fmin.oninput = function() { const val = Number(opt_fmin.value); @@ -363,6 +429,11 @@ function addSidebarConfig(win, options, sidebar) { } }; + /** + * Updates the maximum range for the ContourLayer’s voltage frequency variable. + * + * @returns + */ opt_fmax.oninput = function() { const val = Number(opt_fmax.value); @@ -383,6 +454,11 @@ function addSidebarConfig(win, options, sidebar) { } }; + /** + * Updates TopologyLayer's opacity value for drawing lines. + * + * @returns + */ opt_opacity.oninput = function() { const val = Number(opt_opacity.value); @@ -397,11 +473,14 @@ function addSidebarConfig(win, options, sidebar) { dt = dt.toUTCString(); document.cookie = `opacity${win.num}=${val};expires=${dt};path=/`; - - win.legend.update(); } }; + /** + * Turns on and off the rendering of the ZoneLayer. + * + * @returns + */ opt_togglezones.onclick = function() { win.zoneLayer.toggleRender(); @@ -417,6 +496,11 @@ function addSidebarConfig(win, options, sidebar) { document.cookie = `togglezones${win.num}=${val};expires=${dt};path=/`; }; + /** + * Turns on and off whether TopologyLayer includes the node labels. + * + * @returns + */ opt_togglebuslabels.onclick = function() { const val = opt_togglebuslabels.checked; @@ -437,6 +521,12 @@ function addSidebarConfig(win, options, sidebar) { opt_loadconfig_input.type = "file"; document.body.appendChild(opt_loadconfig_input); + /** + * Loads in and reads a simulation file from the user. The Window sets the history and workspace from the + * file, then immediately begins and ends a simulation to set up the UI. + * + * @returns + */ opt_loadconfig_input.onchange = function() { if (opt_loadconfig_input.files.length > 0) { let fr = new FileReader(); @@ -454,6 +544,11 @@ function addSidebarConfig(win, options, sidebar) { } }; + /** + * Click event for load config. Doesn't do much. + * + * @returns + */ opt_loadconfig.onclick = function() { opt_loadconfig_input.click(); }; @@ -463,6 +558,11 @@ function addSidebarConfig(win, options, sidebar) { opt_saveconfig_a.style.display = "none"; document.body.appendChild(opt_saveconfig_a); + /** + * Downloads a JSON configuration file from the current settings. + * + * @returns + */ opt_saveconfig.onclick = function() { let json = JSON.stringify(options); let blob = new Blob([json], {type: "application/json"}); @@ -479,6 +579,12 @@ function addSidebarConfig(win, options, sidebar) { opt_loadsimulation_input.type = "file"; document.body.appendChild(opt_loadsimulation_input); + /** + * Loads in and reads a simulation file from the user. The Window sets the history and workspace + * from the file, then immediately begins and ends a simulation to set up the UI. + * + * @returns + */ opt_loadsimulation_input.onchange = function() { if (opt_loadsimulation_input.files.length > 0) { let fr = new FileReader(); @@ -502,6 +608,12 @@ function addSidebarConfig(win, options, sidebar) { opt_savesimulation_a.style.display = "none"; document.body.appendChild(opt_savesimulation_a); + /** + * Downloads a simulation file containing the information on the workspace and history of + * the current simulation. + * + * @returns + */ opt_savesimulation.onclick = function() { let blob = new Blob([win.save()]); diff --git a/agvis/static/js/ContMulti.js b/agvis/static/js/ContMulti.js index 09526cc..882bff1 100644 --- a/agvis/static/js/ContMulti.js +++ b/agvis/static/js/ContMulti.js @@ -1,32 +1,68 @@ +/* **************************************************************************************** + * File Name: ContMulti.js + * Authors: Nicholas West, Nicholas Parsly + * Date: 9/16/2023 (last modified) + * + * Description: ContMulti.js contains the code relating to the MultiContiLayer. + * MultiContLayer shares many things with the ContourLayer (like + * MultiTopLayer does with TopologyLayer). Once again, the main difference + * is that the MultiContLayer uses data from a newlayer as opposed to the + * Window's workspace. + * + * API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/modeling/contmulti.html + * ****************************************************************************************/ + /* -const contourVertexShader = ` -precision mediump float; -attribute vec2 aPosition; -attribute float aValue; -uniform mat4 uProjection; -varying float vValue; - -void main() { - gl_Position = uProjection * vec4(aPosition, 0, 1); - vValue = aValue; -} -`; - -const contourFragmentShader = ` -precision mediump float; -varying float vValue; -uniform sampler2D uColormapSampler; -uniform float uScaleMin; -uniform float uScaleMax; - -void main() { - float value = (vValue - uScaleMin) / (uScaleMax - uScaleMin); - gl_FragColor = texture2D(uColormapSampler, vec2(value, 0.0)); -} -`; + const contourVertexShader = ` + precision mediump float; + attribute vec2 aPosition; + attribute float aValue; + uniform mat4 uProjection; + varying float vValue; + + void main() { + gl_Position = uProjection * vec4(aPosition, 0, 1); + vValue = aValue; + } + `; + + const contourFragmentShader = ` + precision mediump float; + varying float vValue; + uniform sampler2D uColormapSampler; + uniform float uScaleMin; + uniform float uScaleMax; + + void main() { + float value = (vValue - uScaleMin) / (uScaleMax - uScaleMin); + gl_FragColor = texture2D(uColormapSampler, vec2(value, 0.0)); + } + `; */ -//The mutilayer version of the contour layer + +/** + * Render the multi contour layer + * + * @param {HTML Canvas Element} canvas - The canvas that the layer will be drawn on. + * @param {Point Class} size - Represents the current size of the map in pixels. + * @param {LatLngBounds Class} bounds - Represents the geographical bounds of the map. + * @param {Function} project - The latLngToContainerPoint function specifically for CanvasLayer._map. + * @param {Boolean} needsProjectionUpdate - Determines whether the Layer’s projection needs to be updated. + * + * @var {NDArray} busLatLngCoords - Stores the latitude and longitude for each node. + * @var {NDArray} busPixelCoords - Stores the pixel coordinates for each node. + * @var {NDArray} busTriangles - Stores the triangles used to create the heat map. + * @var {WebGL2RenderingContext} gl - The WebGL2RenderingContext for the canvas. + * @var {ProgramInfo} programInfo - The ProgramInfo for the canvas. + * @var {WebGLTexture} uColormapSampler - The WebGLTexture for the canvas. + * @returns + */ function renderMultiCont(canvas, { size, bounds, project, needsProjectionUpdate }) { + + // =================================================== + // Initialize variables + // =================================================== + const context = this._context; if (!context) return; const SysParam = this._newlayer.data; @@ -40,7 +76,7 @@ function renderMultiCont(canvas, { size, bounds, project, needsProjectionUpdate const temparr = []; let x; - //Select data based on where the timer for the animation is at + // Select data based on where the timer for the animation is at for (let j = 0; j < this._newlayer.data["history"]["t"].length; j++) { let val = Number(this._newlayer.data["history"]["t"][j]); @@ -50,7 +86,6 @@ function renderMultiCont(canvas, { size, bounds, project, needsProjectionUpdate break; } } - temparr.push(this._newlayer.data["history"]["varvgs"][x].length); @@ -62,7 +97,10 @@ function renderMultiCont(canvas, { size, bounds, project, needsProjectionUpdate this._cache.set(SysParam, paramCache); } - //I don't entirely understand how it works, but it basically uses delauney triangles to create heat maps between nodes, then it uses a gradient function to smooth it over + /** + * I don't entirely understand how it works, but it basically uses delauney + * triangles to create heat maps between nodes, then it uses a gradient function to smooth it over. + */ const nelems = Bus.idx.length; let { busLatLngCoords } = paramCache; @@ -210,11 +248,31 @@ function renderMultiCont(canvas, { size, bounds, project, needsProjectionUpdate } } +/** + * The MultContLayer Class + * + * @var {Object} MultiContLayer._context - Another name for the Window's workspace + * @var {Object} MultiContLayer._variableRange - Minimum and maximum index for a given variable in "begin" and "end" respectively + * @var {Object} MultiContLayer._variableRelIndices - Stores the ranges for all the variables + * @var {Number} MultiContLayer._uScaleMin - Minimum range of a variable + * @var {Number} MultiContLayer._uScaleMax - Maximum range of a variable + * @var {Number} MultiContLayer._opacity - The opacity for the heatmap. Applied in a fragment shader. + * @var {Boolean} MultiContLayer._render - Determines if MultiContLayer has been rendered or not. + * @returns + */ L.MultiContLayer = L.CanvasLayer.extend({ options: { render: renderMultiCont, }, + /** + * Initializes the MultiContLayer's setting + * + * @constructs MultiContLayer + * @param {*} newlayer + * @param {Object} options - (Optional) The options Object from Window. Unused beyond being passed to the CanvasLayer initialization function. + * @returns + */ initialize(newlayer, options) { this._context = null; this._variableRange = null; @@ -229,21 +287,47 @@ L.MultiContLayer = L.CanvasLayer.extend({ L.CanvasLayer.prototype.initialize.call(this, options); }, + /** + * Updates the values for the variables and then re-renders the MultiContLayer. + * + * @memberof MultiContLayer + * @param {Object} - The workspace from Window. + * @returns + */ update(context) { this._context = context; - //console.log("stuff"); - this.redraw(); + this.redraw(); }, + /** + * Handles adding the MultiContLayer to the map. + * + * @memberof MultiContLayer + * @param {map} map - The map from Window + * @returns + */ onAdd(map) { L.CanvasLayer.prototype.onAdd.call(this, map); this.getPane().classList.add("multicont-pane" + this._newlayer.num); }, + /** + * Passes the relative indices for the simulation variables from Window to MultiContLayer. + * + * @memberof MultiContLayer + * @param {Object} idx - Relative indices + * @returns + */ storeRelativeIndices(idx) { this._variableRelIndices = idx; }, + /** + * Changes the simulation variable being used for the animation and requests that the current frame be redrawn. + * + * @param {String} name - The name of the variable used to key into the MultiContLayer._variableRelIndices Object. + * @returns + */ showVariable(name) { // updates the name of variables for the contour map this.variableName = name; @@ -252,18 +336,38 @@ L.MultiContLayer = L.CanvasLayer.extend({ this.redraw(); }, - //Update range for the current shown variable + /** + * Passes the range values used in the animation from the configuration settings to the MultiContLayer. + * + * @memberof MultiContLayer + * + * @param {Number} lower + * @param {Number} upper + * @returns + */ updateRange(lower, upper){ this._uScaleMax = upper; this._uScaleMin = lower; }, + /** + * Switches the state of MultiContLayer._render + * + * @memberof MultiContLayer + * @returns + */ toggleRender() { this._render = !this._render; console.log("MultiContour rendering: ", this._render); }, - //Used for prioritize button, just copies over everything + /** + * Changes the newlayer’s current values to be those from another newlayer. Used exclusively for the “Prioritize Layer” button. + * + * @memberof MultiContLayer + * @param {Object} oldlayer - The newlayer that the values are being taken from. + * @returns + */ stealVals(oldlayer) { this._context = oldlayer._context; @@ -274,7 +378,6 @@ L.MultiContLayer = L.CanvasLayer.extend({ this._cache = oldlayer._cache; this.variableName = oldlayer.variableName; this._render = oldlayer._render; - } }); diff --git a/agvis/static/js/ContourLayer.js b/agvis/static/js/ContourLayer.js index 1fcfcba..622588a 100644 --- a/agvis/static/js/ContourLayer.js +++ b/agvis/static/js/ContourLayer.js @@ -1,33 +1,77 @@ +/* **************************************************************************************** + * File Name: ContourLayer.js + * Authors: Nicholas West, Nicholas Parsly + * Date: 9/20/2023 (last modified) + * + * Description: ContourLayer.js contains the code for the ContourLayer class. This + * class handles displaying the heatmap animations for a given power + * system. The heatmap bounds are done using the Delaunay Triangulation + * implementation from the D3.js library. The actual visuals for each + * heatmap are done using TWGL to apply a texture to each triangle’s + * fragments based on their interpolated values. + * + * API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/modeling/contour.html + * ****************************************************************************************/ + +/** + * The vertex shader for the Contour-type Layers. Passes the values for a given + * variable to the fragment shader so that they can be interpolated for rendering + * the heat map. + */ const contourVertexShader = ` -precision mediump float; -attribute vec2 aPosition; -attribute float aValue; -uniform mat4 uProjection; -varying float vValue; - -void main() { - gl_Position = uProjection * vec4(aPosition, 0, 1); - vValue = aValue; -} + precision mediump float; + attribute vec2 aPosition; + attribute float aValue; + uniform mat4 uProjection; + varying float vValue; + + void main() { + gl_Position = uProjection * vec4(aPosition, 0, 1); + vValue = aValue; + } `; +/** + * The fragment shader for the Contour-type Layers. Maps the color in a texture + * to each fragment based on the interpolated variable value, the minimum of the + * variable range, and the maximum of the variable range. + */ const contourFragmentShader = ` -precision mediump float; -varying float vValue; -uniform sampler2D uColormapSampler; -uniform float uScaleMin; -uniform float uScaleMax; -uniform float uOpacity; - -void main() { - float value = (vValue - uScaleMin) / (uScaleMax - uScaleMin); - vec4 color = texture2D(uColormapSampler, vec2(value, 0.0)); - color.a *= uOpacity; - gl_FragColor = color; -} + precision mediump float; + varying float vValue; + uniform sampler2D uColormapSampler; + uniform float uScaleMin; + uniform float uScaleMax; + uniform float uOpacity; + + void main() { + float value = (vValue - uScaleMin) / (uScaleMax - uScaleMin); + vec4 color = texture2D(uColormapSampler, vec2(value, 0.0)); + color.a *= uOpacity; + gl_FragColor = color; + } `; +/** + * Handles rendering for the ContourLayer. Most of the function consists of determining + * locations of the nodes if they aren’t in the cache yet, creating all the triangles, + * and then setting up WebGL with TWGL. A gradient texture is applied to each fragment, + * which is rendered on the canvas. The color of each fragment is based off the variable + * data from known locations. Any major modifications to ContourLayer’s rendering + * function are probably best left to those with a decent level of familiarity with WebGL. + * + * @param {HTMLCanvasElement} canvas - The canvas element to render to. + * @param {Point Class} size - (Unused) The size of the canvas. + * @param {LatLngBounds Class} bounds - (Unused) The geographical bounds of the map. + * @param {Function} project - The latLngToContainerPoint function specifically for CanvasLayer._map. + * @param {Boolean} needsProjectionUpdate - Determines whether the Layer’s projection needs to be updated. + * @returns + */ function renderContour(canvas, { size, bounds, project, needsProjectionUpdate }) { + // ================================================== + // Initialize variables + // ================================================== + const context = this._context; if (!context) return; const SysParam = context.SysParam; @@ -171,6 +215,10 @@ function renderContour(canvas, { size, bounds, project, needsProjectionUpdate }) const uOpacity = this._opacity; + // ================================================== + // Render + // ================================================== + if(this._render) { gl.viewport(0, 0, gl.canvas.width, gl.canvas.height); gl.enable(gl.BLEND); @@ -199,6 +247,15 @@ L.ContourLayer = L.CanvasLayer.extend({ render: renderContour, }, + /** + * Set the ContourLayer's starting variables. + * + * @memberof ContourLayer + * @param {Object} options - The options Object from Window. Unused beyond being passed to CanvasLayer's + * initialization function, seemed to be initially used to set certain variables, + * but those values are instead hardcoded into the initialization. + * @returns + */ initialize(options) { this._context = null; this._variableRange = null; @@ -213,20 +270,48 @@ L.ContourLayer = L.CanvasLayer.extend({ L.CanvasLayer.prototype.initialize.call(this, options); }, + /** + * Updates the values for the variables and then re-renders the ContourLayer. + * + * @memberof ContourLayer + * @param {Object} context - The context Object from Window. + * @returns + */ update(context) { this._context = context; this.redraw(); }, + /** + * Add the CountourLayer to the map. + * @memberof ContourLayer + * @param {Map Class} map + * @returns + */ onAdd(map) { L.CanvasLayer.prototype.onAdd.call(this, map); this.getPane().classList.add("contour-pane"); }, + /** + * Passes the relative indices for the simulation variables from Window to ContourLayer. + * + * @memberof ContourLayer + * @param {Object} idx - Relative indices + * @returns + */ storeRelativeIndices(idx) { this._variableRelIndices = idx; }, + /** + * Changes the simulation variable being used for the animation and requests + * that the current frame be redrawn. + * + * @memberof ContourLayer + * @param {String} name - The name of the variable used to key into the ContourLayer._variableRelIndices Object. + * @returns + */ showVariable(name) { // updates the name of variables for the contour map this.variableName = name; @@ -235,16 +320,37 @@ L.ContourLayer = L.CanvasLayer.extend({ this.redraw(); }, + /** + * Passes the range values used in the animation from the configuration + * settings to the ContourLayer. + * + * @memberof ContourLayer + * @param {Number} lower - The lower bound of the range. + * @param {Number} upper - The upper bound of the range. + * @returns + */ updateRange(lower, upper){ this._uScaleMax = upper; this._uScaleMin = lower; }, + /** + * The function that switches the state of ContourLayer._render. + * + * @memberof ContourLayer + * @returns + */ toggleRender() { this._render = !this._render; console.log("Contour rendering: ", this._render); }, + /** + * Updates the opacity value of ContourLayer using the value passed from the Playback Bar. + * + * @memberof ContourLayer + * @returns + */ updateOpacity(opacity) { this._opacity = opacity; this.redraw(); diff --git a/agvis/static/js/LayerControl.js b/agvis/static/js/LayerControl.js index 8908882..b35f659 100644 --- a/agvis/static/js/LayerControl.js +++ b/agvis/static/js/LayerControl.js @@ -1,19 +1,39 @@ +/* **************************************************************************************** + * File Name: LayerControl.js + * Authors: Nicholas West, Nicholas Parsly + * Date: 9/20/2023 (last modified) + * + * Description: LayerControl.js contains the code for the “Add Layers” menu, which + * handles the data parsing, variable management, and UI for the IDR and + * MultiLayer functionality. Effectively, LayerControl.js contains the + * dynamic equivalent of Window.js, ControlLayer.js, and SimTimeBox.js. + * This partially explains why it is one of the largest files currently + * in AGVis. LayerControl uses both the Papa Parse library and the + * SheetJs library for file reading. + * + * API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/modeling/layer.html + * ****************************************************************************************/ + //Table containing the sidebar const layers_html = ` -
- -
-
-
-Added Layers: -
-` - -//Nicholas Parsly -//A function that takes a xlsx file and returns an object of objects with arrays representing each column of data in each sheet +
+ +
+
+
+ Added Layers: +
`; + +/** + * A function that takes a xlsx file and returns an object + * of objects with arrays representing each column of data in each sheet + * + * @author Nicholas Parsly + * @returns + */ + /* function xlsxReader() { - const opt_addlayer_input = document.createElement("input"); opt_addlayer_input.style.display = "none"; opt_addlayer_input.id = "fin"; @@ -84,20 +104,29 @@ function xlsxReader() { }; return output; - -} */ -//Swaps rows and columns in a 2D array +/** + * Swaps rows and columns in a 2D array + * + * @param {Array} a - The array to transpose + * @returns + */ function transpose(a) { return Object.keys(a[0]).map(function(c) { return a.map(function(r) { return r[c]; }); }); } -//Begins the setup for the animation. Stores where the info relating to the three possible variables is stored +/** + * Begins the setup for the animation. Stores where the info relating to + * the three possible variables is stored. Does some initial setup for letting + * a MultiContLayer display a simulation animation. Called in tandem with endMultiSim(). + * + * @param {Object} newlayer - The newlayer containing the MultiContLayer that needs to start its simulation. + * @returns + */ function startMultiSim(newlayer) { - let nBus = newlayer.data["history"]["nBus"]; let variableRelIndices = {}; variableRelIndices["V"] = {"begin": 0, "end": nBus}; @@ -110,16 +139,27 @@ function startMultiSim(newlayer) { newlayer.cont.showVariable("freq"); } -//Finishes the setup and renders the multicont layer, allowing for the simulation to be played +/** + * Turns on the display for a MultiContLayer and requests it to draw its current frame. + * + * @param {Object} newlayer - The newlayer containing the MultiContLayer that needs to display its simulation. + * @param {Window} win - AGVis’s Window. Passed to the function for updating the MultiContLayer. + * @returns + */ function endMultiSim(newlayer, win) { - newlayer.time = newlayer.end_time; newlayer.cont.toggleRender(); newlayer.cont.update(win.workspace); } - -//Adds in the Layers Sidebar +/** + * Adds the “Add Layers” menu and sets up the “Add Layer” button. + * + * @param {Window} win - AGVis’s Window. Passed to stepRead() when files are uploaded. + * @param {Object} options - The Window’s options variable. Passed to stepRead() when files are uploaded. + * @param {Sidebar} sidebar - Adds the “Add Layers” menu to its display. Passed to stepRead() when files are uploaded. + * @returns {String} table_id - A string containing the element id of the configuration panel + */ function addSidebarLayers(win, options, sidebar) { const table_id = "layerpanel" + win.num; @@ -141,24 +181,25 @@ function addSidebarLayers(win, options, sidebar) { opt_addlayer_input.accept = ".xlsx"; document.body.appendChild(opt_addlayer_input); - //Once the file is uploaded + /** + * Upon a user uploading a file, creates a newlayer, sets its initial values, + * and calls stepRead() on the files. + * + * @returns + */ opt_addlayer_input.onchange = function() { - //Check if a file was actually uploaded if (opt_addlayer_input.files.length > 0) { - //Get its name and check for the proper file extension fname = opt_addlayer_input.files[0].name; ext = fname.substring(fname.lastIndexOf(".") + 1); if (ext != "xlsx") { - alert("Please use a specified file type."); return; } - var reader = new FileReader(); const newlayer = {}; @@ -174,23 +215,17 @@ function addSidebarLayers(win, options, sidebar) { newlayer.options.tmax = 1; if (reader.readAsBinaryString) { - //Reads the data reader.onload = function (dat) { - //Convert the data to csv and then that to an array let wb = XLSX.read(dat.target.result, {type: "binary"}); for (let k = 0; k < wb.SheetNames.length; k++) { - //Check for simulation sheet if (wb.SheetNames[k] == "O_His") { - - newlayer.sim = true; newlayer.time = 0.0; newlayer.timescale = 1; - newlayer.data["history"] = {}; let rows = XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[k]]); @@ -219,7 +254,6 @@ function addSidebarLayers(win, options, sidebar) { //Check for the simulation presets else if (wb.SheetNames[k] == "S_Set") { - newlayer.preset = true; newlayer.s_show = "freq"; newlayer.s_tstamp = false; @@ -235,104 +269,69 @@ function addSidebarLayers(win, options, sidebar) { //Loop through each column, checking for specific data types and assigning them accordingly for (let n = 0; n < turn.length; n++) { - let typeval = turn[n][0]; let sval = turn[n][1]; + if (typeval == "") { - continue; } - else if (typeval == "show") { - if (sval.toLowerCase() == "v") { - newlayer.s_show = "V"; } - else if (sval.toLowerCase() == "t") { - newlayer.s_show = "theta"; } } - else if (typeval == "freq") { - newlayer.options.fmin = Number(sval); - newlayer.options.fmax = Number(turn[n][2]); - + newlayer.options.fmax = Number(turn[n][2]); } - else if (typeval == "v_mag") { - newlayer.options.vmin = Number(sval); newlayer.options.vmax = Number(turn[n][2]); } - else if (typeval == "v_ang") { - newlayer.options.tmin = Number(sval); newlayer.options.tmax = Number(turn[n][2]); } - else if (typeval == "tstamp") { - if (sval.toLowerCase() == "y" || sval.toLowerCase() == "yes") { - - newlayer.s_tstamp = true; } } - else if (typeval == "tdate") { - newlayer.s_tdate = sval; } - else if (typeval == "ttime") { - newlayer.s_ttime = sval; } - else if (typeval == "tinc") { - if (sval.toLowerCase() == "seconds") { - newlayer.s_string = "Seconds"; newlayer.s_tinc = "s"; } - else if (sval.toLowerCase() == "minutes") { - newlayer.s_string = "Minutes"; newlayer.s_tinc = "min"; } - else if (sval.toLowerCase() == "hours") { - newlayer.s_string = "Hours"; newlayer.s_tinc = "h"; } - else if (sval.toLowerCase() == "days") { - newlayer.s_string = "Days"; newlayer.s_tinc = "day"; } } - else if (typeval == "tnum") { - newlayer.s_tnum = Number(sval); } } } - - //Otherwise store the data based on the sheet and column names - else { - + else { newlayer.data[wb.SheetNames[k]] = {}; let rows = XLSX.utils.sheet_to_csv(wb.Sheets[wb.SheetNames[k]]); let data = Papa.parse(rows); @@ -340,9 +339,7 @@ function addSidebarLayers(win, options, sidebar) { let csv = transpose(data.data); for (let m = 0; m < csv.length; m++) { - if (csv[m][0] == "") { - continue; } @@ -356,14 +353,12 @@ function addSidebarLayers(win, options, sidebar) { //If there isn't a free space in the array, add the new layer to it if (win.mnumfree == 0) { - newlayer.num = win.multilayer.length; win.multilayer.push(newlayer); } //If there is free space, put it in the most recent freed space and then check if there is another free space for the next layer else { - newlayer.num = win.mlayercur; win.mnumfree = win.mnumfree - 1; win.multilayer[newlayer.num] = newlayer; @@ -371,7 +366,6 @@ function addSidebarLayers(win, options, sidebar) { for (let j = 0; j < win.multilayer.length; j++) { if (win.multilayer[j] == null) { - win.mlayercur = j; break; } @@ -381,7 +375,6 @@ function addSidebarLayers(win, options, sidebar) { console.log(newlayer); newlayer.topo = L.multitopLayer(newlayer).addTo(win.map); - //ids for the dynamically generated elements let lstring = "line" + newlayer.num; let ltog = "toggle" + newlayer.num; @@ -407,7 +400,6 @@ function addSidebarLayers(win, options, sidebar) { let lid = "id" + newlayer.num; - //Div that stores the other elements it gets deleted when the newlayer is delted const elem = document.createElement("div"); const br = document.createElement("br"); @@ -434,17 +426,18 @@ function addSidebarLayers(win, options, sidebar) { clabel.for = ltog; clabel.innerText = "Toggle Rendering"; - //Toggles the rendering of the multitop layer + /** + * Toggles the rendering for a newlayer’s MultiTopLayer. + * + * @returns + */ cbox.onchange = function() { - let cid = this.id.slice(6); let cnum = Number(cid); let clayer = win.multilayer[cnum]; clayer.topo.toggleRender(); clayer.topo.update(win.workspace); win.searchLayer.update(win.searchLayer._context, win); - - }; //Add the checkbox to the div @@ -458,8 +451,14 @@ function addSidebarLayers(win, options, sidebar) { dbutton.value = "Delete Layer"; dbutton.style.cssFloat = "right"; + /** + * Deletes the newlayer it is associated with. Turns off the rendering for both + * the newlayer’s MultiTopLayer and MultiContLayer, and removes it from + * Window.multilayer. Also removes it from the SearchLayer and removes its UI elements. + * + * @returns + */ dbutton.onclick = function() { - //List some basic info on what's deleted let bid = this.id.slice(6); let bnum = Number(bid); @@ -474,7 +473,6 @@ function addSidebarLayers(win, options, sidebar) { blayer.cont.redraw(); } - //Set the newlayer to null in the array, increase the amount of free spaces, and set the current free layer to this newlayer's num win.multilayer[bnum] = null; win.mlayercur = bnum; @@ -519,9 +517,12 @@ function addSidebarLayers(win, options, sidebar) { clabel3.for = ltog3; clabel3.innerText = "Toggle Custom Line Colors"; - //Toggle for the custom node colors - ctog2.onchange = function() { - + /** + * Toggles whether to use custome node colors for a newlayer. + * + * @returns + */ + ctog2.onchange = function() { let cid = this.id.slice(8); let cnum = Number(cid); let clayer = win.multilayer[cnum]; @@ -529,7 +530,11 @@ function addSidebarLayers(win, options, sidebar) { clayer.topo.update(win.workspace); }; - //Toggle for the custom line colors + /** + * Toggles whether to use custom line colors for a newlayer. + * + * @returns + */ ctog3.onchange = function() { let cid = this.id.slice(8); @@ -546,7 +551,12 @@ function addSidebarLayers(win, options, sidebar) { pbut.value = "Prioritize Layer"; pbut.style.cssFloat = "right"; - //Create an identical copy of the prioritized newlayer, add it to the map so it renders above everything, and then delete the old one + /** + * Create an identical copy of the prioritized newlayer, add it to the map + * so it renders above everything, and then delete the old one. + * + * @returns + */ pbut.onclick = function() { let bid = this.id.slice(8); @@ -574,7 +584,6 @@ function addSidebarLayers(win, options, sidebar) { blayer.cont._render = false; blayer.cont.redraw(); } - } elem.appendChild(ctog2); @@ -592,7 +601,6 @@ function addSidebarLayers(win, options, sidebar) { ndiv3.id = ldiv3; elem.appendChild(ndiv3); - //Color input for the nodes const color1 = document.createElement("input"); color1.id = lcolor1; @@ -606,15 +614,17 @@ function addSidebarLayers(win, options, sidebar) { clabel4.for = lcolor1; clabel4.innerText = "Custom Node Color"; - //Update the node color values in the multitop - color1.onchange = function() { - + /** + * Prompts the user to select a color for a newlayer's nodes + * + * @returns + */ + color1.onchange = function() { let cid = this.id.slice(7); let cnum = Number(cid); let clayer = win.multilayer[cnum]; clayer.topo.updateCNVal(this.value); clayer.topo.update(win.workspace); - }; elem.appendChild(color1); @@ -624,7 +634,6 @@ function addSidebarLayers(win, options, sidebar) { ndiv4.id = ldiv4; elem.appendChild(ndiv4); - //Same as above, just for the lines const color2 = document.createElement("input"); color2.id = lcolor2; @@ -638,21 +647,22 @@ function addSidebarLayers(win, options, sidebar) { clabel5.for = lcolor2; clabel5.innerText = "Custom Line Color"; - //Update the line color values in the multitop + /** + * Prompts the user to select a color for a newlayer’s lines. + * + * @returns + */ color2.onchange = function() { - let cid = this.id.slice(7); let cnum = Number(cid); let clayer = win.multilayer[cnum]; clayer.topo.updateCLVal(this.value); clayer.topo.update(win.workspace); - }; elem.appendChild(color2); elem.appendChild(clabel5); - //Node opacity const ndiv5 = document.createElement("div"); elem.appendChild(ndiv5); @@ -667,16 +677,17 @@ function addSidebarLayers(win, options, sidebar) { range1.style.marginLeft = "15px"; range1.style.marginRight = "5px"; - const rlabel1 = document.createElement("label"); rlabel1.for = lrange1; rlabel1.id = llabel1; rlabel1.innerText = "Node Opacity (0-100) -- Value: " + range1.value; - - - //Update the node opacity values and change the display on the input + + /** + * Updates the node opacity for a newlayer along with the UI. + * + * @returns + */ range1.onchange = function() { - let cid = this.id.slice(7); let cnum = Number(cid); let clayer = win.multilayer[cnum]; @@ -684,15 +695,11 @@ function addSidebarLayers(win, options, sidebar) { lab1.innerText = "Node Opacity (0-100) -- Value: " + this.value; clayer.topo.updateNOp(this.value); clayer.topo.update(win.workspace); - }; - elem.appendChild(range1); elem.appendChild(rlabel1); - - - + //Line opacity const ndiv6 = document.createElement("div"); elem.appendChild(ndiv6); @@ -706,16 +713,18 @@ function addSidebarLayers(win, options, sidebar) { range2.value = 50; range2.style.marginLeft = "15px"; range2.style.marginRight = "5px"; - const rlabel2 = document.createElement("label"); rlabel2.for = lrange2; rlabel2.id = llabel2; rlabel2.innerText = "Line Opacity (0-100) -- Value: " + range2.value; - ///Change the line opacity values and change the display on the input + /** + * Change the line opacity values and change the display on the input + * + * @returns + */ range2.onchange = function() { - let cid = this.id.slice(7); let cnum = Number(cid); let clayer = win.multilayer[cnum]; @@ -723,13 +732,11 @@ function addSidebarLayers(win, options, sidebar) { lab2.innerText = "Line Opacity (0-100) -- Value: " + this.value; clayer.topo.updateLOp(this.value); clayer.topo.update(win.workspace); - }; elem.appendChild(range2); elem.appendChild(rlabel2); - //Line width //Line width was requested before node size, which is why it appears earlier const ndiv7 = document.createElement("div"); elem.appendChild(ndiv7) @@ -743,16 +750,18 @@ function addSidebarLayers(win, options, sidebar) { range3.value = 2; range3.style.marginLeft = "15px"; range3.style.marginRight = "5px"; - const rlabel3 = document.createElement("label"); rlabel3.for = lrange3; rlabel3.id = llabel3; rlabel3.innerText = "Line Thickness (1-7) -- Value: " + range3.value; - //Change the line width values and change the display on the input + /** + * Change the line width values and change the display on the input + * + * @returns + */ range3.onchange = function() { - let cid = this.id.slice(7); let cnum = Number(cid); let clayer = win.multilayer[cnum]; @@ -760,13 +769,11 @@ function addSidebarLayers(win, options, sidebar) { lab3.innerText = "Line Thickness (1-7) -- Value: " + this.value; clayer.topo.updateLThick(this.value); clayer.topo.update(win.workspace); - }; elem.appendChild(range3); elem.appendChild(rlabel3); - //Node size const ndiv8 = document.createElement("div"); elem.appendChild(ndiv8) @@ -781,13 +788,16 @@ function addSidebarLayers(win, options, sidebar) { range4.style.marginLeft = "15px"; range4.style.marginRight = "5px"; - const rlabel4 = document.createElement("label"); rlabel4.for = lrange4; rlabel4.id = llabel4; rlabel4.innerText = "Node Size (4-36) -- Value: " + range4.value; - //Change the node size values and change the display on the input + /** + * Change the node size values and change the display on the input + * + * @returns + */ range4.onchange = function() { let cid = this.id.slice(7); @@ -805,24 +815,21 @@ function addSidebarLayers(win, options, sidebar) { //These handle if the node and line colors were preset in the file if (newlayer.data.Bus.color != null) { - color1.value = newlayer.data.Bus.color[0]; newlayer.topo.updateCNVal(color1.value); ctog2.click(); } - + if (newlayer.data.Line.color != null) { - color2.value = newlayer.data.Line.color[0]; newlayer.topo.updateCLVal(color2.value); ctog3.click(); } - - - //Contour Layer Simulation stuff + // ========================================= + // Simulation Utility + // ========================================= if (newlayer.sim) { - newlayer.cont = L.multicontLayer(newlayer).addTo(win.map); startMultiSim(newlayer); @@ -882,65 +889,71 @@ function addSidebarLayers(win, options, sidebar) { //If presets are available, the chosen variable is set else { - newlayer.cont.variableName = newlayer.s_show; if (newlayer.s_show == "freq") { - - fbut.checked = true; - newlayer.cont.showVariable("freq"); - newlayer.cont.updateRange(newlayer.options.fmin, newlayer.options.fmax); + fbut.checked = true; + newlayer.cont.showVariable("freq"); + newlayer.cont.updateRange(newlayer.options.fmin, newlayer.options.fmax); } - else if (newlayer.s_show == "V") { - - vbut.checked = true; - newlayer.cont.showVariable("V"); - newlayer.cont.updateRange(newlayer.options.vmin, newlayer.options.vmax); + vbut.checked = true; + newlayer.cont.showVariable("V"); + newlayer.cont.updateRange(newlayer.options.vmin, newlayer.options.vmax); } - else if (newlayer.s_show == "theta") { - - tbut.checked = true; - newlayer.cont.showVariable("theta"); - newlayer.cont.updateRange(newlayer.options.tmin, newlayer.options.tmax); + tbut.checked = true; + newlayer.cont.showVariable("theta"); + newlayer.cont.updateRange(newlayer.options.tmin, newlayer.options.tmax); } } - - - //On update, change the shown variable in multicont, and update its range to the set range for that variable + /** + * Updates the displayed simulation variable to be Voltage Frequency, or whatever + * values have been put in place of Voltage Frequency. Also updates the heatmap + * ranges to those corresponding with Voltage Frequency. + * + * @returns + */ fbut.onchange = function() { - let cid = this.id.slice(10); console.log(cid); let cnum = Number(cid); let clayer = win.multilayer[cnum]; clayer.cont.showVariable("freq"); clayer.cont.updateRange(newlayer.options.fmin, newlayer.options.fmax); - }; + /** + * Updates the displayed simulation variable to be Voltage Magnitude, or whatever + * values have been put in place of Voltage Magnitude. Also updates the heatmap + * ranges to those corresponding with Voltage Magnitude. + * + * @returns + */ vbut.onchange = function() { - let cid = this.id.slice(8); console.log(cid); let cnum = Number(cid); let clayer = win.multilayer[cnum]; clayer.cont.showVariable("V"); clayer.cont.updateRange(newlayer.options.vmin, newlayer.options.vmax); - }; + /** + * Updates the displayed simulation variable to be Voltage Angle, or whatever + * values have been put in place of Voltage Angle. Also updates the heatmap + * ranges to those corresponding with Voltage Angle. + * + * @returns + */ tbut.onchange = function() { - let cid = this.id.slice(6); console.log(cid); let cnum = Number(cid); let clayer = win.multilayer[cnum]; clayer.cont.showVariable("theta"); clayer.cont.updateRange(newlayer.options.tmin, newlayer.options.tmax); - }; simdiv1.appendChild(fbut); @@ -973,7 +986,6 @@ function addSidebarLayers(win, options, sidebar) { let tminid = "tmin_" + newlayer.num; let tmaxid = "tmax_" + newlayer.num; - const str1 = document.createElement("tr"); const str2 = document.createElement("tr"); const str3 = document.createElement("tr"); @@ -1000,7 +1012,7 @@ function addSidebarLayers(win, options, sidebar) { const spara2 = document.createElement("span"); const spara3 = document.createElement("span"); - //Once again, by default it is frequency, votlage magnitude and voltage angle + //Once again, by default it is frequency, voltage magnitude and voltage angle //Might allow user to customize the names of the variables later spara1.innerText = " - "; spara2.innerText = " - "; @@ -1085,315 +1097,345 @@ function addSidebarLayers(win, options, sidebar) { sinput5.value = newlayer.options.tmin; sinput6.value = newlayer.options.tmax; - //On change update the value for that variable in the newlayer options - //If that specific variable is being shown, update the range in multicont as well + // ====================================================== + // Update Ranges + // ====================================================== + + /** + * Updates the minimum range variable for Voltage Frequency in the newlayer. Also + * updates ranges in the newlayer’s MultiContLayer if Voltage Frequency is the currently + * shown variable. + * + * @returns + */ sinput1.oninput = function() { - let val = Number(sinput1.value); if (val === val) { - newlayer.options.fmin = val; if (newlayer.cont.variableName == "freq") { - newlayer.cont.updateRange(newlayer.options.fmin, newlayer.options.fmax); } } }; + /** + * Updates the maximum range variable for Voltage Frequency in the newlayer. Also + * updates ranges in the newlayer’s MultiContLayer if Voltage Frequency is the currently + * shown variable. + * + * @returns + */ sinput2.oninput = function() { - let val = Number(sinput2.value); if (val === val) { - newlayer.options.fmax = val; if (newlayer.cont.variableName == "freq") { - newlayer.cont.updateRange(newlayer.options.fmin, newlayer.options.fmax); } } }; + /** + * Updates the minimum range variable for Voltage Magnitude in the newlayer. Also + * updates ranges in the newlayer’s MultiContLayer if Voltage Magnitude is the currently + * shown variable. + * + * @returns + */ sinput3.oninput = function() { - let val = Number(sinput3.value); if (val === val) { - newlayer.options.vmin = val; if (newlayer.cont.variableName == "V") { - newlayer.cont.updateRange(newlayer.options.vmin, newlayer.options.vmax); } } }; + /** + * Updates the maximum range variable for Voltage Magnitude in the newlayer. Also + * updates ranges in the newlayer’s MultiContLayer if Voltage Magnitude is the currently + * shown variable. + * + * @returns + */ sinput4.oninput = function() { - let val = Number(sinput4.value); if (val === val) { - newlayer.options.vmax = val; if (newlayer.cont.variableName == "V") { - newlayer.cont.updateRange(newlayer.options.vmin, newlayer.options.vmax); } } }; + /** + * Updates the minimum range variable for Voltage Angle in the newlayer. Also + * updates ranges in the newlayer’s MultiContLayer if Voltage Angle is the currently + * shown variable. + * + * @returns + */ sinput5.oninput = function() { - let val = Number(sinput5.value); if (val === val) { - newlayer.options.tmin = val; if (newlayer.cont.variableName == "theta") { - newlayer.cont.updateRange(newlayer.options.tmin, newlayer.options.tmax); } } }; - + + /** + * Updates the maximum range variable for Voltage Angle in the newlayer. Also + * updates ranges in the newlayer’s MultiContLayer if Voltage Angle is the currently + * shown variable. + * + * @returns + */ sinput6.oninput = function() { - let val = Number(sinput6.value); if (val === val) { - newlayer.options.tmax = val; if (newlayer.cont.variableName == "theta") { - newlayer.cont.updateRange(newlayer.options.tmin, newlayer.options.tmax); } } }; - //Put a break between the animation settings and the timestamp settings - const hr3 = document.createElement("hr"); - hr3.style.marginTop = "10px"; - hr3.style.marginBottom = "10px"; - elem.appendChild(hr3); - - const h3 = document.createElement("h3"); - h3.innerText = "Timestamp Settings"; - elem.appendChild(h3); - + //Put a break between the animation settings and the timestamp settings + const hr3 = document.createElement("hr"); + hr3.style.marginTop = "10px"; + hr3.style.marginBottom = "10px"; + elem.appendChild(hr3); + + const h3 = document.createElement("h3"); + h3.innerText = "Timestamp Settings"; + elem.appendChild(h3); - const simdiv4 = document.createElement("div"); - - //Check if user wants to use timestamp - const stog1 = document.createElement("input"); - stog1.id = "ny_" + newlayer.num; - stog1.type = "checkbox"; - stog1.value = "ny"; - ctog3.style.marginLeft = "15px"; - ctog3.style.marginRight = "5px"; - - const slabel4 = document.createElement("label"); - slabel4.for = "ny_" + newlayer.num; - slabel4.innerText = "Use Timestamp?"; - - if (newlayer.preset) { + const simdiv4 = document.createElement("div"); - stog1.checked = newlayer.s_tstamp; - } - - simdiv4.appendChild(slabel4); - simdiv4.appendChild(stog1); - - - elem.appendChild(simdiv4); - - //Date setting - const simdiv5 = document.createElement("div"); - const sdate1 = document.createElement("input"); - sdate1.id = "ts_date_" + newlayer.num; - sdate1.type = "date"; - - const slabel5 = document.createElement("label"); - slabel5.for = "ts_date_" + newlayer.num; - slabel5.innerText = "Select a Date: "; - - if (newlayer.preset) { + //Check if user wants to use timestamp + const stog1 = document.createElement("input"); + stog1.id = "ny_" + newlayer.num; + stog1.type = "checkbox"; + stog1.value = "ny"; + ctog3.style.marginLeft = "15px"; + ctog3.style.marginRight = "5px"; - sdate1.value = newlayer.s_tdate; - } - - simdiv5.appendChild(slabel5); - simdiv5.appendChild(sdate1); - - - elem.appendChild(simdiv5); - - //Time of day setting - const simdiv6 = document.createElement("div"); - const stime1 = document.createElement("input"); - stime1.id = "ts_time_" + newlayer.num; - stime1.type = "time"; - - const slabel6 = document.createElement("label"); - slabel6.for = "ts_time_" + newlayer.num; - slabel6.innerText = "Select a Time: "; + const slabel4 = document.createElement("label"); + slabel4.for = "ny_" + newlayer.num; + slabel4.innerText = "Use Timestamp?"; + + if (newlayer.preset) { + + stog1.checked = newlayer.s_tstamp; + } - if (!newlayer.preset) { + simdiv4.appendChild(slabel4); + simdiv4.appendChild(stog1); + + + elem.appendChild(simdiv4); + + //Date setting + const simdiv5 = document.createElement("div"); + const sdate1 = document.createElement("input"); + sdate1.id = "ts_date_" + newlayer.num; + sdate1.type = "date"; + + const slabel5 = document.createElement("label"); + slabel5.for = "ts_date_" + newlayer.num; + slabel5.innerText = "Select a Date: "; + + if (newlayer.preset) { + + sdate1.value = newlayer.s_tdate; + } + + simdiv5.appendChild(slabel5); + simdiv5.appendChild(sdate1); + + + elem.appendChild(simdiv5); + + //Time of day setting + const simdiv6 = document.createElement("div"); + const stime1 = document.createElement("input"); + stime1.id = "ts_time_" + newlayer.num; + stime1.type = "time"; + + const slabel6 = document.createElement("label"); + slabel6.for = "ts_time_" + newlayer.num; + slabel6.innerText = "Select a Time: "; + + if (!newlayer.preset) { - stime1.value = "00:00:00"; - } - - else { + stime1.value = "00:00:00"; + } - stime1.value = newlayer.s_ttime; - } + else { + + stime1.value = newlayer.s_ttime; + } - simdiv6.appendChild(slabel6); - simdiv6.appendChild(stime1); - - elem.appendChild(simdiv6); + simdiv6.appendChild(slabel6); + simdiv6.appendChild(stime1); + + elem.appendChild(simdiv6); - //Select option for what increment the timer should increase in - const simdiv7 = document.createElement("div"); - const sselect1 = document.createElement("select"); - sselect1.id = "ts_inc_" + newlayer.num; - - const soption1 = document.createElement("option"); - soption1.value = "ms"; - soption1.id = "ms_" + newlayer.num; - soption1.innerText = "Milliseconds"; - - const soption2 = document.createElement("option"); - soption2.value = "s"; - soption2.id = "s_" + newlayer.num; - soption2.innerText = "Seconds"; - - const soption3 = document.createElement("option"); - soption3.value = "min"; - soption3.id = "min_" + newlayer.num; - soption3.innerText = "Minutes"; - - const soption4 = document.createElement("option"); - soption4.value = "h"; - soption4.id = "h_" + newlayer.num; - soption4.innerText = "Hours"; - - const soption5 = document.createElement("option"); - soption5.value = "day"; - soption5.id = "day_" + newlayer.num; - soption5.innerText = "Days"; - - - //And by how much of that increment per second - const slabel7 = document.createElement("label"); - slabel7.for = "ts_inc_" + newlayer.num; - slabel7.innerText = "Select an Increment: "; - - const simdiv8 = document.createElement("div"); - const sinput7 = document.createElement("input"); - sinput7.type = "number"; - sinput7.id = "ts_num_" + newlayer.num; - sinput7.value = "1"; - sinput7.min = "0"; - sinput7.step = "1"; - - const slabel8 = document.createElement("label"); - slabel8.for = "ts_num_" + newlayer.num; - - slabel8.id = "ts_lab_" + newlayer.num; - - - if (!newlayer.preset) { - slabel8.innerText = "Number of Milliseconds per Second:"; - soption1.selected = true; - } - - else { + // ================================================================ + // Select option for what increment the timer should increase in + // ================================================================ + + const simdiv7 = document.createElement("div"); + const sselect1 = document.createElement("select"); + sselect1.id = "ts_inc_" + newlayer.num; - sinput7.value = newlayer.s_tnum; - sselect1.value = newlayer.s_tinc; - switch(newlayer.s_tinc) { - - case "s": - soption2.selected = true; - slabel8.innerText = "Number of Seconds per Second:"; - break; - - case "min": - soption3.selected = true; - slabel8.innerText = "Number of Minutes per Second:"; - break; - - case "h": - soption4.selected = true; - slabel8.innerText = "Number of Hours per Second:"; - break; - - case "day": - soption5.selected = true; - slabel8.innerText = "Number of Days per Second:"; - break; + const soption1 = document.createElement("option"); + soption1.value = "ms"; + soption1.id = "ms_" + newlayer.num; + soption1.innerText = "Milliseconds"; + + const soption2 = document.createElement("option"); + soption2.value = "s"; + soption2.id = "s_" + newlayer.num; + soption2.innerText = "Seconds"; - default: - soption1.select = "selected"; + const soption3 = document.createElement("option"); + soption3.value = "min"; + soption3.id = "min_" + newlayer.num; + soption3.innerText = "Minutes"; + + const soption4 = document.createElement("option"); + soption4.value = "h"; + soption4.id = "h_" + newlayer.num; + soption4.innerText = "Hours"; + + const soption5 = document.createElement("option"); + soption5.value = "day"; + soption5.id = "day_" + newlayer.num; + soption5.innerText = "Days"; + + //And by how much of that increment per second + const slabel7 = document.createElement("label"); + slabel7.for = "ts_inc_" + newlayer.num; + slabel7.innerText = "Select an Increment: "; + + const simdiv8 = document.createElement("div"); + const sinput7 = document.createElement("input"); + sinput7.type = "number"; + sinput7.id = "ts_num_" + newlayer.num; + sinput7.value = "1"; + sinput7.min = "0"; + sinput7.step = "1"; + + const slabel8 = document.createElement("label"); + slabel8.for = "ts_num_" + newlayer.num; + + slabel8.id = "ts_lab_" + newlayer.num; + + if (!newlayer.preset) { slabel8.innerText = "Number of Milliseconds per Second:"; + soption1.selected = true; + } + else { + sinput7.value = newlayer.s_tnum; + sselect1.value = newlayer.s_tinc; + + switch(newlayer.s_tinc) { + case "s": + soption2.selected = true; + slabel8.innerText = "Number of Seconds per Second:"; + break; + + case "min": + soption3.selected = true; + slabel8.innerText = "Number of Minutes per Second:"; + break; + + case "h": + soption4.selected = true; + slabel8.innerText = "Number of Hours per Second:"; + break; + + case "day": + soption5.selected = true; + slabel8.innerText = "Number of Days per Second:"; + break; + default: + soption1.select = "selected"; + slabel8.innerText = "Number of Milliseconds per Second:"; + } } - } - - sselect1.appendChild(soption1); - sselect1.appendChild(soption2); - sselect1.appendChild(soption3); - sselect1.appendChild(soption4); - sselect1.appendChild(soption5); - simdiv7.appendChild(slabel7); - simdiv7.appendChild(sselect1); - simdiv8.appendChild(slabel8); - simdiv8.appendChild(sinput7); - elem.appendChild(simdiv7); - elem.appendChild(simdiv8); - - //Update the text for the increments per second label on change - sselect1.onchange = function() { - - const slab8 = document.getElementById("ts_lab_" + newlayer.num); - switch(this.value) { - - case "s": - - slab8.innerText = "Number of Seconds per Second:"; - break; + sselect1.appendChild(soption1); + sselect1.appendChild(soption2); + sselect1.appendChild(soption3); + sselect1.appendChild(soption4); + sselect1.appendChild(soption5); + simdiv7.appendChild(slabel7); + simdiv7.appendChild(sselect1); + simdiv8.appendChild(slabel8); + simdiv8.appendChild(sinput7); + + elem.appendChild(simdiv7); + elem.appendChild(simdiv8); + + /** + * Update the text for the increments per second label on change + * + * Updates the text in the newlayer’s Custom Timestamp UI when a new + * increment type is selected. As a side note, most of the UI elements + * for the newlayer Custom Timestamp features do not have event functions + * because their values are simply checked by the PlayMulti. + * + * @returns + */ + sselect1.onchange = function() { + const slab8 = document.getElementById("ts_lab_" + newlayer.num); + switch(this.value) { + + case "s": - case "min": - - slab8.innerText = "Number of Minutes per Second:"; - break; + slab8.innerText = "Number of Seconds per Second:"; + break; + + case "min": + + slab8.innerText = "Number of Minutes per Second:"; + break; + + case "h": - case "h": - - slab8.innerText = "Number of Hours per Second:"; - break; - - case "day": - - slab8.innerText = "Number of Days per Second:"; - break; + slab8.innerText = "Number of Hours per Second:"; + break; - default: - slab8.innerText = "Number of Milliseconds per Second:"; + case "day": + + slab8.innerText = "Number of Days per Second:"; + break; - } - }; - } - + default: + slab8.innerText = "Number of Milliseconds per Second:"; + + } + }; + } //Add the div to the table let ls = document.getElementById("layerstore"); @@ -1405,9 +1447,6 @@ function addSidebarLayers(win, options, sidebar) { }; - - - //Read the first file they input reader.readAsBinaryString(opt_addlayer_input.files[0]); } @@ -1415,6 +1454,12 @@ function addSidebarLayers(win, options, sidebar) { } }; + /** + * Calls the addlayer_input function. + * + * @returns + * @see opt_addlayer_input + */ opt_addlayer.onclick = function() { opt_addlayer_input.click(); }; diff --git a/agvis/static/js/Legend.js b/agvis/static/js/Legend.js index 547cceb..6e06bb1 100644 --- a/agvis/static/js/Legend.js +++ b/agvis/static/js/Legend.js @@ -1,20 +1,20 @@ -/** - * ================================================================================================== +/* **************************************************************************************** * File Name: Legend.js * Author: Zack Malkmus - * Date: 9/6/2023 - * Description: Creates a draggable and updating legend for LTB AGVis - * ================================================================================================== - */ + * Date: 9/6/2023 (last modified) + * + * Description: The DynamicLegend class creates a new dynamic legend for AGVis that is + * draggable and updatable. + * + * API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/?badge=stable + * ****************************************************************************************/ /** - * Create a new dynamic legend for AGVis. + * @class DynamicLegend + * @extends {L.Control} * - * This class creates a new legend for AGVis that is draggable and updatable - * - * @author Zack Malkmus * @param {Object} win - The AGVis window that the legend is associated with. - * @returns {Object} The new legend element. + * @returns {DynamicLegend} */ L.DynamicLegend = L.Control.extend({ options: { @@ -22,11 +22,11 @@ L.DynamicLegend = L.Control.extend({ }, /** - * Initialize the legend. - * @memberof L.DynamicLegend - * @param {Object} win - The AGVis window that the legend is associated with. - * @returns {void} - * @constructor + * Initialize the legend. + * + * @constructs L.DynamicLegend + * @param {Object} win - The AGVis window that the legend is associated with. + * @returns */ initialize: function(win) { this.win = win; @@ -34,8 +34,9 @@ L.DynamicLegend = L.Control.extend({ /** * Create the legend element. + * * @memberof L.DynamicLegend - * @returns {Element} - The legend element. + * @returns {Element} - The legend element. */ onAdd: function() { // Legend Container @@ -95,9 +96,10 @@ L.DynamicLegend = L.Control.extend({ /** * Set the legend to be draggable. + * * @memberof L.DynamicLegend * @param {Object} e - The event object. - * @returns {void} + * @returns */ onDragStart: function (e) { this.dragging = true; @@ -105,14 +107,14 @@ L.DynamicLegend = L.Control.extend({ this.dragStartY = e.clientY; this.originalX = parseInt(this._container.style.left) || 0; this.originalY = parseInt(this._container.style.top) || 0; - }, /** * Update the legend position while dragging. + * * @memberof L.DynamicLegend * @param {Object} e - The event object. - * @returns {void} + * @returns */ onDrag: function (e) { if (!this.dragging) return; @@ -127,8 +129,9 @@ L.DynamicLegend = L.Control.extend({ /** * Stop dragging the legend. + * * @memberof L.DynamicLegend - * @returns {void} + * @returns */ onDragEnd: function () { this.dragging = false; @@ -136,27 +139,26 @@ L.DynamicLegend = L.Control.extend({ /** * Used to update the legend values when the user changes the state or min/max values of the state. + * * @memberof L.DynamicLegend - * @returns {void} + * @returns + * * @see ConfigControl.js * @see Window.js */ update: function() { - // Voltage Angle if (this.win.state == this.win.states.angl) { this.title.innerHTML = "V Angle"; this.units.innerHTML = "(rad)"; this.min.innerHTML = "" + this.win.options.amin + ""; this.max.innerHTML = "" + this.win.options.amax + ""; } - // Voltage Magnitude else if (this.win.state == this.win.states.volt) { this.title.innerHTML = "V Magnitude"; this.units.innerHTML = "(p.u.)"; this.min.innerHTML = "" + this.win.options.vmin + ""; this.max.innerHTML = "" + this.win.options.vmax + ""; } - // Frequency else if (this.win.state == this.win.states.freq) { this.title.innerHTML = "Frequency"; this.units.innerHTML = "(p.u.)"; diff --git a/agvis/static/js/NDArray.js b/agvis/static/js/NDArray.js index 851d2b6..ce20832 100644 --- a/agvis/static/js/NDArray.js +++ b/agvis/static/js/NDArray.js @@ -1,4 +1,30 @@ +/* **************************************************************************************** + * File Name: NDArray.js + * Authors: Nicholas West, Nicholas Parsly (Comments by Zack Malkmus) + * Date: 9/21/2023 (last modified) + * + * Description: This JavaScript file defines a class called NDArray, which represents + * a multidimensional array with various methods for manipulation. It + * supports both row-major ('C') and column-major ('F') storage orders. + * + * Warning: I did not write create this file, and there was no documentation left + * behind by the creator(s). As such, some of the developer comments may be + * incorrect. I have also left out specifying typing as I am unfamiliar with + * the NDArray class structure. + * + * API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/?badge=stable + * ****************************************************************************************/ + class NDArray { + /** + * Creates an NDArray class object with the specified order and shape. + * + * @constructs NDArray + * + * @param {*} order - Storage Order + * @param {*} shape - Array Dimensions + * @param {*} typedArray - (Optional) + */ constructor(order, shape, typedArray) { this.order = order; this.shape = shape; @@ -12,6 +38,14 @@ class NDArray { this.array = typedArray; } + /** + * Calculates real index from the given index based on the storage order + * + * @memberof NDArray + * @param {*} index + * @param {*} strict + * @returns {*} + */ _makeIndex(index, strict) { const shape = this.shape; if (index.length !== shape.length) { @@ -33,16 +67,37 @@ class NDArray { return realIndex; } + /** + * Grabs the value at the current index + * + * @memberof NDArray + * @param {*} index + * @returns {*} + */ get(...index) { const realIndex = this._makeIndex(index, true); return this.array[realIndex]; } + /** + * Set the given value to the supplied index. + * + * @memberof NDArray + * @param {*} value + * @param {*} index + */ set(value, ...index) { const realIndex = this._makeIndex(index, true); this.array[realIndex] = value; } + /** + * Retrieves a column from the NDArray (only applicable for column-major storage). + * + * @memberof NDArray + * @param {*} n - The column number to retrieve + * @returns {*} - The nth column of the NDArray + */ column(n) { if (this.order !== 'F') { throw 'bad order, expected "' + this.order + '"'; @@ -53,6 +108,13 @@ class NDArray { return this.array.slice(start, end); } + /** + * Retrieves a row from the NDArray (only applicable for row-major storage). + * + * @memberof NDArray + * @param {*} n - The row number to retrieve + * @returns {*} - The nth row of the NDArray + */ row(n) { if (this.order !== 'C') { throw 'bad order, expected "' + this.order + '"'; @@ -63,8 +125,14 @@ class NDArray { return this.array.slice(start, end); } + /** + * Checks if the NDArray represents a column vector and calculates its extents. + * !I'm not entirely sure what this one is used for. + * + * @memberof NDArray + * @returns {*} - New NDArray representing the subarray + */ extents() { - if (this.shape[0] !== 1) { throw 'extents: expected column vector'; } @@ -80,10 +148,25 @@ class NDArray { return { begin: typedArray[0], end: typedArray[typedArray.length - 1] + 1 }; } + /** + * Create a new subarray from begin to end + * + * @memberof NDArray + * @param {*} begin - Starting index + * @param {*} end - Final index + * @returns {NDArray} - New subarray + */ subarray({ begin, end }) { return new NDArray(this.order, [1, end - begin - 1], this.array.subarray(begin, end)); } + /** + * Retrieves values from the NDArray based on an input index array + * + * @memberof NDArray + * @param {NDArray} idx - NDArray representing the indices to retrieve + * @returns {NDArray} - New NDArray containing values at the specified indices + */ subindex(idx){ if (this.shape[0] !== 1) { throw 'extents: expected column vector'; diff --git a/agvis/static/js/PlayMulti.js b/agvis/static/js/PlayMulti.js index 54e42a1..c91b223 100644 --- a/agvis/static/js/PlayMulti.js +++ b/agvis/static/js/PlayMulti.js @@ -1,27 +1,65 @@ +/* **************************************************************************************** + * File Name: PlayMulti.js + * Authors: Nicholas West, Nicholas Parsly + * Date: 9/26/2023 (last modified) + * + * Description: PlayMulti.js contains the code for the PlayMulti control, which handles + * the simulation controls for newlayers. It is mostly the same as the + * PlaybackControl, though it does also incorporate some aspects from + * SimTimeBox since the timer for each newlayer is just some updating text as + * opposed to a full time box. + * + * API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/modeling/playmulti.html + * ****************************************************************************************/ + +/** + * @class PlayMulti + * @extends {L.Control} + * + * @var {Object} newlayer - The newlayer associated with the specific Playback Bar. + * @var {Element} playbackbar - The multiplier for how much time passes per timestep. Setting it to 0 pauses the animation. + * @var {Number} prev - The previous Playback Bar value. Used to check if the timer actually needs to update. + * + * @returns {PlayMulti} + */ let PlayMulti = L.Control.extend({ options: { position: "bottomleft" }, - //Multilayer version of the playback bar + /** + * Primarily just sets PlayMulti.win and calls the Leaflet Util initialization function. + * + * @constructs PlayMulti + * @param {*} newlayer - The newlayer associated with the specific Playback Bar. + * @param {Object} options - (optional) Passed to leaflet + * @param {*} elem + * @param {Window} win - The primary window for AGVis + * @returns + */ initialize: function(newlayer, options, elem, win) { this.newlayer = newlayer; this.playbackbar = null; if (options) L.Util.setOptions(this, options); - //}, - let paused = false; let playbackspeed = 1.0; - //Define style + // =============================================================== + // Define the CSS styles for the playback control. + // =============================================================== + let div = L.DomUtil.create('div', 'leaflet-bar leaflet-control'); div.style.backgroundColor = "white"; div.style.boxSizing = "border-box"; div.style.padding = "4px"; div.id = "pbar" + newlayer.num; + // =============================================================== + // Create the playback control. + // =============================================================== + L.DomEvent.disableClickPropagation(div); div.appendChild(document.createElement("br")); let playbackbar = L.DomUtil.create('input', '', div); @@ -36,12 +74,21 @@ let PlayMulti = L.Control.extend({ this.newlayer.curtime = Number(Date.now()); this.prev = Number(newlayer.end_time); - //Set time for manual inpout change + /** + * Updates the Window’s time whenever the user changes the range input. + * + * @memberof PlayMulti + * @returns + */ playbackbar.oninput = function() { newlayer.time = Number(playbackbar.value); newlayer.cont.update(this.win); } + // =============================================================== + // Create the playback control buttons. + // =============================================================== + let ldiv = L.DomUtil.create('div', '', div); ldiv.style.float = "left"; @@ -49,7 +96,6 @@ let PlayMulti = L.Control.extend({ pausebutton.type = "button"; pausebutton.value = "Pause"; - //Pause and unpause functionality pausebutton.onclick = function() { paused = !paused; @@ -62,30 +108,30 @@ let PlayMulti = L.Control.extend({ } } - //Stop button (should really probably be called the restart button) let stopbutton = L.DomUtil.create('input', '', ldiv); stopbutton.type = "button"; stopbutton.value = "Restart"; + /** + * Resets the animation back to the beginning. + * + * @memberof PlayMulti + * @returns + */ stopbutton.onclick = function() { newlayer.time = playbackbar.min; playbackbar.value = playbackbar.min; - - - } let rdiv = L.DomUtil.create('div', '', div); rdiv.style.float = "right"; - //Custom speed input let playbackspeedrange = L.DomUtil.create('input', '', ldiv); playbackspeedrange.type = "range"; playbackspeedrange.value = 2; playbackspeedrange.min = -1; playbackspeedrange.max = 6; playbackspeedrange.step = 1; - let playbackspeedspan = L.DomUtil.create('span', '', rdiv); playbackspeedspan.innerHTML = " 1x "; @@ -97,6 +143,13 @@ let PlayMulti = L.Control.extend({ playbackspeedtext.disabled = true; playbackspeedtext.size = 4; + /** + * Updates the Window’s timescale when the user changes the input bar. Also handles + * adjusting settings when a user selects the custom playback speed option. + * + * @memberof PlayMulti + * @returns + */ playbackspeedrange.oninput = function() { if (playbackspeedrange.value < 0) { playbackspeedtext.disabled = false; @@ -130,11 +183,17 @@ let PlayMulti = L.Control.extend({ newlayer.timescale = playbackspeed; } - playbackspeedspan.innerHTML = " " + val + "x "; } } + /** + * Sets the Window’s timescale to the value the user input in the text box if + * the custom playback speed option has been selected. + * + * @memberof PlayMulti + * @returns + */ playbackspeedtext.oninput = function() { const val = Number(playbackspeedtext.value); @@ -142,10 +201,13 @@ let PlayMulti = L.Control.extend({ playbackspeed = val; if (!paused) { newlayer.timescale = playbackspeed; - } } } + + // =============================================================== + // Create div2 to hold the playback control. + // =============================================================== let div2 = document.createElement("div"); div.style.marginTop = "10px"; @@ -156,7 +218,13 @@ let PlayMulti = L.Control.extend({ onRemove: function(options) {}, - //Called every 17 milliseconds, updates the animations + /** + * Updates the Playback Bar’s value based on the Window’s current time, requests that the MultiContLayer + * updates, and checks for Custom Timestamp settings. + * + * @param {Number} dt - The number of seconds between the current update and the most recent update. + * @param {Number} timestep - The current time from the window. + */ updatePlaybackBar: function(dt, timestep) { if (this.playbackbar) { @@ -168,7 +236,6 @@ let PlayMulti = L.Control.extend({ this.playbackbar.value = this.playbackbar.max; } - else { this.playbackbar.value = Number(this.playbackbar.value) + pt; } @@ -189,17 +256,13 @@ let PlayMulti = L.Control.extend({ //If you are, use the provided timer, not the pure seconds one let dval2 = document.getElementById("ts_date_" + this.newlayer.num).value; let nval2 = Number(document.getElementById("ts_num_" + this.newlayer.num).value); - let yval2 = document.getElementById("ny_" + this.newlayer.num).checked; + // let yval2 = document.getElementById("ny_" + this.newlayer.num).checked; let tval2 = document.getElementById("ts_time_" + this.newlayer.num).value; - if ((dval2 == "") || (nval2 < 0) || (!(Number.isFinite(nval2))) || (tval2 == "")) { timerup.innerText = "Simulation Time: " + this.newlayer.time; - } - else { - //msmult is the millisecond multiplier. It determines how many ms need to be added to the time per frame. let msmult = 1; let ival2 = document.getElementById("ts_inc_" + this.newlayer.num).value; @@ -245,9 +308,7 @@ let PlayMulti = L.Control.extend({ } } - else { - timerup.innerText = "Simulation Time: " + this.newlayer.time; } diff --git a/agvis/static/js/PlaybackControl.js b/agvis/static/js/PlaybackControl.js index 1848049..505633e 100644 --- a/agvis/static/js/PlaybackControl.js +++ b/agvis/static/js/PlaybackControl.js @@ -1,8 +1,44 @@ +/* **************************************************************************************** + * File Name: PlaybackControl.js + * Authors: Nicholas West, Nicholas Parsly + * Date: 9/21/2023 (last modified) + * + * Description: PlaybackControl.js contains the code for the PlaybackControl class, also + * known as the Playback Bar. The Playback Bar handles the UI for the + * ContourLayer animations. It updates the Window’s timescale when the user + * changes the animation speed, sets the time when the user restarts or scrubs + * through the animation, and changes the opacity setting for ContourLayer’s + * rendering based on the user’s input. + * + * API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/modeling/playback.html + * ****************************************************************************************/ + +/** + * @class PlaybackControl + * @extends {L.Control} + * + * @param {Window} win - The Window the Playback Bar is associated with. + * @param {Object} options - (optional) Passed to leaflet + * + * @var {HTML Input Element} opacitybar - The range input for the opacity slider. + * @var {HTML Input Element} playbackbar - The range input for the playback slider. + * @var {Boolean} paused - Used by the pause button to determine what state the animation is in. + * @var {Number} playbackspeed - The current speed of the animation. + * + * @returns {PlaybackControl} + */ let PlaybackControl = L.Control.extend({ options: { position: "bottomleft" }, + /** + * Sets PlaybackControl.win and calls the Leaflet Util Initialization function. + * + * @constructs PlaybackControl + * @param {Window} win - The Window the Playback Bar is associated with. + * @param {Object} options - (optional) Passed to leaflet + */ initialize: function(win, options) { this.win = win; this.opacitybar = null; @@ -11,6 +47,13 @@ let PlaybackControl = L.Control.extend({ if (options) L.Util.setOptions(this, options); }, + /** + * Adds all the UI elements to the map. Also sets up all of their event functions. + * + * @memberof PlaybackControl + * @param {Object} options - Completely unused. + * @returns + */ onAdd: function(options) { const { win } = this; let paused = false; @@ -33,6 +76,12 @@ let PlaybackControl = L.Control.extend({ playbackbar.step = 0.01; playbackbar.value = win.end_time; + /** + * Updates the Window’s time whenever the user changes the range input. + * + * @memberof PlaybackControl + * @returns + */ playbackbar.oninput = function() { win.time = Number(playbackbar.value); } @@ -44,6 +93,12 @@ let PlaybackControl = L.Control.extend({ pausebutton.type = "button"; pausebutton.value = "Pause"; + /** + * Toggles whether the animation is paused or not. When paused, timescale is set to 0. When not, it is set to whatever timescale the user has selected. + * + * @memberof PlaybackControl + * @returns + */ pausebutton.onclick = function() { paused = !paused; @@ -56,6 +111,7 @@ let PlaybackControl = L.Control.extend({ } } + // Stop button let stopbutton = L.DomUtil.create('input', '', ldiv); stopbutton.type = "button"; stopbutton.value = "Stop"; @@ -106,6 +162,12 @@ let PlaybackControl = L.Control.extend({ playbackspeedtext.disabled = true; playbackspeedtext.size = 4; + /** + * Updates the Window’s timescale when the user changes the input bar. Also handles adjusting settings when a user selects the custom playback speed option. + * + * @memberof PlaybackControl + * @returns + */ playbackspeedrange.oninput = function() { if (playbackspeedrange.value < 0) { playbackspeedtext.disabled = false; @@ -143,6 +205,12 @@ let PlaybackControl = L.Control.extend({ } } + /** + * Sets the Window’s timescale to the value the user input in the text box if the custom playback speed option has been selected. + * + * @memberof PlaybackControl + * @returns + */ playbackspeedtext.oninput = function() { const val = Number(playbackspeedtext.value); @@ -160,11 +228,16 @@ let PlaybackControl = L.Control.extend({ onRemove: function(options) {}, + /** + * Updates the Playback Bar’s value based on the Window’s current time. + * + * @memberof PlaybackControl + * @param {Number} value - The time passed from the window + * @returns + */ updatePlaybackBar: function(value) { if (this.playbackbar) { this.playbackbar.value = value; } } - - }); diff --git a/agvis/static/js/SearchLayer.js b/agvis/static/js/SearchLayer.js index 720a315..a33be14 100644 --- a/agvis/static/js/SearchLayer.js +++ b/agvis/static/js/SearchLayer.js @@ -1,4 +1,37 @@ +/* **************************************************************************************** + * File Name: SearchLayer.js + * Authors: Nicholas West, Nicholas Parsly + * Date: 9/26/2023 (last modified) + * + * Description: Places the markers on the SearchLayer. First it loops through the + * multilayer Objects to determine whether their MultiTopLayers are rendered. + * If a MultiTopLayer is not rendered, then its markers will not be added to + * the SearchLayer. After that it checks if the nodes received through DiME + * need markers to be added to the SearchLayer. If they do, it adds them and + * updates appropriate variables accordingly. + * + * API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/modeling/search.html + * ****************************************************************************************/ + +/** + * @class SearchLayer + * @extends {L.LayerGroup} + * + * @var {Object} _context - The Window’s workspace. + * @var {WeakMap} _cache - Caches the data from the context. For the most part it goes unused, though it does see use as one of the methods of checking if markers need to be placed. + * @var {Boolean} _needs_update - Set to true when initialized. Another one of the ways the update function checks if markers need to be placed. + * @var {Marker} marker - The two variables named “marker” do the same thing, namely they are added to the SearchLayer so that they can be searched. They contain location and identification data for nodes. + * + * @returns {SearchLayer} + */ L.SearchLayer = L.LayerGroup.extend({ + + /** + * Sets the SearchLayer’s starting variables. + * + * @constructs L.SearchLayer + * @param {Object} options - (Optional) The options Object from Window. Unused beyond being passed to the LayerGroup’s initialization function. + */ initialize(options) { this._context = null; this._cache = new WeakMap(); @@ -14,29 +47,34 @@ L.SearchLayer = L.LayerGroup.extend({ }); }, + /** + * Places the markers on the SearchLayer. First it loops through the multilayer Objects to + * determine whether their MultiTopLayers are rendered. If a MultiTopLayer is not rendered, + * then its markers will not be added to the SearchLayer. After that it checks if the nodes + * received through DiME need markers to be added to the SearchLayer. If they do, it adds + * them and updates appropriate variables accordingly. + * + * @memberof SearchLayer + * @param {Object} context - The workspace from Window. + * @param {Window} win - The Window itself. Included in order to access the MultiLayer data. + * @returns + */ update(context, win) { - if (win.multilayer.length > 0) { - this.clearLayers(); for (let i = 0; i < win.multilayer.length; i++) { - let mlayer = win.multilayer[i]; if (mlayer == null) { - continue; } if (!mlayer.topo._render) { - continue; } for (let j = 0; j < mlayer.data.Bus.idx.length; j++) { - - const lat = mlayer.data.Bus.ycoord[j]; const lng = mlayer.data.Bus.xcoord[j]; const idx = mlayer.data.Bus.idx[j]; @@ -50,7 +88,6 @@ L.SearchLayer = L.LayerGroup.extend({ } } - if (!this._needs_update) { return; } @@ -75,10 +112,8 @@ L.SearchLayer = L.LayerGroup.extend({ } const Bus = SysParam.Bus; - if (win.multilayer.length == 0) { - this.clearLayers(); } diff --git a/agvis/static/js/TimeBox.js b/agvis/static/js/TimeBox.js index a84a5de..4cce7c1 100644 --- a/agvis/static/js/TimeBox.js +++ b/agvis/static/js/TimeBox.js @@ -1,64 +1,100 @@ +/* **************************************************************************************** + * File Name: TimeBox.js + * Authors: Nicholas West, Nicholas Parsly + * Date: 9/26/2023 (last modified) + * + * Description: TimeBox.js contains the code the SimTimeBox class, which is extended from + * a Leaflet Control. The SimTimeBox updates timer in the top left corner of + * the map when a simulation occurs. It also handles the calculations and + * checking for the Custom Timestamp feature. + * + * API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/modeling/timebox.html + * ****************************************************************************************/ + +/** + * @class SimTimeBox + * @extends {L.Control} + * + * @var {Number} simulation_time - The current time for the simulation. + * @var {String} text - The text that will be displayed in the SimTimeBox. + * @var {String} dval2 - The date selected by the user for the Custom Timestamp feature. + * @var {String} nval2 - The number of increments for the Custom Timestamp feature. + * @var {String} yval2 - Whether SimTimeBox will try to set a Custom Timestamp or not. + * @var {String} tval2 - The hour, minute, and second of the day selected by the user for the Custom Timestamp feature. + * @var {Number} msmult - The multiple of milliseconds equal to the increment selected by the user. + * @var {String} ival2 - The size of each increment on the timer. + * @var {Date} ddate - The Date determined from the Custom Timestamp settings. + * @var {String} isostring - The ISO string of the Date determined from the Custom Timestamp settings. + * + * @returns {SimTimeBox} + */ L.SimTimeBox = L.Control.extend({ - simulation_time: 0.0, + simulation_time: 0.0, //The current time for the simulation. + /** + * Adds the SimTimeBox to the map and sets the initial time to 0. + * + * @param {*} map + * @returns + */ onAdd: function(map) { - this.simulation_time = 0; - this.text = L.DomUtil.create('div'); - this.text.id = "info_text"; - this.text.innerHTML = "

Simulation time: " + this.simulation_time + "

"; - return this.text; - - }, - - onRemove: function(map) { - // Nothing to do here - + if (typeof L !== 'undefined') { + this.simulation_time = 0; + this.text = L.DomUtil.create('div'); + this.text.id = "info_text"; + this.text.innerHTML = "

Simulation time: " + this.simulation_time + "

"; + return this.text; + } + else { + console.error("Leaflet not found."); + return null; + } }, + /** + * Updates the SimTimeBox based on the time passed by the Window. The time value is + * taken from the simulation data’s timestep values. Depending on user inputs in + * the Configuration Settings, it will either just display that time or use it to + * calculate a Custom Timestamp. + * + * @memberof SimTimeBox + * @param {Number} t - The time provided by the Window. + * @returns + */ update: function(t){ - this.simulation_time = t; - let dval2 = document.getElementById("ts_date").value; - let nval2 = Number(document.getElementById("ts_num").value); - let yval2 = document.getElementById("ny").value; - let tval2 = document.getElementById("ts_time").value; + let dval2 = document.getElementById("ts_date").value; // The date selected by the user for the Custom Timestamp feature. + let nval2 = Number(document.getElementById("ts_num").value); // The number of increments for the Custom Timestamp feature. + let yval2 = document.getElementById("ny").value; // Whether SimTimeBox will try to set a Custom Timestamp or not. + let tval2 = document.getElementById("ts_time").value; // The hour, minute, and second of the day selected by the user for the Custom Timestamp feature. //If they aren't using Timestamp or didn't put in proper information, use the default display. if (yval2 === "No") { this.text.innerHTML = "

Simulation time: " + this.simulation_time + "

"; } - else { if ((dval2 == "") || (nval2 < 0) || (!(Number.isFinite(nval2))) || (tval2 == "")) { this.text.innerHTML = "

Simulation time: " + this.simulation_time + "

"; } - else { - - //msmult is the millisecond multiplier. It determines how many ms need to be added to the time per frame. - let msmult = 1; - let ival2 = document.getElementById("ts_inc").value; - let sdate = Date.parse(dval2 + " " + tval2); + let msmult = 1; // The multiple of milliseconds equal to the increment selected by the user. + let ival2 = document.getElementById("ts_inc").value; // The size of each increment on the timer. + let sdate = Date.parse(dval2 + " " + tval2); // The Date determined from the Custom Timestamp settings. let fdate = sdate; //Could be reversed in order to be a litle more efficient, but this is more readable. switch(ival2) { - case "s": - msmult = 1000; break; case "min": - msmult = 1000 * 60; break; case "h": - msmult = 1000 * 60 * 60; break; case "day": - msmult = 1000 * 60 * 60 * 24; break; default: diff --git a/agvis/static/js/TopMulti.js b/agvis/static/js/TopMulti.js index 70e0d86..2e4423b 100644 --- a/agvis/static/js/TopMulti.js +++ b/agvis/static/js/TopMulti.js @@ -1,4 +1,41 @@ -//topmulti.js is basically just the topology layer but adjusted to work for multiple layers and file input +/* **************************************************************************************** + * File Name: TopMulti.js + * Authors: Nicholas West, Nicholas Parsly + * Date: 9/26/2023 (last modified) + * + * Description: TopMulti.js contains the code for the MultiTopLayer. You’ll notice many + * similarities between this code and that of the standard TopologyLayer. + * This is due to the fact that the MultiTopLayer was built off of the + * TopologyLayer. The major difference between them is that the MultiTopLayer + * has more customization features and uses newlayer data as opposed to the + * Window’s workspace. + * + * API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/modeling/topmulti.html + * ****************************************************************************************/ + +/** + * Renders for the MultiTopLayer. It establishes many lookup variables for specific node + * types, but these go unused for the most part. Lines are drawn between node locations by + * the Canvas Context. Line color, width, and opacity is handled by simply modifying + * strokeStyle and linewidth variables of the Canvas Context. Nodes are placed after the + * lines are drawn, and their icons depend on their associated image in busToImageLookup. + * + * Node customization is handled by initially placing the nodes at their user-selected + * size and then running through the image data to change the RGBA values at node + * locations. Node locations are determined by finding pixels of pure white and one + * specific shade of pink associated with the SYN type nodes. These pixels are then + * recolored. If a user selects a line color that is pure white or that shade of pink, + * the color is imperceptibly changed to not exactly match that RGB value. This prevents + * lines from being incorrectly recolored. Lines and nodes are drawn in order of appearance + * in the data. + * + * @param {HTML Canvas Element} canvas - The canvas that the layer will be drawn on. + * @param {Point} size - Represents the current size of the map in pixels. + * @param {LatLngBounds} bounds - Represents the geographical bounds of the map. + * @param {Function} project - The latLngToContainerPoint function specifically for CanvasLayer._map. + * @param {Boolean} needsProjectionUpdate - Determines whether the Layer’s projection needs to be updated. + * @returns + */ function renderMultiTop(canvas, { size, bounds, project, needsProjectionUpdate }) { const images = this._images; if (!images.allLoaded) return; @@ -213,36 +250,34 @@ function renderMultiTop(canvas, { size, bounds, project, needsProjectionUpdate } if(this._render) { /* - if (this._states) { - for (let zone of this._states) { - ctx.fillStyle = zone.color; - - for (let i = 0; i < zone.coords.shape[0]; i++) { - const lat = zone.coords.get(i, 0); - const lon = zone.coords.get(i, 1); - - const {x, y} = project(L.latLng(lat, lon)); - - if (i === 0) { - ctx.beginPath(); - ctx.moveTo(x, y); - } else { - ctx.lineTo(x, y); - } - } + if (this._states) { + for (let zone of this._states) { + ctx.fillStyle = zone.color; - ctx.closePath(); - ctx.fill("evenodd"); - } - } - */ - + for (let i = 0; i < zone.coords.shape[0]; i++) { + const lat = zone.coords.get(i, 0); + const lon = zone.coords.get(i, 1); + + const {x, y} = project(L.latLng(lat, lon)); + + if (i === 0) { + ctx.beginPath(); + ctx.moveTo(x, y); + } else { + ctx.lineTo(x, y); + } + } + + ctx.closePath(); + ctx.fill("evenodd"); + } + } + */ //{$this._opacity} - change back ctx.strokeStyle = `rgba(0, 0, 0, ${this._lop})`; if (this._cline) { - ctx.strokeStyle = `rgba(${this._lr}, ${this._lg}, ${this._lb}, ${this._lop})`; } @@ -253,7 +288,6 @@ function renderMultiTop(canvas, { size, bounds, project, needsProjectionUpdate } for (let i=0; i < Line.idx.length; ++i){ //const voltageRating = Line.Vn1.get(0, i); //if (voltageRating <= zoomToLineVoltageRatingMinLookup.get(zoomLevel)) continue; - const fromNumber = Line.bus1[i]; const fromIndex = busToIndexLookup.get(fromNumber); const fromX = busPixelCoords.get(fromIndex, 0); @@ -265,8 +299,6 @@ function renderMultiTop(canvas, { size, bounds, project, needsProjectionUpdate } const toY = busPixelCoords.get(toIndex, 1); const dist = Math.hypot(toX - fromX, toY - fromY); - - if (dist > 12) { ctx.moveTo(fromX, fromY); @@ -282,8 +314,7 @@ function renderMultiTop(canvas, { size, bounds, project, needsProjectionUpdate } let ncop = Math.trunc(Math.round((this._nop * 255))); let lcop = Math.trunc(Math.round((this._lop * 255))); -//this._nop = Math.trunc((rval1 / 100.0) * 255); - + //this._nop = Math.trunc((rval1 / 100.0) * 255); // Draws the buses (vertices) for (let i=0; i < nelems; ++i) { @@ -303,7 +334,6 @@ function renderMultiTop(canvas, { size, bounds, project, needsProjectionUpdate } } if ((!this._cnode) && (1 != Math.floor(this._nop))) { - var cimg = ctx.getImageData(0, 0, canvas.width, canvas.height); for (let j = 0; j < cimg.data.length; j = j + 4) { @@ -312,27 +342,23 @@ function renderMultiTop(canvas, { size, bounds, project, needsProjectionUpdate } continue; } - + cimg.data[j + 3] = ncop; } - + ctx.putImageData(cimg, 0, 0); } if (this._cnode) { - var cimg = ctx.getImageData(0, 0, canvas.width, canvas.height); for (let j = 0; j < cimg.data.length; j = j + 4) { - if (cimg.data[j + 3] == 0) { - continue; } if ((cimg.data[j] == 0xfa && cimg.data[j + 1] == 0x80 && cimg.data[j + 2] == 0x72) || (cimg.data[j] == 0xff && cimg.data[j + 1] == 0xff && cimg.data[j + 2] == 0xff)) { - cimg.data[j] = (this._nr & cimg.data[j]); cimg.data[j + 1] = (this._ng & cimg.data[j + 1]); cimg.data[j + 2] = (this._nb & cimg.data[j + 2]); @@ -341,9 +367,7 @@ function renderMultiTop(canvas, { size, bounds, project, needsProjectionUpdate } if (1 != Math.floor(this._nop)) { - if (cimg.data[j + 3] != lcop) { - cimg.data[j + 3] = ncop; } } @@ -351,52 +375,54 @@ function renderMultiTop(canvas, { size, bounds, project, needsProjectionUpdate } ctx.putImageData(cimg, 0, 0); } + + /* + //Better but still laggy + if (this._cnode) { - /* - //Better but still laggy - if (this._cnode) { + let cv = document.createElement("canvas"); + cv.width = 12; + cv.height = 12; + let c2 = cv.getContext("2d"); + c2.drawImage(image, 0, 0, size, size); + let cimg = c2.getImageData(0, 0, 12, 12); - let cv = document.createElement("canvas"); - cv.width = 12; - cv.height = 12; - let c2 = cv.getContext("2d"); - c2.drawImage(image, 0, 0, size, size); - let cimg = c2.getImageData(0, 0, 12, 12); - - for (let j = 0; j < cimg.data.length; j = j + 4) { - - cimg.data[j] = (this._nr & cimg.data[j]); - cimg.data[j + 1] = (this._ng & cimg.data[j + 1]); - cimg.data[j + 2] = (this._nb & cimg.data[j + 2]); - - } - - ctx.putImageData(cimg, (x - (size / 2)), (y - (size / 2))); - - } - - else { - - ctx.drawImage(image, x - size/2, y - size/2, size, size); - } - */ - //This will make it so the line and node colors don't interact, but it makes the browser incredibly laggy - /* - if (this._cnode) { - - var cimg = ctx.getImageData((x - (size / 2)), (y - (size / 2)), (x + (size / 2)), (y + (size / 2))); + for (let j = 0; j < cimg.data.length; j = j + 4) { - for (let j = 0; j < cimg.data.length; j = j + 4) { - cimg.data[j] = (this._nr & cimg.data[j]); cimg.data[j + 1] = (this._ng & cimg.data[j + 1]); cimg.data[j + 2] = (this._nb & cimg.data[j + 2]); + + } + + ctx.putImageData(cimg, (x - (size / 2)), (y - (size / 2))); + + } + + else { + + ctx.drawImage(image, x - size/2, y - size/2, size, size); + } + */ + + //This will make it so the line and node colors don't interact, but it makes the browser incredibly laggy + + /* + if (this._cnode) { + + var cimg = ctx.getImageData((x - (size / 2)), (y - (size / 2)), (x + (size / 2)), (y + (size / 2))); + + for (let j = 0; j < cimg.data.length; j = j + 4) { - } - - ctx.putImageData(cimg, (x - (size / 2)), (y - (size / 2))); + cimg.data[j] = (this._nr & cimg.data[j]); + cimg.data[j + 1] = (this._ng & cimg.data[j + 1]); + cimg.data[j + 2] = (this._nb & cimg.data[j + 2]); + } - */ + + ctx.putImageData(cimg, (x - (size / 2)), (y - (size / 2))); + } + */ /* if (this._cnode) { @@ -419,11 +445,47 @@ function renderMultiTop(canvas, { size, bounds, project, needsProjectionUpdate } } } +/** + * @class MultiTopLayer + * @extends {L.CanvasLayer} + * + * @param {Object} newlayer - The newlayer data from the backend. + * @param {Object} options - The options for the layer. + * + * @var {Object} _context - The context is just another name for the Window’s workspace. For the most part, goes unused. + * @var {WeakMap} _cache - Caches the information needed to determine which buses are specific types so that those buses can be given special icons. This primarily goes unused. + * @var {Boolean} _render - Determines if the MultiTopLayer is displayed. + * @var {Boolean} _render_bus_ids - Determines if the Bus IDs are rendered along with the buses. This is primarily for debugging purposes. + * @var {Boolean} _cnode - Whether to use custom node colors or not. + * @var {Boolean} _cline - Whether to use custom line colors or not. + * @var {Number} _nr - The red value of the custom node color. + * @var {Number} _ng - The green value of the custom node color. + * @var {Number} _nb - The blue value of the custom node color. + * @var {Number} _lr - The red value of the custom line color. + * @var {Number} _lg - The green value of the custom line color. + * @var {Number} _lb - The blue value of the custom line color. + * @var {Number} _nop - The opacity of the nodes. + * @var {Number} _lop - The opacity of the lines. + * @var {Number} _lthick - The thickness of the lines. + * @var {Number} _nsize - The size of the nodes. + * @var {Object} _newlayer - The newlayer associated with the MultiTopLayer. + * @var {Object} _images - Contains the icons for the various types of nodes. + * + * @returns {MultiTopLayer} + */ L.MultiTopLayer = L.CanvasLayer.extend({ options: { render: renderMultiTop, }, + /** + * Sets MultiTopLayer variables. + * + * @constructs MultiTopLayer + * @param {Object} newlayer + * @param {Object} options + * @returns + */ initialize(newlayer, options) { this._context = null; this._cache = new WeakMap(); @@ -482,11 +544,25 @@ L.MultiTopLayer = L.CanvasLayer.extend({ L.CanvasLayer.prototype.initialize.call(this, options); }, + /** + * Updates the values for the nodes and lines and then re-renders the MultiTopLayer. + * + * @memberof MultiTopLayer + * @param {Object} context + * @returns + */ update(context) { this._context = context; this.redraw(); }, + /** + * Handles adding the MultiTopLayer to the map. + * + * @memberof MultiTopLayer + * @param {Map} map - The map from Window + * @returns + */ onAdd(map) { L.CanvasLayer.prototype.onAdd.call(this, map); console.log("Adding multitop"); @@ -494,23 +570,45 @@ L.MultiTopLayer = L.CanvasLayer.extend({ this.getPane().classList.add("multitop-pane" + this._newlayer.num); }, + /** + * Switches the state of MultiTopLayer._render. + * + * @memberof MultiTopLayer + * @returns + */ toggleRender() { this._render = !this._render; //console.log("Topology rendering: ", this._render); }, + /** + * Switches the state of MultiTopLayer._cnode. + * + * @memberof MultiTopLayer + * @returns + */ toggleCNode() { - this._cnode = !this._cnode; }, + /** + * Switches the state of MultiTopLayer._cline. + * + * @memberof MultiTopLayer + * @returns + */ toggleCLine() { - this._cline = !this._cline; }, + /** + * Updates the node color values based on user input. + * + * @memberof MultiTopLayer + * @param {String} cval1 - The RGB string from the input. + * @returns + */ updateCNVal(cval1) { - this._nr = parseInt(cval1.slice(1, 3), 16); this._ng = parseInt(cval1.slice(3, 5), 16); this._nb = parseInt(cval1.slice(5, 7), 16); @@ -520,8 +618,14 @@ L.MultiTopLayer = L.CanvasLayer.extend({ console.log("Blue: " + this._nb); }, + /** + * Updates the line color values based on user input. Also ensures that the line color does not match specific values needed when recoloring nodes. + * + * @memberof MultiTopLayer + * @param {String} cval2 - The RGB string from the input. + * @returns + */ updateCLVal(cval2) { - this._lr = parseInt(cval2.slice(1, 3), 16); this._lg = parseInt(cval2.slice(3, 5), 16); this._lb = parseInt(cval2.slice(5, 7), 16); @@ -539,14 +643,26 @@ L.MultiTopLayer = L.CanvasLayer.extend({ } }, + /** + * Updates the opacity value for nodes and normalizes it to a 0-1 range. + * + * @memberof MultiTopLayer + * @param {Number} rval1 - The value from the range input. + * @returns + */ updateNOp(rval1) { - //this._nop = Math.trunc((rval1 / 100.0) * 255); this._nop = rval1 / 100.0; }, + /** + * Updates the opacity value for lines and normalizes it to a 0-1 range. + * + * @memberof MultiTopLayer + * @param {Number} rval2 + * @returns + */ updateLOp(rval2) { - //this._lop = Math.trunc((rval2 / 100.0) * 255); let temp = rval2; @@ -558,18 +674,36 @@ L.MultiTopLayer = L.CanvasLayer.extend({ this._lop = temp / 100.0; }, + /** + * Updates the thickness value for the lines. + * + * @memberof MultiTopLayer + * @param {Number} rval3 - The value from the range input. + * @returns + */ updateLThick(rval3) { - this._lthick = rval3; }, + /** + * Updates the size value for nodes. + * + * @memberof MultiTopLayer + * @param {Number} rval4 - The value from the range input. + * @returns + */ updateNSize(rval4) { - this._nsize = rval4; }, + /** + * Changes the newlayer’s current values to be those from another newlayer. Used exclusively for the “Prioritize Layer” button. + * + * @memberof MultiTopLayer + * @param {Object} oldlayer - The newlayer that the values are being taken from. + * @returns + */ stealVals(oldlayer) { - this._render = oldlayer._render; this._render_bus_ids = oldlayer._render_bus_ids; this._cnode = oldlayer._cnode; diff --git a/agvis/static/js/TopologyLayer.js b/agvis/static/js/TopologyLayer.js index b2ded22..adeccf0 100644 --- a/agvis/static/js/TopologyLayer.js +++ b/agvis/static/js/TopologyLayer.js @@ -1,3 +1,29 @@ +/* **************************************************************************************** + * File Name: TopologyLayer.js + * Authors: Nicholas West, Nicholas Parsley + * Date: 9/28/2023 (last modified) + * + * Description: TopologyLayer.js contains the code the TopologyLayer class. The + * ToplogyLayer handles displaying the nodes (buses) and lines for a + * given power system. + * + * API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/modeling/topology.html + * ****************************************************************************************/ + +/** + * Renders for the TopologyLayer. It establishes many lookup variables for specific node + * types, but these go unused for the most part. Lines are drawn between node locations + * by the Canvas Context. Nodes are placed after the lines are drawn, and their icons + * depend on their associated image in busToImageLookup. Lines and nodes are drawn in order + * of appearance in the data. + * + * @param {HTML Canvas Element} canvas - The canvas that the layer will be drawn on. + * @param {Point} size - Represents the current size of the map in pixels. + * @param {LatLngBounds} bounds - Represents the geographical bounds of the map. + * @param {Function} project - The latLngToContainerPoint function specifically for CanvasLayer._map. + * @param {Boolean} needsProjectionUpdate - Determines whether the Layer’s projection needs to be updated. + * @returns + */ function renderTopology(canvas, { size, bounds, project, needsProjectionUpdate }) { const images = this._images; if (!images.allLoaded) return; @@ -258,11 +284,33 @@ function renderTopology(canvas, { size, bounds, project, needsProjectionUpdate } } } +/** + * @class TopologyLayer + * @extends {L.CanvasLayer} + * + * @var {Object} _context - Another name for the Window's workspace object. + * @var {WeakMap} _cache - Caches the information needed to determine which buses are specific types so that those buses can be given special icons. This primarily goes unused. + * @var {Boolean} _render - Determines if the TopologyLayer is displayed. + * @var {Boolean} _render_bus_ids - Determines if the Bus IDs are rendered along with the buses. This is primarily for debugging purposes. + * @var {Number} _opacity - The opacity setting for the lines drawn between the buses. + * @var {Object} _images - Contains the icons for the various types of nodes. + * + * @returns {TopologyLayer} + */ L.TopologyLayer = L.CanvasLayer.extend({ options: { render: renderTopology, }, + /** + * Sets the TopologyLayer’s starting variables. + * + * @constructs ToplogyLayer + * @param {Object} options - The options Object from Window. Unused beyond being passed to the CanvasLayer's + * initialization function, seemed to be initially used to set certain variables, + * but those values are instead hardcoded into the initialization. + * @returns + */ initialize(options) { this._context = null; this._cache = new WeakMap(); @@ -303,16 +351,36 @@ L.TopologyLayer = L.CanvasLayer.extend({ L.CanvasLayer.prototype.initialize.call(this, options); }, + /** + * Updates the values for the nodes and lines and then re-renders the TopologyLayer. + * + * @memberof TopologyLayer + * @param {Object} context - The workspace from Window. + * @returns + */ update(context) { this._context = context; this.redraw(); }, + /** + * Handles adding the TopologyLayer to the map. + * + * @memberof TopologyLayer + * @param {Map} map - The map from Window. + * @returns + */ onAdd(map) { L.CanvasLayer.prototype.onAdd.call(this, map); this.getPane().classList.add("topology-pane"); }, + /** + * Switches the state of TopologyLayer._render. + * + * @memberof TopologyLayer + * @returns + */ toggleRender() { this._render = !this._render; //console.log("Topology rendering: ", this._render); diff --git a/agvis/static/js/Window.js b/agvis/static/js/Window.js index f0c1082..39c32e1 100644 --- a/agvis/static/js/Window.js +++ b/agvis/static/js/Window.js @@ -1,9 +1,63 @@ +/* **************************************************************************************** + * File Name: Window.js + * Authors: Nicholas West, Nicholas Parsly, and Zack Malkmus + * Date: 9/15/2023 (last modified) + * + * Important: The window class is the main class for AGVis. It contains 5 layers: + * (Tile Layer | Zone Layer | Topology Layer | Contour Layer | User Layer) + * + * Description: Windows initialize the map and all of the layers, and contains the + * main thread. They handle timing for the animations and receiving data + * from DiME. They also instantiate most of the Layers and UI elements + * used for displaying data.Developers that want to add new features to + * AGVis will inevitably have to either interface with a Window directly + * or with one of its components. + * + * Note: Each layer is described in their respective files and on the github + * https://ltb.readthedocs.io/projects/agvis/en/latest/modeling/index.html#development + * + * API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/modeling/window.html + * ****************************************************************************************/ + +/** + * @class Window + * + * @param {Number} num - The number of the window. + * @param {Object} options - The options for the window. + * @param {Object} dimec - The DiME client (Unused). + * @param + * + * @var {Object} workspace - Contains the data of all variables for the current timestep. + * @var {Object} history - Contains the data of all variabes for all timesteps. + * @var {Object} states - The enum for the window's view state. + * @var {Number} state - The window's current view state. + * @var {Object} options - The options for the window. + * @var {Array} multilayer - An array of newlayer Objects containing the data necessary for displaying simulations added by file upload. + * + * @example const window1 = new Window(1, options[0]); + * @see index.html + */ class Window { + /** + * Instantiates the layers along with various UI elements. It also begins + * the timer loop for simulations uploaded by users instead of sent through DiME. + * + * @constructs Window + * @param {Number} num - The number of the window. + * @param {Object} options - The options for the window. + * @param {Object} dimec - The DiME client (Unused). + * @returns + */ constructor(num, options, dimec) { + + // ==================================================================== + // Initialize workspace and history + // ==================================================================== + this.workspace = {}; this.history = {}; - // Keep track of the view state + // Enum to track the window's view state this.states = { angl: 0, volt: 1, @@ -18,9 +72,16 @@ class Window { this.multihistory = []; this.mlayercur = 0; this.mnumfree = 0; - - //Loops every 17 milliseconds to update the animation for the independent simulation data - //Animation step is associated with receiving info from DiME, so we have to use this for the bundled version + + /** + * The timer loop for the playback bar. It updates the playback bar every 17 milliseconds. + * Animation step is associated with receiving info from DiME, so we have to use this for + * the bundled version. + * + * @memberof Window + * @param {Object} multilayer - The array of multilayers. + * @returns + */ setInterval(function(multilayer) { let timestep = Number(Date.now()); for (let i = 0; i < multilayer.length; i++) { @@ -61,11 +122,15 @@ class Window { this.map = L.map(this.map_name, { minZoom: 3, - maxZoom: 10, + maxZoom: 18, zoomSnap: 0.01, center: [40, -100], zoom: 5, }); + + // =================================================================== + // Initialize AGVis layers and UI elements + // =================================================================== this.map.handshake = true; this.legend = new L.DynamicLegend(this, options).addTo(this.map); @@ -79,15 +144,21 @@ class Window { this.map.addControl(this.searchLayer.control); this.simTimeBox = L.simTimeBox({ position: 'topright' }).addTo(this.map); - // side bar + /** + * The function that is called when the user clicks on the map. It is used to toggle the sidebar. + * + * @memberof Window + * @param {Object} e - The event object. + * @returns + */ const sidebar = L.control.sidebar({ - autopan: true, // whether to maintain the centered map point when opening the sidebar - closeButton: true, // whether t add a close button to the panes + autopan: true, // whether to maintain the centered map point when opening the sidebar + closeButton: true, // whether t add a close button to the panes container: this.map_name + '_sidebar', // the DOM container or #ID of a predefined sidebar container that should be used - position: 'right', // left or right + position: 'right', // left or right }).addTo(this.map); - /* add a new panel */ + // Add new panels to the sidebar let visPlotName = this.map_name + "Vis"; let visPane = ''; @@ -107,11 +178,11 @@ class Window { addSidebarLayers(this, options, sidebar); sidebar.addPanel({ - id: 'plotPanel', // UID, used to access the panel - tab: '', // content can be passed as HTML string, - pane: visPane, // DOM elements can be passed, too - title: 'LTB Plot Panel', // an optional pane header - position: 'top' // optional vertical alignment, defaults to 'top' + id: 'plotPanel', // UID, used to access the panel + tab: '', // content can be passed as HTML string, + pane: visPane, // DOM elements can be passed, too + title: 'LTB Plot Panel', // an optional pane header + position: 'top' // optional vertical alignment, defaults to 'top' }); sidebar.addPanel({ @@ -123,6 +194,14 @@ class Window { }); } + /** + * This getter retrieves the variables stored in Window.history, allowing for the + * playback of simulations. + * + * @memberof Window + * @param {String} varname - The name of the variable to retrieve. + * @param {Number} currentTimeInSeconds - The current time in seconds. + */ historyKeeper(varname, currentTimeInSeconds) { const varHistory = this.history[varname]; let value; @@ -141,6 +220,13 @@ class Window { return true; } + /** + * Begins drawing the simulation once initial data is received from DiME. + * Initializes Contour Layer and starts its animation. + * + * @memberof Window + * @returns + */ startSimulation() { const busVoltageIndices = this.workspace.Idxvgs.Bus.V.array; const busThetaIndices = this.workspace.Idxvgs.Bus.theta.array; @@ -195,6 +281,13 @@ class Window { this.contourLayer.updateRange(fmin, fmax); } + /** + * Called once all the simulation data has been received. + * It sets the end time for the animation and adds the Playback Bar UI element. + * + * @memberof Window + * @return + */ endSimulation() { this.time = this.end_time = Number(this.workspace.Varvgs.t.toFixed(2)); this.pbar.addTo(this.map); @@ -222,6 +315,12 @@ class Window { } } */ + /** + * Creates and calls the step() and reset() functions to draw the animation + * + * @memberof Window + * @returns + */ async drawThread() { const lineSpec = { "$schema": "https://vega.github.io/schema/vega-lite/v4.json", @@ -259,6 +358,13 @@ class Window { let firstTime = null; + /** + * Finds the difference between the current time and the previous step() call’s time and + * updates the variables and the Layers based on the new time. It also updates the SimTimeBox display. + * + * @param {*} currentTime + * @returns + */ function step(currentTime) { requestAnimationFrame(step); @@ -317,6 +423,12 @@ class Window { } } + /** + * Resets the variable for telling if an animation is starting from the beginning. + * + * @memberof Window + * @returns + */ function reset() { firstTime = null; if (self.p1 !== undefined) @@ -331,6 +443,16 @@ class Window { return reset; } + /** + * The main AGVis program. Starts and stops the simulation based on input from the + * DiME server. Also creates some UI elements for changing simulation view. + * Invoked in index.html after creating the window. + * + * @memberof Window + * @returns + * + * @see index.html + */ async mainThread() { const self = this; @@ -339,7 +461,11 @@ class Window { this.map.resetTime = resetTime; this.resetTime = resetTime; - /// Bar of icons for voltage, theta and frequency + // ==================================================================== + // UI Buttons + // ==================================================================== + + // voltage angle const thetaButton = L.easyButton('Θ', function(btn, map) { const amin = (self.options.amin === undefined) ? -1.0 : self.options.amin; const amax = (self.options.amax === undefined) ? 1.0 : self.options.amax; @@ -349,7 +475,8 @@ class Window { self.state = self.states.angl; self.legend.update(); }); - + + // voltage magnitude const voltageButton = L.easyButton('V', function(btn, map) { const vmin = (self.options.vmin === undefined) ? 0.8 : self.options.vmin; const vmax = (self.options.vmax === undefined) ? 1.2 : self.options.vmax; @@ -360,6 +487,7 @@ class Window { self.legend.update(); }); + // frequency const freqButton = L.easyButton('f', function(btn, map) { const fmin = (self.options.fmin === undefined) ? 0.9998 : self.options.fmin; const fmax = (self.options.fmax === undefined) ? 1.0002 : self.options.fmax; @@ -386,6 +514,10 @@ class Window { const toggleLayerButtons = [rendContourButton, rendCommunicationButton]; const toggleLayerBar = L.easyBar(toggleLayerButtons).addTo(this.map); + // ==================================================================== + // DiME + // ==================================================================== + newdimeserver: for (;;) { this.dimec = new dime.DimeClient(this.options.dimehost, this.options.dimeport); let dime_updated = this.dime_updated(); @@ -445,6 +577,15 @@ class Window { } } + /** + * Sets the history and workspace when loading a previous simulation from a DiME file upload. + * Note that this is separate from the MultiLayer and IDR features. The button for this is + * found in the Configuration menu. + * + * @memberof Window + * @param {Object} buf - The Array buffer from the file upload + * @returns + */ load(buf) { let {workspace, history} = dime.dimebloads(buf); @@ -452,6 +593,13 @@ class Window { this.history = history; } + /** + * Downloads a DiME file of the current simulation. + * Note that this is separate from the MultiLayer and IDR features. + * + * @memberof Window + * @returns {Object} - The DiME file of the current simulation. + */ save() { return dime.dimebdumps({history: this.history, workspace: this.workspace}); } diff --git a/agvis/static/js/ZoneLayer.js b/agvis/static/js/ZoneLayer.js index c4ea88c..3394fe9 100644 --- a/agvis/static/js/ZoneLayer.js +++ b/agvis/static/js/ZoneLayer.js @@ -1,5 +1,38 @@ +/* **************************************************************************************** + * File Name: ZoneLayer.js + * Authors: Nicholas West, Nicholas Parsley + * Date: 9/28/2023 (last modified) + * + * Description: ZoneLayer.js contains the code for the ZoneLayer class. The ZoneLayer + * highlights certain areas of the map with colors. These areas are + * determined by a GeoJSON file. For the most part, ZoneLayer is fairly + * self-explanatory. During its initialization, it calls a chain of functions + * that ensure that the necessary data is loaded before rendering. It is also + * guaranteed to be drawn underneath the TopologyLayer and ContourLayer. The + * ZoneLayer extends from the Leaflet GeoJSON class. + * + * API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/modeling/zone.html + * ****************************************************************************************/ + +/** + * @class ZoneLayer + * @extends {L.GeoJSON} + * @param {Object} options - The leaflet options passed to the ZoneLayer. + * @var {Boolean} _render - Determines whether the ZoneLayer is rendered or not. + * @var {Response} geojson - The main thing to note with the geojson variable is the fetch command used to initialize it. + * @returns {ZoneLayer} + */ L.ZoneLayer = L.GeoJSON.extend({ options: { + /** + * Determines which colors are assigned to what zones based on the GeoJSON data. Adjusting the return values of the switch statement + * can change the colors of the zones. The cases for the switch statement will most likely have to be changed if a different + * GeoJSON file is used. + * + * @memberof ZoneLayer + * @param {Object} feature - Contains the properties of the GeoJSON file that are used to determine the color of specific zones. + * @returns + */ style: function(feature) { switch (feature.properties.NERC) { case 'MRO': return {color: "#ff0000"}; @@ -14,6 +47,13 @@ L.ZoneLayer = L.GeoJSON.extend({ } }, + /** + * Initialize the ZoneLayer variables. + * + * @constructs ZoneLayer + * @param {Object} options + * @returns + */ initialize(options) { L.GeoJSON.prototype.initialize.call(this, null, options); this._render = false; @@ -28,6 +68,14 @@ L.ZoneLayer = L.GeoJSON.extend({ })(this); }, + /** + * Adds GeoJSON data to the layer but also manipulates the order in which different + * map layers are rendered by moving the GeoJSON layer's pane within the DOM structure. + * + * @memberof ZoneLayer + * @param {*} geojson + * @returns + */ addData(geojson) { L.GeoJSON.prototype.addData.call(this, geojson); @@ -50,6 +98,12 @@ L.ZoneLayer = L.GeoJSON.extend({ } }, + /** + * Toggles the rendering of the ZoneLayer. + * + * @memberof ZoneLayer + * @returns + */ toggleRender() { this._render = !this._render; console.log("Zone rendering: ", this._render); diff --git a/agvis/web.py b/agvis/web.py new file mode 100644 index 0000000..d02c9e4 --- /dev/null +++ b/agvis/web.py @@ -0,0 +1,107 @@ +# ================================================================================ +# File Name: web.py +# Author: Zack Malkmus +# Date: 10/20/2023 (last modified) +# Description: The backend for the AGVis web application. +# API Docs: https://ltb.readthedocs.io/projects/agvis/en/latest/?badge=stable +# ================================================================================ + +import os +import sys +import subprocess +from flask import Flask, render_template, send_from_directory +from .flask_configurations import DefaultConfig +from .flask_configurations import DevelopmentConfig +from .flask_configurations import ProductionConfig +import requests + +class AgvisWeb(): + """ + The backend for the AGVis web application. + This class handles all routes and web application logic. + """ + + def __init__(self): + """ + Initializes the AgvisWeb class with operating system name and + application directory. + """ + self.os_name = sys.platform + self.app_dir = os.path.dirname(os.path.abspath(__file__)) + self.app = self.create_app() + + def create_app(self): + """ + Create the Flask application. + + Returns + ------- + app + Flask application + """ + app = Flask(__name__) + app.requests_session = requests.Session() + app.config.from_object(DefaultConfig()) + + # Add Routes + @app.route('/') + def index(): + return render_template('index.html') + + @app.route('/', methods=['GET']) + def static_proxy(path): + return send_from_directory('static', path) + + return app + + + def run(self, host='localhost', port=8810, workers=1, dev=False): + """ + Run the AGVis web application using Gunicorn. + For windows, use Flask's development server. + """ + try: + # Print out the URL to access the application + print(f"AGVis will serve static files from directory \"{os.path.join(os.getcwd(), 'agvis/static')}\"") + print(f"at the URL http://{host}:{port}. Open your web browser and navigate to the URL to access the application.") + print("\nStarting AGVis... Press Ctrl+C to stop.\n") + + # Check if AGVis is running on Windows + if (self.os_name == 'win32' or self.os_name == 'cygwin' or self.os_name == 'msys'): + print("WARNING: AGVis is running on Windows. This is not recommended for production use.") + print("Please use a Linux-based operating system for production use.") + dev = True + + # Run flask as a development server + if (dev): + self.app.config.from_object(DevelopmentConfig()) + + command = [ + 'flask', + '--app', 'main', + 'run', + '--host', host, + '--port', str(port), + '--no-reload' + ] + # Run flask as a production server + else: + self.app.config.from_object(ProductionConfig()) + + command = [ + 'gunicorn', + '-b', f'{host}:{port}', + '-w', str(workers), + '--timeout', '600', + 'agvis.main:app' + ] + + # Start the application + with self.app.requests_session: + subprocess.run(command, check=True, cwd=self.app_dir) + + except KeyboardInterrupt: + print('\nAGVis has been stopped. You may now close the browser.') + + except Exception as e: + print(f'An unexpected error has occured while trying to start AGVis: {e}') \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index e84bc0f..256663e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -59,8 +59,8 @@ # -- Project information ----------------------------------------------------- project = 'AGVis' -copyright = '2023, Nicholas Parsly, Jinning Wang' -author = 'Nicholas Parsly, Jinning Wang' +copyright = '2023-2024, Nicholas Parsly, Jinning Wang, Zackary Malkmus' +author = 'Nicholas Parsly, Jinning Wang, Zackary Malkmus' # The full version, including alpha/beta/rc tags # The short X.Y version. diff --git a/docs/source/getting_started/copyright.rst b/docs/source/getting_started/copyright.rst index 6038724..3e646e4 100644 --- a/docs/source/getting_started/copyright.rst +++ b/docs/source/getting_started/copyright.rst @@ -7,7 +7,7 @@ License GNU Public License v3 ********************* -| Copyright :raw-html:`©` 2020-2023, Nick West, Nicholas Parsly, Jinning Wang +| Copyright :raw-html:`©` 2020-2024, Nick West, Nicholas Parsly, Jinning Wang, Zackary Malkmus AGVis is free software; you can redistribute it and/or modify it under the terms of the `GNU General Public License `_ diff --git a/docs/source/getting_started/install.rst b/docs/source/getting_started/install.rst index f025b3e..35e1c10 100644 --- a/docs/source/getting_started/install.rst +++ b/docs/source/getting_started/install.rst @@ -103,6 +103,10 @@ the code and, for example, develop new models or routines. The benefit of development mode installation is that changes to source code will be reflected immediately without re-installation. +.. note:: + + Windows users must use the development mode installation to run AGVis. + Step 1: Get AGVis source code As a developer, you are strongly encouraged to clone the source code using ``git`` diff --git a/docs/source/getting_started/tutorial/cli.rst b/docs/source/getting_started/tutorial/cli.rst index 1044f14..766035e 100644 --- a/docs/source/getting_started/tutorial/cli.rst +++ b/docs/source/getting_started/tutorial/cli.rst @@ -63,6 +63,11 @@ You can open the web application in your browser. :alt: webapplication :width: 960px +.. note:: + + Windows users will have to use `agvis run` from the `agvis` directory. + This directory must contain both `app.py` and `__main__.py`. + stop .......... diff --git a/go.sh b/go.sh index e0911f2..6cb8b06 100755 --- a/go.sh +++ b/go.sh @@ -20,6 +20,12 @@ piptrustedhost= [ -f env.sh ] && . env.sh build() { + # Set Agvis Branch + local BRANCH_NAME="master" + if [ "$1" ]; then + BRANCH_NAME=$1 + fi + # Clone ANDES and DiME if they don't exist if [ ! -d "../andes" ]; then echo "Cloning ANDES repository..." @@ -41,12 +47,16 @@ build() { ${target:+--target $target} \ ${pipindex:+--build-arg PIP_INDEX_URL=$pipindex} \ ${piptrustedhost:+--build-arg PIP_TRUSTED_HOST=$piptrustedhost} \ + --build-arg BRANCH_NAME="$BRANCH_NAME" \ -t $tag . } +run_tests() { + docker run -u root --rm $tag sh -c "cd tests && pytest" +} + dev2() { - google-chrome --incognito http://localhost:8810/ 2> /dev/null > /dev/null &! - + google-chrome --incognito http://localhost:8810/ 2> /dev/null > /dev/null & tmux split-window -v tmux split-window -v tmux select-layout tiled @@ -180,7 +190,7 @@ dev-benchmark() { } dev() { - google-chrome --incognito http://localhost:8810/ 2> /dev/null > /dev/null &! + google-chrome --incognito http://localhost:8810/ 2> /dev/null > /dev/null & tmux split-window -v tmux split-window -v @@ -201,11 +211,11 @@ dime-cygwin() { #################################################################################################### # dev-cygwin() { -# google-chrome --incognito http://localhost:8810/ 2> /dev/null > /dev/null &! +# google-chrome --incognito http://localhost:8810/ 2> /dev/null > /dev/null & -# mintty --exec "docker run --rm -t -v C:/cygwin64/`pwd`/static:/srv -p 8810:8810 $tag python3 -m http.server -d /srv $((port+0))" &! -# mintty --exec "docker run --rm -t -p 5000:5000 -p 8818:8818 $tag dime -vv -l tcp:5000 -l ws:$((port+8))" &! -# #mintty --exec "docker run --rm -t $tag andes -v 10 run /home/cui/wecc_vis.xlsx --dime tcp://127.0.0.1:5000 -r tds" &! +# mintty --exec "docker run --rm -t -v C:/cygwin64/`pwd`/static:/srv -p 8810:8810 $tag python3 -m http.server -d /srv $((port+0))" & +# mintty --exec "docker run --rm -t -p 5000:5000 -p 8818:8818 $tag dime -vv -l tcp:5000 -l ws:$((port+8))" & +# #mintty --exec "docker run --rm -t $tag andes -v 10 run /home/cui/wecc_vis.xlsx --dime tcp://127.0.0.1:5000 -r tds" & # } # http-cygwin() { diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/_test_dime_client.py b/tests/_test_dime_client.py new file mode 100644 index 0000000..77ddb3d --- /dev/null +++ b/tests/_test_dime_client.py @@ -0,0 +1,81 @@ +# ================================================================================ +# File Name: test_dime_client.py +# Author: Zack Malkmus +# Date: 11/16/2023 (last modified) +# Description: Test cases for AGVis-DiME integration +# Note: Running this test mulitple times in succession will result in +# failure as DiME takes a while to terminate. +# ================================================================================ + +from dime import DimeClient +import pytest +import time +import subprocess +import psutil + +@pytest.fixture(scope="module") +def dime_server(): + """Fixture to start and stop the dime server""" + server = start_dime_server() + yield server + stop_dime_server(server) + +def start_dime_server(): + """Spin up the dime server""" + try: + command = "dime -l tcp:8888" + server = subprocess.Popen(command) + time.sleep(0.5) # Let DiME start + return server + except Exception as e: + print(f"Failed to start DiME server: {e}") + server.terminate() + yield + +def stop_dime_server(server): + try: + """ + For some reason, dime spawns zombie processes that are hard to terminate. + This code ensures all instances of dime that we just spawned are removed. + """ + parent = psutil.Process(server.pid) + for child in parent.children(recursive=True): + child.terminate() + _, alive = psutil.wait_procs(parent.children(), timeout=5) + if alive: + for child in alive: + child.kill() + parent.kill() + parent.wait(timeout=5) + except Exception as e: + print(f"Failed to stop DiME server: {e}") + +def test_dime(dime_server): + d1 = None + d2 = None + d3 = None + + server = dime_server + + assert server != None + + try: + d1 = DimeClient("tcp", "localhost", 8888) + d2 = DimeClient("tcp", "localhost", 8888) + d3 = DimeClient("tcp", "localhost", 8888) + d1.join("turtle", "lizard") + d2.join("crocodile") + d3.join("dragon", "wyvern") + assert d1.devices() == ['crocodile', 'wyvern', 'lizard', 'dragon', 'turtle'] + except Exception as e: + print(f"Test Failed: {e}") + assert False + finally: + if d1: + d1.close() + if d2: + d2.close() + if d3: + d3.close() + time.sleep(1) + stop_dime_server(server) \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..6dfa43d --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,11 @@ +import pytest +from agvis.web import AgvisWeb + +@pytest.fixture +def app(): + agvis_web = AgvisWeb() + yield agvis_web.app + +@pytest.fixture +def client(app): + return app.test_client() \ No newline at end of file diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..0c1771f --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,74 @@ +# ================================================================================ +# File Name: test_client.py +# Author: Zack Malkmus +# Date: 11/20/2023 (last modified) +# Description: Test the CLI for AGVis +# ================================================================================ + +import pytest +import argparse +import subprocess +from agvis.cli import create_parser, main + +@pytest.fixture +def parser(): + return create_parser() + +def test_create_parser(parser): + assert isinstance(parser, argparse.ArgumentParser) + +def test_agvis_run_default_port(monkeypatch, client): + c1 = "agvis run --dev=True" + + def mock_subprocess_run(c2, **kwargs): + assert c2 == c1 + return subprocess.CompletedProcess(args=c2, returncode=0, stdout=b'', stderr=b'') + + monkeypatch.setattr(subprocess, 'run', mock_subprocess_run) + + with client: + response = client.get('/') + assert response.status_code == 200 + +def test_agvis_run_custom_port(monkeypatch, client): + c1 = "agvis run --dev=True --port=9000" + + def mock_subprocess_run(c2, **kwargs): + assert c2 == c1 + return subprocess.CompletedProcess(args=c2, returncode=0, stdout=b'', stderr=b'') + + monkeypatch.setattr(subprocess, 'run', mock_subprocess_run) + + with client: + response = client.get('/') + assert response.status_code == 200 + +def test_agvis_misc_license(monkeypatch, capsys): + c1 = "agvis misc --license" + + def mock_subprocess_run(c2, **kwargs): + assert c2 == c1 + return subprocess.CompletedProcess(args=c2, returncode=0, stdout=b'', stderr=b'') + + monkeypatch.setattr(subprocess, 'run', mock_subprocess_run) + + with capsys.disabled(), pytest.raises(SystemExit): + main() + +# ================================================================================ +# DISABLED: This test runs different locally than on GitHub Actions +# ================================================================================ + +# def test_agvis_run_invalid_command(monkeypatch, capsys): +# c1 = "agvis invalid_command" + +# def mock_subprocess_run(c2, **kwargs): +# assert c2 == c1 +# return subprocess.CompletedProcess(args=c2, stdout=b'', stderr=b'Invalid command') + +# monkeypatch.setattr(subprocess, 'run', mock_subprocess_run) + +# with capsys.disabled(), pytest.raises(SystemExit) as exc_info: +# main() + +# assert exc_info.value.code == 2 \ No newline at end of file diff --git a/tests/test_web.py b/tests/test_web.py new file mode 100644 index 0000000..c94d68a --- /dev/null +++ b/tests/test_web.py @@ -0,0 +1,24 @@ +# ================================================================================ +# File Name: test_web.py +# Author: Zack Malkmus +# Date: 11/16/2023 (last modified) +# Description: Tests AGVis' Flask web application +# ================================================================================ + +import os, os.path + +def test_index(client): + response = client.get('/') + assert response.status_code == 200 + +def test_static_js_files(client): + path = os.path.dirname(os.path.abspath(__file__)) + path = os.path.join(path, '../agvis/static/js') + assert os.path.exists(path) + for filename in os.listdir(path): + response = client.get(f'/static/js/{filename}') + assert response.status_code == 200 + +def test_nonexistent_route(client): + response = client.get('/nonexistent') + assert response.status_code == 404 \ No newline at end of file