diff --git a/.gitignore b/.gitignore index 89559c0b..9079e45b 100644 --- a/.gitignore +++ b/.gitignore @@ -55,10 +55,27 @@ target/ # Mac OS X stuff .DS_Store -# nengo_viz specific -*.py.cfg +# nengo_gui specific +# *.py.cfg .ipynb_checkpoints # Vagrant .vagrant Vagrantfile + +# Node JS +node_modules/ + +# jsDoc +out/ + +# TypeScript +typings/ +nengo_gui/static/**/*.js +nengo_gui/static/**/*.js.map + +# JS files associated with tests +**/tests/*.js + +# IDE related files +.vscode/ diff --git a/.jscs.json b/.jscs.json index fc6f0f0f..fe1b7ee4 100644 --- a/.jscs.json +++ b/.jscs.json @@ -1,4 +1,6 @@ { + "excludeFiles": ["nengo_gui/static/dist/**"], + "requireCurlyBraces": [ "if", "else", @@ -11,10 +13,20 @@ "requireOperatorBeforeLineBreak": true, "maximumLineLength": { "value": 80, - "allExcept": ["comments", "regex"] + "allExcept": ["regex"] }, "validateIndentation": 4, + "requireCapitalizedComments": true, + "requireSpaceAfterLineComment": true, + "disallowMultipleSpaces": true, + "requireKeywordsOnNewLine": [ + "do", "for", "while", + "switch", "case", + "try", "catch", "finally", + "return" + ], + "disallowMultipleLineStrings": true, "disallowMixedSpacesAndTabs": true, "disallowTrailingWhitespace": true, @@ -33,15 +45,25 @@ "try", "catch" ], + "requireSpaceAfterComma": { + "allExcept": ["trailing"] + }, "requireSpaceBeforeBinaryOperators": [ "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", "&=", "|=", "^=", "+=", - "+", "-", "*", "/", "%", "<<", ">>", ">>>", "&", + "+", "-", "%", "<<", ">>", ">>>", "&", + "|", "^", "&&", "||", "===", "==", ">=", + "<=", "<", ">", "!=", "!==" + ], + "requireSpaceAfterBinaryOperators": [ + "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", + "&=", "|=", "^=", "+=", + + "+", "-", "%", "<<", ">>", ">>>", "&", "|", "^", "&&", "||", "===", "==", ">=", "<=", "<", ">", "!=", "!==" ], - "requireSpaceAfterBinaryOperators": true, "requireSpacesInConditionalExpression": true, "requireSpaceBeforeBlockStatements": true, "requireSpacesInForStatement": true, diff --git a/CHANGES.rst b/CHANGES.rst index 6d575099..cb5b9090 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,11 +13,11 @@ Release History .. Changes should be organized in one of several sections: - - API changes - - Improvements - - Behavioural changes - - Bugfixes - - Documentation + - Added + - Changed + - Deprecated + - Removed + - Fixed 0.2.1 (unreleased) ================== diff --git a/browserslist b/browserslist new file mode 100644 index 00000000..6d38c21a --- /dev/null +++ b/browserslist @@ -0,0 +1,5 @@ +# Browsers that Nengo GUI supports +Chrome >= 50 +Firefox >= 45 +Safari >= 9 +# Should test with Microsoft Edge, IE, Opera diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 00000000..c0ceb93c --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,30 @@ +.MathJax .mi, .MathJax .mo { + color: inherit; +} + +.container.docutils { + margin: inherit; + padding: inherit; + width: inherit; +} + +.container.docutils.toggle { + margin: 0; + padding: 0; +} + +.toggle .header { + clear: both; + cursor: pointer; + display: block; +} + +.toggle .header::after { + content: " ▼"; + display: inline; +} + +.toggle .header.open::after { + content: " ▲"; + display: inline; +} diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html new file mode 100644 index 00000000..c7e67b49 --- /dev/null +++ b/docs/_templates/layout.html @@ -0,0 +1,22 @@ +{# Import the theme's layout. #} +{% extends "!layout.html" %} + +{%- block extrahead %} + +{# Call the parent block #} +{{ super() }} +{%- endblock %} + +{%- block footer %} + +{{ super() }} +{%- endblock %} diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 00000000..7177e86d --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,131 @@ +************* +Nengo GUI API +************* + +.. automodule:: nengo_gui.config + :members: + :undoc-members: + +.. automodule:: nengo_gui.exec_env + :members: + :undoc-members: + +.. automodule:: nengo_gui.gui + :members: + :undoc-members: + +.. automodule:: nengo_gui.guibackend + :members: + :undoc-members: + +.. automodule:: nengo_gui.ipython + :members: + :undoc-members: + +.. automodule:: nengo_gui.layout + :members: + :undoc-members: + +.. automodule:: nengo_gui.main + :members: + :undoc-members: + +.. automodule:: nengo_gui.modal_js + :members: + :undoc-members: + +.. automodule:: nengo_gui.namefinder + :members: + :undoc-members: + +.. automodule:: nengo_gui.page + :members: + :undoc-members: + +.. automodule:: nengo_gui.password + :members: + :undoc-members: + + +.. automodule:: nengo_gui.server + :members: + :undoc-members: + +.. automodule:: nengo_gui.static_plots + :members: + :undoc-members: + +.. automodule:: nengo_gui.user_action + :members: + :undoc-members: + +.. automodule:: nengo_gui.version + :members: + :undoc-members: + +.. automodule:: nengo_gui.viz + :members: + :undoc-members: + +Components +========== + +.. automodule:: nengo_gui.components.component + :members: + :undoc-members: + +.. automodule:: nengo_gui.components.ace_editor + :members: + :undoc-members: + +.. automodule:: nengo_gui.components.editor + :members: + :undoc-members: + +.. automodule:: nengo_gui.components.htmlview + :members: + :undoc-members: + +.. automodule:: nengo_gui.components.netgraph + :members: + :undoc-members: + +.. automodule:: nengo_gui.components.pointer + :members: + :undoc-members: + +.. automodule:: nengo_gui.components.raster + :members: + :undoc-members: + +.. automodule:: nengo_gui.components.sim_control + :members: + :undoc-members: + +.. automodule:: nengo_gui.components.slider + :members: + :undoc-members: + +.. automodule:: nengo_gui.components.spa_plot + :members: + :undoc-members: + +.. automodule:: nengo_gui.components.spa_similarity + :members: + :undoc-members: + +.. automodule:: nengo_gui.components.spike_grid + :members: + :undoc-members: + +.. automodule:: nengo_gui.components.value + :members: + :undoc-members: + +.. automodule:: nengo_gui.components.voltage + :members: + :undoc-members: + +.. automodule:: nengo_gui.components.xyvalue + :members: + :undoc-members: diff --git a/docs/codeflow.rst b/docs/codeflow.rst deleted file mode 100644 index 38f08363..00000000 --- a/docs/codeflow.rst +++ /dev/null @@ -1,6 +0,0 @@ -************************** -How Nengo GUI's Code Works -************************** - -For details on how all the code of Nengo GUI is organised and interacts with -itself, please check out this presentation: `https://docs.google.com/presentation/d/1XtAv3GDW2f7pRjKDJcRmqgwWxSQRXcsDZxUiK4hRrjs/edit?usp=sharing` by Sean Aubin. diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..257a19ee --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- + +import sys + +try: + import nengo + import nengo_gui + import guzzle_sphinx_theme +except ImportError: + print("To build the documentation, nengo_gui and guzzle_sphinx_theme must " + "be installed in the current environment. Please install these and " + "their requirements first. A virtualenv is recommended!") + sys.exit(1) + +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.githubpages', + 'sphinx.ext.intersphinx', + 'sphinx.ext.mathjax', + 'sphinx.ext.todo', + 'sphinx.ext.viewcode', + 'guzzle_sphinx_theme', + 'numpydoc', +] + +# -- sphinx.ext.autodoc +autoclass_content = 'both' # class and __init__ docstrings are concatenated +autodoc_default_flags = ['members'] +autodoc_member_order = 'bysource' # default is alphabetical + +# -- sphinx.ext.intersphinx +intersphinx_mapping = { + "nengo": ("https://www.nengo.ai/nengo/", None) +} + +# -- sphinx.ext.todo +todo_include_todos = True + +# -- numpydoc config +numpydoc_show_class_members = False + +# -- sphinx +exclude_patterns = ['_build'] +source_suffix = '.rst' +source_encoding = 'utf-8' +master_doc = 'index' + +# Need to include https Mathjax path for sphinx < v1.3 +mathjax_path = ("https://cdn.mathjax.org/mathjax/latest/MathJax.js" + "?config=TeX-AMS-MML_HTMLorMML") + +project = u'Nengo GUI' +authors = u'Applied Brain Research' +copyright = nengo.__copyright__ +version = '.'.join(nengo_gui.__version__.split('.')[:2]) # Short X.Y version +release = nengo_gui.__version__ # Full version, with tags +pygments_style = 'default' + +# -- Options for HTML output -------------------------------------------------- + +pygments_style = "sphinx" +templates_path = ["_templates"] +html_static_path = ["_static"] + +html_theme_path = guzzle_sphinx_theme.html_theme_path() +html_theme = "guzzle_sphinx_theme" + +html_theme_options = { + "project_nav_name": "Nengo GUI %s" % (version,), + "base_url": "https://www.nengo.ai/nengo_gui", +} + +html_title = "Nengo GUI {0} docs".format(release) +htmlhelp_basename = 'Nengo GUI' +html_last_updated_fmt = '' # Suppress 'Last updated on:' timestamp +html_show_sphinx = False + +# -- Options for LaTeX output ------------------------------------------------- + +latex_elements = { + 'papersize': 'letterpaper', + 'pointsize': '11pt', + # 'preamble': '', +} + +latex_documents = [ + # (source start file, target, title, author, documentclass [howto/manual]) + ('index', 'nengo_gui.tex', html_title, authors, 'manual'), +] + +# -- Options for manual page output ------------------------------------------- + +man_pages = [ + # (source start file, name, description, authors, manual section). + ('index', 'nengo_gui', html_title, [authors], 1) +] + +# -- Options for Texinfo output ----------------------------------------------- + +texinfo_documents = [ + # (source start file, target, title, author, dir menu entry, + # description, category) + ('index', 'nengo_gui', html_title, authors, 'Nengo GUI', + 'Large-scale neural simulation in Python', 'Miscellaneous'), +] diff --git a/docs/dev_guide.rst b/docs/dev_guide.rst index 6083585b..3a826946 100644 --- a/docs/dev_guide.rst +++ b/docs/dev_guide.rst @@ -1,20 +1,25 @@ *************** -Developer Guide +Developer guide *************** -If you would like access the the development version of nengo_gui, you can -download it from guthub: +If you would like access +the development version of Nengo GUI, +you can download it from Github: -.. code:: shell +.. code:: bash git clone https://github.com/nengo/nengo_gui cd nengo_gui - python setup.py develop --user + pip install -e . -The following sections will help you understand how to contribute to Nengo GUI -development. +For information on the architecture of Nengo GUI, +see `this presentation +`_. + +For information on becoming a Nengo GUI developer, see +`the general Nengo developer guide `_. .. toctree:: - :maxdepth: 2 - codeflow - workflow + :maxdepth: 1 + + api diff --git a/docs/getting_started.rst b/docs/getting_started.rst index f9d648a3..efb04e5e 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -1,36 +1,34 @@ *************** -Getting Started +Getting started *************** Installation ============ -The simplest way to install is with the standard Python installation system: +The simplest way to install is with ``pip``. -.. code:: shell +.. code:: bash pip install nengo_gui -Running nengo_gui +Running Nengo GUI ================= -There are two ways to run nengo_gui. First, you can use it from the command -line by running the installed script: +There are two ways to run Nengo GUI. +First, you can use it from the command line by running: .. code:: shell - nengo_gui + nengo -If you specify a file to load, nengo_gui will do so: +If you specify a file, the GUI will open that file: .. code:: shell - nengo_gui myfile.py - -Alternatively, you can start nengo_gui manually from within your code. To -do so, add this to the bottom of your file that defines your Nengo model. + nengo myfile.py -.. code:: python +Alternatively, you can start the GUI manually from within your code. +To do so, add to the bottom your Nengo script:: import nengo_gui nengo_gui.GUI(__file__).start() @@ -38,32 +36,42 @@ do so, add this to the bottom of your file that defines your Nengo model. Basic usage =========== -The graph of the Nengo network should appear. Rectangles are nengo.Nodes, -ellipses are nengo.Ensembles, and rounded rectangles are nengo.Networks. - -Items can be dragged to move them and resized by dragging their edge or via -the scroll wheel. - -To start (or continue) the simulation, click the play button in the lower -right. A spinning gear icon indicates the model is in the process of being -built (or re-built after new graphs are added). - -Clicking on an item will show a menu of options, depending on what you -have clicked on. Here are some of the standard options for network items: - - - value: show a graph of the decoded output value over time - - xy-value: show a state-space plot of two decoded values against each other - - spikes: show the spiking activity of the nengo.Ensemble - - slider: show sliders that let you adjust the value in a nengo.Node - - expand/collapse: reveal or hide the insides of a nengo.Network - -Once you have graphs, you can also click on them to adjust their options. For -example: - - - set range: adjust the limits of the graph - - show label/hide label: whether to show the title at the top of the graph - - remove: get rid of the graph - -The graphs record their data from previous time steps. You can show this -previous data by dragging the transparent area in the time axis at the -bottom (beside the play button). \ No newline at end of file +The graph of the Nengo network should appear +when you start the GUI. +A rectangle is a `nengo.Node`, +a group of ellipses is a `nengo.Ensemble`, +and a rounded rectangle is a `nengo.Network`. + +Items can be dragged to move them +and resized by dragging their edges or corners. + +To start (or continue) the simulation, +click the play button in the lower right. +A spinning gear icon indicates that the model +is in the process of being built +(or re-built after new plots are added). + +Right-clicking on an object will show a menu of options, +depending on what you have clicked on. +Here are some of the most useful options for objects: + +- **Value**: Show a plot of the decoded output value over time. +- **XY-value**: + Show a state-space plot of two decoded values against each other. +- **Spikes**: Show the spiking activity of a `nengo.Ensemble`. +- **Slider**: Show sliders to adjust the value in a `nengo.Node`. +- **Expand / collapse network**: + Reveal or hide the insides of a `nengo.Network`. + +Once you have plots, +you can also right-click on them to adjust their options. +For example: + +- **Set range**: Adjust the limits of the plot. +- **Show / hide label**: Whether to show the title at the top of the plot. +- **Remove**: Get rid of the plot. + +Plots record their data from previous time steps. +You can show this previous data by dragging +the transparent area in the time axis at the bottom +(beside the play button). diff --git a/docs/index.rst b/docs/index.rst index 182f24e4..a19547d3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,8 +12,6 @@ the model is running. :maxdepth: 2 getting_started - examples - user_guide dev_guide Indices and tables @@ -21,4 +19,4 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` -* :ref:`search` \ No newline at end of file +* :ref:`search` diff --git a/docs/workflow.rst b/docs/workflow.rst deleted file mode 100644 index 7e40dcf3..00000000 --- a/docs/workflow.rst +++ /dev/null @@ -1,67 +0,0 @@ -******************** -Development workflow -******************** - -Development happens on `Github `_. -Feel free to fork any of our repositories and send a pull request! -However, note that we ask contributors to sign -`a copyright assignment agreement `_. - -Code style: Python -================== - -We adhere to -`PEP8 `_, -and use ``flake8`` to automatically check for adherence on all commits. - -We use ``numpydoc`` and -`NumPy guidelines `_ -for docstrings, as they are a bit nicer to read in plain text, -and produce decent output with Sphinx. - -Code style: JavaScript -====================== - -We adhere to a modified version of the Google style guide using -`JSCS `_. Our custom rules for JSCS are saved in the -``jscs.json`` file in the root of this repository. - -Unit testing -============ - -We use `PyTest `_ -to run our Python unit tests and `Mocha `_ -for JavaScript. Eventually these tests will run -on `Travis-CI `_. Please contribute unit tests -where possible. - -Git -=== - -We use a pretty strict ``git`` workflow -to ensure that the history of the ``master`` branch -is clean and readable. -Every commit in the ``master`` branch should pass -unit testing, including PEP8. - -Developers should never edit code on the ``master`` branch. -When changing code, create a new topic branch -that implements your new feature or fixes a bug. -When your branch is ready to be reviewed, -push it to Github and create a pull request. -One or more people will review your pull request, -and over one or many cycles of review, -your PR will be accepted or rejected. -We almost never reject PRs, -though we do let them languish in the limbo -of the PR queue if we're not sure -if they're quite ready yet. - -Terry Stewart primarily repsonsible for reviewing your work, -and merging it into the ``master`` branch when it's been accepted. -He is the only person allowed to push to the ``master`` branch. - -If you have any questions about our workflow, -or how you can best climb the learning curve -that Nengo GUI and ``git`` present, please contact -the development lead, `Sean `_. \ No newline at end of file diff --git a/examples/net.py.cfg b/examples/net.py.cfg new file mode 100644 index 00000000..f905feb1 --- /dev/null +++ b/examples/net.py.cfg @@ -0,0 +1,29 @@ +_viz_ace_editor = nengo_gui.components.AceEditor() +_viz_net_graph = nengo_gui.components.NetGraph() +_viz_sim_control = nengo_gui.components.SimControl() +_viz_config[_viz_sim_control].kept_time = 4.0 +_viz_config[_viz_sim_control].shown_time = 0.5 +_viz_config[a].pos=(0.5769230769230769, 0.21000000000000002) +_viz_config[a].size=(0.07692307692307693, 0.1) +_viz_config[b].pos=(0.8846153846153847, 0.5) +_viz_config[b].size=(0.07692307692307693, 0.1) +_viz_config[ens].pos=(0.3072289156626506, 0.5) +_viz_config[ens].size=(0.060240963855421686, 0.1) +_viz_config[model].pos=(0.11662077368779833, 0.1211053797056143) +_viz_config[model].size=(0.8442598538023351, 0.8442598538023351) +_viz_config[model].expanded=True +_viz_config[model].has_layout=True +_viz_config[result].pos=(0.5481927710843373, 0.5) +_viz_config[result].size=(0.060240963855421686, 0.1) +_viz_config[stimulus_A].pos=(0.0783132530120482, 0.22000000000000003) +_viz_config[stimulus_A].size=(0.048192771084337345, 0.08) +_viz_config[stimulus_B].pos=(0.0783132530120482, 0.78) +_viz_config[stimulus_B].size=(0.048192771084337345, 0.08) +_viz_config[subnet].pos=(0.8493975903614457, 0.5) +_viz_config[subnet].size=(0.12048192771084337, 0.4) +_viz_config[subnet].expanded=True +_viz_config[subnet].has_layout=True +_viz_config[subsubnet].pos=(0.19230769230769232, 0.5) +_viz_config[subsubnet].size=(0.15384615384615385, 0.4) +_viz_config[subsubnet].expanded=False +_viz_config[subsubnet].has_layout=False \ No newline at end of file diff --git a/examples/spa.py.cfg b/examples/spa.py.cfg new file mode 100644 index 00000000..3c1bb7a1 --- /dev/null +++ b/examples/spa.py.cfg @@ -0,0 +1,52 @@ +_viz_0 = nengo_gui.components.SpaSimilarity(model.a,target=u'default') +_viz_config[_viz_0].max_value = 1.5 +_viz_config[_viz_0].min_value = -1.5 +_viz_config[_viz_0].height = 0.11723329425556858 +_viz_config[_viz_0].label_visible = True +_viz_config[_viz_0].width = 0.06548788474132286 +_viz_config[_viz_0].y = 0.5806451612903225 +_viz_config[_viz_0].x = 0.6176470588235294 +_viz_config[_viz_0].show_pairs = False +_viz_1 = nengo_gui.components.Pointer(model.a,target=u'default') +_viz_config[_viz_1].label_visible = True +_viz_config[_viz_1].width = 0.06548788474132286 +_viz_config[_viz_1].y = 0.3218034116610221 +_viz_config[_viz_1].x = 0.2779011436736956 +_viz_config[_viz_1].show_pairs = False +_viz_config[_viz_1].max_size = 1000.0 +_viz_config[_viz_1].height = 0.11723329425556858 +_viz_ace_editor = nengo_gui.components.AceEditor() +_viz_net_graph = nengo_gui.components.NetGraph() +_viz_sim_control = nengo_gui.components.SimControl() +_viz_config[_viz_sim_control].kept_time = 4.0 +_viz_config[_viz_sim_control].shown_time = 0.5 +_viz_config[model].pos=(0, 0) +_viz_config[model].size=(1.0, 1.0) +_viz_config[model].expanded=True +_viz_config[model].has_layout=True +_viz_config[model.a].pos=(0.46594629993451253, 0.4305109102598038) +_viz_config[model.a].size=(0.11764705882352941, 0.12903225806451613) +_viz_config[model.a].expanded=False +_viz_config[model.a].has_layout=False +_viz_config[model.a.state].expanded=False +_viz_config[model.a.state].has_layout=False +_viz_config[model.b].pos=(0.5, 0.8387096774193548) +_viz_config[model.b].size=(0.11764705882352941, 0.12903225806451613) +_viz_config[model.b].expanded=False +_viz_config[model.b].has_layout=False +_viz_config[model.b.state].expanded=False +_viz_config[model.b.state].has_layout=False +_viz_config[model.c].pos=(0.8529411764705882, 0.6451612903225805) +_viz_config[model.c].size=(0.11764705882352941, 0.12903225806451613) +_viz_config[model.c].expanded=False +_viz_config[model.c].has_layout=False +_viz_config[model.c.state].expanded=False +_viz_config[model.c.state].has_layout=False +_viz_config[model.cortical].pos=(0.5, 0.16129032258064516) +_viz_config[model.cortical].size=(0.4, 0.12903225806451613) +_viz_config[model.cortical].expanded=False +_viz_config[model.cortical].has_layout=False +_viz_config[model.input].pos=(0.14705882352941177, 0.6451612903225805) +_viz_config[model.input].size=(0.11764705882352941, 0.12903225806451613) +_viz_config[model.input].expanded=False +_viz_config[model.input].has_layout=False \ No newline at end of file diff --git a/nengo_gui/__init__.py b/nengo_gui/__init__.py index a75e8070..e7e1c1d2 100644 --- a/nengo_gui/__init__.py +++ b/nengo_gui/__init__.py @@ -1,5 +1,7 @@ +from . import exec_env, server + from .gui import GUI, InteractiveGUI from .viz import Viz # deprecated from .version import version as __version__ -from .namefinder import NameFinder +from .netgraph import NameFinder from .main import main, old_main diff --git a/nengo_gui/client.py b/nengo_gui/client.py new file mode 100644 index 00000000..2bc742a3 --- /dev/null +++ b/nengo_gui/client.py @@ -0,0 +1,156 @@ +from collections import defaultdict +from functools import partial +import inspect +import json +import warnings +import weakref + +import numpy as np +from nengo.utils.compat import is_array, with_metaclass + + +class NengoGUIConfig(json.JSONEncoder): + def default(self, obj): + if hasattr(obj, "to_json"): + return obj.to_json() + return super(NengoGUIConfig, self).default(obj) + + +def bind(name): + def _bind(method): + if isinstance(method, property): + raise RuntimeError("Bind to the method, not the property") + + if isinstance(method, staticmethod): + method.__func__.__route__ = name + return method.__func__ + elif isinstance(method, classmethod): + method.__func__.__route__ = name + return method + else: + method.__route__ = name + return method + return _bind + + +class ClientConnection(object): + def __init__(self, ws): + self.ws = ws + self.callbacks = defaultdict(set) + + @staticmethod + def _element(callback): + if inspect.ismethod(callback): + return weakref.ref(callback.__self__), callback.__func__.__name__ + elif isinstance(callback, partial): + return weakref.ref(callback.args[0]), callback + else: + return weakref.ref(callback), None + + def _prune(self, name): + if len(self.callbacks[name]) == 0: + del self.callbacks[name] + + def bind(self, name, callback): + """Define a function name that the client can call.""" + self.callbacks[name].add(self._element(callback)) + + def is_bound(self, name): + return name in self.callbacks + + def dispatch(self, name, **kwargs): + """Call a function bound to this page.""" + # Iterate over a copy so we can remove stale elements + retval = None + for ref, meth in self.callbacks[name].copy(): + if meth is None: + cb = ref() + elif isinstance(meth, partial): + cb = meth if ref() is not None else None + else: + cb = getattr(ref(), meth, None) + + if cb is None: + self.callbacks[name].remove((ref, meth)) + else: + retval = cb(**kwargs) + + # Do this check after iterating in case size changes during iteration + self._prune(name) + if not self.is_bound(name): + warnings.warn("Nothing bound for %r" % (name,)) + + # We return the value of the last call + return retval + + def send(self, name, **kwargs): + """Send a message to the client.""" + assert self.ws is not None + self.ws.write_text(json.dumps([name, kwargs], cls=NengoGUIConfig)) + + def unbind(self, name, callback=None): + if callback is None: + del self.callbacks[name] + else: + el = self._element(callback) + if el in self.callbacks[name]: + self.callbacks[name].remove(el) + self._prune(name) + + +class Bindable(type): + """A metaclass used to bind methods exposed to the client.""" + + def __call__(cls, *args, **kwargs): + """Override default __call__ behavior so that methods get bound.""" + inst = type.__call__(cls, *args, **kwargs) + assert isinstance(inst.client, ClientConnection), inst.client + + # Bind methods and static functions + def bindable(f): + if inspect.ismethod(f) or inspect.isfunction(f): + return (hasattr(f, "__route__") or + (hasattr(f, "__func__") + and hasattr(f.__func__, "__route__"))) + + for _, method in inspect.getmembers(inst, predicate=bindable): + inst.client.bind(method.__route__.format(self=inst), method) + + # Bind properties + is_prop = lambda f: isinstance(f, property) + for _, prop in inspect.getmembers(type(inst), predicate=is_prop): + + # Check if the get, set, or del are bound + for attr in ("fget", "fset", "fdel"): + f = getattr(prop, attr) + if hasattr(f, "__route__"): + # If so, we manually bind the instance to the function + # with partial, which makes it act like an instance method + inst.client.bind(f.__route__.format(self=inst), + partial(f, inst)) + + return inst + + +class ExposedToClient(with_metaclass(Bindable)): + def __init__(self, client): + self.client = client + + +class FastClientConnection(object): + def __init__(self, ws): + self.ws = ws + self.callback = None + self.dtype = None + + def bind(self, callback, dtype=np.float64): + self.callback = callback + self.dtype = None + + def receive(self, data): + if self.callback is not None: + self.callback(np.frombuffer(data, dtype=self.dtype)) + + def send(self, data): + assert is_array(data) + self.ws.write_binary(data.tobytes()) diff --git a/nengo_gui/compat.py b/nengo_gui/compat.py new file mode 100644 index 00000000..95796455 --- /dev/null +++ b/nengo_gui/compat.py @@ -0,0 +1,41 @@ +from __future__ import absolute_import + +import sys + +# Only test for Python 2 so that we have less changes for Python 4 +PY2 = sys.version_info[0] == 2 + +if PY2: + import BaseHTTPServer as server + from Cookie import SimpleCookie + import SocketServer as socketserver + from urllib2 import urlopen + from urlparse import parse_qs, unquote, urlparse + execfile = execfile + +else: + from http import server + from http.cookies import SimpleCookie + import socketserver + from urllib.parse import parse_qs, unquote, urlparse + from urllib.request import urlopen + + def execfile(path, globals, locals): + if globals is None: + globals = sys._getframe(1).f_globals + if locals is None: + locals = sys._getframe(1).f_locals + with open(path, "r") as fp: + code = fp.read() + compiled = compile(code, path, mode="exec") + exec(compiled, globals, locals) + + +assert execfile +assert parse_qs +assert server +assert SimpleCookie +assert socketserver +assert unquote +assert urlopen +assert urlparse diff --git a/nengo_gui/components/__init__.py b/nengo_gui/components/__init__.py index c3d5f146..101d575e 100644 --- a/nengo_gui/components/__init__.py +++ b/nengo_gui/components/__init__.py @@ -1,28 +1,14 @@ -# flake8: noqa -from .component import Component +from .base import Component, Position from .slider import Slider from .value import Value from .xyvalue import XYValue -from .sim_control import SimControl from .raster import Raster from .voltage import Voltage -from .pointer import Pointer -from .netgraph import NetGraph -from .ace_editor import AceEditor -from .editor import NoEditor -from .spa_similarity import SpaSimilarity +from .spa import SpaPointer, SpaSimilarity from .htmlview import HTMLView from .spike_grid import SpikeGrid -# Old versions of the .cfg files used Templates which had slightly different -# names than the Components currently use. This code allows us to -# successfully parse those old .cfg files -SliderTemplate = Slider -ValueTemplate = Value -XYValueTemplate = XYValue -SimControlTemplate = SimControl -RasterTemplate = Raster -VoltageTemplate = Voltage -PointerTemplate = Pointer -NetGraphTemplate = NetGraph -AceEditorTemplate = AceEditor +from .connection import Connection +from .ensemble import Ensemble +from .network import Network +from .node import Node diff --git a/nengo_gui/components/ace_editor.py b/nengo_gui/components/ace_editor.py deleted file mode 100644 index 53616d03..00000000 --- a/nengo_gui/components/ace_editor.py +++ /dev/null @@ -1,94 +0,0 @@ -import json -import os - -from nengo_gui.components.editor import Editor -import nengo_gui.exec_env - - -class AceEditor(Editor): - - config_defaults = {} - - def __init__(self): - super(AceEditor, self).__init__() - self.pending_messages = [] - - def attach(self, page, config, uid): - super(AceEditor, self).attach(page, config, uid) - self.current_code = page.code - self.serve_code = True - self.last_error = None - self.last_stdout = None - - def update_code(self, code): - self.current_code = code - self.serve_code = True - - def update_client(self, client): - while self.pending_messages: - client.write_text(self.pending_messages.pop()) - if self.serve_code: - i = json.dumps({'code': self.current_code}) - client.write_text(i) - self.serve_code = False - if nengo_gui.exec_env.is_executing(): - return - error = self.page.error - stdout = self.page.stdout - if error != self.last_error or stdout != self.last_stdout: - if error is None: - short_msg = None - else: - if '\n' in error['trace']: - short_msg = error['trace'].rsplit('\n', 2)[-2] - else: - short_msg = error['trace'] - client.write_text(json.dumps({'error': error, - 'short_msg': short_msg, - 'stdout': stdout})) - self.last_error = error - self.last_stdout = stdout - - def javascript(self): - return 'Nengo.ace = new Nengo.Ace("%s", {})' % (id(self),) - - def message(self, msg): - data = json.loads(msg) - self.current_code = data['code'] - - save_as = data.get('save_as', None) - if save_as is not None: - if os.path.exists(save_as): - msg = ("Could not rename to %s; " - "File already exists" % save_as) - self.pending_messages.append(json.dumps( - {'filename': save_as, - 'valid': False, - 'error': msg})) - else: - try: - self.page.filename_cfg = save_as + '.cfg' - self.page.save_config(force=True) - self.page.filename = save_as - with open(self.page.filename, 'w') as f: - f.write(self.current_code) - self.pending_messages.append(json.dumps( - {'filename': save_as, - 'valid': True})) - self.page.net_graph.update_code(self.current_code) - except IOError: - msg = "Could not save %s; permission denied" % save_as - self.pending_messages.append(json.dumps( - {'filename': save_as, - 'valid': False, - 'error': msg})) - elif data['save']: - try: - with open(self.page.filename, 'w') as f: - f.write(self.current_code) - except IOError: - print("Could not save %s; permission denied" % - self.page.filename) - self.page.net_graph.update_code(self.current_code) - else: - self.page.net_graph.update_code(self.current_code) diff --git a/nengo_gui/components/base.py b/nengo_gui/components/base.py new file mode 100644 index 00000000..ddd0a79b --- /dev/null +++ b/nengo_gui/components/base.py @@ -0,0 +1,174 @@ +from nengo_gui.client import ExposedToClient, FastClientConnection +from nengo_gui.exceptions import NotAttachedError + + +class Position(object): + __slots__ = ("left", "top", "width", "height") + + def __init__(self, left=0, top=0, width=100, height=100): + self.left = left + self.top = top + self.width = width + self.height = height + + def to_json(self): + return {"left": self.left, + "top": self.top, + "width": self.width, + "height": self.height} + + def __repr__(self): + return "Position(left={!r}, top={!r}, width={!r}, height={!r})".format( + self.left, self.top, self.width, self.height) + + +class Component(ExposedToClient): + """Abstract handler for a particular Component of the user interface. + + Each part of the user interface has part of the code on the server-side + (in Python) and a part on the client-side (in Javascript). These two sides + communicate via WebSockets, and the server-side is always a subclass of + Component. + + Each Component can be configured via the nengo.Config system. Components + can add required nengo objects into the model to allow them to gather + required data or input overriding data (in the case of Pointer and Slider) + to/from the running model. Communication from server to + client is done via ``Component.update_client()``, which is called regularly + by the ``Server.ws_viz_component`` handler. Communication from client to + server is via ``Component.message()``. + """ + + def __init__(self, client, obj, uid, pos=None, label_visible=True): + super(Component, self).__init__(client) + self.obj = obj + self._uid = uid + self.pos = pos + self.label_visible = label_visible + + @property + def label(self): + """Return a readable label for an object. + + An important difference between a label and a name is that a label + does not have to be unique in a namespace. + + If the object has a .label set, this will be used. Otherwise, it + uses names, which thanks to the NameFinder will be legal + Python code for referring to the object given the current locals() + dictionary ("model.ensembles[1]" or "ens" or "model.buffer.state"). + If it has to use names, it will only use the last part of the + label (after the last "."). This avoids redundancy in nested displays. + """ + label = self.obj.label + if label is None: + label = self.uid + if '.' in label: + label = label.rsplit('.', 1)[1] + return label + + @property + def uid(self): + return self._uid + + # TODO: rename + def add_nengo_objects(self, network): + """Add or modify the nengo model before build. + + Components may need to modify the underlying nengo.Network by adding + Nodes and Connections or modifying the structure in other ways. + This method will be called for all Components just before the build + phase. + """ + pass + + def create(self): + """Instruct the client to create this object.""" + raise NotImplementedError("Components must implement `create`") + + def delete(self): + """Instruct the client to delete this object.""" + raise NotImplementedError("Components must implement `delete`") + + def dumps(self, names): + """Important to do correctly, as it's used in the config file.""" + raise NotImplementedError("Components must implement `dumps`") + + # TODO: rename + def remove_nengo_objects(self, network): + """Undo the effects of add_nengo_objects. + + After the build is complete, remove the changes to the nengo.Network + so that it is all set to be built again in the future. + """ + pass + + def similar(self, other): + """Determine whether this component is similar to another component. + + Similar, in this case, means that the `.diff` method can be used to + mutate the other component to be the same as this component. + """ + return self.uid == other.uid and type(self) == type(other) + + def to_json(self): + d = self.__dict__.copy() + d["cls"] = type(self).__name__ + return d + + def update(self, other): + """Update the client based on another version of this component.""" + if self.label != other.label: + self.client.send("%s.label" % self.uid, label=self.label) + + # def javascript_config(self, cfg): + # """Convert the nengo.Config information into javascript. + + # This is needed so we can send that config information to the client. + # """ + # for attr in self.config._clsparams.params: + # if attr in cfg: + # raise AttributeError("Value for %s is already set in the " + # "config of this component. Do not try to " + # "modify it via this function. Instead " + # "modify the config directly." % (attr)) + # else: + # cfg[attr] = getattr(self.config, attr) + # return json.dumps(cfg) + + # def code_python(self, uids): + # """Generate Python code for this Component. + + # This is used in the .cfg file to generate a valid Python expression + # that re-creates this Component. + + # The input uids is a dictionary from Python objects to strings that + # refer to those Python objects (the reverse of the locals() dictionary) + # """ + # args = self.code_python_args(uids) + # name = self.__class__.__name__ + # return 'nengo_gui.components.%s(%s)' % (name, ','.join(args)) + + # def code_python_args(self, uids): + # """Return a list of strings giving the constructor arguments. + + # This is used by code_python to re-create the Python string that + # generated this Component, so it can be saved in the .cfg file. + + # The input uids is a dictionary from Python objects to strings that + # refer to those Python objects (the reverse of the locals() dictionary) + # """ + # return [] + + +class Widget(Component): + + def __getattr__(self, name): + # NB: This method will only be called is `name` is not an attribute + if name == "fast_client": + raise NotAttachedError("This Widget is not yet attached.") + raise AttributeError("%r object has no attribute %r" + % (type(self).__name__, name)) + + def attach(self, fast_client): + self.fast_client = fast_client diff --git a/nengo_gui/components/component.py b/nengo_gui/components/component.py deleted file mode 100644 index fb1d6ff3..00000000 --- a/nengo_gui/components/component.py +++ /dev/null @@ -1,124 +0,0 @@ -import json - - -class Component(object): - """Abstract handler for a particular Component of the user interface. - - Each part of the user interface has part of the code on the server-side - (in Python) and a part on the client-side (in Javascript). These two sides - communicate via WebSockets, and the server-side is always a subclass of - Component. - - Each Component can be configured via the nengo.Config system. Components - can add required nengo objects into the model to allow them to gather - required data or input overriding data (in the case of Pointer and Slider) - to/from the running model. Communication from server to - client is done via Component.update_client(), which is called regularly - by the Server.ws_viz_component handler. Communication from client to - server is via Component.message(). - """ - - # The parameters that will be stored in the .cfg file for this Component - # Subclasses should override this as needed. - config_defaults = dict(x=0, y=0, width=100, height=100, label_visible=True) - - def __init__(self, component_order=0): - # when generating Javascript for all the Components in a Page, they - # will be sorted by component_order. This way some Components can - # be defined before others. - self.component_order = component_order - - # If we have reloaded the model (while typing in the editor), we need - # to swap out the old Component with the new one. - self.replace_with = None - - # If we have been swapped out, keep track of the id of the original - # component, since that's the identifier needed to refer to it on - # the client side - self.original_id = id(self) - - def attach(self, page, config, uid): - """Connect the Component to a Page.""" - self.config = config # the nengo.Config[component] for this component - self.page = page # the Page this component is in - self.uid = uid # The Python string referencing this component - - def update_client(self, client): - """Send any required information to the client. - - This method is called regularly by Server.ws_viz_component(). You - send text data to the client-side via a WebSocket as follows: - client.write_text(data) - You send binary data as: - client.write_binary(data) - """ - pass - - def message(self, msg): - """Receive data from the client. - - Any data sent by the client ove the WebSocket will be passed into - this method. - """ - print('unhandled message', msg) - - def finish(self): - """Close this Component""" - pass - - def add_nengo_objects(self, page): - """Add or modify the nengo model before build. - - Components may need to modify the underlying nengo.Network by adding - Nodes and Connections or modifying the structure in other ways. - This method will be called for all Components just before the build - phase. - """ - pass - - def remove_nengo_objects(self, page): - """Undo the effects of add_nengo_objects. - - After the build is complete, remove the changes to the nengo.Network - so that it is all set to be built again in the future. - """ - pass - - def javascript_config(self, cfg): - """Convert the nengo.Config information into javascript. - - This is needed so we can send that config information to the client. - """ - for attr in self.config._clsparams.params: - if attr in cfg: - raise AttributeError("Value for %s is already set in the " - "config of this component. Do not try to " - "modify it via this function. Instead " - "modify the config directly." % (attr)) - else: - cfg[attr] = getattr(self.config, attr) - return json.dumps(cfg) - - def code_python(self, uids): - """Generate Python code for this Component. - - This is used in the .cfg file to generate a valid Python expression - that re-creates this Component. - - The input uids is a dictionary from Python objects to strings that - refer to those Python objects (the reverse of the locals() dictionary) - """ - args = self.code_python_args(uids) - name = self.__class__.__name__ - return 'nengo_gui.components.%s(%s)' % (name, ','.join(args)) - - def code_python_args(self, uids): - """Return a list of strings giving the constructor arguments. - - This is used by code_python to re-create the Python string that - generated this Component, so it can be saved in the .cfg file. - - The input uids is a dictionary from Python objects to strings that - refer to those Python objects (the reverse of the locals() dictionary) - """ - return [] diff --git a/nengo_gui/components/connection.py b/nengo_gui/components/connection.py new file mode 100644 index 00000000..faa8e369 --- /dev/null +++ b/nengo_gui/components/connection.py @@ -0,0 +1,56 @@ +import nengo + +from .base import Component + + +class Connection(Component): + + # TODO: would be nice to not have to get namefinder here + def __init__(self, client, obj, uid, namefinder, + pos=None, label_visible=True): + super(Connection, self).__init__( + client, obj, uid, pos=pos, label_visible=label_visible) + self.pre = self._get_pre(self.obj) + self.post = self._get_post(self.obj) + self.pre_uid = namefinder[self.pre] + self.post_uid = namefinder[self.post] + + def create(self): + # TODO: figure out args to pass to this + self.client.send("netgraph.create_connection", + pre=self.pre_uid, + post=self.post_uid) + + def update(self, other): + super(Connection, self).update(other) + if self.pre_uid != other.pre_uid or self.post_uid != other.post_uid: + self.client.send("%s.reconnect" % self.uid, + pre=self.pre_uid, post=self.post_uid) + + # if the connection has changed, tell javascript + # pres = self.get_parents( + # other.pre, + # default_labels=new_name_finder.known_name)[:-1] + # posts = self.get_parents( + # other.post, + # default_labels=new_name_finder.known_name)[:-1] + # self.to_be_sent.append(dict( + # type='reconnect', uid=uid, + # pres=pres, posts=posts)) + # return True + + @staticmethod + def _get_pre(conn): + pre = conn.pre_obj + if isinstance(pre, nengo.ensemble.Neurons): + pre = pre.ensemble + return pre + + @staticmethod + def _get_post(conn): + post = conn.post_obj + if isinstance(post, nengo.connection.LearningRule): + post = post.connection.post_obj + if isinstance(post, nengo.ensemble.Neurons): + post = post.ensemble + return post diff --git a/nengo_gui/components/editor.py b/nengo_gui/components/editor.py deleted file mode 100644 index c90e92fb..00000000 --- a/nengo_gui/components/editor.py +++ /dev/null @@ -1,24 +0,0 @@ -from nengo_gui.components.component import Component - - -class Editor(Component): - config_defaults = {} - - def __init__(self): - # the IPython integration requires this component to be early - # in the list - super(Editor, self).__init__(component_order=-8) - - def update_code(self, msg): - pass - - def javascript(self): - return 'Nengo.disable_editor();' - - -class NoEditor(Editor): - def __init__(self): - super(NoEditor, self).__init__() - - def message(self, msg): - pass diff --git a/nengo_gui/components/ensemble.py b/nengo_gui/components/ensemble.py new file mode 100644 index 00000000..d774f99c --- /dev/null +++ b/nengo_gui/components/ensemble.py @@ -0,0 +1,25 @@ +from .base import Component + + +class Ensemble(Component): + + @property + def dimensions(self): + return self.obj.dimensions + + @property + def n_neurons(self): + return self.obj.n_neurons + + def create(self): + self.client.send("netgraph.create_ensemble", + uid=self.uid, + label=self.label, + pos=self.pos, + dimensions=self.dimensions, + labelVisible=self.label_visible) + + def similar(self, other): + return (super(Ensemble, self).similar(other) + and self.dimensions == other.dimensions + and self.n_neurons == other.n_neurons) diff --git a/nengo_gui/components/htmlview.py b/nengo_gui/components/htmlview.py index b7486c79..1741afad 100644 --- a/nengo_gui/components/htmlview.py +++ b/nengo_gui/components/htmlview.py @@ -1,45 +1,41 @@ -import collections - -from nengo_gui.components.component import Component +from .base import Component class HTMLView(Component): - """Arbitrary HTML display taking input from a Node - - See nengo_gui/examples/basics/html.py for example usage""" - - def __init__(self, obj): - super(HTMLView, self).__init__() - self.obj = obj - self.obj_output = obj.output - self.data = collections.deque() - - def attach(self, page, config, uid): - super(HTMLView, self).attach(page, config, uid) - self.label = page.get_label(self.obj) - - def add_nengo_objects(self, page): - with page.model: - self.obj.output = self.gather_data - - def remove_nengo_objects(self, page): - self.obj.output = self.obj_output - - def gather_data(self, t, *x): - value = self.obj_output(t, *x) - data = '%g %s' % (t, self.obj_output._nengo_html_) - self.data.append(data) - return value - - def update_client(self, client): - while len(self.data) > 0: - item = self.data.popleft() - client.write_text(item) - - def javascript(self): - info = dict(uid=id(self), label=self.label) - json = self.javascript_config(info) - return 'new Nengo.HTMLView(main, sim, %s);' % json - - def code_python_args(self, uids): - return [uids[self.obj]] + """Arbitrary HTML display taking input from a Node. + + See nengo_gui/examples/basics/html.py for example usage. + + Note that, because the HTML to send across the websocket is text, this + component is not a widget as it does not benefit from a fast binary + websocket connection. + """ + + def __init__(self, client, obj, uid, pos=None, label_visible=True): + super(HTMLView, self).__init__( + client, obj, uid, pos=pos, label_visible=label_visible) + self._old_output = None + + def add_nengo_objects(self, model): + + def send_to_client(t, *x): + value = self._old_output(t, *x) + self.client.send("%s.html" % (self.uid,), + t=t, html=self._old_output._nengo_html_) + return value + + self._old_output = self.obj.output + self.obj.output = send_to_client + + def create(self): + self.client.send("netgraph.create_htmlview", + uid=self.uid, + pos=self.pos, + label=self.label, + labelVisible=self.label_visible, + dimensions=1, # TODO + syanpse=0.005) # TODO + + def remove_nengo_objects(self, network): + self.obj.output = self._old_output + self._old_output = None diff --git a/nengo_gui/components/netgraph.py b/nengo_gui/components/netgraph.py deleted file mode 100644 index efebcbf2..00000000 --- a/nengo_gui/components/netgraph.py +++ /dev/null @@ -1,580 +0,0 @@ -import time -import os -import traceback -import collections -import threading - -import nengo -from nengo import spa -import json - -from nengo_gui.components.component import Component -from nengo_gui.components.value import Value -from nengo_gui.components.slider import OverriddenOutput -from nengo_gui.modal_js import infomodal -import nengo_gui.user_action -import nengo_gui.layout - -class NetGraph(Component): - """Handles computations and communications for NetGraph on the JS side. - - Communicates to all NetGraph components for creation, deletion and - manipulation. - """ - - config_defaults = {} - configs = {} - - def __init__(self): - # this component must be ordered before all the normal graphs (so that - # other graphs are on top of the NetGraph), so its - # order is between that of SimControl and the default (0) - super(NetGraph, self).__init__(component_order=-5) - - # this lock ensures safety between check_for_reload() and update_code() - self.code_lock = threading.Lock() - self.new_code = None - - self.uids = {} - self.parents = {} - self.initialized_pan_and_zoom = False - - def attach(self, page, config, uid): - super(NetGraph, self).attach(page, config, uid) - self.layout = nengo_gui.layout.Layout(self.page.model) - self.to_be_expanded = collections.deque([self.page.model]) - self.to_be_sent = collections.deque() - - self.networks_to_search = [self.page.model] - - try: - self.last_modify_time = os.path.getmtime(self.page.filename) - except OSError: - self.last_modify_time = None - except TypeError: # happens if self.filename is None - self.last_modify_time = None - self.last_reload_check = time.time() - - def check_for_reload(self): - if self.page.filename is not None: - try: - t = os.path.getmtime(self.page.filename) - if self.last_modify_time is None or self.last_modify_time < t: - self.reload() - self.last_modify_time = t - except OSError: - pass - - with self.code_lock: - new_code = self.new_code - # the lock is in case update_code() is called between these lines - self.new_code = None - - if new_code is not None: - self.reload(code=new_code) - - def update_code(self, code): - """Set new version of code to display.""" - with self.code_lock: - self.new_code = code - - def reload(self, code=None): - """Called when new code has been detected - checks that the page is not currently being used - and thus can be updated""" - with self.page.lock: - self._reload(code=code) - - def _reload(self, code=None): - """Loads and executes the code, removing old items, - updating changed items - and adding new ones""" - - old_locals = self.page.last_good_locals - old_default_labels = self.page.default_labels - - if code is None: - with open(self.page.filename) as f: - code = f.read() - if self.page.code == code: - # don't re-execute the identical code - return - else: - # send the new code to the client - self.page.editor.update_code(code) - - self.page.execute(code) - - if self.page.error is not None: - return - - name_finder = nengo_gui.NameFinder(self.page.locals, self.page.model) - - self.networks_to_search = [self.page.model] - self.parents = {} - - removed_uids = {} - rebuilt_objects = [] - - # for each item in the old model, find the matching new item - # for Nodes, Ensembles, and Networks, this means to find the item - # with the same uid. For Connections, we don't really have a uid, - # so we use the uids of the pre and post objects. - for uid, old_item in nengo.utils.compat.iteritems(dict(self.uids)): - try: - new_item = eval(uid, self.page.locals) - except: - new_item = None - - # check to make sure the new item's uid is the same as the - # old item. This is to catch situations where an old uid - # happens to still refer to something in the new model, but that's - # not the normal uid for that item. For example, the uid - # "ensembles[0]" might still refer to something even after that - # ensemble is removed. - new_uid = self.page.get_uid(new_item, - default_labels=name_finder.known_name) - if new_uid != uid: - new_item = None - - same_class = False - for cls in [nengo.Ensemble, nengo.Node, nengo.Network, nengo.Connection]: - if isinstance(new_item, cls) and isinstance(old_item, cls): - same_class = True - break - - # find reasons to delete the object. Any deleted object will - # be recreated, so try to keep this to a minimum - keep_object = True - if new_item is None: - keep_object = False - elif not same_class: - # don't allow changing classes - keep_object = False - elif self.get_extra_info(new_item) != self.get_extra_info(old_item): - keep_object = False - - if not keep_object: - self.to_be_sent.append(dict( - type='remove', uid=uid)) - del self.uids[uid] - removed_uids[old_item] = uid - rebuilt_objects.append(uid) - else: - # fix aspects of the item that may have changed - if self._reload_update_item(uid, old_item, new_item, - name_finder): - # something has changed about this object, so rebuild - # the components that use it - rebuilt_objects.append(uid) - - self.uids[uid] = new_item - - self.to_be_expanded.append(self.page.model) - - self.page.name_finder = name_finder - self.page.default_labels = name_finder.known_name - self.page.config = self.page.load_config() - self.page.uid_prefix_counter = {} - self.layout = nengo_gui.layout.Layout(self.page.model) - self.page.code = code - - orphan_components = [] - rebuild_components = [] - - # items that are shown in components, but not currently displayed - # in the NetGraph (i.e. things that are inside collapsed - # Networks, but whose values are being shown in a graph) - collapsed_items = [] - - # remove graphs no longer associated to NetgraphItems - removed_items = list(removed_uids.values()) - for c in self.page.components[:]: - for item in c.code_python_args(old_default_labels): - if item not in self.uids.keys() and item not in collapsed_items: - - # item is a python string that is an argument to the - # constructor for the Component. So it could be 'a', - # 'model.ensembles[3]', 'True', or even 'target=a'. - # We need to evaluate this string in the context of the - # locals dictionary and see what object it refers to - # so we can determine whether to rebuild this component. - # - # The following lambda should do this, handling both - # the normal argument case and the keyword argument case. - safe_eval = ('(lambda *a, **b: ' - 'list(a) + list(b.values()))(%s)[0]') - - # this Component depends on an item inside a collapsed - # Network, so we need to check if that component has - # changed or been removed - old_obj = eval(safe_eval % item, old_locals) - - try: - new_obj = eval(safe_eval % item, self.page.locals) - except: - # the object this Component depends on no longer exists - new_obj = None - - if new_obj is None: - removed_items.append(item) - elif not isinstance(new_obj, old_obj.__class__): - rebuilt_objects.append(item) - elif (self.get_extra_info(new_obj) != - self.get_extra_info(old_obj)): - rebuilt_objects.append(item) - - # add this to the list of collapsed items, so we - # don't recheck it if there's another Component that - # also depends on this - collapsed_items.append(item) - - if item in rebuilt_objects: - self.to_be_sent.append(dict(type='delete_graph', - uid=c.original_id, - notify_server=False)) - rebuild_components.append(c.uid) - self.page.components.remove(c) - break - else: - for item in c.code_python_args(old_default_labels): - if item in removed_items: - self.to_be_sent.append(dict(type='delete_graph', - uid=c.original_id, - notify_server=False)) - orphan_components.append(c) - break - - components = [] - # the old names for the old components - component_uids = [c.uid for c in self.page.components] - - for name, obj in list(self.page.locals.items()): - if isinstance(obj, Component): - # the object has been removed, so the Component should - # be removed as well - if obj in orphan_components: - continue - - # this is a Component that was previously removed, - # but is still in the config file, or it has to be - # rebuilt, so let's recover it - if name not in component_uids: - self.page.add_component(obj) - self.to_be_sent.append(dict(type='js', - code=obj.javascript())) - components.append(obj) - continue - - # otherwise, find the corresponding old Component - index = component_uids.index(name) - old_component = self.page.components[index] - if isinstance(obj, (nengo_gui.components.SimControlTemplate, - nengo_gui.components.AceEditorTemplate, - nengo_gui.components.NetGraphTemplate)): - # just keep these ones - components.append(old_component) - else: - # replace these components with the newly generated ones - try: - self.page.add_component(obj) - old_component.replace_with = obj - obj.original_id = old_component.original_id - except: - traceback.print_exc() - print('failed to recreate plot for %s' % obj) - components.append(obj) - - components.sort(key=lambda x: x.component_order) - - self.page.components = components - - # notifies SimControl to pause the simulation - self.page.changed = True - - def _reload_update_item(self, uid, old_item, new_item, new_name_finder): - """Tell the client about changes to the item due to reload.""" - changed = False - if isinstance(old_item, (nengo.Node, - nengo.Ensemble, - nengo.Network)): - old_label = self.page.get_label(old_item) - new_label = self.page.get_label( - new_item, default_labels=new_name_finder.known_name) - - if old_label != new_label: - self.to_be_sent.append(dict( - type='rename', uid=uid, name=new_label)) - changed = True - if isinstance(old_item, nengo.Network): - if self.page.config[old_item].expanded: - self.to_be_expanded.append(new_item) - changed = True - - elif isinstance(old_item, nengo.Connection): - old_pre = old_item.pre_obj - old_post = old_item.post_obj - new_pre = new_item.pre_obj - new_post = new_item.post_obj - if isinstance(old_pre, nengo.ensemble.Neurons): - old_pre = old_pre.ensemble - if isinstance(old_post, nengo.connection.LearningRule): - old_post = old_post.connection.post_obj - if isinstance(old_post, nengo.ensemble.Neurons): - old_post = old_post.ensemble - if isinstance(new_pre, nengo.ensemble.Neurons): - new_pre = new_pre.ensemble - if isinstance(new_post, nengo.connection.LearningRule): - new_post = new_post.connection.post_obj - if isinstance(new_post, nengo.ensemble.Neurons): - new_post = new_post.ensemble - - old_pre = self.page.get_uid(old_pre) - old_post = self.page.get_uid(old_post) - new_pre = self.page.get_uid( - new_pre, default_labels=new_name_finder.known_name) - new_post = self.page.get_uid( - new_post, default_labels=new_name_finder.known_name) - - if new_pre != old_pre or new_post != old_post: - # if the connection has changed, tell javascript - pres = self.get_parents( - new_pre, - default_labels=new_name_finder.known_name)[:-1] - posts = self.get_parents( - new_post, - default_labels=new_name_finder.known_name)[:-1] - self.to_be_sent.append(dict( - type='reconnect', uid=uid, - pres=pres, posts=posts)) - changed = True - return changed - - def get_parents(self, uid, default_labels=None): - while uid not in self.parents: - net = self.networks_to_search.pop(0) - net_uid = self.page.get_uid(net, default_labels=default_labels) - for n in net.nodes: - n_uid = self.page.get_uid(n, default_labels=default_labels) - self.parents[n_uid] = net_uid - for e in net.ensembles: - e_uid = self.page.get_uid(e, default_labels=default_labels) - self.parents[e_uid] = net_uid - for n in net.networks: - n_uid = self.page.get_uid(n, default_labels=default_labels) - self.parents[n_uid] = net_uid - self.networks_to_search.append(n) - parents = [uid] - while parents[-1] in self.parents: - parents.append(self.parents[parents[-1]]) - return parents - - def modified_config(self): - self.page.modified_config() - - def update_client(self, client): - now = time.time() - if now > self.last_reload_check + 0.5: - self.check_for_reload() - self.last_reload_check = now - - if not self.initialized_pan_and_zoom: - self.send_pan_and_zoom(client) - self.initialized_pan_and_zoom = True - - while len(self.to_be_sent) > 0: - info = self.to_be_sent.popleft() - client.write_text(json.dumps(info)) - - if len(self.to_be_expanded) > 0: - with self.page.lock: - network = self.to_be_expanded.popleft() - self.expand_network(network, client) - - def javascript(self): - return 'new Nengo.NetGraph(main, {uid:"%s"});' % id(self) - - def message(self, msg): - try: - info = json.loads(msg) - except ValueError: - print('invalid message', repr(msg)) - return - action = info.get('act', None) - undo = info.get('undo', None) - if action is not None: - del info['act'] - if action in ('auto_expand', 'auto_collapse'): - getattr(self, 'act_' + action[5:])(**info) - elif action in ('pan', 'zoom', 'create_modal'): - # These should not use the undo stack - getattr(self, 'act_' + action)(**info) - else: - act = nengo_gui.user_action.create_action(action, self, **info) - self.page.undo_stack.append([act]) - del self.page.redo_stack[:] - elif undo is not None: - if undo == '1': - self.undo() - else: - self.redo() - else: - print('received message', msg) - - def undo(self): - if self.page.undo_stack: - action = self.page.undo_stack.pop() - re = [] - for act in action: - act.undo() - re.insert(0, act) - self.page.redo_stack.append(re) - - def redo(self): - if self.page.redo_stack: - action = self.page.redo_stack.pop() - un = [] - for act in action: - act.apply() - un.insert(0, act) - self.page.undo_stack.append(un) - - def act_expand(self, uid): - net = self.uids[uid] - self.to_be_expanded.append(net) - self.page.config[net].expanded = True - self.modified_config() - - def act_collapse(self, uid): - net = self.uids[uid] - self.page.config[net].expanded = False - self.remove_uids(net) - self.modified_config() - - def remove_uids(self, net): - for items in [net.ensembles, net.networks, net.nodes, net.connections]: - for item in items: - uid = self.page.get_uid(item) - if uid in self.uids: - del self.uids[uid] - for n in net.networks: - self.remove_uids(n) - - def act_pan(self, x, y): - self.page.config[self.page.model].pos = x, y - self.modified_config() - - def act_zoom(self, scale, x, y): - self.page.config[self.page.model].size = scale, scale - self.page.config[self.page.model].pos = x, y - self.modified_config() - - def act_create_modal(self, uid, **info): - js = infomodal(self, uid, **info) - self.to_be_sent.append(dict(type='js', code=js)) - - def expand_network(self, network, client): - if not self.page.config[network].has_layout: - pos = self.layout.make_layout(network) - for obj, layout in pos.items(): - self.page.config[obj].pos = layout['y'], layout['x'] - self.page.config[obj].size = layout['h'] / 2, layout['w'] / 2 - self.page.config[network].has_layout = True - - if network is self.page.model: - parent = None - else: - parent = self.page.get_uid(network) - for ens in network.ensembles: - self.create_object(client, ens, type='ens', parent=parent) - for node in network.nodes: - self.create_object(client, node, type='node', parent=parent) - for net in network.networks: - self.create_object(client, net, type='net', parent=parent) - for conn in network.connections: - self.create_connection(client, conn, parent=parent) - self.page.config[network].expanded = True - - def create_object(self, client, obj, type, parent): - uid = self.page.get_uid(obj) - if uid in self.uids: - return - - pos = self.page.config[obj].pos - if pos is None: - import random - pos = random.uniform(0, 1), random.uniform(0, 1) - self.page.config[obj].pos = pos - size = self.page.config[obj].size - if size is None: - size = (0.1, 0.1) - self.page.config[obj].size = size - label = self.page.get_label(obj) - self.uids[uid] = obj - info = dict(uid=uid, label=label, pos=pos, type=type, size=size, - parent=parent) - if type == 'net': - info['expanded'] = self.page.config[obj].expanded - info.update(self.get_extra_info(obj)) - - client.write_text(json.dumps(info)) - - def get_extra_info(self, obj): - '''Determine helper information for each nengo object. - - This is used by the client side to configure the display. It is also - used by the reload() code to determine if a NetGraph object should - be recreated. - ''' - info = {} - if isinstance(obj, nengo.Node): - if obj.output is None or ( - isinstance(obj.output, OverriddenOutput) - and obj.output.base_output is None): - info['passthrough'] = True - if callable(obj.output) and hasattr(obj.output, '_nengo_html_'): - info['html'] = True - info['dimensions'] = int(obj.size_out) - elif isinstance(obj, nengo.Ensemble): - info['dimensions'] = int(obj.size_out) - info['n_neurons'] = int(obj.n_neurons) - elif Value.default_output(obj) is not None: - info['default_output'] = True - - info['sp_targets'] = ( - nengo_gui.components.spa_plot.SpaPlot.applicable_targets(obj)) - return info - - def send_pan_and_zoom(self, client): - pan = self.page.config[self.page.model].pos - if pan is None: - pan = 0, 0 - zoom = self.page.config[self.page.model].size - if zoom is None: - zoom = 1.0 - else: - zoom = zoom[0] - client.write_text(json.dumps(dict(type='pan', pan=pan))) - client.write_text(json.dumps(dict(type='zoom', zoom=zoom))) - - def create_connection(self, client, conn, parent): - uid = self.page.get_uid(conn) - if uid in self.uids: - return - pre = conn.pre_obj - if isinstance(pre, nengo.ensemble.Neurons): - pre = pre.ensemble - post = conn.post_obj - if isinstance(post, nengo.connection.LearningRule): - post = post.connection.post - if isinstance(post, nengo.base.ObjView): - post = post.obj - if isinstance(post, nengo.ensemble.Neurons): - post = post.ensemble - pre = self.page.get_uid(pre) - post = self.page.get_uid(post) - self.uids[uid] = conn - pres = self.get_parents(pre)[:-1] - posts = self.get_parents(post)[:-1] - info = dict(uid=uid, pre=pres, post=posts, type='conn', parent=parent) - client.write_text(json.dumps(info)) diff --git a/nengo_gui/components/network.py b/nengo_gui/components/network.py new file mode 100644 index 00000000..d9199de8 --- /dev/null +++ b/nengo_gui/components/network.py @@ -0,0 +1,37 @@ +from nengo.spa.module import Module + +from .base import Component + + +# TODO: has_layout? +class Network(Component): + + def __init__(self, client, obj, uid, + pos=None, label_visible=True, + expanded=False, has_layout=False): + super(Network, self).__init__(client, obj, uid, pos, label_visible) + self.expanded = expanded + self.has_layout = has_layout + + @property + def output(self): + """Used in value plots""" + if isinstance(self.obj, Module) and "default" in self.obj.outputs: + return self.obj.outputs["default"][0] + elif hasattr(self.obj, "output"): + return self.obj.output + return None + + def create(self): + # TODO: figure out args to pass to this + self.client.send("netgraph.create_network", + uid=self.uid, + pos=self.pos, + label=self.label, + labelVisible=self.label_visible, + dimensions=1, # TODO + expanded=self.expanded) + + def similar(self, other): + return (super(Network, self).similar(other) + and self.output == other.output) diff --git a/nengo_gui/components/node.py b/nengo_gui/components/node.py new file mode 100644 index 00000000..71bde0bd --- /dev/null +++ b/nengo_gui/components/node.py @@ -0,0 +1,35 @@ +from .base import Component +from .slider import OverriddenOutput + + +class Node(Component): + + @property + def dimensions(self): + return self.obj.size_out + + @property + def html(self): + return (callable(self.obj.output) + and hasattr(self.obj.output, '_nengo_html_')) + + @property + def passthrough(self): + return self.obj.output is None or ( + isinstance(self.obj.output, OverriddenOutput) + and self.obj.output.base_output is None) + + def create(self): + # TODO: differentiate passthrough from normal? + self.client.send("netgraph.create_node", + uid=self.uid, + pos=self.pos, + label=self.label, + labelVisible=self.label_visible, + dimensions=self.dimensions) + + def similar(self, other): + return (super(Node, self).similar(other) + and self.dimensions == other.dimensions + and self.passthrough == other.passthrough + and self.html == other.html) diff --git a/nengo_gui/components/pointer.py b/nengo_gui/components/pointer.py deleted file mode 100644 index 6457c236..00000000 --- a/nengo_gui/components/pointer.py +++ /dev/null @@ -1,112 +0,0 @@ -import copy -import itertools - -import nengo -import nengo.spa as spa -import numpy as np - -from nengo_gui.components.component import Component -from nengo_gui.components.spa_plot import SpaPlot - - -class Pointer(SpaPlot): - """Server side component for the Semantic Pointer Cloud""" - - config_defaults = dict(show_pairs=False, **Component.config_defaults) - - def __init__(self, obj, **kwargs): - super(Pointer, self).__init__(obj, **kwargs) - - # the semantic pointer value as set by the user in the GUI - # a value of 'None' means do not override - self.override_target = None - - # The white list indicates the networks whose user-defined - # over-ride value can be inserted on the input - # thus the value loops in from the output to the input. - # All other networks have their value inserted on the output. - # Looping-in has the advantage of actually changing the - # neural activity of the population, rather than just changing - # the output. - self.loop_in_whitelist = [spa.Buffer, spa.Memory, spa.State] - - self.node = None - self.conn1 = None - self.conn2 = None - - def add_nengo_objects(self, page): - with page.model: - output = self.obj.outputs[self.target][0] - self.node = nengo.Node(self.gather_data, - size_in=self.vocab_out.dimensions, - size_out=self.vocab_out.dimensions) - self.conn1 = nengo.Connection(output, self.node, synapse=0.01) - loop_in = type(self.obj) in self.loop_in_whitelist - if loop_in and self.target == 'default': - input = self.obj.inputs[self.target][0] - self.conn2 = nengo.Connection(self.node, input, synapse=0.01) - else: - self.conn2 = nengo.Connection(self.node, output, synapse=0.01) - - def remove_nengo_objects(self, page): - page.model.connections.remove(self.conn1) - page.model.connections.remove(self.conn2) - page.model.nodes.remove(self.node) - - def gather_data(self, t, x): - vocab = self.vocab_out - key_similarities = np.dot(vocab.vectors, x) - over_threshold = key_similarities > 0.01 - matches = zip(key_similarities[over_threshold], - np.array(vocab.keys)[over_threshold]) - if self.config.show_pairs: - self.vocab_out.include_pairs = True - pair_similarities = np.dot(vocab.vector_pairs, x) - over_threshold = pair_similarities > 0.01 - pair_matches = zip(pair_similarities[over_threshold], - np.array(vocab.key_pairs)[over_threshold]) - matches = itertools.chain(matches, pair_matches) - - text = ';'.join(['%0.2f%s' % ( min(sim, 9.99), key) for sim, key in matches]) - - # msg sent as a string due to variable size of pointer names - msg = '%g %s' % (t, text) - self.data.append(msg) - if self.override_target is None: - return np.zeros(self.vocab_out.dimensions) - else: - v = (self.override_target.v - x) * 3 - return v - - def update_client(self, client): - while len(self.data) > 0: - data = self.data.popleft() - client.write_text(data) - - def javascript(self): - info = dict(uid=id(self), label=self.label) - json = self.javascript_config(info) - return 'new Nengo.Pointer(main, sim, %s);' % json - - def code_python_args(self, uids): - return [uids[self.obj], 'target=%r' % self.target] - - def message(self, msg): - if msg == ':empty:': - self.override_target = None - elif msg[0:12] == ':check only:': - if len(msg) == 12: - self.data.append("good_pointer") - else: - vocab = copy.deepcopy(self.vocab_out) - try: - vocab.parse(msg[12:]) - self.data.append("good_pointer") - except: - self.data.append("bad_pointer") - else: - # The message value is the new value for the output of the pointer - try: - self.override_target = self.vocab_out.parse(msg) - except: - self.override_target = None diff --git a/nengo_gui/components/raster.py b/nengo_gui/components/raster.py index 2d46623f..c749d170 100644 --- a/nengo_gui/components/raster.py +++ b/nengo_gui/components/raster.py @@ -1,76 +1,73 @@ -import struct -import collections - import nengo import numpy as np -from nengo_gui.components.component import Component +from ..client import bind +from .base import Widget -class Raster(Component): +class Raster(Widget): """Plot showing spike events over time.""" - config_defaults = dict(n_neurons=10, - **Component.config_defaults) + def __init__(self, client, obj, uid, + n_neurons=10, pos=None, label_visible=True): + super(Raster, self).__init__( + client, obj, uid, pos=pos, label_visible=label_visible) + + self.chosen = None # Filled in when n_neurons set - def __init__(self, obj): - super(Raster, self).__init__() - self.neuron_type = obj.neuron_type - self.obj = obj.neurons - self.data = collections.deque() - self.max_neurons = obj.n_neurons + self.n_neurons = n_neurons self.conn = None self.node = None - self.chosen = None + @property + def max_neurons(self): + return self.obj.n_neurons + + @property + def n_neurons(self): + return self._n_neurons + + @n_neurons.setter + @bind("{self.uid}.n_neurons") + def n_neurons(self, n_neurons): + self._n_neurons = min(n_neurons, self.max_neurons) + self.chosen = np.linspace( + 0, self.max_neurons-1, self._n_neurons).astype(int) + @property + def neurons(self): + return self.obj.neurons - def attach(self, page, config, uid): - super(Raster, self).attach(page, config, uid) - self.label = page.get_label(self.obj.ensemble) + @property + def neuron_type(self): + return self.obj.neuron_type - def add_nengo_objects(self, page): - with page.model: - self.node = nengo.Node(self.gather_data, size_in=self.max_neurons) + def add_nengo_objects(self, model): + + def fast_send_to_client(t, x): + indices = np.nonzero(x[self.chosen])[0] + self.fast_client.send(np.hstack((t, indices))) + + with model: + self.node = nengo.Node(fast_send_to_client, + size_in=self.max_neurons, + size_out=0) if 'spikes' in self.neuron_type.probeable: - self.conn = nengo.Connection(self.obj, self.node, synapse=None) - - def remove_nengo_objects(self, page): - page.model.nodes.remove(self.node) - if 'spikes' in self.neuron_type.probeable: - page.model.connections.remove(self.conn) - - def gather_data(self, t, x): - if self.chosen is None: - self.compute_chosen_neurons() - indices = np.nonzero(x[self.chosen])[0] - data = struct.pack(' 0: - data = self.data.popleft() - client.write_binary(data) - - def javascript(self): - info = dict(uid=id(self), label=self.label, - max_neurons=self.max_neurons) - json = self.javascript_config(info) - return 'new Nengo.Raster(main, sim, %s);' % json - - def code_python_args(self, uids): - return [uids[self.obj.ensemble]] - - def message(self, msg): - if msg.startswith('n_neurons:'): - n_neurons = min(int(msg[10:]), self.max_neurons) - self.page.config[self].n_neurons = n_neurons - self.compute_chosen_neurons() - self.page.modified_config() + self.conn = nengo.Connection( + self.neurons, self.node, synapse=None) + + def create(self): + self.client.send("netgraph.create_raster", + uid=self.uid, + pos=self.pos, + label=self.label, + labelVisible=self.label_visible, + nNeurons=self.n_neurons) + + def remove_nengo_objects(self, model): + model.nodes.remove(self.node) + self.node = None + if self.conn is not None: + model.connections.remove(self.conn) + self.conn = None diff --git a/nengo_gui/components/sim_control.py b/nengo_gui/components/sim_control.py deleted file mode 100644 index b05a80c1..00000000 --- a/nengo_gui/components/sim_control.py +++ /dev/null @@ -1,221 +0,0 @@ -import time -import timeit -import struct - -import numpy as np -import nengo -import json - -from nengo_gui.components.component import Component -import nengo_gui.exec_env -from nengo_gui.server import WebSocketFrame - - -class SimControl(Component): - """Controls simulation via control node embedded in the neural model. - - Also instantiates and communicates with the SimControl and the Toolbar - on the JavaScript side, which includes the task of back-end selection.""" - - config_defaults = dict(shown_time=0.5, kept_time=4.0) - - def __init__(self, dt=0.001): - # this component must be the very first one defined, so - # its component_order is the smallest overall - super(SimControl, self).__init__(component_order=-10) - self.paused = True - self.last_tick = None - self.rate = 0.0 - self.model_dt = dt - self.rate_tau = 0.5 - self.last_send_rate = None - self.sim_ticks = 0 - self.skipped = 1 - self.time = 0.0 - self.last_status = None - self.next_ping_time = None - self.send_config_options = False - self.reset_inform = False - self.node = None - self.target_rate = 1.0 # desired speed of simulation - self.target_scale = None # desired proportion of full speed - self.delay_time = 0.0 # amount of delay per time step - self.rate_proportion = 1.0 # current proportion of full speed - self.smart_sleep_offset = 0.0 # difference from actual sleep time - - def attach(self, page, config, uid): - super(SimControl, self).attach(page, config, uid) - self.shown_time = config.shown_time - self.kept_time = config.kept_time - - def add_nengo_objects(self, page): - with page.model: - self.node = nengo.Node(self.control, size_out=0) - - def remove_nengo_objects(self, page): - page.model.nodes.remove(self.node) - - def finish(self): - self.page.finish() - - def control(self, t): - """Node embedded in the model to control simulation progression. - - Sleeps while the simulation is paused. - """ - - self.actual_model_dt = t - self.time - self.time = t - self.sim_ticks += 1 - - now = timeit.default_timer() - if self.last_tick is not None: - dt = now - self.last_tick - if dt == 0: - self.skipped += 1 - else: - rate = self.actual_model_dt * self.skipped / dt - decay = np.exp(-dt / self.rate_tau) - self.rate *= decay - self.rate += (1 - decay) * rate - self.skipped = 1 - - if self.actual_model_dt > 0: - # compute current proportion of full speed - self.rate_proportion = 1.0 - ((self.rate * self.delay_time) / - self.actual_model_dt) - - # if we have a desired proportion, use it to control delay_time - # Note that we need last_tick to not be None so that we have a - # valid dt value. - if self.target_scale is not None and self.last_tick is not None: - s = self.target_scale - if s <=0: - self.delay_time = 0.5 - else: - self.delay_time = (1.0/s - s) * (dt - self.delay_time) - - # if we have a desired rate, do a simple P-controller to get there - if self.target_rate is not None: - rate_error = self.rate - self.target_rate - delta = rate_error * 0.0000002 - self.delay_time += delta - - self.delay_time = np.clip(self.delay_time, 0, 0.5) - - if self.delay_time > 0: - self.smart_sleep(self.delay_time) - - self.last_tick = now - - # Sleeps to prevent the simulation from advancing - # while the simulation is paused - while self.paused and self.page.sim is not None: - time.sleep(0.01) - self.last_tick = None - - def busy_sleep(self, delay_time): - now = timeit.default_timer() - start = now - while now < start + delay_time: - now = timeit.default_timer() - - def smart_sleep(self, delay_time): - """Attempt to sleep for an amount of time without a busy loop. - - This keeps track of the difference between the requested time.sleep() - time and the actual amount of time slept, and then subtracts that - difference from future smart_sleep calls. This should give an - overall consistent sleep() time even if the actual sleep() time - is inaccurate. - """ - t = delay_time + self.smart_sleep_offset - if t >= 0: - start = timeit.default_timer() - time.sleep(t) - end = timeit.default_timer() - self.smart_sleep_offset += delay_time - (end - start) - else: - self.smart_sleep_offset += delay_time - - def config_settings(self, data): - for i in data: - print(i) - - def update_client(self, client): - now = time.time() - # send off a ping now and then so we'll notice when connection closes - if self.next_ping_time is None or now > self.next_ping_time: - client.write_frame(WebSocketFrame( - 1, 0, WebSocketFrame.OP_PING, 0, b'')) - self.next_ping_time = now + 2.0 - - if self.page.changed: - self.paused = True - self.page.sim = None - self.page.changed = False - if not self.paused or self.reset_inform: - client.write_binary(struct.pack( - ' 0: - to_client = self.to_client.popleft() - client.write_binary(to_client) - - def message(self, msg): - index, value = msg.split(',') - index = int(index) - if value == 'reset': - self.from_client[index] = np.nan - else: - self.from_client[index] = float(value) - - def code_python_args(self, uids): - return [uids[self.node]] + start_value[...] = self.base_output + self.client.send("netgraph.create_slider", + uid=self.uid, + pos=self.pos, + label=self.label, + labelVisible=self.label_visible, + dimensions=self.obj.size_out, + synapse=0.005, # TODO + startValue=[float(x) for x in start_value]) + + def remove_nengo_objects(self, model): + # If we're setting the output back to None, clear size_out + # to avoid a warning when size_out is automatically set + if self.base_output is None: + self.obj.size_out = None + self.obj.output = self.base_output + + @bind("{self.uid}.forget") + def forget(self): + # Make sure we're currently running + if self.obj.output is not self.base_output: + # A bit of a hack, but to forget user-specified values we set all + # of the values to nan as nan values are not overridden. + nans = np.zeros(self.obj.size_out) * np.nan + # Send directly to the fast client + self.fast_client.receive(nans.tobytes()) diff --git a/nengo_gui/components/spa.py b/nengo_gui/components/spa.py new file mode 100644 index 00000000..821cdba8 --- /dev/null +++ b/nengo_gui/components/spa.py @@ -0,0 +1,227 @@ +import copy + +import numpy as np +import nengo +from nengo.spa import Buffer, Memory, State + +from ..client import bind +from .base import Widget + + +class SpaWidget(Widget): + def __init__(self, client, obj, uid, + target="default", show_pairs=False, + pos=None, label_visible=True): + super(SpaWidget, self).__init__(client, obj, uid, pos, label_visible) + self.target = target + self.show_pairs = show_pairs + + @property + def keys(self): + if self.show_pairs: + # TODO: is this needed? + # while self.vocab.key_pairs is None: + # time.sleep(0.001) + return self.vocab.keys + self.vocab.key_pairs + else: + return self.vocab.keys + + @property + def n_lines(self): + return len(self.keys) + + @property + def show_pairs(self): + return self.vocab.include_pairs + + @show_pairs.setter + @bind("{self.uid}.show_pairs") + def show_pairs(self, val): + if val != self.vocab.include_pairs: + self.vocab.include_pairs = val + keys = self.keys + self.client.send("%s.set_keys" % self.uid, keys=keys) + + @property + def vectors(self): + if self.show_pairs: + # TODO: is this needed? + # while self.vocab.key_pairs is None: + # time.sleep(0.001) + return self.vocab.vectors + self.vocab.vectors_pairs + else: + return self.vocab.vectors + + @property + def vocab(self): + return self.obj.outputs[self.target][1] + + +class SpaPointer(SpaWidget): + """Server side component for the Semantic Pointer Cloud""" + + # This white list indicates the networks whose user-defined + # override value can be inserted on the input + # thus the value loops in from the output to the input. + # All other networks have their value inserted on the output. + # Looping-in has the advantage of actually changing the + # neural activity of the population, rather than just changing + # the output. + CAN_LOOP_IN = [Buffer, Memory, State] + + def __init__(self, client, obj, uid, + target="default", show_pairs=False, override=None, + pos=None, label=None): + super(SpaPointer, self).__init__( + client, obj, uid, target, show_pairs, pos, label) + + # the semantic pointer value as set by the user in the GUI + # a value of 'None' means do not override + self.override = override + + self.node = None + self.conn1 = None + self.conn2 = None + + @bind("{self.uid}.check_target") + def check_target(self, target): + vocab = copy.deepcopy(self.vocab) + try: + vocab.parse(target) + self.client.send("%s.check_target", ok=True) + except: + self.client.send("%s.check_target", ok=False) + + @property + def override(self): + return self._override + + @override.setter + @bind("{self.uid}.set_override") # TODO: was set_target + def override(self, override): + if override is not None: + # Add the pointer to the vocab if not yet present + override = self.vocab.parse(override) + self._override = override + + def add_nengo_objects(self, network): + + def send_to_client(t, x): + similarities = np.dot(self.vectors, x) + over_threshold = similarities > 0.01 + matches = zip(similarities[over_threshold], + np.array(self.keys)[over_threshold]) + + self.client.send("%s.matches" % (self.uid,), data=';'.join( + ['%0.2f%s' % (min(sim, 9.99), key) for sim, key in matches])) + + if self.override is None: + return np.zeros(self.vocab.dimensions) + else: + # TODO: Why - x and * 3?? + return (self.override.v - x) * 3 + + with network: + output = self.obj.outputs[self.target][0] + self.node = nengo.Node(send_to_client, + size_in=self.vocab.dimensions, + size_out=self.vocab.dimensions) + self.conn1 = nengo.Connection(output, self.node, synapse=0.01) + loop_in = type(self.obj) in self.CAN_LOOP_IN + if loop_in and self.target == 'default': + input = self.obj.inputs[self.target][0] + self.conn2 = nengo.Connection(self.node, input, synapse=0.01) + else: + self.conn2 = nengo.Connection(self.node, output, synapse=0.01) + + def create(self): + self.client.send("netgraph.create_spa_pointer", + uid=self.uid, + pos=self.pos, + label=self.label, + labelVisible=self.label_visible, + dimensions=1, # TODO + synapse=0.005, # TODO + showPairs=self.show_pairs) + + def remove_nengo_objects(self, model): + model.connections.remove(self.conn1) + model.connections.remove(self.conn2) + model.nodes.remove(self.node) + self.conn1, self.conn2, self.node = None, None, None + + +class SpaSimilarity(SpaWidget): + """Line graph showing semantic pointer decoded values over time""" + + def __init__(self, client, obj, uid, + ylim=(-1.5, 1.5), target="default", show_pairs=False, + pos=None, label=None): + super(SpaSimilarity, self).__init__( + client, obj, uid, target, show_pairs, pos, label) + + # Nengo objects for data collection + self.node = None + self.conn = None + + def add_nengo_objects(self, model): + + last_n_lines = np.array(self.n_lines) + data = np.zeros(self.n_lines + 1) + + def fast_send_to_client(t, x): + if self.n_lines != last_n_lines: + data.resize(self.n_lines + 1) + self.update() + last_n_lines[...] = self.n_lines + n_keys = len(self.vocab.keys) + + data[0] = t + data[1:1+n_keys] = np.dot(self.vocab.vectors, x) + if self.show_pairs: + data[1+n_keys:] = np.dot(self.vocab.vector_pairs, x) + self.fast_client.send(data) + + with model: + output = self.obj.outputs[self.target][0] + self.node = nengo.Node( + fast_send_to_client, size_in=self.vocab.dimensions) + self.conn = nengo.Connection(output, self.node, synapse=0.01) + + def create(self): + # TODO: get n_lines from this.labels.length + self.client.send("netgraph.create_spa_similarity", + uid=self.uid, + pos=self.pos, + dimensions=1, # TODO + synapse=0.005, # TODO + xlim=[-0.5, 0], # TODO + ylim=[-1, 1]) # TODO + + def remove_nengo_objects(self, model): + """Undo the changes made by add_nengo_objects.""" + model.connections.remove(self.conn) + model.nodes.remove(self.node) + self.conn, self.node = None, None + + def update(self): + self.client.send("%s.reset_legend_and_data" % self.uid, + keys=self.keys) + + # TODO: figure out what update_legend was doing + + # # pass all the missing keys + # legend_update = [] + # legend_update += (vocab.keys[self.old_vocab_length:]) + # self.old_vocab_length = len(vocab.keys) + # # and all the missing pairs if we're showing pairs + # if self.config.show_pairs: + # # briefly there can be no pairs, so catch the error + # try: + # legend_update += vocab.key_pairs[self.old_pairs_length:] + # self.old_pairs_length = len(vocab.key_pairs) + # except TypeError: + # pass + + # self.data.append( + # '["update_legend", "%s"]' % ('","'.join(legend_update))) diff --git a/nengo_gui/components/spa_plot.py b/nengo_gui/components/spa_plot.py deleted file mode 100644 index b970b4b5..00000000 --- a/nengo_gui/components/spa_plot.py +++ /dev/null @@ -1,38 +0,0 @@ -import collections - -from nengo.spa.module import Module - -from nengo_gui.components.component import Component - - -class SpaPlot(Component): - """Parent class for pointer.Pointer and spa_similarity.SpaSimilarity""" - - def __init__(self, obj, **kwargs): - super(SpaPlot, self).__init__() - self.obj = obj - self.data = collections.deque() - self.target = kwargs.get('args', 'default') - self.vocab_out = obj.outputs[self.target][1] - - def attach(self, page, config, uid): - super(SpaPlot, self).attach(page, config, uid) - self.label = page.get_label(self.obj) - self.vocab_out.include_pairs = config.show_pairs - - def update_client(self, client): - while len(self.data) > 0: - data = self.data.popleft() - client.write_text(data) - - def code_python_args(self, uids): - return [uids[self.obj], 'target=%r' % self.target] - - @staticmethod - def applicable_targets(obj): - targets = [] - if isinstance(obj, Module): - for target_name, (obj, vocab) in obj.outputs.items(): - if vocab is not None: - targets.append(target_name) - return targets diff --git a/nengo_gui/components/spa_similarity.py b/nengo_gui/components/spa_similarity.py deleted file mode 100644 index a588f640..00000000 --- a/nengo_gui/components/spa_similarity.py +++ /dev/null @@ -1,100 +0,0 @@ -import numpy as np -import nengo - -from nengo_gui.components.component import Component -from nengo_gui.components.spa_plot import SpaPlot - - -class SpaSimilarity(SpaPlot): - """Line graph showing semantic pointer decoded values over time""" - - config_defaults = dict(max_value=1.5, min_value=-1.5, - show_pairs=False, - **Component.config_defaults) - - def __init__(self, obj, **kwargs): - super(SpaSimilarity, self).__init__(obj, **kwargs) - - self.old_vocab_length = len(self.vocab_out.keys) - self.old_pairs_length = 0 - self.labels = self.vocab_out.keys - self.previous_pairs = False - - # Nengo objects for data collection - self.node = None - self.conn = None - - def add_nengo_objects(self, page): - with page.model: - output = self.obj.outputs[self.target][0] - self.node = nengo.Node(self.gather_data, - size_in=self.vocab_out.dimensions) - self.conn = nengo.Connection(output, self.node, synapse=0.01) - - def remove_nengo_objects(self, page): - """Undo the changes made by add_nengo_objects.""" - page.model.connections.remove(self.conn) - page.model.nodes.remove(self.node) - - def gather_data(self, t, x): - vocab = self.vocab_out - - if self.old_vocab_length != len(vocab.keys): - self.update_legend(vocab) - - # get the similarity and send it - key_similarity = np.dot(vocab.vectors, x) - simi_list = ['{:.2f}'.format(simi) for simi in key_similarity] - - if self.config.show_pairs: - - # briefly there can be no pairs, so catch the error - try: - pair_similarity = np.dot(vocab.vector_pairs, x) - simi_list += ['{:.2f}'.format(simi) for simi in pair_similarity] - except TypeError: - pass - - if(simi_list != []): - self.data.append( '["data_msg", %g, %s]' - %( t, ",".join(simi_list) ) ) - - def update_legend(self, vocab): - # pass all the missing keys - legend_update = [] - legend_update += (vocab.keys[self.old_vocab_length:]) - self.old_vocab_length = len(vocab.keys) - # and all the missing pairs if we're showing pairs - if self.config.show_pairs: - # briefly there can be no pairs, so catch the error - try: - legend_update += vocab.key_pairs[self.old_pairs_length:] - self.old_pairs_length = len(vocab.key_pairs) - except TypeError: - pass - - self.data.append('["update_legend", "%s"]' - %('","'.join(legend_update))) - - def javascript(self): - """Generate the javascript that will create the client-side object""" - info = dict(uid=id(self), label=self.label, n_lines=len(self.labels), - synapse=0, pointer_labels=self.labels) - json = self.javascript_config(info) - return 'new Nengo.SpaSimilarity(main, sim, %s);' % json - - def message(self, msg): - """Message receive function for show_pairs toggling and reset""" - vocab = self.vocab_out - # Send the new labels - if self.config.show_pairs: - vocab.include_pairs = True - self.data.append( - '["reset_legend_and_data", "%s"]' % ( - '","'.join(vocab.keys + vocab.key_pairs))) - # if we're starting to show pairs, track pair length - self.old_pairs_length = len(vocab.key_pairs) - else: - vocab.include_pairs = False - self.data.append('["reset_legend_and_data", "%s"]' - % ('","'.join(vocab.keys))) diff --git a/nengo_gui/components/spike_grid.py b/nengo_gui/components/spike_grid.py index 86204f1d..8c4d9bc5 100644 --- a/nengo_gui/components/spike_grid.py +++ b/nengo_gui/components/spike_grid.py @@ -1,77 +1,85 @@ import nengo import numpy as np -import struct -from nengo_gui.components.component import Component +from .base import Widget -class SpikeGrid(Component): +class SpikeGrid(Widget): """Represents an ensemble of neurons as squares in a grid. - + The color of the squares corresponds to the neuron spiking. """ - def __init__(self, obj, n_neurons=None): - super(SpikeGrid, self).__init__() - self.obj = obj - self.data = [] - self.max_neurons = self.obj.neurons.size_out - if n_neurons is None: - n_neurons = self.max_neurons - self.n_neurons = n_neurons - self.pixels_x = np.ceil(np.sqrt(self.n_neurons)) - self.pixels_y = np.ceil(float(self.n_neurons) / self.pixels_x) - self.n_pixels = self.pixels_x * self.pixels_y - self.struct = struct.Struct(' self.n_neurons: - x = x[:self.n_neurons] - y = np.zeros(int(self.n_pixels), dtype=np.uint8) - if self.max_value > 0: - y[:x.size] = x * 255 / self.max_value - data = self.struct.pack(t, *y) - self.data.append(data) - - def update_client(self, client): - length = len(self.data) - if length > 0: - item = bytes().join(self.data[:length]) - del self.data[:length] - try: - client.write_binary(item) - except: - # if there is a communication problem, just drop the frames - # (this usually happens when there is too much data to send) - pass - - def javascript(self): - info = dict(uid=id(self), label=self.label, - pixels_x=self.pixels_x, pixels_y=self.pixels_y) - json = self.javascript_config(info) - return 'new Nengo.Image(main, sim, %s);' % json - - def code_python_args(self, uids): - args = [uids[self.obj]] - if self.n_neurons != self.max_neurons: - args.append('n_neurons=%d' % self.n_neurons) - return args + @property + def max_neurons(self): + return self.obj.neurons.size_out + + @property + def n_pixels(self): + return self.pixels_x * self.pixels_y + + @property + def pixels_x(self): + return int(np.ceil(np.sqrt(self.n_neurons))) + + @property + def pixels_y(self): + return int(np.ceil(float(self.n_neurons) / self.pixels_x)) + + def add_nengo_objects(self, model): + + def fast_send_to_client(t, x): + self.max_value = max(self.max_value, np.max(x)) + + # TODO: Does this every happen? Can it??? + # if x.size > self.n_neurons: + # x = x[:self.n_neurons] + y = (x * 255 / self.max_value).astype(np.uint8) + self.fast_client.send(y) + + # try: + # client.write_binary(item) + # except: + # # if there is a communication problem, just drop the frames + # # (this usually happens when there is too much data to send) + # pass + + # y = np.zeros(int(self.n_pixels), dtype=np.uint8) + # if self.max_value > 0: + # y[:x.size] = x * 255 / self.max_value + # data = self.struct.pack(t, *y) + # self.data.append(data) + + with model: + self.node = nengo.Node( + fast_send_to_client, + size_in=self.obj.neurons.size_out, + size_out=0) + self.conn = nengo.Connection( + self.obj.neurons, self.node, synapse=0.01) + + def create(self): + self.client.send("netgraph.create_spike_grid", + uid=self.uid, + pos=self.pos, + label=self.label, + labelVisible=self.label_visible, + dimensions=1, # TODO + synapse=0.005, # TODO + xlim=[-0.5, 0], # TODO + ylim=[-1, 1]) # TODO + + def remove_nengo_objects(self, model): + model.connections.remove(self.conn) + model.nodes.remove(self.node) + self.conn, self.node = None, None diff --git a/nengo_gui/components/tests/__init__.py b/nengo_gui/components/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/nengo_gui/components/tests/conftest.py b/nengo_gui/components/tests/conftest.py deleted file mode 100644 index 4af5625d..00000000 --- a/nengo_gui/components/tests/conftest.py +++ /dev/null @@ -1 +0,0 @@ -from nengo_gui import conftest #imports conftest functions from nengo_gui \ No newline at end of file diff --git a/nengo_gui/components/tests/test_base.py b/nengo_gui/components/tests/test_base.py new file mode 100644 index 00000000..7b437b72 --- /dev/null +++ b/nengo_gui/components/tests/test_base.py @@ -0,0 +1,70 @@ +import pytest + +from nengo_gui.components.base import Component, Position, Widget +from nengo_gui.exceptions import NotAttachedError + + +class TestPosition(object): + + def test_defaults(self): + pos = Position() + assert pos.x == 0 + assert pos.y == 0 + assert pos.width == 100 + assert pos.height == 100 + + def test_repr(self): + pos = Position() + assert repr(pos) == "Position(x=0, y=0, width=100, height=100)" + + +class TestComponent(object): + + def test_uid_readonly(self, client): + comp = Component(client, None, "comp") + assert comp.uid == "comp" + with pytest.raises(AttributeError): + comp.uid = "comp" + + def test_similar(self, client): + c1 = Component(client, None, "c1") + c2 = Component(client, None, "c2") + assert not c1.similar(c2) and not c2.similar(c1) + c2._uid = "c1" + assert c1.similar(c2) and c2.similar(c1) + + def test_update(self, client): + c1 = Component(client, None, "comp") + c2 = Component(client, None, "comp") + c1.update(c2) + assert client.ws.text is None # No update + c1.label = "comp" + c1.update(c2) + assert client.ws.text == '["comp.label", {"label": "comp"}]' + c2.update(c1) + assert client.ws.text == '["comp.label", {"label": null}]' + + def test_must_implement(self, client): + comp = Component(client, None, "comp") + + with pytest.raises(NotImplementedError): + comp.create() + with pytest.raises(NotImplementedError): + comp.delete() + + +class TestWidget(object): + + def test_fast_client(self, client): + widget = Widget(client, None, "widget") + + # For other nonexistent attributes + with pytest.raises(AttributeError): + widget.test + + # For fast_client + with pytest.raises(NotAttachedError): + widget.fast_client + + widget.attach(None) + assert widget.fast_client is None diff --git a/nengo_gui/components/tests/test_connection.py b/nengo_gui/components/tests/test_connection.py new file mode 100644 index 00000000..8de4b3f0 --- /dev/null +++ b/nengo_gui/components/tests/test_connection.py @@ -0,0 +1,51 @@ +import json + +import nengo + +from nengo_gui.components import Connection +from nengo_gui.netgraph import NameFinder + + +def test_create(client): + with nengo.Network(): + a = nengo.Ensemble(10, 1) + b = nengo.Ensemble(10, 1) + c = nengo.Connection(a, b) + + names = NameFinder() + names.update(locals()) + + comp = Connection(client, c, names[c], names) + comp.create() + + assert client.ws.text == '["netgraph.create_connection", {}]' + + +def test_similar_update(client): + with nengo.Network(): + a = nengo.Ensemble(10, 1) + b = nengo.Ensemble(10, 1) + cab = nengo.Connection(a, b) + cba = nengo.Connection(b, a) + + names = NameFinder() + names.update(locals()) + + # Note: uids must be the same + c1 = Connection(client, cab, "conn", names) + c2 = Connection(client, cba, "conn", names) + + assert c1.similar(c2) and c2.similar(c1) + + c1.update(c2) + assert json.loads(client.ws.text) == ["conn.reconnect", { + "pre": "a", "post": "b", + }] + + c2.update(c1) + assert json.loads(client.ws.text) == ["conn.reconnect", { + "pre": "b", "post": "a", + }] + + c1._uid = "notconn" + assert not c1.similar(c2) and not c2.similar(c1) diff --git a/nengo_gui/components/tests/test_ensemble.py b/nengo_gui/components/tests/test_ensemble.py new file mode 100644 index 00000000..151106d3 --- /dev/null +++ b/nengo_gui/components/tests/test_ensemble.py @@ -0,0 +1,31 @@ +import nengo + +from nengo_gui.components import Ensemble + + +def test_create(client): + with nengo.Network(): + a = nengo.Ensemble(10, 1) + + comp = Ensemble(client, a, "a") + comp.create() + assert client.ws.text == '["netgraph.create_ensemble", {}]' + + +def test_similar(client): + with nengo.Network(): + a = nengo.Ensemble(10, 1) + b = nengo.Ensemble(10, 1) + + # Note: uids must be the same + e1 = Ensemble(client, a, "ens") + e2 = Ensemble(client, b, "ens") + + assert e1.similar(e2) and e2.similar(e1) + + a.n_neurons = 20 + assert not e1.similar(e2) and not e2.similar(e1) + + a.n_neurons = 10 + b.dimensions = 2 + assert not e1.similar(e2) and not e2.similar(e1) diff --git a/nengo_gui/components/tests/test_htmlview.py b/nengo_gui/components/tests/test_htmlview.py new file mode 100644 index 00000000..e359fb1e --- /dev/null +++ b/nengo_gui/components/tests/test_htmlview.py @@ -0,0 +1,39 @@ +import json + +import nengo + +from nengo_gui.components import HTMLView + + +def test_create(client): + with nengo.Network(): + n = nengo.Node(None) + + html = HTMLView(client, n, "htmlview") + html.create() + assert json.loads(client.ws.text) == ["netgraph.create_htmlview", { + "label": None, "uid": "htmlview", + }] + + +def test_add_remove(client): + + f = lambda t: 0.0 + f._nengo_html_ = "test" + with nengo.Network() as net: + n = nengo.Node(output=f) + + html = HTMLView(client, n, "htmlview") + html.add_nengo_objects(net) + assert n.output is not f + assert html._old_output is f + assert client.ws.text is None + + n.output(0.0) + assert json.loads(client.ws.text) == ["htmlview.html", { + "t": 0.0, "html": "test", + }] + + html.remove_nengo_objects(net) + assert n.output is f + assert html._old_output is None diff --git a/nengo_gui/components/tests/test_network.py b/nengo_gui/components/tests/test_network.py new file mode 100644 index 00000000..53fb6964 --- /dev/null +++ b/nengo_gui/components/tests/test_network.py @@ -0,0 +1,26 @@ +import nengo + +from nengo_gui.components import Network + + +def test_create(client): + n = nengo.Network() + + comp = Network(client, n, "n") + comp.create() + assert client.ws.text == '["netgraph.create_network", {}]' + + +def test_similar(client): + with nengo.Network() as net1: + net2 = nengo.Network() + + # Note: uids must be the same + n1 = Network(client, net1, "node") + n2 = Network(client, net2, "node") + + assert n1.similar(n2) and n2.similar(n1) + + with net2: + net2.output = nengo.Ensemble(10, 1) + assert not n1.similar(n2) and not n2.similar(n1) diff --git a/nengo_gui/components/tests/test_node.py b/nengo_gui/components/tests/test_node.py new file mode 100644 index 00000000..9d9a5546 --- /dev/null +++ b/nengo_gui/components/tests/test_node.py @@ -0,0 +1,37 @@ +import nengo + +from nengo_gui.components import Node + + +def test_create(client): + with nengo.Network(): + n = nengo.Node([0]) + + comp = Node(client, n, "n") + comp.create() + assert client.ws.text == '["netgraph.create_node", {}]' + + +def test_similar(client): + with nengo.Network(): + node1 = nengo.Node(None, size_in=1) + node2 = nengo.Node(None, size_in=1) + + # Note: uids must be the same + n1 = Node(client, node1, "node") + n2 = Node(client, node2, "node") + + assert n1.similar(n2) and n2.similar(n1) + + node1.size_out = 2 + assert not n1.similar(n2) and not n2.similar(n1) + + node1.size_out = 1 + assert n1.similar(n2) and n2.similar(n1) + node2.output = lambda t: t + assert not n1.similar(n2) and not n2.similar(n1) + + node1.output = lambda t: t + assert n1.similar(n2) and n2.similar(n1) + node1.output._nengo_html_ = None + assert not n1.similar(n2) and not n2.similar(n1) diff --git a/nengo_gui/components/tests/test_raster.py b/nengo_gui/components/tests/test_raster.py new file mode 100644 index 00000000..9fb7f9e6 --- /dev/null +++ b/nengo_gui/components/tests/test_raster.py @@ -0,0 +1,40 @@ +import json + +import nengo +import numpy as np + +from nengo_gui.components import Raster + + +def test_create(client): + with nengo.Network(): + e = nengo.Ensemble(10, 1) + + raster = Raster(client, e, "raster") + raster.create() + assert json.loads(client.ws.text) == ["netgraph.create_raster", { + "max_neurons": 10, "label": None, + }] + + +def test_add_remove(client, fast_client): + with nengo.Network() as net: + e = nengo.Ensemble(10, 1) + + raster = Raster(client, e, "raster", n_neurons=10) + raster.attach(fast_client) + raster.add_nengo_objects(net) + assert raster.node is not None and raster.conn is not None + assert fast_client.ws.binary is None + + # Spike in indices 1 and 3 + raster.node.output(0.0, np.array([0, 1, 0, 1, 0, 0, 0, 0, 0, 0])) + assert fast_client.ws.binary == np.array([0.0, 1.0, 3.0]).tobytes() + + # Spike in indices 2 and 4 + raster.node.output(0.01, np.array([0, 0, 1, 0, 1, 0, 0, 0, 0, 0])) + assert fast_client.ws.binary == np.array([0.01, 2.0, 4.0]).tobytes() + + raster.remove_nengo_objects(net) + assert len(net.nodes) == 0 + assert len(net.connections) == 0 diff --git a/nengo_gui/components/tests/test_slider.py b/nengo_gui/components/tests/test_slider.py new file mode 100644 index 00000000..635c8588 --- /dev/null +++ b/nengo_gui/components/tests/test_slider.py @@ -0,0 +1,202 @@ +import json + +import nengo +import numpy as np + +from nengo_gui.components import Slider + + +def test_create(client): + with nengo.Network(): + n = nengo.Node(None, size_in=2) + + slider = Slider(client, n, "slider") + slider.create() + assert json.loads(client.ws.text) == ["netgraph.create_slider", { + "label": None, + "n_sliders": 2, + "start_value": [0.0, 0.0], + "uid": "slider", + }] + + +def test_passthrough(client, fast_client): + with nengo.Network() as net: + n = nengo.Node(None, size_in=2) + + dummy_backend = lambda: "nengo" + dummy_dt = lambda: 0.001 + client.bind("simcontrol.get_backend", dummy_backend) + client.bind("simcontrol.get_dt", dummy_dt) + + slider = Slider(client, n, "slider") + slider.attach(fast_client) + slider.add_nengo_objects(net) + + step = n.output if callable(n.output) else n.output.make_step( + shape_in=None, shape_out=n.size_out, dt=None, rng=None) + + assert fast_client.ws.binary is None + assert n.output is not slider.base_output + + # Without overriding, output should be same as input + assert (step(0.01, np.array([0.5, 0.5])) == np.array([0.5, 0.5])).all() + assert (step(0.02, np.array([-0.5, -0.5])) == np.array([-0.5, -0.5])).all() + # Value sent over fast client + assert fast_client.ws.binary == np.array([-0.5, -0.5]).tobytes() + + # Override the second dimension + fast_client.receive(np.array([np.nan, 0.0]).tobytes()) + assert (step(0.03, np.array([0.5, 0.5])) == np.array([0.5, 0.0])).all() + assert (step(0.04, np.array([-0.5, -0.5])) == np.array([-0.5, 0.0])).all() + # Whole value still sent over fast client + assert fast_client.ws.binary == np.array([-0.5, -0.5]).tobytes() + + # Override both dimensions + fast_client.receive(np.array([0.0, 0.0]).tobytes()) + assert (step(0.05, np.array([0.5, 0.5])) == np.array([0.0, 0.0])).all() + assert (step(0.06, np.array([-0.5, -0.5])) == np.array([0.0, 0.0])).all() + # Whole value still sent over fast client + assert fast_client.ws.binary == np.array([-0.5, -0.5]).tobytes() + + # Reset, no longer override + slider.reset() + assert (step(0.07, np.array([0.5, 0.5])) == np.array([0.5, 0.5])).all() + assert (step(0.08, np.array([-0.5, -0.5])) == np.array([-0.5, -0.5])).all() + assert fast_client.ws.binary == np.array([-0.5, -0.5]).tobytes() + + slider.remove_nengo_objects(net) + assert n.output is slider.base_output + + +def test_value(client, fast_client): + with nengo.Network() as net: + n = nengo.Node([1.0]) + + dummy_backend = lambda: "nengo" + dummy_dt = lambda: 0.001 + client.bind("simcontrol.get_backend", dummy_backend) + client.bind("simcontrol.get_dt", dummy_dt) + + slider = Slider(client, n, "slider") + slider.attach(fast_client) + slider.add_nengo_objects(net) + + step = n.output if callable(n.output) else n.output.make_step( + shape_in=None, shape_out=n.size_out, dt=None, rng=None) + + assert n.output is not slider.base_output + + # Without overriding, should always output 1.0 + assert step(0.01) == 1.0 + assert step(0.02) == 1.0 + + # Override the output + fast_client.receive(np.array([-1.0]).tobytes()) + assert step(0.03) == -1.0 + assert step(0.04) == -1.0 + + # Reset, no longer override + slider.reset() + assert step(0.05) == 1.0 + assert step(0.06) == 1.0 + + # Value not sent over when using static output + assert fast_client.ws.binary is None + + slider.remove_nengo_objects(net) + assert n.output is slider.base_output + + +def test_callable(client, fast_client): + with nengo.Network() as net: + n = nengo.Node(lambda t: t) + + dummy_backend = lambda: "nengo" + dummy_dt = lambda: 0.001 + client.bind("simcontrol.get_backend", dummy_backend) + client.bind("simcontrol.get_dt", dummy_dt) + + slider = Slider(client, n, "slider") + slider.attach(fast_client) + slider.add_nengo_objects(net) + + step = n.output if callable(n.output) else n.output.make_step( + shape_in=None, shape_out=n.size_out, dt=None, rng=None) + + assert fast_client.ws.binary is None + assert n.output is not slider.base_output + + # Without overriding output should be t + assert step(0.01) == 0.01 + assert step(0.02) == 0.02 + # Value sent over fast client + assert fast_client.ws.binary == np.array([0.02]).tobytes() + + # Override output + fast_client.receive(np.array([-1.0]).tobytes()) + assert step(0.03) == -1.0 + assert step(0.04) == -1.0 + # Original value still sent over fast client + assert fast_client.ws.binary == np.array([0.04]).tobytes() + + # Reset, no longer override + slider.reset() + assert step(0.05) == 0.05 + assert step(0.06) == 0.06 + assert fast_client.ws.binary == np.array([0.06]).tobytes() + + slider.remove_nengo_objects(net) + assert n.output is slider.base_output + + +def test_process(client, fast_client): + with nengo.Network() as net: + n = nengo.Node(nengo.processes.PresentInput( + inputs=[[0.1, 0.1], [0.2, 0.2]], presentation_time=0.01, + ), size_out=2) + + dummy_backend = lambda: "nengo" + dummy_dt = lambda: 0.001 + client.bind("simcontrol.get_backend", dummy_backend) + client.bind("simcontrol.get_dt", dummy_dt) + + slider = Slider(client, n, "slider") + slider.attach(fast_client) + slider.add_nengo_objects(net) + + step = n.output if callable(n.output) else n.output.make_step( + shape_in=(0,), shape_out=(n.size_out,), dt=0.01, rng=None) + + assert fast_client.ws.binary is None + assert n.output is not slider.base_output + + # Without overriding, output should be from process + assert (step(0.01) == np.array([0.1, 0.1])).all() + assert (step(0.02) == np.array([0.2, 0.2])).all() + # Value sent over fast client + print(np.frombuffer(fast_client.ws.binary)) + assert fast_client.ws.binary == np.array([0.2, 0.2]).tobytes() + + # Override the second dimension + fast_client.receive(np.array([np.nan, 0.0]).tobytes()) + assert (step(0.03) == np.array([0.1, 0.0])).all() + assert (step(0.04) == np.array([0.2, 0.0])).all() + # Whole value still sent over fast client + assert fast_client.ws.binary == np.array([0.2, 0.2]).tobytes() + + # Override both dimensions + fast_client.receive(np.array([0.0, 0.0]).tobytes()) + assert (step(0.05) == np.array([0.0, 0.0])).all() + assert (step(0.06) == np.array([0.0, 0.0])).all() + # Whole value still sent over fast client + assert fast_client.ws.binary == np.array([0.2, 0.2]).tobytes() + + # Reset, no longer override + slider.reset() + assert (step(0.07) == np.array([0.1, 0.1])).all() + assert (step(0.08) == np.array([0.2, 0.2])).all() + assert fast_client.ws.binary == np.array([0.2, 0.2]).tobytes() + + slider.remove_nengo_objects(net) + assert n.output is slider.base_output diff --git a/nengo_gui/components/tests/test_spa.py b/nengo_gui/components/tests/test_spa.py new file mode 100644 index 00000000..3197d6fe --- /dev/null +++ b/nengo_gui/components/tests/test_spa.py @@ -0,0 +1,121 @@ +import json + +import nengo.spa +import numpy as np + +from nengo_gui.components.spa import SpaPointer, SpaSimilarity, SpaWidget + + +class TestSpaWidget(object): + + def test_show_pairs(self, client): + with nengo.spa.SPA() as model: + model.state = nengo.spa.State(16) + + widget = SpaWidget(client, model.state, "widget") + widget.vocab.parse("A+B") + assert not widget.show_pairs + assert widget.keys == ["A", "B"] + widget.show_pairs = True + assert widget.show_pairs + assert widget.keys == ["A", "B", "A*B"] + assert client.ws.text == ( + '["widget.set_keys", {"keys": ["A", "B", "A*B"]}]') + client.dispatch("widget.show_pairs", val=False) + assert client.ws.text == '["widget.set_keys", {"keys": ["A", "B"]}]' + assert not widget.show_pairs + assert widget.keys == ["A", "B"] + + +class TestSpaPointer(object): + + def test_create(self, client): + with nengo.spa.SPA() as model: + model.state = nengo.spa.State(16) + + pointer = SpaPointer(client, model.state, "pointer") + assert pointer.override is None + pointer.create() + assert json.loads(client.ws.text) == [ + "netgraph.create_spa_pointer", {"label": None, "uid": "pointer"} + ] + + def test_add_remove(self, client): + with nengo.spa.SPA() as model: + model.state = nengo.spa.State(16) + + pointer = SpaPointer(client, model.state, "pointer") + assert pointer.override is None + pointer.vocab.add("A", np.ones(16)) + pointer.add_nengo_objects(model) + + # No override, input v: return 0, max similarity + out = pointer.node.output(0.001, np.ones(16)) + assert np.all(out == 0) + assert client.ws.text == '["pointer.matches", {"data": "9.99A"}]' + + # Override, input 0: return v*3, no similarity + pointer.override = "A" + out = pointer.node.output(0.002, np.zeros(16)) + assert np.allclose(out, 3 * np.ones(16)) + assert client.ws.text == '["pointer.matches", {"data": ""}]' + + # Override, input v: return 0, max similarity + pointer.override = "A" + out = pointer.node.output(0.003, np.ones(16)) + assert np.allclose(out, 0) + assert client.ws.text == '["pointer.matches", {"data": "9.99A"}]' + + pointer.remove_nengo_objects(model) + assert pointer.node is None + assert pointer.conn1 is None + assert pointer.conn2 is None + + +class TestSpaSimilarity(object): + + def test_create(self, client): + with nengo.spa.SPA() as model: + model.state = nengo.spa.State(16) + + similarity = SpaSimilarity(client, model.state, "similarity") + similarity.vocab.parse("A+B+C") + similarity.create() + assert json.loads(client.ws.text) == [ + "netgraph.create_spa_similarity", { + "keys": ["A", "B", "C"], + "label": None, + "uid": "similarity", + } + ] + + def test_add_remove(self, client, fast_client): + with nengo.spa.SPA() as model: + model.state = nengo.spa.State(2) + + similarity = SpaSimilarity(client, model.state, "similarity") + similarity.attach(fast_client) + similarity.vocab.add("A", np.ones(2)) + similarity.add_nengo_objects(model) + + assert similarity.node.output(0.001, np.ones(2)) is None + assert client.ws.text is None + assert fast_client.ws.binary == np.array([0.001, 2.0]).tobytes() + + similarity.vocab.add("B", np.zeros(2)) + assert similarity.node.output(0.002, np.ones(2)) is None + assert client.ws.text == ( + '["similarity.reset_legend_and_data", {"keys": ["A", "B"]}]') + assert fast_client.ws.binary == np.array([0.002, 2.0, 0.0]).tobytes() + + similarity.show_pairs = True + assert similarity.node.output(0.003, np.ones(2)) is None + assert client.ws.text == ( + '["similarity.reset_legend_and_data", {"keys": ["A", "B", "A*B"]}]' + ) + assert (fast_client.ws.binary == + np.array([0.003, 2.0, 0.0, 0.0]).tobytes()) + + similarity.remove_nengo_objects(model) + assert similarity.node is None + assert similarity.conn is None diff --git a/nengo_gui/components/tests/test_spike_grid.py b/nengo_gui/components/tests/test_spike_grid.py new file mode 100644 index 00000000..79057822 --- /dev/null +++ b/nengo_gui/components/tests/test_spike_grid.py @@ -0,0 +1,42 @@ +import json + +import nengo +import numpy as np + +from nengo_gui.components import SpikeGrid + + +def test_create(client): + with nengo.Network(): + e = nengo.Ensemble(10, 1) + + grid = SpikeGrid(client, e, "grid") + grid.create() + assert json.loads(client.ws.text) == ["netgraph.create_spike_grid", { + "label": None, "pixels_x": 4, "pixels_y": 3, + }] + + +def test_add_remove(client, fast_client): + with nengo.Network() as net: + e = nengo.Ensemble(5, 1) + + grid = SpikeGrid(client, e, "grid", n_neurons=10) + grid.attach(fast_client) + grid.add_nengo_objects(net) + assert grid.node is not None and grid.conn is not None + assert fast_client.ws.binary is None + + grid.node.output(0.0, np.array([0, 1, 0, 1, 0])) + assert (fast_client.ws.binary == + np.array([0, 255, 0, 255, 0], dtype=np.uint8).tobytes()) + + grid.node.output(0.01, np.array([0, 0, 0.5, 0, 0.5])) + assert (fast_client.ws.binary == + np.array([0, 0, 127, 0, 127], dtype=np.uint8).tobytes()) + + grid.remove_nengo_objects(net) + assert len(net.nodes) == 0 + assert len(net.connections) == 0 + assert grid.conn is None + assert grid.node is None diff --git a/nengo_gui/components/tests/test_value.py b/nengo_gui/components/tests/test_value.py new file mode 100644 index 00000000..c59d1c12 --- /dev/null +++ b/nengo_gui/components/tests/test_value.py @@ -0,0 +1,40 @@ +import json + +import nengo +import numpy as np + +from nengo_gui.components import Value + + +def test_create(client): + with nengo.Network(): + e = nengo.Ensemble(10, 1) + + value = Value(client, e, "value") + + value.create() + assert json.loads(client.ws.text) == ["netgraph.create_value", { + "label": None, "n_lines": 1, "uid": "value", + }] + + +def test_add_remove(client, fast_client): + with nengo.Network() as net: + e = nengo.Ensemble(5, 1) + + value = Value(client, e, "value") + value.attach(fast_client) + value.add_nengo_objects(net) + assert value.node is not None and value.conn is not None + assert fast_client.ws.binary is None + + value.node.output(0.0, np.array([0.1])) + assert fast_client.ws.binary == np.array([0.0, 0.1]).tobytes() + value.node.output(0.01, np.array([-0.1])) + assert fast_client.ws.binary == np.array([0.01, -0.1]).tobytes() + + value.remove_nengo_objects(net) + assert len(net.nodes) == 0 + assert len(net.connections) == 0 + assert value.conn is None + assert value.node is None diff --git a/nengo_gui/components/tests/test_voltage.py b/nengo_gui/components/tests/test_voltage.py new file mode 100644 index 00000000..8a5817fc --- /dev/null +++ b/nengo_gui/components/tests/test_voltage.py @@ -0,0 +1,33 @@ +import json + +import nengo +import numpy as np + +from nengo_gui.components import Voltage + + +# TODO: test the horrible hack + +def test_create(client): + with nengo.Network(): + e = nengo.Ensemble(10, 1) + + voltage = Voltage(client, e, "voltage") + voltage.create() + # Note: n_neurons defaults to 5 + assert json.loads(client.ws.text) == ["netgraph.create_voltage", { + "n_neurons": 5, "label": None, "synapse": 0, "uid": "voltage", + }] + + +def test_add_remove(client, fast_client): + with nengo.Network() as net: + e = nengo.Ensemble(10, 1) + + voltage = Voltage(client, e, "voltage") + voltage.add_nengo_objects(net) + assert voltage.probe is not None + + voltage.remove_nengo_objects(net) + assert len(net.probes) == 0 + assert voltage.probe is None diff --git a/nengo_gui/components/tests/test_xyvalue.py b/nengo_gui/components/tests/test_xyvalue.py new file mode 100644 index 00000000..131bab1c --- /dev/null +++ b/nengo_gui/components/tests/test_xyvalue.py @@ -0,0 +1,40 @@ +import json + +import nengo +import numpy as np + +from nengo_gui.components import XYValue + + +def test_create(client): + with nengo.Network(): + e = nengo.Ensemble(10, 2) + + xyvalue = XYValue(client, e, "xyvalue") + + xyvalue.create() + assert json.loads(client.ws.text) == ["netgraph.create_xyvalue", { + "label": None, "n_lines": 2, "uid": "xyvalue", + }] + + +def test_add_remove(client, fast_client): + with nengo.Network() as net: + e = nengo.Ensemble(5, 2) + + xyvalue = XYValue(client, e, "xyvalue") + xyvalue.attach(fast_client) + xyvalue.add_nengo_objects(net) + assert xyvalue.node is not None and xyvalue.conn is not None + assert fast_client.ws.binary is None + + xyvalue.node.output(0.0, np.array([0.1, 0.2])) + assert fast_client.ws.binary == np.array([0.0, 0.1, 0.2]).tobytes() + xyvalue.node.output(0.01, np.array([-0.1, -0.2])) + assert fast_client.ws.binary == np.array([0.01, -0.1, -0.2]).tobytes() + + xyvalue.remove_nengo_objects(net) + assert len(net.nodes) == 0 + assert len(net.connections) == 0 + assert xyvalue.conn is None + assert xyvalue.node is None diff --git a/nengo_gui/components/value.py b/nengo_gui/components/value.py index 90c1311b..b7463689 100644 --- a/nengo_gui/components/value.py +++ b/nengo_gui/components/value.py @@ -1,111 +1,100 @@ -import struct - +import numpy as np import nengo -from nengo import spa +import nengo.spa -from nengo_gui.components.component import Component +from ..client import bind +from .base import Widget -class Value(Component): +class Value(Widget): """The server-side system for a Value plot.""" - # the parameters to be stored in the .cfg file - config_defaults = dict(max_value=1, min_value=-1, - show_legend=False, legend_labels=[], - synapse=0.01, - **Component.config_defaults) - - def __init__(self, obj): - super(Value, self).__init__() - # the object whose decoded value should be displayed - self.obj = obj - - # the pending data to be sent to the client - self.data = [] - - # grab the output of the object - self.output = obj - default_out = Value.default_output(self.obj) - if default_out is not None: - self.output = default_out + def __init__(self, client, obj, uid, + ylim=(-1, 1), legend_labels=None, synapse=0.01, legend=False, + pos=None, label_visible=True): + super(Value, self).__init__( + client, obj, uid, pos=pos, label_visible=label_visible) - # the number of data values to send - self.n_lines = int(self.output.size_out) - - # the binary data format to sent in. In this case, it is a list of - # floats, with the first float being the time stamp and the rest - # being the vector values, one per dimension. - self.struct = struct.Struct('<%df' % (1 + self.n_lines)) + self.ylim = ylim + self.legend_labels = [] if legend_labels is None else legend_labels + self.legend = legend + self.synapse = synapse # Nengo objects for data collection self.node = None self.conn = None - def attach(self, page, config, uid): - super(Value, self).attach(page, config, uid) - # use the label of the object being plotted as our label - self.label = page.get_label(self.obj) - - def add_nengo_objects(self, page): + @property + def n_lines(self): + return self.output.size_out + + @property + def output(self): + if isinstance(self.obj, nengo.Network) and hasattr(self.obj, "output"): + return self.obj.output + else: + return self.obj + + @property + def synapse(self): + return self._synapse + + @synapse.setter + @bind("{self.uid}.synapse") + def synapse(self, synapse): + self._synapse = synapse + + # TODO: when GUI sets synapse, should also rebuild sim (don't do it here) + # if msg.startswith('synapse:'): + # synapse = float(msg[8:]) + # self.page.config[self].synapse = synapse + # self.page.modified_config() + # self.page.sim = None + + def add_nengo_objects(self, model): # create a Node and a Connection so the Node will be given the # data we want to show while the model is running. - with page.model: - self.node = nengo.Node(self.gather_data, - size_in=self.n_lines) - synapse = self.page.config[self].synapse - self.conn = nengo.Connection(self.output, self.node, - synapse=synapse) - - def remove_nengo_objects(self, page): + + data = np.zeros(1 + self.n_lines, dtype=np.float64) + + def fast_send_to_client(t, x): + data[0] = t + data[1:] = x + self.fast_client.send(data) + + with model: + self.node = nengo.Node( + fast_send_to_client, size_in=self.n_lines, size_out=0) + self.conn = nengo.Connection( + self.output, self.node, synapse=self.synapse) + + def create(self): + self.client.send("netgraph.create_value", + uid=self.uid, + pos=self.pos, + label=self.label, + labelVisible=self.label_visible, + dimensions=self.n_lines, + synapse=0.005, # TODO + xlim=[-0.5, 0], # TODO + ylim=[-1, 1]) # TODO + + def dumps(self, names): + """Important to do correctly, as it's used in the config file.""" + return ("Value(client, {names[self.obj]}, {self.uid}, " + "ylim={self.ylim}, legend_labels={self.legend_labels}, " + "synapse={self.synapse}, legend={self.legend}, " + "pos={self.pos}, label={self.label}".format( + names=names, self=self)) + + def remove_nengo_objects(self, model): # undo the changes made by add_nengo_objects - page.model.connections.remove(self.conn) - page.model.nodes.remove(self.node) - - def gather_data(self, t, x): - """This is the Node function for the Node created in add_nengo_objects - It will be called by the running model, and will store the data - that should be sent to the client""" - self.data.append(self.struct.pack(t, *x)) - - def update_client(self, client): - length = len(self.data) - if length > 0: - # we do this slicing because self.gather_data is concurrently - # appending things to self.data. This means that self.data may - # increase in length during this call, so we do the slicing - # and deletion to maintain thread safety - item = bytes().join(self.data[:length]) - del self.data[:length] - client.write_binary(item) - - def javascript(self): - # generate the javascript that will create the client-side object - info = dict(uid=id(self), label=self.label, - n_lines=self.n_lines) - - json = self.javascript_config(info) - return 'new Nengo.Value(main, sim, %s);' % json - - def code_python_args(self, uids): - # generate the list of strings for the .cfg file to save this Component - # (this is the text that would be passed in to the constructor) - return [uids[self.obj]] - - def message(self, msg): - if msg.startswith('synapse:'): - synapse = float(msg[8:]) - self.page.config[self].synapse = synapse - self.page.modified_config() - self.page.sim = None - - @staticmethod - def default_output(obj): - """Find default output object for the input object if it exists""" - output = None - if isinstance(obj, spa.module.Module): - if 'default' in obj.outputs.keys(): - output = obj.outputs['default'][0] - elif isinstance(obj, nengo.network.Network): - if hasattr(obj, 'output'): - output = obj.output - return output + model.connections.remove(self.conn) + model.nodes.remove(self.node) + self.conn, self.node = None, None + + # TODO: make sure code_python_args never needed + # def code_python_args(self, uids): + # # generate the list of strings for the .cfg file to save this Component + # # (this is the text that would be passed in to the constructor) + # return [uids[self.obj]] diff --git a/nengo_gui/components/voltage.py b/nengo_gui/components/voltage.py index e9b646af..055a0640 100644 --- a/nengo_gui/components/voltage.py +++ b/nengo_gui/components/voltage.py @@ -1,62 +1,38 @@ from __future__ import division import nengo -import numpy as np -import struct -from nengo_gui.components.component import Component +from .base import Widget -class Voltage(Component): +class Voltage(Widget): """Represents neuron voltage over time.""" - config_defaults = dict( - max_value=5.0, min_value=0.0, **Component.config_defaults) - - def __init__(self, obj, n_neurons=5): - super(Voltage, self).__init__() - self.obj = obj.neurons - self.data = [] - self.max_neurons = int(self.obj.size_out) + def __init__(self, client, obj, uid, + ylim=(0, 5), n_neurons=5, pos=None, label_visible=True): + super(Voltage, self).__init__( + client, obj, uid, pos=pos, label_visible=label_visible) self.n_neurons = min(n_neurons, self.max_neurons) - self.struct = struct.Struct('<%df' % (1 + self.n_neurons)) - - def attach(self, page, config, uid): - super(Voltage, self).attach(page, config, uid) - self.label = page.get_label(self.obj.ensemble) - - def add_nengo_objects(self, page): - with page.model: - self.probe = nengo.Probe(self.obj[:self.n_neurons], 'voltage') - - def remove_nengo_objects(self, page): - page.model.probes.remove(self.probe) - - def format_data(self, t, x): - data = self.struct.pack(t, *x[:self.n_neurons]) - self.data.append(data) - - def update_client(self, client): - sim = self.page.sim - if sim is None: - return - - # TODO: this is hack to delete probe data in Nengo 2.0.1, since we - # can't limit the size of probes. Fix this up with Nengo 2.1. - data = sim.data.raw[self.probe][:] - del sim.data.raw[self.probe][:] # clear the data - trange = sim.trange()[-len(data):] - - for t, datum in zip(trange, data): - datum = (datum + np.arange(self.n_neurons)) - packet = self.struct.pack(t, *datum) - client.write_binary(packet) - - def javascript(self): - info = dict(uid=id(self), label=self.label, - n_lines=self.n_neurons, synapse=0) - json = self.javascript_config(info) - return 'new Nengo.Value(main, sim, %s);' % json - - def code_python_args(self, uids): - return [uids[self.obj.ensemble]] + + @property + def max_neurons(self): + return self.obj.neurons.size_out + + def add_nengo_objects(self, model): + with model: + # Note: this probe is read in simcontrol.control, which is a + # huge terrible hack. + self.probe = nengo.Probe( + self.obj.neurons[:self.n_neurons], 'voltage') + + def create(self): + self.client.send("netgraph.create_voltage", + uid=self.uid, + label=self.label, + labelVisible=self.label_visible, + n_neurons=self.n_neurons, + synapse=0) + + def remove_nengo_objects(self, model): + model.probes.remove(self.probe) + self.probe = None diff --git a/nengo_gui/components/xyvalue.py b/nengo_gui/components/xyvalue.py index 6119fcde..4da2827a 100644 --- a/nengo_gui/components/xyvalue.py +++ b/nengo_gui/components/xyvalue.py @@ -1,53 +1,49 @@ -import struct -import collections - import nengo +import numpy as np -from nengo_gui.components.component import Component - +from .base import Widget -class XYValue(Component): - """Represents (at least) two dimensional values as co-ordinates on an - x-y plot.""" - config_defaults = dict(max_value=1, min_value=-1, index_x=0, index_y=1, - **Component.config_defaults) +# TODO: does this need a separate widget from value? +class XYValue(Widget): + """Represents two values as co-ordinates on an x-y plot.""" - def __init__(self, obj): - super(XYValue, self).__init__() - self.obj = obj - self.data = collections.deque() - self.n_lines = int(obj.size_out) - self.struct = struct.Struct('<%df' % (1 + self.n_lines)) + def __init__(self, client, obj, uid, + xlim=(-1, 1), ylim=(-1, 1), index_x=0, index_y=1, + pos=None, label_visible=True): + super(XYValue, self).__init__(client, obj, uid, + pos=pos, label_visible=label_visible) self.node = None self.conn = None - def attach(self, page, config, uid): - super(XYValue, self).attach(page, config, uid) - self.label = page.get_label(self.obj) + @property + def n_lines(self): + return int(self.obj.size_out) - def add_nengo_objects(self, page): - with page.model: - self.node = nengo.Node(self.gather_data, - size_in=self.obj.size_out) - self.conn = nengo.Connection(self.obj, self.node, synapse=0.01) + def add_nengo_objects(self, model): - def remove_nengo_objects(self, page): - page.model.connections.remove(self.conn) - page.model.nodes.remove(self.node) + data = np.zeros(self.n_lines + 1) - def gather_data(self, t, x): - self.data.append(self.struct.pack(t, *x)) + def fast_send_to_client(t, x): + data[0] = t + data[1:] = x + self.fast_client.send(data) - def update_client(self, client): - while len(self.data) > 0: - data = self.data.popleft() - client.write_binary(data) - - def javascript(self): - info = dict(uid=id(self), n_lines=self.n_lines, label=self.label) - json = self.javascript_config(info) - return 'new Nengo.XYValue(main, sim, %s);' % json + with model: + self.node = nengo.Node(fast_send_to_client, + size_in=self.obj.size_out, + size_out=0) + # TODO: make synapse modifiable? + self.conn = nengo.Connection(self.obj, self.node, synapse=0.01) - def code_python_args(self, uids): - return [uids[self.obj]] + def create(self): + self.client.send("netgraph.create_xyvalue", + uid=self.uid, + n_lines=self.n_lines, + label=self.label, + labelVisible=self.label_visible) + + def remove_nengo_objects(self, model): + model.connections.remove(self.conn) + model.nodes.remove(self.node) + self.conn, self.node = None, None diff --git a/nengo_gui/config.py b/nengo_gui/config.py index 91ad8118..112659c4 100644 --- a/nengo_gui/config.py +++ b/nengo_gui/config.py @@ -1,83 +1,113 @@ -import inspect +import json +import re +import warnings import nengo +from nengo.utils.compat import iteritems -import nengo_gui.components - -def make_param(name, default): - try: - # the most recent way of making Parameter objects - p = nengo.params.Parameter(name=name, default=default) - except TypeError: - # this was for older releases of nengo (v2.0.3 and earlier) - p = nengo.params.Parameter(default=default) - return p - -class Config(nengo.Config): - def __init__(self): - super(Config, self).__init__() - for cls in [nengo.Ensemble, nengo.Node]: - self.configures(cls) - self[cls].set_param('pos', make_param(name='pos', default=None)) - self[cls].set_param('size', make_param(name='size', default=None)) - self.configures(nengo.Network) - self[nengo.Network].set_param('pos', - make_param(name='pos', default=None)) - self[nengo.Network].set_param('size', - make_param(name='size', default=None)) - self[nengo.Network].set_param('expanded', - make_param(name='expanded', - default=False)) - self[nengo.Network].set_param('has_layout', - make_param(name='has_layout', - default=False)) - - for clsname, cls in inspect.getmembers(nengo_gui.components): - if inspect.isclass(cls): - if issubclass(cls, nengo_gui.components.component.Component): - if cls != nengo_gui.components.component.Component: - self.configures(cls) - for k, v in cls.config_defaults.items(): - p = make_param(name=k, default=v) - self[cls].set_param(k, p) - - def dumps(self, uids): - lines = [] - for obj, uid in sorted(uids.items(), key=lambda x: x[1]): - - if isinstance(obj, (nengo.Ensemble, nengo.Node, nengo.Network)): - if self[obj].pos is not None: - lines.append('_viz_config[%s].pos=%s' % (uid, - self[obj].pos)) - if self[obj].size is not None: - lines.append('_viz_config[%s].size=%s' % (uid, - self[obj].size)) - if isinstance(obj, nengo.Network): - lines.append('_viz_config[%s].expanded=%s' - % (uid, self[obj].expanded)) - lines.append('_viz_config[%s].has_layout=%s' - % (uid, self[obj].has_layout)) - - elif isinstance(obj, nengo_gui.components.component.Component): - lines.append('%s = %s' % (uid, obj.code_python(uids))) - for k in obj.config_defaults.keys(): - v = getattr(self[obj], k) - val = repr(v) - - try: - recovered_v = eval(val, {}) - except: - raise ValueError("Cannot save %s to config. Only " - "values that can be successfully " - "evaluated are allowed." % (val)) - - if recovered_v != v: - raise ValueError("Cannot save %s to config, recovery " - "failed. Only " - "values that can be recovered after " - "being entered into the config file " - "can be saved." % (val)) - - lines.append('_viz_config[%s].%s = %s' % (uid, k, val)) - - return '\n'.join(lines) +from nengo_gui.client import NengoGUIConfig +from nengo_gui.components import Position + + +def upgrade(old_text, locals): + """Upgrades a .cfg file from the old format (GUI 0.2).""" + + new_config = {} + + # All lines in the old files were assignments, so we won't + # use a full-fledged Python parser for this + cfgline = re.compile(r"(?P\S+?)\[(?P\S+)\]\.(?P\S+)") + compline = re.compile( + r"nengo_gui\.components\.(?P\S+)\((?P\S+)\)") + + for line in old_text.splitlines(): + left, right = line.split("=", 1) + left, right = left.strip(), right.strip() + + # AceEditor, NetGraph and SimControl are no longer in cfg file + removed = ["AceEditor", "NetGraph", "SimControl"] + if any(r in right for r in removed) or "sim_control" in left: + continue + + # Setting a value on a config item + elif "]." in left: + # Use a regex to parse left + match = cfgline.match(left) + if match is None: + raise ValueError("Could not parse %r" % left) + + # TODO: some sanity checking on cfg group + obj = match.group("obj") + kw = match.group("kw") + if obj not in new_config: + # Could be a Nengo object with position / size. + # Try to figure out what type of object and add it. + try: + o = eval(obj, locals) + if isinstance(o, nengo.Network): + cls = "Network" + else: + cls = type(o).__name__ + new_config[obj] = {"cls": cls} + except Exception as e: + warnings.warn("Skipping %r: %s" % (obj, e)) + if obj in new_config: + new_config[obj][kw] = eval(right) + + # Making a new component + else: + assert "nengo_gui.components" in right + obj = left + match = compline.match(right) + if match is None: + raise ValueError("Could not parse %r" % right) + args = match.group("arg").split(",") + new_config[obj] = { + "cls": match.group("cls").replace("Template", ""), + "obj": args[0], + } + for arg in args[1:]: + key, val = arg.split("=") + new_config[obj][key] = eval(val) + + # Some components have been renamed + if new_config[obj]["cls"] == "Pointer": + new_config[obj]["cls"] = "SpaPointer" + + # Additional changes + for obj, kwargs in iteritems(new_config): + # pos and size now one object + if "size" in kwargs: + pos = kwargs.pop("pos") + size = kwargs.pop("size") + kwargs["pos"] = Position( + left=pos[0], top=pos[1], width=size[0], height=size[1]) + + # x, y, width, height now one object + if "width" in kwargs: + kwargs["pos"] = Position(left=kwargs.pop("x"), + top=kwargs.pop("y"), + width=kwargs.pop("width"), + height=kwargs.pop("height")) + + # show_legend now legend + if "show_legend" in kwargs: + kwargs["legend"] = kwargs.pop("show_legend") + + # max_value, min_value now ylim + if "max_value" in kwargs: + maxval = kwargs.pop("max_value") + minval = kwargs.pop("min_value") + kwargs["ylim"] = (minval, maxval) + + # Make sure label_visible is in there + kwargs.setdefault("label_visible", True) + + # The scale of things is quite different now. + # Scale to a reasonable size. + kwargs["pos"].height *= 600 + kwargs["pos"].width *= 600 + kwargs["pos"].left *= 600 + kwargs["pos"].top *= 600 + + return json.dumps(new_config, cls=NengoGUIConfig, indent=2, sort_keys=True) diff --git a/nengo_gui/conftest.py b/nengo_gui/conftest.py index a1e1f4d5..27e0130a 100644 --- a/nengo_gui/conftest.py +++ b/nengo_gui/conftest.py @@ -1,27 +1,51 @@ -from __future__ import print_function -import time +import os +from pkg_resources import resource_filename + import pytest -from selenium import webdriver + +from nengo_gui.client import ClientConnection, FastClientConnection + + +class MockWebSocket(object): + def __init__(self): + self.binary = None + self.binary_history = [] + self.text = None + self.text_history = [] + + def write_binary(self, binary): + self.binary_history.append(binary) + self.binary = binary + + def write_text(self, text): + self.text_history.append(text) + self.text = text + + +@pytest.fixture +def client(): + return ClientConnection(MockWebSocket()) -@pytest.fixture(scope="module") -def driver(request): +@pytest.fixture +def fast_client(): + return FastClientConnection(MockWebSocket()) - driver = webdriver.Firefox() - driver.get('localhost:8080/') - driver.maximize_window() - time.sleep(4) - def fin(): - driver.close() +@pytest.fixture +def example(): + return lambda ex: resource_filename("nengo_gui", "examples/%s" % (ex,)) - request.addfinalizer(fin) - try: - assert driver.title != "Problem loading page" - except AssertionError: - print("ERROR: The 'nengo' server is not currently running. " - "Start the server before running tests.") - raise +def pytest_generate_tests(metafunc): + examples = [] + example_dir = resource_filename("nengo_gui", "examples") + for subdir, _, files in os.walk(example_dir): + if (os.path.sep + '.') in subdir: + continue + examples.extend([ + os.path.join(subdir, f) for f in files if f.endswith(".py") + ]) - return driver + if "all_examples" in metafunc.funcargnames: + metafunc.parametrize("all_examples", examples) diff --git a/nengo_gui/editor.py b/nengo_gui/editor.py new file mode 100644 index 00000000..69115e33 --- /dev/null +++ b/nengo_gui/editor.py @@ -0,0 +1,100 @@ +from nengo_gui.client import bind, ExposedToClient + + +class Stream(ExposedToClient): + """A stream that only sends when output has changed.""" + + def __init__(self, name, client): + super(Stream, self).__init__(client) + self.name = name + self._output = None + self._line = None + + def clear(self): + if self._output is not None or self._line is not None: + self._output = None + self._line = None + self.send() + + def send(self): + raise NotImplementedError() + + @bind("editor.{self.name}") + def set(self, output, line=None): + if output != self._output or line != self._line: + self._output = output + self._line = line + self.send() + + +class TerminalStream(Stream): + def send(self): + if self._line is not None: + print("L%d: %s" % (self._line, self._output)) + elif self._output is not None: + print(self._output) + + +class NetworkStream(Stream): + def send(self): + self.client.send("editor.%s" % (self.name,), + output=self._output, line=self._line) + + +class Editor(ExposedToClient): + def __init__(self, client): + super(Editor, self).__init__(client) + self.stdout = None + self.stderr = None + + @property + def code(self): + return "" + + @bind("editor.ready") + def ready(self): + pass + + def send_filename(self): + pass + + def update(self, code): + pass + + +class NoEditor(Editor): + def __init__(self, client): + super(NoEditor, self).__init__(client) + self.stdout = TerminalStream("stdout", self.client) + self.stderr = TerminalStream("stderr", self.client) + + +class AceEditor(Editor): + + def __init__(self, client): + super(AceEditor, self).__init__(client) + self.stdout = NetworkStream("stdout", self.client) + self.stderr = NetworkStream("stderr", self.client) + + self._code = None + + @property + @bind("editor.get_code") + def code(self): + return self._code + + @code.setter + @bind("editor.set_code") + def code(self, code): + if code != self._code: + self._code = code + + @bind("editor.sync") + def sync(self): + self.client.send("editor.code", code=self.code) + self.stdout.send() + self.stderr.send() + + def send_filename(self, filename, error=None): + self.client.send("editor.filename", + filename=filename, error=error) diff --git a/nengo_gui/examples/basics/2d_representation.py b/nengo_gui/examples/basics/2d_representation.py index 65c3002d..5784f9eb 100644 --- a/nengo_gui/examples/basics/2d_representation.py +++ b/nengo_gui/examples/basics/2d_representation.py @@ -6,7 +6,6 @@ # integrate-and-fire neurons. import nengo -import numpy as np model = nengo.Network() with model: diff --git a/nengo_gui/examples/basics/combining.py b/nengo_gui/examples/basics/combining.py index b74e9f15..c9993059 100644 --- a/nengo_gui/examples/basics/combining.py +++ b/nengo_gui/examples/basics/combining.py @@ -26,4 +26,4 @@ # The square brackets define which dimension the input will project to nengo.Connection(a, output[1]) - nengo.Connection(b, output[0]) \ No newline at end of file + nengo.Connection(b, output[0]) diff --git a/nengo_gui/examples/basics/ensemble_array.py b/nengo_gui/examples/basics/ensemble_array.py index 393e8d87..12cfad04 100644 --- a/nengo_gui/examples/basics/ensemble_array.py +++ b/nengo_gui/examples/basics/ensemble_array.py @@ -24,4 +24,4 @@ # Connect the model elements, just feedforward nengo.Connection(sin, a.input) nengo.Connection(a.output, b) - nengo.Connection(b, c.input) \ No newline at end of file + nengo.Connection(b, c.input) diff --git a/nengo_gui/examples/basics/ensemble_array.py.cfg b/nengo_gui/examples/basics/ensemble_array.py.cfg new file mode 100644 index 00000000..5b43ba77 --- /dev/null +++ b/nengo_gui/examples/basics/ensemble_array.py.cfg @@ -0,0 +1,67 @@ +_viz_0 = nengo_gui.components.Value(a) +_viz_config[_viz_0].synapse = 0.01 +_viz_config[_viz_0].max_value = 1 +_viz_config[_viz_0].min_value = -1 +_viz_config[_viz_0].legend_labels = [u'label_0', u'label_1'] +_viz_config[_viz_0].height = 0.1644745445278466 +_viz_config[_viz_0].label_visible = True +_viz_config[_viz_0].width = 0.11829408640999423 +_viz_config[_viz_0].show_legend = False +_viz_config[_viz_0].y = 1.1055931806598083 +_viz_config[_viz_0].x = 0.3254703706064484 +_viz_1 = nengo_gui.components.Value(c) +_viz_config[_viz_1].synapse = 0.01 +_viz_config[_viz_1].max_value = 1 +_viz_config[_viz_1].min_value = -1 +_viz_config[_viz_1].legend_labels = [u'label_0', u'label_1'] +_viz_config[_viz_1].height = 0.1644745445278466 +_viz_config[_viz_1].label_visible = True +_viz_config[_viz_1].width = 0.11829408640999423 +_viz_config[_viz_1].show_legend = False +_viz_config[_viz_1].y = 1.103948435214527 +_viz_config[_viz_1].x = 0.8654706609367949 +_viz_2 = nengo_gui.components.Slider(sin) +_viz_config[_viz_2].label_visible = True +_viz_config[_viz_2].width = 0.05914704320499711 +_viz_config[_viz_2].x = 0.053756182601454636 +_viz_config[_viz_2].y = 1.1013843061532724 +_viz_config[_viz_2].max_value = 1 +_viz_config[_viz_2].min_value = -1 +_viz_config[_viz_2].height = 0.1644745445278466 +_viz_ace_editor = nengo_gui.components.AceEditor() +_viz_net_graph = nengo_gui.components.NetGraph() +_viz_sim_control = nengo_gui.components.SimControl() +_viz_config[_viz_sim_control].kept_time = 4.0 +_viz_config[_viz_sim_control].shown_time = 0.5 +_viz_config[a].pos=(0.32795698924731187, 0.5) +_viz_config[a].size=(0.10752688172043011, 0.4) +_viz_config[a].expanded=True +_viz_config[a].has_layout=True +_viz_config[a.ea_ensembles[0]].pos=(0.49999999999999994, 0.2) +_viz_config[a.ea_ensembles[0]].size=(0.09803921568627451, 0.1) +_viz_config[a.ea_ensembles[1]].pos=(0.49999999999999994, 0.7999999999999999) +_viz_config[a.ea_ensembles[1]].size=(0.09803921568627451, 0.1) +_viz_config[a.input].pos=(0.12745098039215685, 0.5) +_viz_config[a.input].size=(0.07843137254901959, 0.08) +_viz_config[a.output].pos=(0.872549019607843, 0.5) +_viz_config[a.output].size=(0.07843137254901959, 0.08) +_viz_config[b].pos=(0.5967741935483871, 0.5) +_viz_config[b].size=(0.053763440860215055, 0.1) +_viz_config[c].pos=(0.8655913978494624, 0.5) +_viz_config[c].size=(0.10752688172043011, 0.4) +_viz_config[c].expanded=True +_viz_config[c].has_layout=True +_viz_config[c.ea_ensembles[0]].pos=(0.49999999999999994, 0.2) +_viz_config[c.ea_ensembles[0]].size=(0.09803921568627451, 0.1) +_viz_config[c.ea_ensembles[1]].pos=(0.49999999999999994, 0.7999999999999999) +_viz_config[c.ea_ensembles[1]].size=(0.09803921568627451, 0.1) +_viz_config[c.input].pos=(0.12745098039215685, 0.5) +_viz_config[c.input].size=(0.07843137254901959, 0.08) +_viz_config[c.output].pos=(0.872549019607843, 0.5) +_viz_config[c.output].size=(0.07843137254901959, 0.08) +_viz_config[model].pos=(0.12861743759071817, -0.044436633405907096) +_viz_config[model].size=(0.7127747007423403, 0.7127747007423403) +_viz_config[model].expanded=True +_viz_config[model].has_layout=True +_viz_config[sin].pos=(0.06989247311827958, 0.5) +_viz_config[sin].size=(0.043010752688172046, 0.08) \ No newline at end of file diff --git a/nengo_gui/examples/basics/html.py b/nengo_gui/examples/basics/html.py index 29054fc3..db8271aa 100644 --- a/nengo_gui/examples/basics/html.py +++ b/nengo_gui/examples/basics/html.py @@ -16,7 +16,7 @@ # # The second example shows that the custom HTML can also be based on the values # represented by neurons. Here, we make a Node that reads in a value that is -# treated as an amount. If the value is above 0.5, the HTML says "large", +# treated as an amount. If the value is above 0.5, the HTML says "large", # if the value is below -0.5, it says "small", and otherwise it says "medium". # # Finally, the third example uses SVG and trigonometry to create a simple @@ -30,7 +30,7 @@ model = nengo.Network() with model: - + # Example 1: a timer def timer_function(t): if t < 1.0: @@ -43,9 +43,8 @@ def timer_function(t): timer_function._nengo_html_ = '

Go!

' return 1 timer = nengo.Node(timer_function) - - - # Example 2: displaying a value + + # Example 2: displaying a value def amount_function(t, x): if x < -0.5: amount_function._nengo_html_ = '

small

' @@ -58,9 +57,8 @@ def amount_function(t, x): display_amount = nengo.Node(amount_function, size_in=1) nengo.Connection(stim_amount, amount) nengo.Connection(amount, display_amount) - - - # Example 3: a two-joint arm + + # Example 3: a two-joint arm def arm_function(t, angles): len0 = 50 len1 = 30 @@ -71,14 +69,13 @@ def arm_function(t, angles): x3 = x2 + len1 * np.sin(angles[0] + angles[1]) y3 = y2 - len1 * np.cos(angles[0] + angles[1]) arm_function._nengo_html_ = ''' - - - - + + + + '''.format(**locals()) stim_angles = nengo.Node([0.3, 0.3]) angles = nengo.Ensemble(n_neurons=200, dimensions=2) arm = nengo.Node(arm_function, size_in=2) nengo.Connection(stim_angles, angles) nengo.Connection(angles, arm) - \ No newline at end of file diff --git a/nengo_gui/examples/basics/inhibitory_gating.py b/nengo_gui/examples/basics/inhibitory_gating.py index c89730a5..a966bceb 100644 --- a/nengo_gui/examples/basics/inhibitory_gating.py +++ b/nengo_gui/examples/basics/inhibitory_gating.py @@ -1,5 +1,5 @@ # Nengo Example: Inhibitory Gating of Ensembles - +# # ## Step 1: Create the network # # Our model consists of two ensembles (called A and B) that receive inputs from diff --git a/nengo_gui/examples/basics/multiplication.py b/nengo_gui/examples/basics/multiplication.py index bd944538..fb339a43 100644 --- a/nengo_gui/examples/basics/multiplication.py +++ b/nengo_gui/examples/basics/multiplication.py @@ -1,5 +1,5 @@ # Nengo Example: Multiplication - +# # This example will show you how to multiply two values. The model # architecture can be thought of as a combination of the combining demo and # the squaring demo. Essentially, we project both inputs independently into a @@ -7,6 +7,7 @@ # product of the first and second vector elements). import nengo + model = nengo.Network() with model: # Create 4 ensembles of leaky integrate-and-fire neurons @@ -18,12 +19,12 @@ prod = nengo.Ensemble(n_neurons=100, dimensions=1, radius=1) - # These next two lines make all of the encoders in the Combined population # point at the corners of the cube. This improves the quality of the # computation. - combined.encoders = nengo.dists.Choice([[1,1],[-1,1],[1,-1],[-1,-1]]) - + combined.encoders = nengo.dists.Choice( + [[1, 1], [-1, 1], [1, -1], [-1, -1]]) + stim_a = nengo.Node([0]) stim_b = nengo.Node([0]) @@ -40,4 +41,4 @@ def product(x): return x[0] * x[1] # Connect the combined ensemble to the output ensemble - nengo.Connection(combined, prod, function=product) \ No newline at end of file + nengo.Connection(combined, prod, function=product) diff --git a/nengo_gui/examples/basics/single_neuron.py b/nengo_gui/examples/basics/single_neuron.py index f9350f9b..78937912 100644 --- a/nengo_gui/examples/basics/single_neuron.py +++ b/nengo_gui/examples/basics/single_neuron.py @@ -6,17 +6,18 @@ # there is only one neuron. import nengo + model = nengo.Network() with model: neuron = nengo.Ensemble(n_neurons=1, dimensions=1) # Set intercept to 0.5 - neuron.intercepts=nengo.dists.Uniform(-.5, -.5) + neuron.intercepts = nengo.dists.Uniform(-.5, -.5) # Set the maximum firing rate of the neuron to 100hz - neuron.max_rates=nengo.dists.Uniform(100, 100) + neuron.max_rates = nengo.dists.Uniform(100, 100) # Sets the neurons firing rate to increase for positive input - neuron.encoders=[[1]] + neuron.encoders = [[1]] stim = nengo.Node(0) # Connect the input signal to the neuron - nengo.Connection(stim, neuron) \ No newline at end of file + nengo.Connection(stim, neuron) diff --git a/nengo_gui/examples/basics/squaring.py b/nengo_gui/examples/basics/squaring.py index cab0769a..a84025d5 100644 --- a/nengo_gui/examples/basics/squaring.py +++ b/nengo_gui/examples/basics/squaring.py @@ -1,10 +1,11 @@ # Nengo Example: Squaring the Input - +# # This demo shows you how to construct a network that squares the value # encoded in a first population in the output of a second population. # Create the model object import nengo + model = nengo.Network() with model: # Create two ensembles of 100 leaky-integrate-and-fire neurons @@ -22,4 +23,4 @@ def square(x): return x[0] * x[0] # Connection ensemble a to ensemble b - nengo.Connection(a, b, function=square) \ No newline at end of file + nengo.Connection(a, b, function=square) diff --git a/nengo_gui/examples/basics/two_neurons.py b/nengo_gui/examples/basics/two_neurons.py index c5761c63..70ca8da3 100644 --- a/nengo_gui/examples/basics/two_neurons.py +++ b/nengo_gui/examples/basics/two_neurons.py @@ -11,19 +11,17 @@ # reasonable representation of a scalar value. import nengo -import numpy as np model = nengo.Network() with model: neurons = nengo.Ensemble(n_neurons=2, dimensions=1) # Set the intercepts at .5 - neurons.intercepts=nengo.dists.Uniform(-.5, -.5) + neurons.intercepts = nengo.dists.Uniform(-.5, -.5) # Set the max firing rate at 100hz - neurons.max_rates=nengo.dists.Uniform(100, 100) + neurons.max_rates = nengo.dists.Uniform(100, 100) # One 'on' and one 'off' neuron - neurons.encoders=[[1],[-1]] - - # make a + neurons.encoders = [[1], [-1]] + stim = nengo.Node([0]) nengo.Connection(stim, neurons, synapse=0.01) diff --git a/nengo_gui/examples/default.json b/nengo_gui/examples/default.json new file mode 100644 index 00000000..a640d697 --- /dev/null +++ b/nengo_gui/examples/default.json @@ -0,0 +1,69 @@ +{ + "_viz_0": { + "cls": "Value", + "label_visible": true, + "legend": false, + "legend_labels": [ + "label_0" + ], + "obj": "a", + "pos": { + "height": 70.33997655334115, + "left": 462.75848551210237, + "top": 537.2548882310801, + "width": 50.16722408026756 + }, + "synapse": 0.01, + "ylim": [ + -1, + 1 + ] + }, + "_viz_1": { + "cls": "Slider", + "label_visible": true, + "obj": "stim", + "pos": { + "height": 90.67509346098738, + "left": 113.95125886942171, + "top": 300.3088766314966, + "width": 32.33522354607953 + }, + "ylim": [ + -1, + 1 + ] + }, + "a": { + "cls": "Ensemble", + "label_visible": true, + "pos": { + "height": 150.0, + "left": 464.9297725782068, + "top": 297.2797471961704, + "width": 90.9090909090909 + } + }, + "model": { + "cls": "Network", + "expanded": true, + "has_layout": true, + "label_visible": true, + "pos": { + "height": 427.66482044540237, + "left": 231.03431258051188, + "top": 100.53641766478532, + "width": 427.66482044540237 + } + }, + "stim": { + "cls": "Node", + "label_visible": true, + "pos": { + "height": 120.0, + "left": 118.18181818181817, + "top": 300.0, + "width": 72.72727272727272 + } + } +} \ No newline at end of file diff --git a/nengo_gui/examples/function_space/03-attend.py.cfg b/nengo_gui/examples/function_space/03-attend.py.cfg new file mode 100644 index 00000000..eea5f3fa --- /dev/null +++ b/nengo_gui/examples/function_space/03-attend.py.cfg @@ -0,0 +1,38 @@ +_viz_0 = nengo_gui.components.Value(choice) +_viz_config[_viz_0].synapse = 0.01 +_viz_config[_viz_0].x = 0.7258064516129032 +_viz_config[_viz_0].height = 0.11723329425556858 +_viz_config[_viz_0].width = 0.09900990099009901 +_viz_config[_viz_0].max_value = 1 +_viz_config[_viz_0].y = 0.44411764705882356 +_viz_config[_viz_0].show_legend = False +_viz_config[_viz_0].label_visible = True +_viz_config[_viz_0].legend_labels = [] +_viz_config[_viz_0].min_value = -1 +_viz_ace_editor = nengo_gui.components.AceEditor() +_viz_net_graph = nengo_gui.components.NetGraph() +_viz_sim_control = nengo_gui.components.SimControl() +_viz_config[_viz_sim_control].shown_time = 0.5 +_viz_config[_viz_sim_control].kept_time = 4.0 +_viz_config[choice].pos=(0.6720430107526882, 0.4147058823529412) +_viz_config[choice].size=(0.053763440860215055, 0.029411764705882353) +_viz_config[ens].pos=(0.4032258064516129, 0.5) +_viz_config[ens].size=(0.053763440860215055, 0.029411764705882353) +_viz_config[model].pos=(0, 0) +_viz_config[model].size=(1.0, 1.0) +_viz_config[model].expanded=True +_viz_config[model].has_layout=True +_viz_config[model.networks[0]].pos=(0.13440860215053763, 0.14705882352941177) +_viz_config[model.networks[0]].size=(0.10752688172043011, 0.11764705882352941) +_viz_config[model.networks[0]].expanded=False +_viz_config[model.networks[0]].has_layout=False +_viz_config[model.networks[1]].pos=(0.13440860215053763, 0.5) +_viz_config[model.networks[1]].size=(0.10752688172043011, 0.11764705882352941) +_viz_config[model.networks[1]].expanded=False +_viz_config[model.networks[1]].has_layout=False +_viz_config[plot].pos=(0.9301075268817204, 0.5) +_viz_config[plot].size=(0.043010752688172046, 0.023529411764705882) +_viz_config[stim].pos=(0.13440860215053763, 0.8529411764705882) +_viz_config[stim].size=(0.10752688172043011, 0.11764705882352941) +_viz_config[stim].expanded=False +_viz_config[stim].has_layout=False \ No newline at end of file diff --git a/nengo_gui/examples/recurrent/controlled_integrator.py b/nengo_gui/examples/recurrent/controlled_integrator.py index 4dc03a71..19596ecc 100644 --- a/nengo_gui/examples/recurrent/controlled_integrator.py +++ b/nengo_gui/examples/recurrent/controlled_integrator.py @@ -6,7 +6,7 @@ # 2. Control - the control signal to the integrator # # A controlled integrator accumulates input, but its state can be directly -# manipulated by the control signal. +# manipulated by the control signal. # # We can use standard network-creation commands to begin creating our # controlled integrator. We create a Network, and then we create a population @@ -42,4 +42,4 @@ # a transform nengo.Connection(a, a[0], function=lambda x: x[0] * x[1], - synapse=tau) \ No newline at end of file + synapse=tau) diff --git a/nengo_gui/examples/recurrent/controlled_oscillator.py b/nengo_gui/examples/recurrent/controlled_oscillator.py index ff767809..e64f73a8 100644 --- a/nengo_gui/examples/recurrent/controlled_oscillator.py +++ b/nengo_gui/examples/recurrent/controlled_oscillator.py @@ -5,7 +5,7 @@ import nengo -tau = 0.1 # Post-synaptic time constant for feedback +tau = 0.1 # Post-synaptic time constant for feedback w_max = 10 # Maximum frequency is w_max/(2*pi) model = nengo.Network() diff --git a/nengo_gui/examples/recurrent/integrator.py b/nengo_gui/examples/recurrent/integrator.py index 80160db9..8f990535 100644 --- a/nengo_gui/examples/recurrent/integrator.py +++ b/nengo_gui/examples/recurrent/integrator.py @@ -3,13 +3,13 @@ # This demo implements a one-dimensional neural integrator. # # This is the first example of a recurrent network in the demos. It shows how -# neurons can be used to implement stable dynamics. Such dynamics are -# important for memory, noise cleanup, statistical inference, and many +# neurons can be used to implement stable dynamics. Such dynamics are +# important for memory, noise cleanup, statistical inference, and many # other dynamic transformations. # -# Note that since the integrator constantly sums its input, it will -# saturate quickly if you leave the input non-zero. This makes it clear -# that neurons have a finite range of representation. Such saturation +# Note that since the integrator constantly sums its input, it will +# saturate quickly if you leave the input non-zero. This makes it clear +# that neurons have a finite range of representation. Such saturation # effects can be exploited to perform useful computations # (e.g. soft normalization). @@ -23,12 +23,12 @@ # Create a piecewise step function for input stim = nengo.Node([0]) - - # Connect the population to itself using a long time constant (tau) + + # Connect the population to itself using a long time constant (tau) # for stability tau = 0.1 nengo.Connection(a, a, synapse=tau) # Connect the input using the same time constant as on the recurrent # connection to make it more ideal - nengo.Connection(stim, a, transform=tau, synapse=tau) \ No newline at end of file + nengo.Connection(stim, a, transform=tau, synapse=tau) diff --git a/nengo_gui/examples/recurrent/lorenz_attractor.py b/nengo_gui/examples/recurrent/lorenz_attractor.py index d460e9ee..d4cd69dc 100644 --- a/nengo_gui/examples/recurrent/lorenz_attractor.py +++ b/nengo_gui/examples/recurrent/lorenz_attractor.py @@ -8,24 +8,25 @@ # dx2/dt = x0 * x1 - beta * x2 # # Since x2 is centered around approximately rho, and since NEF ensembles -# are usually optimized to represent values within a certain radius of the +# are usually optimized to represent values within a certain radius of the # origin, we substitute x2' = x2 - rho, giving these equations: # # dx0/dt = sigma * (x1 - x0) # dx1/dt = - x0 * x2' - x1 # dx2/dt = x0 * x1 - beta * (x2 + rho) - rho # -# For more information, see +# For more information, see # http://compneuro.uwaterloo.ca/publications/eliasmith2005b.html -# "Chris Eliasmith. A unified approach to building and controlling +# "Chris Eliasmith. A unified approach to building and controlling # spiking attractor networks. Neural computation, 7(6):1276-1314, 2005." +import nengo + tau = 0.1 sigma = 10 beta = 8.0/3 rho = 28 -import nengo def feedback(x): dx0 = -sigma * x[0] + sigma * x[1] @@ -36,6 +37,7 @@ def feedback(x): dx1 * tau + x[1], dx2 * tau + x[2]] + model = nengo.Network(seed=1) with model: state = nengo.Ensemble(2000, 3, radius=30) diff --git a/nengo_gui/examples/recurrent/lorenz_attractor.py.cfg b/nengo_gui/examples/recurrent/lorenz_attractor.py.cfg new file mode 100644 index 00000000..559283fa --- /dev/null +++ b/nengo_gui/examples/recurrent/lorenz_attractor.py.cfg @@ -0,0 +1,11 @@ +_viz_ace_editor = nengo_gui.components.AceEditor() +_viz_net_graph = nengo_gui.components.NetGraph() +_viz_sim_control = nengo_gui.components.SimControl() +_viz_config[_viz_sim_control].kept_time = 4.0 +_viz_config[_viz_sim_control].shown_time = 0.5 +_viz_config[model].pos=(0.19144080494505522, 0.1932027135545571) +_viz_config[model].size=(0.7757364659745805, 0.7757364659745805) +_viz_config[model].expanded=True +_viz_config[model].has_layout=True +_viz_config[state].pos=(0.5, 0.5) +_viz_config[state].size=(0.33333333333333337, 0.25) \ No newline at end of file diff --git a/nengo_gui/examples/recurrent/oscillator.py b/nengo_gui/examples/recurrent/oscillator.py index c34c3726..620bfecb 100644 --- a/nengo_gui/examples/recurrent/oscillator.py +++ b/nengo_gui/examples/recurrent/oscillator.py @@ -1,10 +1,10 @@ # Nengo Example: A Simple Harmonic Oscillator # -# This demo implements a simple harmonic oscillator in a 2D neural -# population. The oscillator is more visually interesting on its own than -# the integrator, but the principle at work is the same. Here, instead of -# having the recurrent input just integrate (i.e. feeding the full input -# value back to the population), we have two dimensions which interact. +# This demo implements a simple harmonic oscillator in a 2D neural +# population. The oscillator is more visually interesting on its own than +# the integrator, but the principle at work is the same. Here, instead of +# having the recurrent input just integrate (i.e. feeding the full input +# value back to the population), we have two dimensions which interact. import nengo @@ -20,4 +20,6 @@ nengo.Connection(stim, neurons[0]) # Create the feedback connection - nengo.Connection(neurons, neurons, transform=[[1, 1], [-1, 1]], synapse=0.1) \ No newline at end of file + nengo.Connection(neurons, neurons, + transform=[[1, 1], [-1, 1]], + synapse=0.1) diff --git a/nengo_gui/examples/tutorial/00-intro.py b/nengo_gui/examples/tutorial/00-intro.py index e8407579..4bdaae6f 100644 --- a/nengo_gui/examples/tutorial/00-intro.py +++ b/nengo_gui/examples/tutorial/00-intro.py @@ -1,11 +1,11 @@ # Tutorial 0: Welcome to Nengo - +# # Nengo is a tool for creating large-scale biologically # realistic neural models. It was developed by the Centre for Theoretical # Neuroscience at the University of Waterloo and the affiliated spin-off # company Applied Brain Research. It has been used to create Spaun, the # first simulated brain that is capable of performing tasks. - +# # This sequence of tutorials takes you through the various features of Nengo. # # You can go to the next tutorial by clicking on the "Open file" icon in the diff --git a/nengo_gui/examples/tutorial/00-intro.py.cfg b/nengo_gui/examples/tutorial/00-intro.py.cfg index 23ef82d4..058ec54a 100644 --- a/nengo_gui/examples/tutorial/00-intro.py.cfg +++ b/nengo_gui/examples/tutorial/00-intro.py.cfg @@ -31,7 +31,5 @@ _viz_config[model].pos=(0.348830468735809, 0.49201138799364114) _viz_config[model].size=(0.6017670646427314, 0.6017670646427314) _viz_config[model].expanded=True _viz_config[model].has_layout=True -_viz_config[slowdown].pos=(-0.20466619557631088, 0.9573856355604904) -_viz_config[slowdown].size=(0.046883981863295635, 0.03943425722388151) _viz_config[stim].pos=(0.19696969696969696, 0.5) -_viz_config[stim].size=(0.1212121212121212, 0.2) \ No newline at end of file +_viz_config[stim].size=(0.1212121212121212, 0.2) diff --git a/nengo_gui/examples/tutorial/01-one-neuron.py b/nengo_gui/examples/tutorial/01-one-neuron.py index b62f8570..98c856a7 100644 --- a/nengo_gui/examples/tutorial/01-one-neuron.py +++ b/nengo_gui/examples/tutorial/01-one-neuron.py @@ -1,12 +1,12 @@ # Tutorial 1: A Single Neuron - +# # Here we show one single neuron. The slider on the left adjusts the input # to that neuron. The top graph shows the voltage in the neuron. When that # voltage is high enough, the neuron "spikes", producing an output (middle # graph). That output releases neurotransmitter which is gradually # reabsorbed. Given only that neurotransmitter output, it is difficult # to reconstruct the original input (bottom graph). - +# # User Interface Tip: You can adjust the amount of time shown on the graphs # by dragging the left side of the gray bar inside the timeline at the bottom # of the screen. Try reducing it down to a smaller size so you can see diff --git a/nengo_gui/examples/tutorial/01-one-neuron.py.cfg b/nengo_gui/examples/tutorial/01-one-neuron.py.cfg index 4d80c68b..7774bd7c 100644 --- a/nengo_gui/examples/tutorial/01-one-neuron.py.cfg +++ b/nengo_gui/examples/tutorial/01-one-neuron.py.cfg @@ -39,7 +39,5 @@ _viz_config[model].pos=(0.29008605754397315, 0.3199774777364316) _viz_config[model].size=(0.6017670646427314, 0.6017670646427314) _viz_config[model].expanded=True _viz_config[model].has_layout=True -_viz_config[slowdown].pos=(0.030859781980228436, 0.9023282461839404) -_viz_config[slowdown].size=(0.03391052259673938, 0.04352059432201608) _viz_config[stim].pos=(0.14178396618598132, 0.3030303030303031) -_viz_config[stim].size=(0.06602639042840559, 0.12121212121212124) \ No newline at end of file +_viz_config[stim].size=(0.06602639042840559, 0.12121212121212124) diff --git a/nengo_gui/examples/tutorial/02-two-neurons.py b/nengo_gui/examples/tutorial/02-two-neurons.py index 5705023d..99eaffdf 100644 --- a/nengo_gui/examples/tutorial/02-two-neurons.py +++ b/nengo_gui/examples/tutorial/02-two-neurons.py @@ -1,11 +1,11 @@ # Tutorial 2: Two neurons - +# # Now we show what happens with two neurons. Notice that the neurons # respond very differently to the two inputs. One neuron responds more # strongly to positive values, while the other responds more to negative # values. This is typical of real neurons (sometimes called "on" and "off" # neurons). - +# # Given these two neurons, more information is available about what the input # is. The bottom graph shows the "decoded" value found by taking the output # of both neurons and combining them. Notice that these two neurons do a diff --git a/nengo_gui/examples/tutorial/02-two-neurons.py.cfg b/nengo_gui/examples/tutorial/02-two-neurons.py.cfg index ca41dba9..1cf2d7d6 100644 --- a/nengo_gui/examples/tutorial/02-two-neurons.py.cfg +++ b/nengo_gui/examples/tutorial/02-two-neurons.py.cfg @@ -39,7 +39,5 @@ _viz_config[model].pos=(0.17221039638865257, 0.2877231522537397) _viz_config[model].size=(0.7127747007423387, 0.7127747007423387) _viz_config[model].expanded=True _viz_config[model].has_layout=True -_viz_config[slowdown].pos=(0.030859781980228436, 0.9023282461839404) -_viz_config[slowdown].size=(0.03391052259673938, 0.04352059432201608) _viz_config[stim].pos=(0.14178396618598132, 0.3030303030303031) -_viz_config[stim].size=(0.06602639042840559, 0.12121212121212124) \ No newline at end of file +_viz_config[stim].size=(0.06602639042840559, 0.12121212121212124) diff --git a/nengo_gui/examples/tutorial/03-many-neurons.py b/nengo_gui/examples/tutorial/03-many-neurons.py index fdbb4a76..46f57e05 100644 --- a/nengo_gui/examples/tutorial/03-many-neurons.py +++ b/nengo_gui/examples/tutorial/03-many-neurons.py @@ -1,16 +1,16 @@ # Tutorial 3: Many neurons - +# # Brains have many neurons. What happens if we use 20 neurons? 50? 100? - +# # The code below shows 20 neurons in a group (we call groups of neurons # an "Ensemble" of neurons). The representation is much more accurate. # To see this, press Play. Now, as you move the slider, the bottom graph -# should follow your movements well. - +# should follow your movements well. +# # Try changing the number of neurons by editing the code below where it says # "n_neurons=20". Try 50. Try 100. The representation should get more and # more accurate. - +# # Nengo Tip: Don't use too big a number! Depending on your computer, using # more than 1000 neurons in a single Ensemble can take a long time to compute. # This is because Nengo needs to figure out how to weight the outputs of all diff --git a/nengo_gui/examples/tutorial/03-many-neurons.py.cfg b/nengo_gui/examples/tutorial/03-many-neurons.py.cfg index bb1ea175..9c3d847d 100644 --- a/nengo_gui/examples/tutorial/03-many-neurons.py.cfg +++ b/nengo_gui/examples/tutorial/03-many-neurons.py.cfg @@ -31,7 +31,5 @@ _viz_config[model].pos=(0.17221039638865257, 0.2877231522537397) _viz_config[model].size=(0.7127747007423387, 0.7127747007423387) _viz_config[model].expanded=True _viz_config[model].has_layout=True -_viz_config[slowdown].pos=(0.030859781980228436, 0.9023282461839404) -_viz_config[slowdown].size=(0.03391052259673938, 0.04352059432201608) _viz_config[stim].pos=(0.14178396618598132, 0.3030303030303031) -_viz_config[stim].size=(0.06602639042840559, 0.12121212121212124) \ No newline at end of file +_viz_config[stim].size=(0.06602639042840559, 0.12121212121212124) diff --git a/nengo_gui/examples/tutorial/04-connecting-neurons.py b/nengo_gui/examples/tutorial/04-connecting-neurons.py index c3b98bc8..839f5825 100644 --- a/nengo_gui/examples/tutorial/04-connecting-neurons.py +++ b/nengo_gui/examples/tutorial/04-connecting-neurons.py @@ -1,17 +1,17 @@ # Tutorial 4: Connecting neurons - +# # So far, we have just fed input to a group of neurons and read their output. # In order to do more interesting things, we need to be able to connect one # group of neurons to another. In this example, we have two groups of neurons # connected together. - +# # If you just make a Connection between two groups of neurons, Nengo will # find the connection weights between each of the neurons such that whatever # value is being represented by the first group of neurons will be passed on # to the second group of neurons. Notice that now if you move the slider, # this affects the value in the first group of neurons, and these in turn # affect the value in the second group of neurons. - +# # Whenever you make a connection, you can specify a "synapse" value. This # indicates the properties of the neurotransmitters and synapses (the actual # connections between neurons). The most important paramter is the @@ -29,7 +29,7 @@ stim = nengo.Node(0) a = nengo.Ensemble(n_neurons=50, dimensions=1) nengo.Connection(stim, a) - + b = nengo.Ensemble(n_neurons=50, dimensions=1) - + nengo.Connection(a, b, synapse=0.01) diff --git a/nengo_gui/examples/tutorial/04-connecting-neurons.py.cfg b/nengo_gui/examples/tutorial/04-connecting-neurons.py.cfg index 7024dc03..4ff9ab40 100644 --- a/nengo_gui/examples/tutorial/04-connecting-neurons.py.cfg +++ b/nengo_gui/examples/tutorial/04-connecting-neurons.py.cfg @@ -47,7 +47,5 @@ _viz_config[model].pos=(-0.002863544871064495, 0.17841592967742728) _viz_config[model].size=(1.0, 1.0) _viz_config[model].expanded=True _viz_config[model].has_layout=True -_viz_config[slowdown].pos=(0.1156716417910446, 0.10255746531373007) -_viz_config[slowdown].size=(0.02485648679678531, 0.023831711189342263) _viz_config[stim].pos=(0.12264150943396226, 0.3030303030303031) -_viz_config[stim].size=(0.07547169811320754, 0.12121212121212124) \ No newline at end of file +_viz_config[stim].size=(0.07547169811320754, 0.12121212121212124) diff --git a/nengo_gui/examples/tutorial/05-computing.py b/nengo_gui/examples/tutorial/05-computing.py index a097285f..b73496b4 100644 --- a/nengo_gui/examples/tutorial/05-computing.py +++ b/nengo_gui/examples/tutorial/05-computing.py @@ -1,15 +1,15 @@ # Tutorial 5: Computing a function - +# # Whenever we make a Connection between groups of neurons, we don't have to # just pass the information from one group of neurons to the next. Instead, # we can also modify that information. We do this by specifying a function, # and Nengo will connect the individual neurons to best approximate that # function. - +# # In the example here, we are computing the square of the value. So for an # input of -1 it should output 1, for 0 it should output 0, and for 1 it should # output 1. - +# # You can change the function by adjusting the computations done in the # part of the code labelled "compute_this". This can be any arbitrary Python # function. For example, try computing the negative of x ("return -x"). Try @@ -23,10 +23,10 @@ stim = nengo.Node(0) a = nengo.Ensemble(n_neurons=50, dimensions=1) nengo.Connection(stim, a) - + b = nengo.Ensemble(n_neurons=50, dimensions=1) - + def compute_this(x): return x * x - + nengo.Connection(a, b, synapse=0.01, function=compute_this) diff --git a/nengo_gui/examples/tutorial/05-computing.py.cfg b/nengo_gui/examples/tutorial/05-computing.py.cfg index 9901f2d7..f9e9222e 100644 --- a/nengo_gui/examples/tutorial/05-computing.py.cfg +++ b/nengo_gui/examples/tutorial/05-computing.py.cfg @@ -47,7 +47,5 @@ _viz_config[model].pos=(-0.019402985074626844, 0.18564920273348517) _viz_config[model].size=(0.9999999999999997, 0.9999999999999997) _viz_config[model].expanded=True _viz_config[model].has_layout=True -_viz_config[slowdown].pos=(0.1156716417910446, 0.10255746531373007) -_viz_config[slowdown].size=(0.02485648679678531, 0.023831711189342263) _viz_config[stim].pos=(0.12264150943396226, 0.3030303030303031) -_viz_config[stim].size=(0.07547169811320754, 0.12121212121212124) \ No newline at end of file +_viz_config[stim].size=(0.07547169811320754, 0.12121212121212124) diff --git a/nengo_gui/examples/tutorial/06-adding.py b/nengo_gui/examples/tutorial/06-adding.py index e176bee6..86720f65 100644 --- a/nengo_gui/examples/tutorial/06-adding.py +++ b/nengo_gui/examples/tutorial/06-adding.py @@ -1,9 +1,9 @@ # Tutorial 6: Adding - +# # If we make two Connections into the same Ensemble, the Ensemble will get # input from both sources, and it will end up representing the sum of all of # its inputs. Here, we use this to add two values together. - +# # Notice that the value being represented by Ensemble c is, most of the time, # the sum of the two inputs (a and b). However, if that value gets too large # (or too small), it does not work very well. This is because every Ensemble @@ -17,7 +17,6 @@ # select "Set range" and set it to "-2,2". Now it should add correctly over # the full range of inputs. - import nengo model = nengo.Network() @@ -25,12 +24,11 @@ stim_a = nengo.Node(0) a = nengo.Ensemble(n_neurons=50, dimensions=1) nengo.Connection(stim_a, a) - + stim_b = nengo.Node(0) b = nengo.Ensemble(n_neurons=50, dimensions=1) nengo.Connection(stim_b, b) - + c = nengo.Ensemble(n_neurons=50, dimensions=1, radius=1) nengo.Connection(a, c) nengo.Connection(b, c) - diff --git a/nengo_gui/examples/tutorial/07-multiple-dimensions.py b/nengo_gui/examples/tutorial/07-multiple-dimensions.py index a25c3e91..f32981a8 100644 --- a/nengo_gui/examples/tutorial/07-multiple-dimensions.py +++ b/nengo_gui/examples/tutorial/07-multiple-dimensions.py @@ -1,16 +1,17 @@ # Tutorial 7: Multiple Dimensions - +# # Ensembles of neurons do not just have to represent one thing. Instead, # a group of neurons can represent multiple values at the same time. We call # the number of values represented at once the "dimensions" of the Ensemble. # So, if a group of neurons is supposed to represent the spatial location of -# something in three dimensions (x, y, z), then we say that it has dimensions=3. - +# something in three dimensions (x, y, z), then we say that it has +# dimensions=3. +# # In this case, three different values are being decoded from each of the # groups of neurons. Nengo decodes these values by finding different ways # of weighting together the actual spiking output (top graphs) in order to -# produce the bottom graphs. - +# produce the bottom graphs. +# # Nengo Tip: If you change the number of dimensions, the graphs and sliders # may not update to reflect those changes. You can force them to do so by # removing them and re-creating them. To remove them, right-click on the graph @@ -27,6 +28,6 @@ stim = nengo.Node([0, 0, 0]) a = nengo.Ensemble(n_neurons=200, dimensions=3) nengo.Connection(stim, a) - + b = nengo.Ensemble(n_neurons=200, dimensions=3) - nengo.Connection(a, b) \ No newline at end of file + nengo.Connection(a, b) diff --git a/nengo_gui/examples/tutorial/08-combining.py b/nengo_gui/examples/tutorial/08-combining.py index c230674c..f57a984e 100644 --- a/nengo_gui/examples/tutorial/08-combining.py +++ b/nengo_gui/examples/tutorial/08-combining.py @@ -1,5 +1,5 @@ # Tutorial 8: Combining Information - +# # Now that we can represent multiple things using the same group of neurons, # we can also combine information together. Here we introduce a new syntax # when making Connections: just can specify which of the multiple dimensions @@ -8,12 +8,12 @@ # counting at zero, so the first dimension is x[0], then x[1], then x[2], and # so on). You can also do this when connecting out of a group, so you could do # something like "Connection(c[1], d[3])". - +# # In the example below, we combine the information from two different Ensembles # (a and b) into a third one (c) that represents both values. Notice that this # is different than the Addition tutorial in that we are keeping both values # separate, rather than adding them together. - +# # Advanced Nengo Tip: Connections support full Python slice notation, so you # can also do things like [-1], or [1:5] or [::-1] and so on. @@ -24,12 +24,11 @@ stim_a = nengo.Node(0) a = nengo.Ensemble(n_neurons=50, dimensions=1) nengo.Connection(stim_a, a) - + stim_b = nengo.Node(0) b = nengo.Ensemble(n_neurons=50, dimensions=1) nengo.Connection(stim_b, b) - + c = nengo.Ensemble(n_neurons=200, dimensions=2) nengo.Connection(a, c[0]) nengo.Connection(b, c[1]) - diff --git a/nengo_gui/examples/tutorial/09-multiplication.json b/nengo_gui/examples/tutorial/09-multiplication.json new file mode 100644 index 00000000..cf49b90e --- /dev/null +++ b/nengo_gui/examples/tutorial/09-multiplication.json @@ -0,0 +1,157 @@ +{ + "_viz_0": { + "cls": "Slider", + "label": "_viz_0", + "obj": "stim_b", + "pos": { + "height": 0.173906, + "width": 0.0569738, + "left": -0.181332, + "top": 0.989538 + }, + "ylim": [ + -1, + 1 + ] + }, + "_viz_1": { + "cls": "Slider", + "label": "_viz_1", + "obj": "stim_a", + "pos": { + "height": 0.173906, + "width": 0.0569738, + "left": -0.183611, + "top": 0.0835091 + }, + "ylim": [ + -1, + 1 + ] + }, + "_viz_2": { + "cls": "Value", + "label": "_viz_2", + "obj": "c", + "pos": { + "height": 0.173906, + "width": 0.113948, + "left": 0.83125, + "top": 0.321147 + }, + "ylim": [ + -1, + 1 + ] + }, + "_viz_3": { + "cls": "Value", + "label": "_viz_3", + "obj": "a", + "pos": { + "height": 0.173906, + "width": 0.113948, + "left": 0.541287, + "top": 0.000904821 + }, + "ylim": [ + -1, + 1 + ] + }, + "_viz_4": { + "cls": "Value", + "label": "_viz_4", + "obj": "b", + "pos": { + "height": 0.173906, + "width": 0.113948, + "left": 0.503087, + "top": 1.08427 + }, + "ylim": [ + -1, + 1 + ] + }, + "_viz_5": { + "cls": "Value", + "label": "_viz_5", + "obj": "d", + "pos": { + "height": 0.173906, + "width": 0.113948, + "left": 1.1082, + "top": 0.918409 + }, + "ylim": [ + -1, + 1 + ] + }, + "a": { + "cls": "Ensemble", + "pos": { + "height": 0.15268966928818553, + "width": 0.1045819652658805, + "left": 0.3071136331212679, + "top": 0.09339768677100643 + } + }, + "b": { + "cls": "Ensemble", + "pos": { + "height": 0.15268966928818553, + "width": 0.1045819652658805, + "left": 0.2717899036590758, + "top": 0.9858844553210839 + } + }, + "c": { + "cls": "Ensemble", + "pos": { + "height": 0.15268966928818553, + "width": 0.1045819652658805, + "left": 0.6787230132831809, + "top": 0.5518145070940784 + } + }, + "d": { + "cls": "Ensemble", + "pos": { + "height": 0.15268966928818553, + "width": 0.1045819652658805, + "left": 1.0970508743467027, + "top": 0.5518145070940784 + } + }, + "model": { + "cls": "Network", + "expanded": true, + "has_layout": true, + "pos": { + "height": 0.6549231553528394, + "width": 0.6549231553528394, + "left": 0.2729728706363317, + "top": 0.2116338393468492 + } + }, + "stim_a": { + "cls": "Node", + "pos": { + "height": 0.12215173543054843, + "width": 0.08366557221270439, + "left": -0.02192932625257712, + "top": 0.10209299823388712 + } + }, + "stim_b": { + "cls": "Node", + "pos": { + "height": 0.12215173543054843, + "width": 0.08366557221270439, + "left": -0.03332407769199389, + "top": 0.9737110192730507 + } + } +} diff --git a/nengo_gui/examples/tutorial/09-multiplication.py b/nengo_gui/examples/tutorial/09-multiplication.py index 77b886b6..e71eed8c 100644 --- a/nengo_gui/examples/tutorial/09-multiplication.py +++ b/nengo_gui/examples/tutorial/09-multiplication.py @@ -1,11 +1,11 @@ # Tutorial 9: Multiplication - +# # Now that we can combine information, we can use this to compute more # complex functions. For example, to multiply two numbers together, we # first make a combined Ensemble as in the previous tutorial, and then we # compute the pruduct of the two numbers by multiplying them together when # we make a Connection out of that combined Ensemble. - +# # Notice that we had to increase the radius of the combined Ensemble to 1.5. # That is because it is representing two values, each of which can be in the # range -1 to 1. In order to make sure the Ensemble is good at representing @@ -19,17 +19,18 @@ stim_a = nengo.Node(0) a = nengo.Ensemble(n_neurons=50, dimensions=1) nengo.Connection(stim_a, a) - + stim_b = nengo.Node(0) b = nengo.Ensemble(n_neurons=50, dimensions=1) nengo.Connection(stim_b, b) - + c = nengo.Ensemble(n_neurons=200, dimensions=2, radius=1.5) nengo.Connection(a, c[0]) nengo.Connection(b, c[1]) - + d = nengo.Ensemble(n_neurons=50, dimensions=1) - + def multiply(x): return x[0] * x[1] + nengo.Connection(c, d, function=multiply) diff --git a/nengo_gui/examples/tutorial/09-multiplication.py.cfg b/nengo_gui/examples/tutorial/09-multiplication.py.cfg deleted file mode 100644 index 9b17f4e8..00000000 --- a/nengo_gui/examples/tutorial/09-multiplication.py.cfg +++ /dev/null @@ -1,69 +0,0 @@ -_viz_0 = nengo_gui.components.SliderTemplate(stim_b) -_viz_config[_viz_0].label_visible = 1 -_viz_config[_viz_0].width = 0.0569738 -_viz_config[_viz_0].x = -0.181332 -_viz_config[_viz_0].y = 0.989538 -_viz_config[_viz_0].max_value = 1 -_viz_config[_viz_0].min_value = -1 -_viz_config[_viz_0].height = 0.173906 -_viz_1 = nengo_gui.components.SliderTemplate(stim_a) -_viz_config[_viz_1].label_visible = 1 -_viz_config[_viz_1].width = 0.0569738 -_viz_config[_viz_1].x = -0.183611 -_viz_config[_viz_1].y = 0.0835091 -_viz_config[_viz_1].max_value = 1 -_viz_config[_viz_1].min_value = -1 -_viz_config[_viz_1].height = 0.173906 -_viz_2 = nengo_gui.components.ValueTemplate(c) -_viz_config[_viz_2].label_visible = 1 -_viz_config[_viz_2].width = 0.113948 -_viz_config[_viz_2].x = 0.83125 -_viz_config[_viz_2].y = 0.321147 -_viz_config[_viz_2].max_value = 1 -_viz_config[_viz_2].min_value = -1 -_viz_config[_viz_2].height = 0.173906 -_viz_3 = nengo_gui.components.ValueTemplate(a) -_viz_config[_viz_3].label_visible = 1 -_viz_config[_viz_3].width = 0.113948 -_viz_config[_viz_3].x = 0.541287 -_viz_config[_viz_3].y = 0.000904821 -_viz_config[_viz_3].max_value = 1 -_viz_config[_viz_3].min_value = -1 -_viz_config[_viz_3].height = 0.173906 -_viz_4 = nengo_gui.components.ValueTemplate(b) -_viz_config[_viz_4].label_visible = 1 -_viz_config[_viz_4].width = 0.113948 -_viz_config[_viz_4].x = 0.503087 -_viz_config[_viz_4].y = 1.08427 -_viz_config[_viz_4].max_value = 1 -_viz_config[_viz_4].min_value = -1 -_viz_config[_viz_4].height = 0.173906 -_viz_5 = nengo_gui.components.ValueTemplate(d) -_viz_config[_viz_5].label_visible = 1 -_viz_config[_viz_5].width = 0.113948 -_viz_config[_viz_5].x = 1.1082 -_viz_config[_viz_5].y = 0.918409 -_viz_config[_viz_5].max_value = 1 -_viz_config[_viz_5].min_value = -1 -_viz_config[_viz_5].height = 0.173906 -_viz_ace_editor = nengo_gui.components.AceEditorTemplate() -_viz_net_graph = nengo_gui.components.NetGraphTemplate() -_viz_sim_control = nengo_gui.components.SimControlTemplate() -_viz_config[_viz_sim_control].kept_time = 4 -_viz_config[_viz_sim_control].shown_time = 0.5 -_viz_config[a].pos=(0.3071136331212679, 0.09339768677100643) -_viz_config[a].size=(0.1045819652658805, 0.15268966928818553) -_viz_config[b].pos=(0.2717899036590758, 0.9858844553210839) -_viz_config[b].size=(0.1045819652658805, 0.15268966928818553) -_viz_config[c].pos=(0.6787230132831809, 0.5518145070940784) -_viz_config[c].size=(0.1045819652658805, 0.15268966928818553) -_viz_config[d].pos=(1.0970508743467027, 0.5518145070940784) -_viz_config[d].size=(0.1045819652658805, 0.15268966928818553) -_viz_config[model].pos=(0.2729728706363317, 0.2116338393468492) -_viz_config[model].size=(0.6549231553528394, 0.6549231553528394) -_viz_config[model].expanded=True -_viz_config[model].has_layout=True -_viz_config[stim_a].pos=(-0.02192932625257712, 0.10209299823388712) -_viz_config[stim_a].size=(0.08366557221270439, 0.12215173543054843) -_viz_config[stim_b].pos=(-0.03332407769199389, 0.9737110192730507) -_viz_config[stim_b].size=(0.08366557221270439, 0.12215173543054843) \ No newline at end of file diff --git a/nengo_gui/examples/tutorial/10-transforms.py b/nengo_gui/examples/tutorial/10-transforms.py index 2ce4c194..dfc827ee 100644 --- a/nengo_gui/examples/tutorial/10-transforms.py +++ b/nengo_gui/examples/tutorial/10-transforms.py @@ -1,10 +1,10 @@ # Tutorial 10: Transforms and scaling - +# # When making Connections, we may want to scale the values being represented. # For example, we might want to just multiply a value by a fixed number like # 0.1 or 10 or something like that. Since this value doesn't change, we do # not need a full multiplication system like in the previous tutorial. - +# # We could implement this sort of connection by writing a function. However, # this is such a common thing to do that Nengo has a shortcut for this by # having a "transform" parameter. The examples below show the equivalence of @@ -12,7 +12,7 @@ # and multiply it by -0.5. For a multidimensional example, d1 and d2 both take # values from c and compute 2*c[0]-c[1]-c[2], but do so in different ways. In # both cases the resulting models are identical. - +# # You can use this trick to quickly define any linear transformation on the # values represented by the Ensembles. @@ -23,29 +23,29 @@ stim_a = nengo.Node(0) a = nengo.Ensemble(n_neurons=50, dimensions=1) nengo.Connection(stim_a, a) - + b1 = nengo.Ensemble(n_neurons=50, dimensions=1) b2 = nengo.Ensemble(n_neurons=50, dimensions=1) - + # the long way to do it def simple_function(a): return -0.5 * a + nengo.Connection(a, b1, function=simple_function) # the shortcut way to do it nengo.Connection(a, b2, transform=-0.5) - - + stim_c = nengo.Node([0, 0, 0]) c = nengo.Ensemble(n_neurons=200, dimensions=3) nengo.Connection(stim_c, c) - - + d1 = nengo.Ensemble(n_neurons=50, dimensions=1) d2 = nengo.Ensemble(n_neurons=50, dimensions=1) - + # the long way to do it def harder_function(c): return 2 * c[0] - c[1] - c[2] + nengo.Connection(c, d1, function=harder_function) # the shortcut way to do it nengo.Connection(c, d2, transform=[[2, -1, -1]]) diff --git a/nengo_gui/examples/tutorial/11-memory.py b/nengo_gui/examples/tutorial/11-memory.py index 08a70bc4..9916eb25 100644 --- a/nengo_gui/examples/tutorial/11-memory.py +++ b/nengo_gui/examples/tutorial/11-memory.py @@ -1,24 +1,24 @@ # Tutorial 11: Memory - +# # Nengo models can also store information over time. To do this, we simply # make a Connection from an Ensemble back to itself. That is, we form a # Connection that will feed a value into an Ensemble that is the same value # that the Ensemble is currently representing. This means we can store data # over time. - +# # To use such a system, connect into it with another Ensemble. If that input # is zero, then the stored value should stay the same as it currently is. # If the input is positive, the stored value should increase. If the input is # negative, it should decrease. - +# # Notice that the input Connection has "transform=0.1". That is to control # how strongly the input affects the stored value. If you make the transform # larger, it will change more quickly. - +# # Also notice that the recurrent Connection from b back to itself has # synapse=0.1. This longer time constant makes the memory more stable, and # is also commonly found in the real brain for recurrent connections. - +# # Mathematical Note: In the case where the input transform is exactly equal to # the recurrent synapse (as it is here), it turns out that the resulting system # should compute the mathematical integral of the input. @@ -30,8 +30,7 @@ stim_a = nengo.Node(0) a = nengo.Ensemble(n_neurons=50, dimensions=1) nengo.Connection(stim_a, a) - + b = nengo.Ensemble(n_neurons=50, dimensions=1) nengo.Connection(b, b, synapse=0.1) nengo.Connection(a, b, transform=0.1) - diff --git a/nengo_gui/examples/tutorial/12-differential-eqns.py b/nengo_gui/examples/tutorial/12-differential-eqns.py index 4bfdf378..a671d7b5 100644 --- a/nengo_gui/examples/tutorial/12-differential-eqns.py +++ b/nengo_gui/examples/tutorial/12-differential-eqns.py @@ -1,20 +1,20 @@ # Tutorial 12: Differential Equations - +# # Recurrent connections can be used to implement not just memory, as in the # previous tutorial, but also any differential equation. - +# # For example, the differential equation for a low-pass filter (a system where -# the output y is a smoothed version of the input x) is +# the output y is a smoothed version of the input x) is # dy/dt = x/tau - y/tau # where tau is the time constant for the smoothing (larger means smoother) - +# # To implement this in nengo, we need an input Connection that computes the # x/tau part, and a recurrent Connection that computes the -y/tau part. - +# # Because these functions are implemented by neurons, the time constant of # the synapse itself turns out to be very important here. We have to take into # account what synapse is being used when making this connection. - +# # While the proof is outside the scope of this tutorial, the resulting rule # is that both your Connections need to get scaled by the synapse value, and # your recurrent Connection must also add the stored value back in. That is, @@ -24,12 +24,12 @@ # (-y/tau) * synapse + y # If we tell Nengo to implement those two functions, we will get the desired # differential equation. - +# # Nengo Tip: In this particular case, those two functions are both linear # functions, and so we could implement them much more easily using the # "transform=" approach (see tutorial 10). This is left as an exercise to the # user. - +# # Try running the model and seeing that y is a slowed-down, smoother version # of x. What happens if you change the input up and down quickly? What # happens with tau=0.1? What about tau=0.01? @@ -41,17 +41,16 @@ stim_x = nengo.Node(0) x = nengo.Ensemble(n_neurons=50, dimensions=1) nengo.Connection(stim_x, x) - + y = nengo.Ensemble(n_neurons=50, dimensions=1) tau = 0.5 synapse = 0.1 - + def input_function(x): return x / tau * synapse + def recurrent_function(y): return (-y / tau) * synapse + y - - + nengo.Connection(x, y, synapse=synapse, function=input_function) nengo.Connection(y, y, synapse=synapse, function=recurrent_function) - diff --git a/nengo_gui/examples/tutorial/13-oscillators.py b/nengo_gui/examples/tutorial/13-oscillators.py index 475ff903..0dda47d9 100644 --- a/nengo_gui/examples/tutorial/13-oscillators.py +++ b/nengo_gui/examples/tutorial/13-oscillators.py @@ -1,22 +1,23 @@ # Tutorial 13: Oscillators - +# # If we do differential equations in multiple dimensions, we can get # oscillators. This gives an Ensemble of neurons that can produce patterns # of behaviour all on its own without any external input. For example, here # is a standard cycle in two dimensions: # dx0/dt = -x1 * s + x0 * (r - x0**2 - x1**2) # dx1/dt = x0 * s + x1 * (r - x0**2 - x1**2) -# where r is the radius of the circle and s is the speed (in radians per second). - +# where r is the radius of the circle and s is the speed +# (in radians per second). +# # As discussed in the previous tutorial, we can convert this into a Nengo # model. In this case there is no input connection, so all we have to do # is multiply by the synapse and add the original value. - +# # Here we introduce a new kind of plot. The XY-value plot shows the same # information as the normal Value plot, but plots the two dimensions together # rather than using time to be the x-axis. This can be convenient for # representing multidimensional data. - +# # Try adjusting the r value to 0.5. Try 1.5. What about 0? # Try adjusting the speed s. What happens when it is very slow (0.5)? 0.1? @@ -26,14 +27,12 @@ with model: x = nengo.Ensemble(n_neurons=200, dimensions=2) - synapse = 0.1 + def oscillator(x): r = 1 s = 6 return [synapse * (-x[1] * s + x[0] * (r - x[0]**2 - x[1]**2)) + x[0], - synapse * ( x[0] * s + x[1] * (r - x[0]**2 - x[1]**2)) + x[1]] + synapse * (x[0] * s + x[1] * (r - x[0]**2 - x[1]**2)) + x[1]] nengo.Connection(x, x, synapse=synapse, function=oscillator) - - diff --git a/nengo_gui/examples/tutorial/14-controlled-oscillator.py b/nengo_gui/examples/tutorial/14-controlled-oscillator.py index 4e0ec8c1..e1466067 100644 --- a/nengo_gui/examples/tutorial/14-controlled-oscillator.py +++ b/nengo_gui/examples/tutorial/14-controlled-oscillator.py @@ -1,20 +1,19 @@ # Tutorial 14: Controlled Oscillator - +# # Here we do the exact same oscillator as in the previous example, but we # introduce a new dimension that lets us control the speed of the oscillation - +# # We use the same differential equation as before: # dx0/dt = -x1 * s + x0 * (r - x0**2 - y0**2) # dx1/dt = x0 * s + x1 * (r - x0**2 - y0**2) # where r is the radius of the circle and s is the speed (in radians per # second). - +# # But, in this case, we make the Ensemble be 3-dimensional and use the third # dimension (x[2]) to represent s. You can control it with a separate input. # This shows how neurons can affect the pattern of activity of another # group of neurons. - import nengo model = nengo.Network() @@ -28,7 +27,7 @@ def oscillator(x): r = 1 s = 10 * x[2] return [synapse * -x[1] * s + x[0] * (r - x[0]**2 - x[1]**2) + x[0], - synapse * x[0] * s + x[1] * (r - x[0]**2 - x[1]**2) + x[1]] + synapse * x[0] * s + x[1] * (r - x[0]**2 - x[1]**2) + x[1]] nengo.Connection(x, x[:2], synapse=synapse, function=oscillator) diff --git a/nengo_gui/examples/tutorial/15-lorenz.py b/nengo_gui/examples/tutorial/15-lorenz.py index 7bb8398e..6dd51b97 100644 --- a/nengo_gui/examples/tutorial/15-lorenz.py +++ b/nengo_gui/examples/tutorial/15-lorenz.py @@ -1,5 +1,5 @@ # Tutorial 15: The Lorenz Chaotic Attractor - +# # Differential equations can also give chaotic behaviour. The classic example # of this is the Lorenz "butterfly" attractor. The equations for it are # @@ -10,9 +10,9 @@ # Note: this is a slight transformation from the standard formulation so # as to centre the value around the origin. For further information, see # http://compneuro.uwaterloo.ca/publications/eliasmith2005b.html -# "Chris Eliasmith. A unified approach to building and controlling -# spiking attractor networks. Neural computation, 7(6):1276-1314, 2005." - +# "Chris Eliasmith. A unified approach to building and controlling +# spiking attractor networks. Neural computation, 7(6):1276-1314, 2005." +# # Since there are three dimensions, we can show three different XY plots # combining the different values in different ways. @@ -20,22 +20,22 @@ model = nengo.Network(seed=3) with model: - + x = nengo.Ensemble(n_neurons=600, dimensions=3, radius=30) synapse = 0.1 + def lorenz(x): sigma = 10 beta = 8.0/3 rho = 28 - + dx0 = -sigma * x[0] + sigma * x[1] dx1 = -x[0] * x[2] - x[1] dx2 = x[0] * x[1] - beta * (x[2] + rho) - rho - + return [dx0 * synapse + x[0], dx1 * synapse + x[1], dx2 * synapse + x[2]] nengo.Connection(x, x, synapse=synapse, function=lorenz) - diff --git a/nengo_gui/examples/tutorial/16-ensemble-properties.py b/nengo_gui/examples/tutorial/16-ensemble-properties.py index 75d767b7..232f91f0 100644 --- a/nengo_gui/examples/tutorial/16-ensemble-properties.py +++ b/nengo_gui/examples/tutorial/16-ensemble-properties.py @@ -1,21 +1,21 @@ # Tutorial 16: Ensemble Properties - +# # In addition to the number of neurons, the number of dimensions, and the # radius, there are other parameters that can be specified when creating an # Ensemble. Here are a few that may be useful. - +# # max_rates # Each neuron has a different maximum firing rate, and this parameter # specifies the random distribution controlling this. The default is # a uniform distribution between 200Hz and 400Hz. - +# # encoders # Each neuron has a different preferred stimulus. For a 1-dimensional # Ensemble, this means that half of the neurons prefer -1 and the other # half prefer +1. This is why some neurons fire more for large values and # some for small values. In the example below, we set all the encoders to # be +1. - +# # intercepts # Each neuron only starts firing when the similarity between the value and # its preferred value reaches a particular limit. This is normally a diff --git a/nengo_gui/examples/tutorial/17-neuron-models.py b/nengo_gui/examples/tutorial/17-neuron-models.py index 5d1961a0..109d3a96 100644 --- a/nengo_gui/examples/tutorial/17-neuron-models.py +++ b/nengo_gui/examples/tutorial/17-neuron-models.py @@ -1,5 +1,5 @@ # Tutorial 17: Neuron Models - +# # Nengo supports multiple different types of neurons. The default is the # "Leaky Integrate-and-Fire" or LIF neuron. Other supported ones are shown # here. The LIFRate neuron acts like the LIF neuron, but does not have spikes. @@ -14,15 +14,15 @@ model = nengo.Network() with model: - + stim = nengo.Node(0) - + a = nengo.Ensemble(n_neurons=50, dimensions=1, neuron_type=nengo.LIF(tau_rc=0.02, tau_ref=0.002)) - + b = nengo.Ensemble(n_neurons=50, dimensions=1, neuron_type=nengo.LIFRate(tau_rc=0.02, tau_ref=0.002)) - + c = nengo.Ensemble(n_neurons=50, dimensions=1, neuron_type=nengo.Sigmoid(tau_ref=0.002)) @@ -40,4 +40,4 @@ nengo.Connection(stim, b) nengo.Connection(stim, c) nengo.Connection(stim, d) - nengo.Connection(stim, e) \ No newline at end of file + nengo.Connection(stim, e) diff --git a/nengo_gui/examples/tutorial/18-networks.py b/nengo_gui/examples/tutorial/18-networks.py index d6b127ec..82b7d1ec 100644 --- a/nengo_gui/examples/tutorial/18-networks.py +++ b/nengo_gui/examples/tutorial/18-networks.py @@ -1,7 +1,7 @@ # Tutorial 18: Networks - +# # To help organize larger models, you can make Networks inside of the main -# model Network. +# model Network. # # In the graphic interface, the items inside these Networks are not shown # by default. If you double-click on a Network you can show (or hide) its @@ -14,14 +14,13 @@ # components that let you easily connect to or from all of the Ensembles at # once. - import nengo model = nengo.Network() with model: stim_a = nengo.Node([0, 0, 0]) stim_b = nengo.Node([0, 0]) - + part1 = nengo.Network() with part1: a = nengo.Ensemble(n_neurons=100, dimensions=3) @@ -31,9 +30,7 @@ nengo.Connection(b, c[3:]) nengo.Connection(stim_a, a) nengo.Connection(stim_b, b) - - + part2 = nengo.networks.EnsembleArray(n_neurons=50, n_ensembles=5) - + nengo.Connection(c, part2.input) - \ No newline at end of file diff --git a/nengo_gui/examples/tutorial/19-spa.py b/nengo_gui/examples/tutorial/19-spa.py index 9f3663d1..5b0815a5 100644 --- a/nengo_gui/examples/tutorial/19-spa.py +++ b/nengo_gui/examples/tutorial/19-spa.py @@ -1,5 +1,5 @@ # Tutorial 19: Semantic Pointers - +# # If we want to represent conceptual information, we need a way to represent # concepts and symbol-like manipulations using Nengo. We do this by treating # concepts like vectors: high-dimensional numerical data. That is, each @@ -10,7 +10,7 @@ # We call these "semantic" because, in general, we would choose these # numerical values such that concepts with similar semantics (like DOG and # CAT) might have similar numerical values. - +# # To help work with these vectors, we introduce a new collection of pre-built # Networks, and a new type of graph. The new pre-built Networks can be # accessed via nengo.spa ("spa" stands for "Semantic Pointer Architecture", @@ -23,7 +23,7 @@ # individual values, it shows how close the currently represented vector is # to the ideal original vectors. Furthermore, you can use it as an input # system as well, and define new concepts. - +# # Press play to start the simulation running. Now right-click on the "vision" # graph (the blank space above the "vision" box in the diagram). Select "Set # value..." and put in CAT as a value. Nengo will randomly generate a new diff --git a/nengo_gui/examples/tutorial/19-spa.py.cfg b/nengo_gui/examples/tutorial/19-spa.py.cfg index ba9b1733..77f8e941 100644 --- a/nengo_gui/examples/tutorial/19-spa.py.cfg +++ b/nengo_gui/examples/tutorial/19-spa.py.cfg @@ -25,11 +25,11 @@ _viz_config[model.memory].pos=(0.7727272727272727, 0.7397494305239181) _viz_config[model.memory].size=(0.18181818181818182, 0.16025056947608207) _viz_config[model.memory].expanded=False _viz_config[model.memory].has_layout=False -_viz_config[model.memory.state].expanded=False -_viz_config[model.memory.state].has_layout=False +_viz_config[model.memory.state_ensembles].expanded=False +_viz_config[model.memory.state_ensembles].has_layout=False _viz_config[model.vision].pos=(0.2272727215791493, 0.7471526195899774) _viz_config[model.vision].size=(0.18181817612460383, 0.15284738041002283) _viz_config[model.vision].expanded=False _viz_config[model.vision].has_layout=False -_viz_config[model.vision.state].expanded=False -_viz_config[model.vision.state].has_layout=False \ No newline at end of file +_viz_config[model.vision.state_ensembles].expanded=False +_viz_config[model.vision.state_ensembles].has_layout=False diff --git a/nengo_gui/examples/tutorial/20-spa-actions.py b/nengo_gui/examples/tutorial/20-spa-actions.py index bd8be44d..2cb0eb57 100644 --- a/nengo_gui/examples/tutorial/20-spa-actions.py +++ b/nengo_gui/examples/tutorial/20-spa-actions.py @@ -1,5 +1,5 @@ # Tutorial 20: Semantic Pointer Actions - +# # A complex cognitive system needs a method to select and perform actions. # In particular, it is useful to have a model that can do one thing at # a time, sequentially. There is significant psychological data indicating @@ -33,8 +33,8 @@ # such as CAT or COW. You can also try a combination, such as # RAT+0.5*COW. The system will (almost always) select a single action # to perform, and that will be the action with the highest utility at that -# moment. If there is no input, or the input is not sufficiently similar -# to any of the four animals it knows, then the speech State is set to +# moment. If there is no input, or the input is not sufficiently similar +# to any of the four animals it knows, then the speech State is set to # zero ("speech=0"). # # The input graph above the Basal Ganglia shows the utilities of the @@ -43,7 +43,6 @@ # information between brain areas, so here it is used to implement the # effects of the actions defined in the Basal Ganglia. -import nengo import nengo.spa as spa D = 32 # the dimensionality of the vectors @@ -60,7 +59,7 @@ 'dot(vision, RAT) --> speech=SQUEAK', 'dot(vision, COW) --> speech=MOO', '0.5 --> speech=0' - ) + ) model.bg = spa.BasalGanglia(actions) model.thalamus = spa.Thalamus(model.bg) diff --git a/nengo_gui/examples/tutorial/20-spa-actions.py.cfg b/nengo_gui/examples/tutorial/20-spa-actions.py.cfg index b63f3731..6b8652bd 100644 --- a/nengo_gui/examples/tutorial/20-spa-actions.py.cfg +++ b/nengo_gui/examples/tutorial/20-spa-actions.py.cfg @@ -63,8 +63,8 @@ _viz_config[model.speech].pos=(0.8938488770881735, 0.6401446654611214) _viz_config[model.speech].size=(0.08695652173913043, 0.2182640144665459) _viz_config[model.speech].expanded=False _viz_config[model.speech].has_layout=False -_viz_config[model.speech.state].expanded=False -_viz_config[model.speech.state].has_layout=False +_viz_config[model.speech.state_ensembles].expanded=False +_viz_config[model.speech.state_ensembles].has_layout=False _viz_config[model.thalamus].pos=(0.6278902533466091, 0.6555153707052435) _viz_config[model.thalamus].size=(0.08695652173913043, 0.23725135623869806) _viz_config[model.thalamus].expanded=False @@ -99,7 +99,7 @@ _viz_config[model.vision].pos=(0.10869565217391304, 0.6528028933092226) _viz_config[model.vision].size=(0.08695652173913043, 0.23815551537070517) _viz_config[model.vision].expanded=False _viz_config[model.vision].has_layout=True -_viz_config[model.vision.state].pos=(0.5, 0.5) -_viz_config[model.vision.state].size=(0.4, 0.4) -_viz_config[model.vision.state].expanded=False -_viz_config[model.vision.state].has_layout=False +_viz_config[model.vision.state_ensembles].pos=(0.5, 0.5) +_viz_config[model.vision.state_ensembles].size=(0.4, 0.4) +_viz_config[model.vision.state_ensembles].expanded=False +_viz_config[model.vision.state_ensembles].has_layout=False diff --git a/nengo_gui/examples/tutorial/21-spa-sequence.py b/nengo_gui/examples/tutorial/21-spa-sequence.py index b3e5fe9b..5d30ec83 100644 --- a/nengo_gui/examples/tutorial/21-spa-sequence.py +++ b/nengo_gui/examples/tutorial/21-spa-sequence.py @@ -1,16 +1,15 @@ # Tutorial 21: Sequential Semantic Pointer Actions - +# # In this example, we define a set of actions that follow through a # repeating sequence (A, B, C, D, E). This shows that you can define # actions which affect the performance of later actions. - +# # In this example we have changed the default value of the optional argument # feedback_synapse, which is the time constant controlling the exponential # decay of the postsynaptic potential. Try using longer time constants # (e.g. 0.1 or 0.5) and observe what changes. How is the stability of # a memory representation related to the synaptic time constant? -import nengo import nengo.spa as spa D = 32 # the dimensionality of the vectors @@ -25,7 +24,7 @@ 'dot(memory, C) --> memory=D', 'dot(memory, D) --> memory=E', 'dot(memory, E) --> memory=A', - ) + ) model.bg = spa.BasalGanglia(actions) model.thalamus = spa.Thalamus(model.bg) diff --git a/nengo_gui/examples/tutorial/22-spa-sequence-controlled.py b/nengo_gui/examples/tutorial/22-spa-sequence-controlled.py index db3c9a08..eecf3f69 100644 --- a/nengo_gui/examples/tutorial/22-spa-sequence-controlled.py +++ b/nengo_gui/examples/tutorial/22-spa-sequence-controlled.py @@ -1,5 +1,5 @@ # Tutorial 22: Controlled Sequence of Semantic Pointer Actions - +# # Here, we expand on the previous sequence example and define a model # that instead of starting from A each time, starts from whatever value # is currently seen. This shows that you can selectively route information @@ -15,7 +15,6 @@ # sequence. This is due to the time needed for the neurons to respond, # and is consistent with psychological reaction time data. -import nengo import nengo.spa as spa D = 32 # the dimensionality of the vectors @@ -32,7 +31,7 @@ 'dot(memory, D) --> memory=E', 'dot(memory, E) --> memory=vision', '0.5 --> memory=vision' - ) + ) model.bg = spa.BasalGanglia(actions) model.thalamus = spa.Thalamus(model.bg) diff --git a/nengo_gui/examples/tutorial/22-spa-sequence-controlled.py.cfg b/nengo_gui/examples/tutorial/22-spa-sequence-controlled.py.cfg index 580dd449..d630866e 100644 --- a/nengo_gui/examples/tutorial/22-spa-sequence-controlled.py.cfg +++ b/nengo_gui/examples/tutorial/22-spa-sequence-controlled.py.cfg @@ -93,22 +93,6 @@ _viz_config[model.thalamus.bg].expanded=False _viz_config[model.thalamus.bg].has_layout=True _viz_config[model.thalamus.bias].pos=(0.15262792014175175, 0.768512911036981) _viz_config[model.thalamus.bias].size=(0.043010752688172046, 0.05555555555555555) -_viz_config[model.thalamus.ensembles[0]].pos=(0.5967741935483871, 0.6527777777777777) -_viz_config[model.thalamus.ensembles[0]].size=(0.053763440860215055, 0.06944444444444445) -_viz_config[model.thalamus.networks[1]].pos=(0.8655913978494624, 0.6527777777777777) -_viz_config[model.thalamus.networks[1]].size=(0.10752688172043011, 0.2777777777777778) -_viz_config[model.thalamus.networks[1]].expanded=True -_viz_config[model.thalamus.networks[1]].has_layout=True -_viz_config[model.thalamus.networks[1].ea_ensembles[0]].pos=(0.49999999999999994, 0.2) -_viz_config[model.thalamus.networks[1].ea_ensembles[0]].size=(0.09803921568627451, 0.1) -_viz_config[model.thalamus.networks[1].ea_ensembles[1]].pos=(0.49999999999999994, 0.7999999999999999) -_viz_config[model.thalamus.networks[1].ea_ensembles[1]].size=(0.09803921568627451, 0.1) -_viz_config[model.thalamus.networks[1].input].pos=(0.12745098039215685, 0.5) -_viz_config[model.thalamus.networks[1].input].size=(0.07843137254901959, 0.08) -_viz_config[model.thalamus.networks[1].output].pos=(0.872549019607843, 0.5) -_viz_config[model.thalamus.networks[1].output].size=(0.07843137254901959, 0.08) -_viz_config[model.thalamus.networks[2]].expanded=False -_viz_config[model.thalamus.networks[2]].has_layout=False _viz_config[model.thalamus.spa].pos=(0.24456685324716096, 0.1507632799672523) _viz_config[model.thalamus.spa].size=(0.4756851457806843, 0.4756851457806843) _viz_config[model.thalamus.spa].expanded=True @@ -118,4 +102,4 @@ _viz_config[model.vision].size=(0.08493913168561505, 0.13276488513830279) _viz_config[model.vision].expanded=False _viz_config[model.vision].has_layout=False _viz_config[model.vision.state_ensembles].expanded=False -_viz_config[model.vision.state_ensembles].has_layout=False \ No newline at end of file +_viz_config[model.vision.state_ensembles].has_layout=False diff --git a/nengo_gui/examples/tutorial/23-spa-binding.py b/nengo_gui/examples/tutorial/23-spa-binding.py index 2668f736..a50b38db 100644 --- a/nengo_gui/examples/tutorial/23-spa-binding.py +++ b/nengo_gui/examples/tutorial/23-spa-binding.py @@ -1,5 +1,5 @@ # Tutorial 23: Binding Concepts - +# # We now show that you can combine semantic pointers together to store # structured information. One of the standard problems in cognitive science # is the Binding Problem: how do different concepts get represented and @@ -42,7 +42,6 @@ # longer to run). We have argued that around 800 dimensions are sufficient for # human working memory capacity. -import nengo import nengo.spa as spa D = 32 # the dimensionality of the vectors @@ -55,6 +54,6 @@ actions = spa.Actions( 'memory = color * shape', - ) + ) model.cortical = spa.Cortical(actions) diff --git a/nengo_gui/examples/tutorial/23-spa-binding.py.cfg b/nengo_gui/examples/tutorial/23-spa-binding.py.cfg index c4c2b93e..2a8b7b26 100644 --- a/nengo_gui/examples/tutorial/23-spa-binding.py.cfg +++ b/nengo_gui/examples/tutorial/23-spa-binding.py.cfg @@ -34,14 +34,6 @@ _viz_config[model.cortical].pos=(0.5, 0.5) _viz_config[model.cortical].size=(0.11764705882352941, 0.1818181818181818) _viz_config[model.cortical].expanded=False _viz_config[model.cortical].has_layout=False -_viz_config[model.cortical.networks[0]].expanded=False -_viz_config[model.cortical.networks[0]].has_layout=False -_viz_config[model.cortical.networks[0].product].expanded=False -_viz_config[model.cortical.networks[0].product].has_layout=False -_viz_config[model.cortical.networks[0].product.sq1].expanded=False -_viz_config[model.cortical.networks[0].product.sq1].has_layout=False -_viz_config[model.cortical.networks[0].product.sq2].expanded=False -_viz_config[model.cortical.networks[0].product.sq2].has_layout=False _viz_config[model.cortical.spa].pos=(0.2229521406535118, 0.4008238542292871) _viz_config[model.cortical.spa].size=(0.5225020088280712, 0.5225020088280712) _viz_config[model.cortical.spa].expanded=True diff --git a/nengo_gui/examples/tutorial/24-spa-unbinding.py b/nengo_gui/examples/tutorial/24-spa-unbinding.py index e405e108..857c6793 100644 --- a/nengo_gui/examples/tutorial/24-spa-unbinding.py +++ b/nengo_gui/examples/tutorial/24-spa-unbinding.py @@ -1,5 +1,5 @@ # Tutorial 24: Unbinding Concepts - +# # Now that we can combine information together into a single structure (see # the previous tutorial), we also need to be able to extract information # back out. We do this by exploiting a pseudo-inverse property of the @@ -21,7 +21,6 @@ # get TRIANGLE. And for TRIANGLE you should get BLUE. Notice that the memory # will gradually decay and fade the longer you try to run the system for. -import nengo import nengo.spa as spa D = 32 # the dimensionality of the vectors @@ -37,6 +36,6 @@ actions = spa.Actions( 'memory = color * shape', 'answer = memory * ~query', - ) - + ) + model.cortical = spa.Cortical(actions) diff --git a/nengo_gui/examples/tutorial/24-spa-unbinding.py.cfg b/nengo_gui/examples/tutorial/24-spa-unbinding.py.cfg index b2f9933b..76259a89 100644 --- a/nengo_gui/examples/tutorial/24-spa-unbinding.py.cfg +++ b/nengo_gui/examples/tutorial/24-spa-unbinding.py.cfg @@ -54,22 +54,6 @@ _viz_config[model.cortical].pos=(0.5, 0.32352941176470584) _viz_config[model.cortical].size=(0.11764705882352941, 0.11764705882352941) _viz_config[model.cortical].expanded=False _viz_config[model.cortical].has_layout=False -_viz_config[model.cortical.networks[0]].expanded=False -_viz_config[model.cortical.networks[0]].has_layout=False -_viz_config[model.cortical.networks[0].product].expanded=False -_viz_config[model.cortical.networks[0].product].has_layout=False -_viz_config[model.cortical.networks[0].product.sq1].expanded=False -_viz_config[model.cortical.networks[0].product.sq1].has_layout=False -_viz_config[model.cortical.networks[0].product.sq2].expanded=False -_viz_config[model.cortical.networks[0].product.sq2].has_layout=False -_viz_config[model.cortical.networks[1]].expanded=False -_viz_config[model.cortical.networks[1]].has_layout=False -_viz_config[model.cortical.networks[1].product].expanded=False -_viz_config[model.cortical.networks[1].product].has_layout=False -_viz_config[model.cortical.networks[1].product.sq1].expanded=False -_viz_config[model.cortical.networks[1].product.sq1].has_layout=False -_viz_config[model.cortical.networks[1].product.sq2].expanded=False -_viz_config[model.cortical.networks[1].product.sq2].has_layout=False _viz_config[model.cortical.spa].pos=(0.445107900344506, 0.28900116461649517) _viz_config[model.cortical.spa].size=(0.629737609329446, 0.629737609329446) _viz_config[model.cortical.spa].expanded=True @@ -91,4 +75,4 @@ _viz_config[model.shape].size=(0.11764705882352941, 0.11764705882352941) _viz_config[model.shape].expanded=False _viz_config[model.shape].has_layout=False _viz_config[model.shape.state_ensembles].expanded=False -_viz_config[model.shape.state_ensembles].has_layout=False \ No newline at end of file +_viz_config[model.shape.state_ensembles].has_layout=False diff --git a/nengo_gui/examples/tutorial/25-spa-parse.py b/nengo_gui/examples/tutorial/25-spa-parse.py index 8d07c353..ca8381ac 100644 --- a/nengo_gui/examples/tutorial/25-spa-parse.py +++ b/nengo_gui/examples/tutorial/25-spa-parse.py @@ -1,5 +1,5 @@ # Tutorial 25: Parsing Simple Commands - +# # In this tutorial, we use both the ability to combine structured information # and the ability to make complex actions to implement a simple two-word # parsing system. This model has a single visual input (vision) and you can @@ -7,7 +7,7 @@ # by HI. It will remember the two terms, store them, and then respond # appropriately by sending HI to its hand. It can also SAY BYE, where the # vector for BYE will be set to the speech system. - +# # The parsing process is implemented with the first two actions. The first # action looks for verbs (WRITE or SAY) and sends them into the memory for # verbs. The second action looks for nouns (YES or NO or HI or BYE or OK) and @@ -18,21 +18,20 @@ # multiple different things at the same time by using the + operation. # Overall, this shows how we can use a single input modality (vision) and treat # the information in different ways as appropriate. - +# # The last two actions deal with recognizing and executing actions. The # first one looks for the VERB WRITE, and if it sees this it will take # whatever is in phrase, extract out the NOUN, and send that to the hand. # Furthermore, it will only do this if it doesn't currently see anything in # vision (via the subtraction). This is to make sure it waits until it is # getting no more visual input before responding. - +# # To test the model, try presenting WRITE to the vision State. The phrase # should fill with WRITE*VERB. Now change the vision input to HI. The phrase # should now fill with NOUN*HI. If you get rid of the visual input (by setting # the value to nothing), it shuold sent HI to the hand. If you try the same # thing with SAY it should send the result to speech. -import nengo import nengo.spa as spa D = 32 # the dimensionality of the vectors @@ -55,10 +54,10 @@ 'dot(vision, WRITE+SAY) --> verb=vision', 'dot(vision, YES+NO+HI+BYE+OK) --> noun=vision', 'dot(phrase, VERB*WRITE) - 2*dot(vision, WRITE+SAY+YES+NO+HI+BYE+OK)' - '--> hand=phrase*~NOUN', + '--> hand=phrase*~NOUN', 'dot(phrase, VERB*SAY) - 2*dot(vision, WRITE+SAY+YES+NO+HI+BYE+OK)' - '--> speech=phrase*~NOUN', - ) + '--> speech=phrase*~NOUN', + ) model.bg = spa.BasalGanglia(actions) model.thalamus = spa.Thalamus(model.bg) diff --git a/nengo_gui/examples/tutorial/25-spa-parse.py.cfg b/nengo_gui/examples/tutorial/25-spa-parse.py.cfg index 6225f559..df35204a 100644 --- a/nengo_gui/examples/tutorial/25-spa-parse.py.cfg +++ b/nengo_gui/examples/tutorial/25-spa-parse.py.cfg @@ -145,38 +145,6 @@ _viz_config[model.thalamus.bg].expanded=False _viz_config[model.thalamus.bg].has_layout=True _viz_config[model.thalamus.bias].pos=(0.06989247311827958, 0.5000000000000001) _viz_config[model.thalamus.bias].size=(0.043010752688172046, 0.01652892561983471) -_viz_config[model.thalamus.ensembles[0]].pos=(0.5967741935483871, 0.10330578512396695) -_viz_config[model.thalamus.ensembles[0]].size=(0.053763440860215055, 0.02066115702479339) -_viz_config[model.thalamus.ensembles[1]].pos=(0.5967741935483871, 0.37603305785123964) -_viz_config[model.thalamus.ensembles[1]].size=(0.053763440860215055, 0.02066115702479339) -_viz_config[model.thalamus.ensembles[2]].pos=(0.5967741935483871, 0.6239669421487604) -_viz_config[model.thalamus.ensembles[2]].size=(0.053763440860215055, 0.02066115702479339) -_viz_config[model.thalamus.ensembles[3]].pos=(0.5967741935483871, 0.8966942148760331) -_viz_config[model.thalamus.ensembles[3]].size=(0.053763440860215055, 0.02066115702479339) -_viz_config[model.thalamus.networks[1]].pos=(0.8655913978494624, 0.10330578512396695) -_viz_config[model.thalamus.networks[1]].size=(0.10752688172043011, 0.08264462809917356) -_viz_config[model.thalamus.networks[1]].expanded=True -_viz_config[model.thalamus.networks[1]].has_layout=True -_viz_config[model.thalamus.networks[1].ea_ensembles[0]].pos=(0.49999999999999994, 0.9090909090909091) -_viz_config[model.thalamus.networks[1].ea_ensembles[0]].size=(0.09803921568627451, 0.04545454545454545) -_viz_config[model.thalamus.networks[1].ea_ensembles[1]].pos=(0.49999999999999994, 0.36363636363636365) -_viz_config[model.thalamus.networks[1].ea_ensembles[1]].size=(0.09803921568627451, 0.04545454545454545) -_viz_config[model.thalamus.networks[1].input].pos=(0.12745098039215685, 0.5) -_viz_config[model.thalamus.networks[1].input].size=(0.07843137254901959, 0.03636363636363636) -_viz_config[model.thalamus.networks[1].output].pos=(0.872549019607843, 0.5) -_viz_config[model.thalamus.networks[1].output].size=(0.07843137254901959, 0.03636363636363636) -_viz_config[model.thalamus.networks[2]].pos=(0.8655913978494624, 0.37603305785123964) -_viz_config[model.thalamus.networks[2]].size=(0.10752688172043011, 0.08264462809917356) -_viz_config[model.thalamus.networks[2]].expanded=False -_viz_config[model.thalamus.networks[2]].has_layout=False -_viz_config[model.thalamus.networks[3]].pos=(0.8655913978494624, 0.6239669421487604) -_viz_config[model.thalamus.networks[3]].size=(0.10752688172043011, 0.08264462809917356) -_viz_config[model.thalamus.networks[3]].expanded=False -_viz_config[model.thalamus.networks[3]].has_layout=False -_viz_config[model.thalamus.networks[4]].pos=(0.8655913978494624, 0.8966942148760331) -_viz_config[model.thalamus.networks[4]].size=(0.10752688172043011, 0.08264462809917356) -_viz_config[model.thalamus.networks[4]].expanded=False -_viz_config[model.thalamus.networks[4]].has_layout=False _viz_config[model.verb].pos=(1.1374084683143597, 0.9564957721561658) _viz_config[model.verb].size=(0.15456115795951292, 0.13460281023501125) _viz_config[model.verb].expanded=False @@ -188,4 +156,4 @@ _viz_config[model.vision].size=(0.15456115795951292, 0.13460281023501125) _viz_config[model.vision].expanded=False _viz_config[model.vision].has_layout=False _viz_config[model.vision.state_ensembles].expanded=False -_viz_config[model.vision.state_ensembles].has_layout=False \ No newline at end of file +_viz_config[model.vision.state_ensembles].has_layout=False diff --git a/nengo_gui/exceptions.py b/nengo_gui/exceptions.py new file mode 100644 index 00000000..3539c8b7 --- /dev/null +++ b/nengo_gui/exceptions.py @@ -0,0 +1,14 @@ +class NengoGuiError(Exception): + pass + + +class NotAttachedError(NengoGuiError): + pass + + +class StartedSimulatorException(NengoGuiError): + pass + + +class StartedGUIException(NengoGuiError): + pass diff --git a/nengo_gui/exec_env.py b/nengo_gui/exec_env.py index eb3a34ae..98e582f0 100644 --- a/nengo_gui/exec_env.py +++ b/nengo_gui/exec_env.py @@ -1,11 +1,12 @@ -import contextlib import importlib import os +import sys import threading import traceback -import sys + from nengo.utils.compat import StringIO +from nengo_gui.exceptions import StartedSimulatorException # list of Simulators to check for known_modules = ['nengo', 'nengo_ocl', 'nengo_distilled', @@ -26,14 +27,6 @@ def discover_backends(): return found_modules -class StartedSimulatorException(Exception): - pass - - -class StartedGUIException(Exception): - pass - - # create a wrapper class that will throw an exception if we are # currently executing a script def make_dummy(cls): @@ -42,39 +35,42 @@ def __init__(self, *args, **kwargs): if is_executing(): raise StartedSimulatorException() super(DummySimulator, self).__init__(*args, **kwargs) + + def __del__(self): + pass + return DummySimulator # thread local storage for storing whether we are executing a script flag = threading.local() -compiled_filename = '' def is_executing(): return getattr(flag, 'executing', False) -def determine_line_number(): - '''Checks stack trace to determine the line number we are currently at. +def determine_line(filename): + """Checks stack trace to determine the line number we are currently at. The filename argument should be the filename given to the code when it was compiled (with compile()) - ''' + """ exc_type, exc_value, exc_traceback = sys.exc_info() if exc_traceback is not None: ex_tb = traceback.extract_tb(exc_traceback) for fn, line, function, code in reversed(ex_tb): - if fn == compiled_filename: + if fn == filename: return line # if we can't find it that way, parse the text of the stack trace # note that this is required for indentation errors and other syntax # problems trace = traceback.format_exc() - pattern = 'File "%s", line ' % compiled_filename + pattern = 'File "%s", line ' % filename index = trace.find(pattern) - if index >=0: + if index >= 0: text = trace[index + len(pattern):].split('\n', 1)[0] if ',' in text: text = text.split(',', 1)[0] @@ -91,6 +87,7 @@ def __init__(self, filename, allow_sim=False): self.directory = os.path.dirname(filename) self.added_directory = None self.allow_sim = allow_sim + def __enter__(self): if self.directory is not None and self.directory not in sys.path: sys.path.insert(0, self.directory) @@ -109,6 +106,7 @@ def __enter__(self): for mod in discover_backends().values(): self.simulators[mod] = mod.Simulator mod.Simulator = make_dummy(mod.Simulator) + return self def __exit__(self, exc_type, exc_value, traceback): for mod, cls in self.simulators.items(): diff --git a/nengo_gui/gui.py b/nengo_gui/gui.py index 5245768f..d78f1ab9 100644 --- a/nengo_gui/gui.py +++ b/nengo_gui/gui.py @@ -1,46 +1,383 @@ -"""Classes to instantiate and manage the life cycle of the nengo_gui -backend.""" +"""Classes to instantiate and manage the life cycle of nengo_gui.""" from __future__ import print_function +import json +import mimetypes +import os +import logging +import pkgutil import select import signal +import ssl import sys import threading +import time import webbrowser +from timeit import default_timer -import nengo_gui -from nengo_gui.guibackend import GuiServer +from nengo_gui import exec_env, paths, server +from nengo_gui.client import ClientConnection, FastClientConnection +from nengo_gui.compat import unquote +from nengo_gui.editor import AceEditor, NoEditor +from nengo_gui.page import Page +from nengo_gui.server import ( + HtmlResponse, HttpRedirect, ServerShutdown, WebSocketFrame) -class ServerShutdown(Exception): - """Causes the server to shutdown when raised.""" - pass +logger = logging.getLogger(__name__) + + +class Context(object): + """Provides context information to the page. + + This can include the locals dictionary, the filename and whether + this model can (or is allowed) to be written to disk. + """ + + def __init__(self, + model=None, + locals=None, + filename=None, + filename_cfg=None, + writeable=True, + backend="nengo"): + self.writeabel = writeable + self.filename_cfg = filename_cfg + self.filename = filename + self.backend = backend + + if model is None and locals is not None: + model = locals.get('model', None) + + if model is None and filename is None: + raise ValueError("No model.") + + self.model = model + self.locals = locals + + @property + def filename(self): + return self._filename + + @filename.setter + def filename(self, value): + self._filename = value + + if value is None: + self.writeable = False + else: + try: + self._filename = os.path.relpath(value) + except ValueError: + # happens on Windows if filename is on a different + # drive than the current directory + pass + + if self.filename_cfg is None: + self.filename_cfg = "%s.cfg" % (self._filename,) + + +class ServerSettings(object): + __slots__ = ('listen_addr', + 'auto_shutdown', + 'password_hash', + 'ssl_cert', + 'ssl_key', + 'session_duration') + + def __init__(self, + listen_addr=('localhost', 8080), + auto_shutdown=2, + password_hash=None, + ssl_cert=None, + ssl_key=None, + session_duration=60 * 60 * 24 * 30): + self.listen_addr = listen_addr + self.auto_shutdown = auto_shutdown + self.password_hash = password_hash + self.ssl_cert = ssl_cert + self.ssl_key = ssl_key + self.session_duration = session_duration + + @property + def use_ssl(self): + if self.ssl_cert is None and self.ssl_key is None: + return False + elif self.ssl_cert is not None and self.ssl_key is not None: + return True + else: + raise ValueError("SSL needs certificate file and key file.") + + +class GuiRequestHandler(server.AuthenticatedHttpWsRequestHandler): + + @server.AuthenticatedHttpWsRequestHandler.http_route('/browse') + @server.RequireAuthentication('/login') + def browse(self): + r = [b'') + return server.HtmlResponse(b''.join(r)) + + @server.AuthenticatedHttpWsRequestHandler.http_route('/login') + def login(self): + session = self.checkpw() + content = [] + + if session.authenticated: + return HttpRedirect('/') + + if 'pw' in self.db and not session.authenticated: + content.append( + b'

Invalid password. Try again.

') + else: + content.append(b'

Please enter the password:

') + + return HtmlResponse(b'\n'.join(content + [ + b'

', + b' ', + b' ', + b' ', + b'

', + ])) + + @server.AuthenticatedHttpWsRequestHandler.http_route('/static') + @server.RequireAuthentication('/login') + def serve_static(self): + """Handles http://host:port/static/* by returning pkg data""" + fn = os.path.join('static', self.route) + mimetype, encoding = mimetypes.guess_type(fn) + data = pkgutil.get_data('nengo_gui', fn) + return server.HttpResponse(data, mimetype) + + @server.AuthenticatedHttpWsRequestHandler.http_route('/') + @server.RequireAuthentication('/login') + def serve_main(self): + if self.route != '/': + raise server.InvalidResource(self.route) + return server.HttpResponse(r""" + + + + + + + + + """.strip().encode("utf-8")) + + @server.AuthenticatedHttpWsRequestHandler.http_route('/favicon.ico') + def serve_favicon(self): + self.route = '/static/favicon.ico' + return self.serve_static() + + @server.AuthenticatedHttpWsRequestHandler.http_route('/bootstrap.min.css.map') + def serve_bootstrap_map(self): + # TODO: should we actually do this ...? + try: + root = os.path.realpath(os.path.join(paths.rootdir, "..")) + fn = os.path.join( + root, 'node_modules/bootstrap/dist/css/bootstrap.min.css.map') + mimetype, encoding = mimetypes.guess_type(fn) + with open(fn, 'rb') as fp: + data = fp.read() + return server.HttpResponse(data, mimetype) + except Exception: + raise server.InvalidResource(self.route) + + @server.AuthenticatedHttpWsRequestHandler.ws_route('/') + @server.RequireAuthentication('/login') + def ws_default(self): + """Handles ws://host:port/component with a websocket""" + # figure out what component is being connected to + + filename = self.query.get('filename', [None])[0] + reset_cfg = self.query.get('reset', [False])[0] + + # One of these per page + client = ClientConnection(self.ws) + page = self.server.create_page(client, filename, reset_cfg=reset_cfg) + + now = default_timer() + next_ping_time = now + + while True: + try: + # Read all data + msg = self.ws.read_frame() + while msg is not None: + route, kwargs = json.loads(msg.data) + page.client.dispatch(route, **kwargs) + msg = self.ws.read_frame() + + # TODO: really...? + # page.config.save(lazy=True) + + # Keep connection alive + now = default_timer() + if next_ping_time is None or now > next_ping_time: + self.ws.write_frame(WebSocketFrame( + 1, 0, WebSocketFrame.OP_PING, 0, b'')) + next_ping_time = now + 2.0 + time.sleep(0.01) + except server.SocketClosedError: + # This error means the server has shut down + page.save(force=True) # Stop nicely + break + except Exception as e: + logger.exception("Error during websocket communication: %s", e) + + self.server.remove_page(page) + + @server.AuthenticatedHttpWsRequestHandler.ws_route('/fast') + @server.RequireAuthentication('/login') + def fast_ws(self): + """Handles ws://host:port/fast with a websocket""" + # figure out what component is being connected to + + client = FastClientConnection(self.ws) + while len(self.server.pages) == 0: + time.sleep(0.1) + page = self.server.pages[0] + # TODO: handle multiple pages + # page = self.server.pages[int(self.query['page'][0])] + uid = self.query.get('uid', [None])[0] + + if uid is None: + component = page.simcontrol + else: + component = page.netgraph.components.by_uid[uid] + component.attach(client) + + now = default_timer() + next_ping_time = now + + while True: + try: + msg = self.ws.read_frame() + while msg is not None: + client.receive(msg.data) + msg = self.ws.read_frame() + + # TODO: do we need to keep alive? + # Keep connection alive + now = default_timer() + if next_ping_time is None or now > next_ping_time: + self.ws.write_frame(WebSocketFrame( + 1, 0, WebSocketFrame.OP_PING, 0, b'')) + next_ping_time = now + 5.0 + time.sleep(0.005) + except server.SocketClosedError: + # This error means the server has shut down + logger.debug("Shutting down fast connection for %r", uid) + break + except Exception as e: + logger.exception("Error in fast connection for %r: %s", uid, e) + + def log_message(self, format, *args): + logger.info(format, *args) + + +class GuiServer(server.ManagedThreadHttpWsServer): + def __init__(self, context, + server_settings=ServerSettings(), + editor=True): + if exec_env.is_executing(): + raise exec_env.StartedGUIException() + self.settings = server_settings + + server.ManagedThreadHttpWsServer.__init__( + self, self.settings.listen_addr, GuiRequestHandler) + if self.settings.use_ssl: + self.socket = ssl.wrap_socket( + self.socket, certfile=self.settings.ssl_cert, + keyfile=self.settings.ssl_key, server_side=True) + + self.sessions.time_to_live = self.settings.session_duration + + # the list of running Pages + self.pages = [] + + # a mapping from uids to Components for all running Pages. + # this is used to connect the websockets to the appropriate Component + self.component_uids = {} + self.component_ids = {} + + self.context = context + self.editor_class = AceEditor if editor else NoEditor + + self._last_access = time.time() + + def create_page(self, client, filename, reset_cfg=False): + """Create a new Page with this configuration""" + if filename is not None: + self.context.filename = filename + self.context.filename_cfg = "{}.cfg".format(filename) + page = Page(client, self.context, self.editor_class) + if reset_cfg: + page.clear_config() + self.pages.append(page) + return page + + def remove_page(self, page): + page.finished = True + self._last_access = time.time() + self.pages.remove(page) + if (not self._shutting_down and self.settings.auto_shutdown > 0 and + len(self.pages) <= 0): + time.sleep(self.settings.auto_shutdown) + earliest_shutdown = self._last_access + self.settings.auto_shutdown + if earliest_shutdown < time.time() and len(self.pages) <= 0: + logging.info( + "No connections remaining to the nengo_gui server.") + self.shutdown() class BaseGUI(object): - """Creates a basic nengo_gui backend server. + """A basic nengo_gui backend server. + + This is used in embedded situations like the Jupyter notebook. Parameters ---------- - model_context : nengo_gui.backend.backend.ModelContext + context : Context Model and its context served by the backend. server_settings : nengo_gui.backend.backend.GuiServerSettings, optional Backend settings. - page_settings : nengo_gui.page.PageSettings, optional - Frontend page settings. """ - def __init__( - self, model_context, server_settings=None, page_settings=None): + def __init__(self, context, server_settings=None, editor=True): if server_settings is None: - server_settings = nengo_gui.guibackend.GuiServerSettings() - if page_settings is None: - page_settings = nengo_gui.page.PageSettings() - - self.model_context = model_context - - self.server = GuiServer( - self.model_context, server_settings, page_settings) + server_settings = ServerSettings() + self.context = context + self.server = GuiServer(self.context, server_settings, editor=editor) def start(self): """Start the backend server and wait until it shuts down.""" @@ -53,15 +390,15 @@ def start(self): class InteractiveGUI(BaseGUI): - """Creates a nengo_gui backend server and provides some useful information - on stdout. Also registers handlers to allow a server shutdown with Ctrl-C. + """A standalone nengo_gui backend server. + + In addition to `.BaseGUI`, this provides useful information on stdout + and registers handlers to allow a server shutdown with Ctrl-C. Parameters ---------- - model_context : nengo_gui.backend.backend.ModelContext + context : Context Model and its context served by the backend. - page_settings : nengo_gui.page.PageSettings, optional - Frontend page settings. port : int Port to listen on. password : str, optional @@ -70,15 +407,34 @@ class InteractiveGUI(BaseGUI): def start(self): protocol = 'https:' if self.server.settings.use_ssl else 'http:' - print("Starting nengo server at %s//%s:%d" % + print("Starting Nengo server at %s//%s:%d" % (protocol, 'localhost', self.server.server_port)) if not sys.platform.startswith('win'): - signal.signal(signal.SIGINT, self._confirm_shutdown) + + def immediate_shutdown(signum, frame): + raise ServerShutdown() + + def confirm_shutdown(signum, frame): + signal.signal(signal.SIGINT, immediate_shutdown) + sys.stdout.write("\nShutdown this Nengo server (y/[n])? ") + sys.stdout.flush() + rlist, _, _ = select.select([sys.stdin], [], [], 10) + if rlist: + line = sys.stdin.readline() + if line[0].lower() == 'y': + immediate_shutdown(signum, frame) + else: + print("Resuming...") + else: + print("No confirmation received. Resuming...") + signal.signal(signal.SIGINT, confirm_shutdown) + + signal.signal(signal.SIGINT, confirm_shutdown) try: self.server.serve_forever(poll_interval=0.02) - print("No connections remaining to the nengo_gui server.") + print("No connections remaining to the Nengo server.") except ServerShutdown: self.server.shutdown() finally: @@ -90,30 +446,10 @@ def start(self): if n_zombie > 0: print("%d zombie threads will close abruptly" % n_zombie) - def _confirm_shutdown(self, signum, frame): - signal.signal(signal.SIGINT, self._immediate_shutdown) - sys.stdout.write("\nShut-down this web server (y/[n])? ") - sys.stdout.flush() - rlist, _, _ = select.select([sys.stdin], [], [], 10) - if rlist: - line = sys.stdin.readline() - if line[0].lower() == 'y': - raise ServerShutdown() - else: - print("Resuming...") - else: - print("No confirmation received. Resuming...") - signal.signal(signal.SIGINT, self._confirm_shutdown) - - def _immediate_shutdown(self, signum, frame): - raise ServerShutdown() - -class GUI(InteractiveGUI): +class GUI(object): """Starts an InteractiveGUI. - Provides an easier instantiation syntax for the use in scripts. - Parameters ---------- filename : str @@ -129,19 +465,15 @@ class GUI(InteractiveGUI): Whether or not to show the editor """ def __init__(self, filename=None, model=None, locals=None, editor=True): - if not editor: - ps = nengo_gui.page.PageSettings( - editor_class=nengo_gui.components.editor.NoEditor) - else: - ps = nengo_gui.page.PageSettings() - super(GUI, self).__init__(nengo_gui.guibackend.ModelContext( - filename=filename, model=model, locals=locals), - page_settings=ps) + context = Context(filename=filename, model=model, locals=locals) + self.gui = InteractiveGUI(context, editor=editor) def start(self): + # TODO: https? t = threading.Thread( target=webbrowser.open, args=('http://localhost:%d' % self.server.server_port,)) + # TODO: daemon? does this get closed? + # t.daemon = True t.start() - - super(GUI, self).start() + self.gui.start() diff --git a/nengo_gui/guibackend.py b/nengo_gui/guibackend.py deleted file mode 100644 index 707afb71..00000000 --- a/nengo_gui/guibackend.py +++ /dev/null @@ -1,404 +0,0 @@ -"""Nengo GUI backend implementation.""" - -from __future__ import print_function - -import hashlib -import json -import logging -import mimetypes -import os -import os.path -import pkgutil -try: - from urllib.parse import unquote -except ImportError: # Python 2.7 - from urllib import unquote -import ssl -import time - -import nengo_gui -import nengo_gui.exec_env -import nengo_gui.page -from nengo_gui import server -from nengo_gui.password import checkpw - - -logger = logging.getLogger(__name__) - - -class Session(object): - __slots__ = ['creation_time', 'authenticated', 'login_host'] - - def __init__(self): - self.creation_time = time.time() - self.authenticated = False - self.login_host = None - - -class SessionExpiredError(Exception): - pass - - -class SessionManager(object): - def __init__(self, time_to_live): - self.time_to_live = time_to_live - self._sessions = {} - - def __getitem__(self, session_id): - session = self._sessions.get(session_id, None) - if (session is None or - session.creation_time + self.time_to_live < time.time()): - del self._sessions[session_id] - raise SessionExpiredError() - return session - - def __len__(self): - return len(self._sessions) - - def new_session(self, request): - session_id = self._new_session_id(request) - session = Session() - self._sessions[session_id] = session - return session_id, session - - def _new_session_id(self, request): - try: - peer = request.getpeername() # not supported on some systems - except: - logger.warning( - "Cannot get peer name. Sessions will not be tied to client.", - exc_info=True) - peer = '' - - session_id = hashlib.sha1() - session_id.update(os.urandom(16)) - for elem in peer: - if isinstance(elem, str): - elem = elem.encode('utf-8') - session_id.update(bytes(elem)) - return session_id.hexdigest() - - -class RequireAuthentication(object): - def __init__(self, login_page): - self.login_page = login_page - - def __call__(self, fn): - def auth_checked(inst): - session = inst.get_session() - has_password = inst.server.settings.password_hash is not None - if has_password and not session.authenticated: - return server.HttpRedirect(self.login_page) - return fn(inst) - return auth_checked - - -class GuiRequestHandler(server.HttpWsRequestHandler): - http_commands = { - '/': 'serve_main', - '/login': 'login_page', - '/static': 'serve_static', - '/browse': 'browse', - '/favicon.ico': 'serve_favicon', - } - - def get_expected_origins(self): - session = self.get_session() - has_password = self.server.settings.password_hash is not None - origins = [] - if not has_password: - origins.append('localhost:' + str(self.server.server_port)) - if self.server.server_port in [80, 443]: - origins.append('localhost') - elif session.login_host is not None: - return [session.login_host] - return origins - - def login_page(self): - session = self.get_session() - content = b'' - - if 'pw' in self.db: - if checkpw(self.db['pw'], self.server.settings.password_hash): - session.authenticated = True - session.login_host = self.headers.get('host', None) - else: - content += b'

Invalid password. Try again.' - content += b'

' - else: - content += b'

Please enter the password:

' - - if session.authenticated: - return server.HttpRedirect('/') - - return server.HtmlResponse(content + b''' -

- - - -

- ''') - - @RequireAuthentication('/login') - def serve_static(self): - """Handles http://host:port/static/* by returning pkg data""" - fn = os.path.join('static', self.resource) - mimetype, encoding = mimetypes.guess_type(fn) - data = pkgutil.get_data('nengo_gui', fn) - return server.HttpResponse(data, mimetype) - - @RequireAuthentication('/login') - def browse(self): - r = [b'') - return server.HtmlResponse(b''.join(r)) - - @RequireAuthentication('/login') - def serve_main(self): - if self.resource != '/': - raise server.InvalidResource(self.resource) - - filename = self.query.get('filename', [None])[0] - reset_cfg = self.query.get('reset', [False])[0] - page = self.server.create_page(filename, reset_cfg=reset_cfg) - - # read the template for the main page - html = pkgutil.get_data('nengo_gui', 'templates/page.html') - if isinstance(html, bytes): - html = html.decode("utf-8") - - # fill in the javascript needed and return the complete page - components = page.create_javascript() - data = (html % dict(components=components)).encode('utf-8') - return server.HttpResponse(data) - - def serve_favicon(self): - self.resource = '/static/favicon.ico' - return self.serve_static() - - @RequireAuthentication('/login') - def ws_default(self): - """Handles ws://host:port/viz_component with a websocket""" - # figure out what component is being connected to - - gui = self.server - uid = int(self.query['uid'][0]) - - component = gui.component_uids[uid] - while True: - try: - if component.replace_with is not None: - component.finish() - component = component.replace_with - - # read all data coming from the component - msg = self.ws.read_frame() - while msg is not None: - if not self._handle_ws_msg(component, msg): - return - msg = self.ws.read_frame() - - # send data to the component - component.update_client(self.ws) - component.page.save_config(lazy=True) - time.sleep(0.01) - except server.SocketClosedError: - # This error means the server has shut down - component.page.save_config(lazy=False) # Stop nicely - break - except: - logger.exception("Error during websocket communication.") - - # After hot loop - component.finish() - - def _handle_ws_msg(self, component, msg): - """Handle websocket message. Returns True when further messages should - be handled and false when no further messages should be processed.""" - if msg.data.startswith('config:'): - return self._handle_config_msg(component, msg) - elif msg.data.startswith('remove'): - return self._handle_remove_msg(component, msg) - else: - try: - component.message(msg.data) - return True - except: - logging.exception('Error processing: %s', repr(msg.data)) - - def _handle_config_msg(self, component, msg): - cfg = json.loads(msg.data[7:]) - old_cfg = {} - for k in component.config_defaults.keys(): - v = getattr( - component.page.config[component], k) - old_cfg[k] = v - if not cfg == old_cfg: - # Register config change to the undo stack - component.page.config_change( - component, cfg, old_cfg) - for k, v in cfg.items(): - setattr( - component.page.config[component], - k, v) - component.page.modified_config() - return True - - def _handle_remove_msg(self, component, msg): - if msg.data != 'remove_undo': - # Register graph removal to the undo stack - component.page.remove_graph(component) - component.page.remove_component(component) - component.page.modified_config() - return False - - def get_session(self): - try: - session_id = self.cookie['_session_id'].value - session = self.server.sessions[session_id] - except KeyError: - session_id, session = self.server.sessions.new_session( - self.request) - except SessionExpiredError: - session_id, session = self.server.sessions.new_session( - self.request) - - self.cookie['_session_id'] = session_id - return session - - def log_message(self, format, *args): - logger.info(format, *args) - - -class ModelContext(object): - """Provides context information to a model. This can include the locals - dictionary, the filename and whether this model can (or is allowed) to be - written to disk.""" - - __slots__ = ['model', 'filename', 'locals', 'writeable'] - - def __init__(self, model=None, locals=None, filename=None, writeable=True): - self.filename = filename - if self.filename is not None: - try: - self.filename = os.path.relpath(filename) - except ValueError: - # happens on Windows if filename is on a different - # drive than the current directory - self.filename = filename - self.writeable = writeable - else: - self.writeable = False - - if model is None and locals is not None: - model = locals.get('model', None) - - if model is None and filename is None: - raise ValueError("No model.") - - self.model = model - self.locals = locals - - -class GuiServerSettings(object): - __slots__ = [ - 'listen_addr', 'auto_shutdown', 'password_hash', 'ssl_cert', 'ssl_key', - 'session_duration'] - - def __init__( - self, listen_addr=('localhost', 8080), auto_shutdown=2, - password_hash=None, ssl_cert=None, ssl_key=None, - session_duration=60 * 60 * 24 * 30): - self.listen_addr = listen_addr - self.auto_shutdown = auto_shutdown - self.password_hash = password_hash - self.ssl_cert = ssl_cert - self.ssl_key = ssl_key - self.session_duration = session_duration - - @property - def use_ssl(self): - if self.ssl_cert is None and self.ssl_key is None: - return False - elif self.ssl_cert is not None and self.ssl_key is not None: - return True - else: - raise ValueError("SSL needs certificate file and key file.") - - -class GuiServer(server.ManagedThreadHttpServer): - def __init__( - self, model_context, server_settings=GuiServerSettings(), - page_settings=nengo_gui.page.PageSettings()): - if nengo_gui.exec_env.is_executing(): - raise nengo_gui.exec_env.StartedGUIException() - self.settings = server_settings - - server.ManagedThreadHttpServer.__init__( - self, self.settings.listen_addr, GuiRequestHandler) - if self.settings.use_ssl: - self.socket = ssl.wrap_socket( - self.socket, certfile=self.settings.ssl_cert, - keyfile=self.settings.ssl_key, server_side=True) - - self.sessions = SessionManager(self.settings.session_duration) - - # the list of running Pages - self.pages = [] - - # a mapping from uids to Components for all running Pages. - # this is used to connect the websockets to the appropriate Component - self.component_uids = {} - - self.model_context = model_context - self.page_settings = page_settings - - self._last_access = time.time() - - def create_page(self, filename, reset_cfg=False): - """Create a new Page with this configuration""" - page = nengo_gui.page.Page( - self, filename=filename, settings=self.page_settings, - reset_cfg=reset_cfg) - self.pages.append(page) - return page - - def remove_page(self, page): - self._last_access = time.time() - self.pages.remove(page) - if (not self._shutting_down and self.settings.auto_shutdown > 0 and - len(self.pages) <= 0): - time.sleep(self.settings.auto_shutdown) - earliest_shutdown = self._last_access + self.settings.auto_shutdown - if earliest_shutdown < time.time() and len(self.pages) <= 0: - logging.info( - "No connections remaining to the nengo_gui server.") - self.shutdown() diff --git a/nengo_gui/ipython.py b/nengo_gui/ipython.py index e894b8d1..f264bb3d 100644 --- a/nengo_gui/ipython.py +++ b/nengo_gui/ipython.py @@ -7,15 +7,12 @@ import uuid import warnings import weakref -try: - from urllib.request import urlopen -except ImportError: - from urllib2 import urlopen from IPython import get_ipython from IPython.display import display, HTML -import nengo_gui +from nengo_gui.compat import urlopen +from nengo_gui.gui import BaseGUI, Context, ServerSettings class ConfigReuseWarning(UserWarning): @@ -66,16 +63,13 @@ def start_server(cls, cfg, model): cls.threads[page.filename_cfg] = server_thread name = model.label - server_settings = nengo_gui.guibackend.GuiServerSettings( - ('localhost', 0)) - model_context = nengo_gui.guibackend.ModelContext( - model=model, locals=get_ipython().user_ns, filename=name, - writeable=False) - page_settings = nengo_gui.page.PageSettings( - filename_cfg=cfg, - editor_class=nengo_gui.components.editor.NoEditor) - server = nengo_gui.gui.BaseGUI( - model_context, server_settings, page_settings) + server_settings = ServerSettings(listen_addr=('localhost', 0)) + context = Context(filename_cfg=cfg, + model=model, + locals=get_ipython().user_ns, + filename=name, + writeable=False) + server = BaseGUI(context, server_settings, editor=False) server_thread = threading.Thread(target=server.start) server_thread.start() cls.servers[cfg] = server diff --git a/nengo_gui/main.py b/nengo_gui/main.py index 36c7ccf8..93a438dc 100644 --- a/nengo_gui/main.py +++ b/nengo_gui/main.py @@ -6,8 +6,8 @@ import nengo_gui import nengo_gui.gui -from nengo_gui.guibackend import ModelContext, GuiServerSettings -from nengo_gui.password import gensalt, hashpw, prompt_pw +from nengo_gui.gui import Context, ServerSettings +from nengo_gui.server.auth import gensalt, hashpw, prompt_pw def old_main(): @@ -39,7 +39,8 @@ def main(): default='nengo', type=str, help='default backend to use') parser.add_argument('--browser', dest='browser', action='store_true') parser.add_argument('--no-browser', dest='browser', action='store_false') - parser.add_argument('--auto-shutdown', nargs=1, type=float, + parser.add_argument( + '--auto-shutdown', nargs=1, type=float, help="Time limit before automatic shutdown. Set to 0 to deactivate.", default=[2]) parser.set_defaults(browser=True) @@ -55,13 +56,13 @@ def main(): password = hashpw(prompt_pw(), gensalt()) else: password = hashpw(args.password, gensalt()) - server_settings = GuiServerSettings( + server_settings = ServerSettings( ('', 8080), args.auto_shutdown[0], password_hash=password, ssl_cert=args.cert[0], ssl_key=args.key[0]) if not server_settings.use_ssl: raise ValueError("Password protection only allowed with SSL.") else: - server_settings = GuiServerSettings( + server_settings = ServerSettings( ('localhost', 8080), args.auto_shutdown[0], ssl_cert=args.cert[0], ssl_key=args.key[0]) @@ -71,10 +72,8 @@ def main(): nengo_gui.__path__[0], 'examples', 'default.py') else: filename = args.filename - page_settings = nengo_gui.page.PageSettings(backend=args.backend) s = nengo_gui.gui.InteractiveGUI( - ModelContext(filename=filename), server_settings, - page_settings=page_settings) + Context(filename=filename, backend=args.backend), server_settings) s.server.auto_shutdown = args.auto_shutdown[0] if args.browser: @@ -90,5 +89,6 @@ def main(): finally: logging.shutdown() + if __name__ == '__main__': main() diff --git a/nengo_gui/modal_js.py b/nengo_gui/modal_js.py index 6c9f43e7..47d67b51 100644 --- a/nengo_gui/modal_js.py +++ b/nengo_gui/modal_js.py @@ -2,13 +2,12 @@ import json -import numpy as np - import nengo from .static_plots import tuning_curve_plot, response_curve_plot from .static_plots import node_output_plot + def infomodal(ng, uid, **args): obj = ng.uids[uid] if isinstance(obj, nengo.Ensemble): @@ -20,14 +19,18 @@ def infomodal(ng, uid, **args): else: raise NotImplementedError() + def add_modal_title_js(title_text): - return 'Nengo.modal.title("%s");' % (title_text) + return 'nengo.modal.title("%s");' % (title_text) + def add_modal_footer_js(footer_text): - return 'Nengo.modal.footer("%s");' % (footer_text) + return 'nengo.modal.footer("%s");' % (footer_text) + def show_modal_js(): - return 'Nengo.modal.show();' + return 'nengo.modal.show();' + def ensemble_infomodal(ng, uid, conn_in_uids, conn_out_uids): ens = ng.uids[uid] @@ -47,13 +50,14 @@ def ensemble_infomodal(ng, uid, conn_in_uids, conn_out_uids): conninfo = conn_infomodal(ng, uid, conn_in_uids, conn_out_uids) - js = ['Nengo.modal.title("Details for \'%s\'");' % ng.page.get_label(ens)] - js.append('Nengo.modal.footer("close");') - js.append('Nengo.modal.ensemble_body("%s", %s, %s, %s);' % ( + js = ['nengo.modal.title("Details for \'%s\'");' % ng.page.get_label(ens)] + js.append('nengo.modal.footer("close");') + js.append('nengo.modal.ensemble_body("%s", %s, %s, %s);' % ( uid, json.dumps(params), json.dumps(plots), json.dumps(conninfo))) - js.append('Nengo.modal.show();') + js.append('nengo.modal.show();') return '\n'.join(js) + def node_infomodal(ng, uid, conn_in_uids, conn_out_uids): node = ng.uids[uid] @@ -67,7 +71,7 @@ def node_infomodal(ng, uid, conn_in_uids, conn_out_uids): js = [add_modal_title_js("Details for \'%s\'" % ( ng.page.get_label(node)))] js.append(add_modal_footer_js('close')) - js.append('Nengo.modal.node_body("%s", %s, %s, %s);' % ( + js.append('nengo.modal.node_body("%s", %s, %s, %s);' % ( uid, json.dumps(params), json.dumps(plots), json.dumps(conninfo))) js.append(show_modal_js()) return '\n'.join(js) @@ -142,7 +146,7 @@ def net_infomodal(ng, uid, conn_in_uids, conn_out_uids): js = [add_modal_title_js("Details for \'%s\'") % ( ng.page.get_label(net))] js.append(add_modal_footer_js('close')) - js.append('Nengo.modal.net_body("%s", %s, %s);' % ( + js.append('nengo.modal.net_body("%s", %s, %s);' % ( uid, json.dumps(stats), json.dumps(conninfo))) js.append(show_modal_js()) return '\n'.join(js) diff --git a/nengo_gui/namefinder.py b/nengo_gui/namefinder.py deleted file mode 100644 index 611d3690..00000000 --- a/nengo_gui/namefinder.py +++ /dev/null @@ -1,51 +0,0 @@ -import nengo - -class NameFinder(object): - def __init__(self, terms, net): - self.base_terms = terms - self.known_name = {} - for k, v in terms.items(): - if not k.startswith('_'): - try: - self.known_name[v] = k - except TypeError: - pass - self.find_names(net) - - def find_names(self, net): - net_name = self.known_name[net] - - base_lists = ['ensembles', 'nodes', 'connections', 'networks'] - all_lists = ['all_ensembles', 'all_nodes', 'all_connections', - 'all_networks', 'all_objects', 'all_probes'] - - classes = (nengo.Node, nengo.Ensemble, nengo.Network, - nengo.Connection) - - for inst_attr in dir(net): - private = inst_attr.startswith('_') - in_lists = inst_attr in base_lists + all_lists - if not private and not in_lists: - attr = getattr(net, inst_attr) - if isinstance(attr, list): - for i, obj in enumerate(attr): - if obj not in self.known_name: - n = '%s.%s[%d]' % (net_name, inst_attr, i) - self.known_name[obj] = n - elif isinstance(attr, classes): - if attr not in self.known_name: - self.known_name[attr] = '%s.%s' % (net_name, inst_attr) - - - for obj_type in base_lists: - for i, obj in enumerate(getattr(net, obj_type)): - name = self.known_name.get(obj, None) - if name is None: - name = '%s.%s[%d]' % (net_name, obj_type, i) - self.known_name[obj] = name - - for n in net.networks: - self.find_names(n) - - def name(self, obj): - return self.known_name[obj] diff --git a/nengo_gui/netgraph.py b/nengo_gui/netgraph.py new file mode 100644 index 00000000..524b4d35 --- /dev/null +++ b/nengo_gui/netgraph.py @@ -0,0 +1,951 @@ +import logging +import os +import threading +import traceback +import warnings +from timeit import default_timer + +import json +import nengo +from nengo.utils.compat import iteritems + +from nengo_gui import components, exec_env, user_action +from nengo_gui.client import bind, ExposedToClient +from nengo_gui.components import Component, Network, Position +from nengo_gui.config import NengoGUIConfig, upgrade +from nengo_gui.editor import AceEditor +from nengo_gui.exceptions import StartedGUIException, StartedSimulatorException +from nengo_gui.layout import Layout +from nengo_gui.modal_js import infomodal +from nengo_gui.simcontrol import SimControl +from nengo_gui.threads import RepeatedThread + +logger = logging.getLogger(__name__) + + +class ComponentManager(object): + NENGO_MAP = { + nengo.Connection: components.Connection, + nengo.Ensemble: components.Ensemble, + nengo.Network: components.Network, + nengo.Node: components.Node, + } + + def __init__(self, client, save_period=2.0): + self.client = client + self.save_period = save_period # minimum time between saves + + self.by_uid = {} + self._components = [] + self.dirty = False + self.last_save = (None, None) + + # TODO: is lock necessary? Throttle save function? + self.save_lock = threading.Lock() + + def __iter__(self): + return iter(self._components) + + def __len__(self): + return len(self._components) + + def add(self, component): + """Add a new Component to an existing Page.""" + self.by_uid[component.uid] = component + self._components.append(component) + + # def create_javascript(self): + # """Generate the javascript for the current network and layout.""" + # assert isinstance(self._components[0], SimControl) + # main = (NetGraph, SimControl, AceEditor) + + # main_js = '\n'.join([c.javascript() for c in self._components + # if isinstance(c, main)]) + # component_js = '\n'.join([c.javascript() for c in self._components + # if not isinstance(c, main)]) + # if not self.context.writeable: + # component_js += "$('#Open_file_button').addClass('deactivated');" + # return main_js, component_js + + def config_change(self, component, new_cfg, old_cfg): + act = ConfigAction(self, + component=component, + new_cfg=new_cfg, + old_cfg=old_cfg) + self.undo_stack.append([act]) + + def create(self): + for comp in self._components: + if comp.uid != "model": + comp.create() + + def load(self, filename, model, locals): + """Load from a JSON file""" + + if filename is not None and filename.endswith(".py.cfg"): + old_filename = filename + filename = "%s.json" % (old_filename[:-len(".py.cfg")],) + if os.path.exists(old_filename): + print("Upgrading %r to new format, %r" + % (old_filename, filename)) + with open(old_filename, "r") as fp: + old_text = fp.read() + new_text = upgrade(old_text, locals=locals) + with open(filename, "w") as fp: + fp.write(new_text) + # TODO: safer remove + os.remove(old_filename) + + config = {} + if filename is not None and os.path.exists(filename): + with open(filename, "r") as fp: + config = json.load(fp) + + if "model" not in config and model is not None: + self.add(Network( + self.client, model, "model", pos=Position(0, 0, 1.0, 1.0))) + + for obj, kwargs in iteritems(config): + cls = getattr(components, kwargs.pop("cls")) + if "obj" in kwargs: + # For most components + kwargs["obj"] = eval(kwargs["obj"], locals) + else: + # For components corresponding to Nengo objects + kwargs["obj"] = eval(obj, locals) + self.add(cls(client=self.client, uid=obj, **kwargs)) + + self.dirty = False + self.last_save = (filename, None) + + def remove_graph(self, component): + self.undo_stack.append([ + RemoveGraph(self.net_graph, component, self.names.uid(component))]) + + def remove(self, component): + """Remove a component from the layout.""" + del self.by_uid[component.uid] + # self.remove_uid(component.uid) + self._components.remove(component) + + def remove_uid(self, uid): + """Remove a generated uid (for when a component is removed).""" + if uid in self.locals: + obj = self.locals[uid] + del self.locals[uid] + del self.names[obj] + else: + warnings.warn("remove_uid called on unknown uid: %s" % uid) + + def save(self, filename, names, force=False): + """Write the .cfg file to disk. + + Parameters + ---------- + force : bool + If True, then always save right now, even if dirty. + """ + if filename != self.last_save[0]: + force = True + + # TODO: look into how to actually determine dirtiness, + # is it worth keeping track? + if not force and not self.dirty: + return + + now = default_timer() + if not force and self.last_save[1] is not None: + if (now - self.last_save) < self.save_period: + return + + with self.save_lock: + self.last_save = now + self.dirty = False + try: + with open(filename, 'w') as f: + # TODO: handle unicode correctly + json.dump(self.by_uid, f, + cls=NengoGUIConfig, + indent=2, + sort_keys=True) + except IOError: + print("Could not save %s; permission denied" % (filename,)) + + def update(self, locals, namefinder): + # Add any components from locals + for name, obj in iteritems(locals): + if isinstance(obj, components.Component): + self.add(obj) + # TODO: attach? + # obj.attach(page=self, config=self.config.cfg[name], uid=name) + + # Make components for Nengo objects not in locals + for obj, uid in iteritems(namefinder.names): + if uid in self.by_uid: + continue + + if isinstance(obj, nengo.Connection): + self.add(components.Connection( + self.client, obj, uid, namefinder)) + elif isinstance(obj, tuple(self.NENGO_MAP)): + for nengocls, compcls in iteritems(self.NENGO_MAP): + if isinstance(obj, nengocls): + self.add(compcls(self.client, obj, name)) + break + + +class LiveContext(object): + def __init__(self, client, model=None, locals=None): + self.client = client + self.model = model + self.locals = locals + if locals is not None: + self.locals = locals.copy() + self.code = None # the code for the network + self.filename = None # filename corresponding to code + + @property + def code(self): + return self._code + + @code.setter + def code(self, val): + if val is not None and val != self._code: + self.client.dispatch("editor.set_code", code=val) + self._code = val + + def execute(self, code): + """Run the given code to generate self.network and self.locals. + + The code will be stored in self.code, any output to stdout will + be sent to the client. + """ + errored = False + + newlocals = {} + + if self.filename is not None: + newlocals['__file__'] = self.filename + + # Clear any existing errors + self.client.dispatch("editor.stderr", output=None, line=None) + + env = exec_env.ExecutionEnvironment(self.filename) + try: + with env: + compiled = compile(code, self.filename, mode='exec') + exec(compiled, newlocals) + # TODO: Should we actually let these warn and continue? + except StartedSimulatorException: + line = exec_env.determine_line(self.filename) + env.stdout.write('Warning: Simulators cannot be manually ' + 'run inside nengo_gui (line %d)\n' % line) + except StartedGUIException: + line = exec_env.determine_line(self.filename) + env.stdout.write('Warning: nengo_gui cannot be run inside ' + 'nengo_gui (line %d)\n' % line) + except Exception: + self.client.dispatch("editor.stderr", + output=traceback.format_exc(), + line=exec_env.determine_line(self.filename)) + errored = True + self.client.dispatch("editor.stdout", + output=env.stdout.getvalue(), + line=None) + + # make sure we've defined a nengo.Network + self.model = newlocals.get('model', None) + if not isinstance(self.model, nengo.Network): + if not errored: + self.client.dispatch( + "editor.stderr", + output="Must declare a nengo.Network called 'model'", + line=None) + errored = True + self.model = None + + self.code = code # TODO: do we definitely want to set code here? + if not errored: + self.locals = newlocals + + def load(self, filename, force=False): + if self.filename == filename and not force: + raise ValueError("That file is already loaded") + try: + with open(filename) as f: + code = f.read() + self.filename = filename + except IOError as e: + logger.error("IOError loading %r: %s", filename, e) + code = '' + self.filename = None + + if code != self.code: + self.execute(code) + + +class NameFinder(object): + + CLASSES = nengo.Node, nengo.Ensemble, nengo.Network, nengo.Connection + TYPELISTS = 'ensembles', 'nodes', 'connections', 'networks' + NETIGNORE = ('all_ensembles', 'all_nodes', 'all_connections', + 'all_networks', 'all_objects', 'all_probes') + TYPELISTS + + def __init__(self, autoprefix="_viz_"): + self.names = {} + self.autoprefix = autoprefix + self.autocount = 0 + + def __contains__(self, obj): + return obj in self.names + + def __getitem__(self, obj): + return self.names[obj] + + def add(self, obj): + """Add this object to the name finder and return its name. + + This is used for new Components being created (so they can have + a unique identifier in the .cfg file). + """ + name = '%s%d' % (self.autoprefix, self.autocount) + used_names = list(self.names.values()) + while name in used_names: + self.autocount += 1 + name = '%s%d' % (self.autoprefix, self.autocount) + self.names[obj] = name + return name + + def update(self, names): + nets = [] + for k, v in iteritems(names): + if not k.startswith('_'): + try: + self.names[v] = k + if isinstance(v, nengo.Network): + nets.append(v) + except TypeError: + pass + + if len(nets) > 1: + logger.info("More than one top-level model defined.") + + for net in nets: + self._parse_network(net) + + def _parse_network(self, net): + net_name = self.names.get(net, None) + for inst_attr in dir(net): + private = inst_attr.startswith('_') + if not private and inst_attr not in self.NETIGNORE: + attr = getattr(net, inst_attr) + if isinstance(attr, list): + for i, obj in enumerate(attr): + if obj not in self.names: + n = '%s.%s[%d]' % (net_name, inst_attr, i) + self.names[obj] = n + elif isinstance(attr, self.CLASSES): + if attr not in self.names: + self.names[attr] = '%s.%s' % (net_name, inst_attr) + + for obj_type in self.TYPELISTS: + for i, obj in enumerate(getattr(net, obj_type)): + name = self.names.get(obj, None) + if name is None: + name = '%s.%s[%d]' % (net_name, obj_type, i) + self.names[obj] = name + + for n in net.networks: + self._parse_network(n) + + +class NetGraph(ExposedToClient): + """Handles computations and communications for NetGraph on the JS side. + + Communicates to all NetGraph components for creation, deletion and + manipulation. + """ + + RELOAD_EVERY = 0.5 # How often to poll for reload + + def __init__(self, client, filename, filename_cfg, + model=None, locals=None): + super(NetGraph, self).__init__(client) + + self.lock = threading.Lock() + + self.layout = None + + self.networks_to_search = [] + + self.undo_stack = [] + self.redo_stack = [] + + self.uids = {} + self.parents = {} + + self.context = LiveContext(client, model, locals) + self.components = ComponentManager(client) + self.names = NameFinder() + + self.load(filename, filename_cfg, force=True) + + self.filethread = RepeatedThread(self.RELOAD_EVERY, self._check_file) + self.filethread.start() # TODO: defer until after load? + + # TODO: These should be done as part of loading the model + + # def attach(self, page, config): + # super(NetGraph, self).attach(page, config) + # self.layout = Layout(page.net.obj) + # self.to_be_expanded.append(page.net.obj) + # self.networks_to_search.append(page.net.obj) + + # try: + # self.last_modify_time = os.path.getmtime(page.net.filename) + # except (OSError, TypeError): + # self.last_modify_time = None + + @property + def filename(self): + return self.context.filename + + @property + def model(self): + return self.context.model + + def set_editor_code(self, code): + self.client.dispatch("editor.code", code=code) + + def add_nengo_objects(self): + for c in self.components: + c.add_nengo_objects(self.context.model) + + def remove_nengo_objects(self): + for c in self.components: + c.remove_nengo_objects(self.context.model) + # TODO: add checks to make sure everything's been removed + + def _check_file(self): + if self.context.filename is not None: + try: + t = os.path.getmtime(self.context.filename) + if self.last_modify_time is None or self.last_modify_time < t: + self.reload() + self.last_modify_time = t + except OSError: + pass + + # TODO: Shouldn't be necessary.. + # with self.lock: + # new_code = self.new_code + # # the lock is in case update_code() is called between these lines + # self.new_code = None + + # if new_code is not None: + # self.reload(code=new_code) + + def load(self, filename, filename_cfg, force=False): + # Load the .py file + self.context.load(filename, force=force) + # Load the .cfg file + self.components.load(filename_cfg, + self.context.model, + self.context.locals) + + # Figure out good names for objects + self.names.update(self.context.locals) + + # Add everything to the component manager + self.components.update(self.context.locals, self.names) + + # if len(self.to_be_expanded) > 0: + # with self.page.lock: + # network = self.to_be_expanded.popleft() + # self.expand_network(network, client) + + def reload(self, code=None): + """Called when new code has been detected + checks that the page is not currently being used + and thus can be updated""" + with self.lock: + if code is None: + with open(self.context.filename) as f: + code = f.read() + self._reload(code=code) + + def _reload(self, code): + if self.context.code == code: + # don't re-execute the identical code + return + else: + # send the new code to the client + self._set_editor_code(code) + + # Do the load step with new objects + context = LiveContext(self.context.filename, None, None) + components = ComponentManager() + names = NameFinder() + + context.load(context.filename, force=True) + # TODO: fix + components.load(context.model, context.locals) + names.update(context.locals) + components.update(context.locals, names) + + # Go through the items in the newly loaded object. + for comp in components: + # If the uid matches an old one, process it + if comp.uid in self.components: + oldcomp = self.components.by_uid[comp.uid] + if not comp.similar(oldcomp): + # Vastly different object, so remove old, add new. + oldcomp.delete(self.client) + comp.create(self.client) + else: + # TODO: or other way around? This makes more sense though + oldcomp.update(comp, self.client) + # That component has now been processed, so remove it + self.components.remove(oldcomp) + + # Otherwise it's new so add it + else: + comp.create(self.client) + + # Any old items not yet processed should be deleted + # NB: Copy list before iterating because it will change size + for oldcomp in list(self.components): + self.components.remove(oldcomp) + + # The client should now be updated, so replace internals + self.context = context + self.components = components + self.names = names + + # TODO: this should be done now? Maybe? Go through + def _reload(self, code): + """Loads and executes the code, removing old items, + updating changed items and adding new ones""" + + # TODO: ??? + old_locals = self.page.last_good_locals + old_default_labels = self.page.default_labels + + if self.context.code == code: + # don't re-execute the identical code + return + else: + # send the new code to the client + self._set_editor_code(code) + + self.page.execute(code) + + if self.page.error is not None: + return + + name_finder = NameFinder(self.page.locals, self.page.model) + + self.networks_to_search = [self.page.model] + self.parents = {} + + removed_uids = {} + rebuilt_objects = [] + + # for each item in the old model, find the matching new item + # for Nodes, Ensembles, and Networks, this means to find the item + # with the same uid. For Connections, we don't really have a uid, + # so we use the uids of the pre and post objects. + for uid, old_item in nengo.utils.compat.iteritems(dict(self.uids)): + try: + new_item = eval(uid, self.page.locals) + except: + new_item = None + + # check to make sure the new item's uid is the same as the + # old item. This is to catch situations where an old uid + # happens to still refer to something in the new model, but that's + # not the normal uid for that item. For example, the uid + # "ensembles[0]" might still refer to something even after that + # ensemble is removed. + new_uid = name_finder[new_item] + if new_uid != uid: + new_item = None + + same_class = False + for cls in (nengo.Ensemble, nengo.Node, + nengo.Network, nengo.Connection): + if isinstance(new_item, cls) and isinstance(old_item, cls): + same_class = True + break + + # find reasons to delete the object. Any deleted object will + # be recreated, so try to keep this to a minimum + keep_object = True + if new_item is None: + keep_object = False + elif not same_class: + # don't allow changing classes + keep_object = False + elif (self.get_extra_info(new_item) != + self.get_extra_info(old_item)): + keep_object = False + + if not keep_object: + self.to_be_sent.append(dict( + type='remove', uid=uid)) + del self.uids[uid] + removed_uids[old_item] = uid + rebuilt_objects.append(uid) + else: + # fix aspects of the item that may have changed + if self._reload_update_item(uid, old_item, new_item, + name_finder): + # something has changed about this object, so rebuild + # the components that use it + rebuilt_objects.append(uid) + + self.uids[uid] = new_item + + # TODO: just call expand geez + self.to_be_expanded.append(self.page.model) + + self.page.name_finder = name_finder + self.page.default_labels = name_finder.known_name + self.page.config = self.page.load_config() + self.page.uid_prefix_counter = {} + self.layout = Layout(self.page.model) + self.page.code = code + + orphan_components = [] + rebuild_components = [] + + # items that are shown in components, but not currently displayed + # in the NetGraph (i.e. things that are inside collapsed + # Networks, but whose values are being shown in a graph) + collapsed_items = [] + + # remove graphs no longer associated to NetgraphItems + removed_items = list(removed_uids.values()) + for c in self.page.components[:]: + for item in c.code_python_args(old_default_labels): + if item not in self.uids and item not in collapsed_items: + + # item is a python string that is an argument to the + # constructor for the Component. So it could be 'a', + # 'model.ensembles[3]', 'True', or even 'target=a'. + # We need to evaluate this string in the context of the + # locals dictionary and see what object it refers to + # so we can determine whether to rebuild this component. + # + # The following lambda should do this, handling both + # the normal argument case and the keyword argument case. + safe_eval = ('(lambda *a, **b: ' + 'list(a) + list(b.values()))(%s)[0]') + + # this Component depends on an item inside a collapsed + # Network, so we need to check if that component has + # changed or been removed + old_obj = eval(safe_eval % item, old_locals) + + try: + new_obj = eval(safe_eval % item, self.page.locals) + except: + # the object this Component depends on no longer exists + new_obj = None + + if new_obj is None: + removed_items.append(item) + elif not isinstance(new_obj, old_obj.__class__): + rebuilt_objects.append(item) + elif (self.get_extra_info(new_obj) != + self.get_extra_info(old_obj)): + rebuilt_objects.append(item) + + # add this to the list of collapsed items, so we + # don't recheck it if there's another Component that + # also depends on this + collapsed_items.append(item) + + if item in rebuilt_objects: + self.to_be_sent.append(dict(type='delete_graph', + uid=c.original_id, + notify_server=False)) + rebuild_components.append(c.uid) + self.page.components.remove(c) + break + else: + for item in c.code_python_args(old_default_labels): + if item in removed_items: + self.to_be_sent.append(dict(type='delete_graph', + uid=c.original_id, + notify_server=False)) + orphan_components.append(c) + break + + components = [] + # the old names for the old components + component_uids = [c.uid for c in self.page.components] + + for name, obj in list(self.page.locals.items()): + if isinstance(obj, Component): + # the object has been removed, so the Component should + # be removed as well + if obj in orphan_components: + continue + + # this is a Component that was previously removed, + # but is still in the config file, or it has to be + # rebuilt, so let's recover it + if name not in component_uids: + self.page.components.add(obj, attach=True) + self.to_be_sent.append(dict(type='js', + code=obj.javascript())) + components.append(obj) + continue + + # otherwise, find the corresponding old Component + index = component_uids.index(name) + old_component = self.page.components[index] + if isinstance(obj, (SimControl, AceEditor, NetGraph)): + # just keep these ones + components.append(old_component) + else: + # replace these components with the newly generated ones + try: + self.page.components.add(obj, attach=True) + old_component.replace_with = obj + obj.original_id = old_component.original_id + except: + traceback.print_exc() + print('failed to recreate plot for %s' % obj) + components.append(obj) + + components.sort(key=lambda x: x.component_order) + + self.page.components = components + + # notifies SimControl to pause the simulation + self.page.changed = True + + def _reload_update_item(self, uid, old_item, new_item, new_name_finder): + """Tell the client about changes to the item due to reload.""" + changed = False + if isinstance(old_item, (nengo.Node, + nengo.Ensemble, + nengo.Network)): + old_label = self.page.names.label(old_item) + new_label = new_name_finder.label(new_item) + + if old_label != new_label: + self.to_be_sent.append(dict( + type='rename', uid=uid, name=new_label)) + changed = True + if isinstance(old_item, nengo.Network): + # TODO: just call expand geez + if self.page.config[old_item].expanded: + self.to_be_expanded.append(new_item) + changed = True + + elif isinstance(old_item, nengo.Connection): + # if new_pre != old_pre or new_post != old_post: + # # if the connection has changed, tell javascript + # pres = self.get_parents( + # new_pre, + # default_labels=new_name_finder.known_name)[:-1] + # posts = self.get_parents( + # new_post, + # default_labels=new_name_finder.known_name)[:-1] + # self.to_be_sent.append(dict( + # type='reconnect', uid=uid, + # pres=pres, posts=posts)) + # changed = True + pass + + return changed + + # def get_parents(self, uid, default_labels=None): + # while uid not in self.parents: + # net = self.networks_to_search.pop(0) + # net_uid = self.page.names.uid(net, names=default_labels) + # for n in net.nodes: + # n_uid = self.page.names.uid(n, names=default_labels) + # self.parents[n_uid] = net_uid + # for e in net.ensembles: + # e_uid = self.page.names.uid(e, names=default_labels) + # self.parents[e_uid] = net_uid + # for n in net.networks: + # n_uid = self.page.names.uid(n, names=default_labels) + # self.parents[n_uid] = net_uid + # self.networks_to_search.append(n) + # parents = [uid] + # while parents[-1] in self.parents: + # parents.append(self.parents[parents[-1]]) + # return parents + + @bind("netgraph.undo") + def undo(self): + if self.page.undo_stack: + action = self.page.undo_stack.pop() + re = [] + for act in action: + act.undo() + re.insert(0, act) + self.page.redo_stack.append(re) + + @bind("netgraph.redo") + def redo(self): + if self.page.redo_stack: + action = self.page.redo_stack.pop() + un = [] + for act in action: + act.apply() + un.insert(0, act) + self.page.undo_stack.append(un) + + @bind("netgraph.expand") + def act_expand(self, uid): + net = self.uids[uid] + # TODO: just call expand geez + self.to_be_expanded.append(net) + self.page.config[net].expanded = True + self.components.dirty = True + + @bind("netgraph.collapse") + def act_collapse(self, uid): + net = self.uids[uid] + self.page.config[net].expanded = False + self.remove_uids(net) + self.components.dirty = True + + def remove_uids(self, net): + for items in [net.ensembles, net.networks, net.nodes, net.connections]: + for item in items: + uid = self.page.names.uid(item) + if uid in self.uids: + del self.uids[uid] + for n in net.networks: + self.remove_uids(n) + + @bind("netgraph.pan") + def act_pan(self, x, y): + self.page.config[self.page.model].pos = x, y + self.components.dirty = True + + @bind("netgraph.zoom") + def act_zoom(self, scale, x, y): + self.page.config[self.page.model].size = scale, scale + self.page.config[self.page.model].pos = x, y + self.components.dirty = True + + @bind("netgraph.create_modal") + def act_create_modal(self, uid, **info): + js = infomodal(self, uid, **info) + self.to_be_sent.append(dict(type='js', code=js)) + + @bind("netgraph.action") + def action(self, action, **kwargs): + if action == "expand": + act = user_action.ExpandCollapse(self, expand=True, **kwargs) + elif action == "collapse": + act = user_action.ExpandCollapse(self, expand=False, **kwargs) + elif action == "create_graph": + act = user_action.CreateGraph(self, **kwargs) + elif action == "pos": + act = user_action.Pos(self, **kwargs) + elif action == "size": + act = user_action.Size(self, **kwargs) + elif action == "pos_size": + act = user_action.PosSize(self, **kwargs) + elif action == "feedforward_layout": + act = user_action.FeedforwardLayout(self, **kwargs) + elif action == "config": + act = user_action.ConfigAction(self, **kwargs) + else: + act = user_action.Action(self, **kwargs) + + self.undo_stack.append([act]) + del self.redo_stack[:] + + def expand_network(self, network, client): + if not self.page.config[network].has_layout: + pos = self.layout.make_layout(network) + for obj, layout in pos.items(): + self.page.config[obj].pos = layout['y'], layout['x'] + self.page.config[obj].size = layout['h'] / 2, layout['w'] / 2 + self.page.config[network].has_layout = True + + if network is self.page.model: + parent = None + else: + parent = self.page.names.uid(network) + for ens in network.ensembles: + self.create_object(client, ens, type='ens', parent=parent) + for node in network.nodes: + self.create_object(client, node, type='node', parent=parent) + for net in network.networks: + self.create_object(client, net, type='net', parent=parent) + for conn in network.connections: + self.create_connection(client, conn, parent=parent) + self.page.config[network].expanded = True + + def create_object(self, client, obj, type, parent): + uid = self.page.names.uid(obj) + if uid in self.uids: + return + + pos = self.page.config[obj].pos + if pos is None: + import random + pos = random.uniform(0, 1), random.uniform(0, 1) + self.page.config[obj].pos = pos + size = self.page.config[obj].size + if size is None: + size = (0.1, 0.1) + self.page.config[obj].size = size + label = self.page.names.label(obj) + self.uids[uid] = obj + info = dict(uid=uid, label=label, pos=pos, type=type, size=size, + parent=parent) + if type == 'net': + info['expanded'] = self.page.config[obj].expanded + info.update(self.get_extra_info(obj)) + + client.write_text(json.dumps(info)) + + def create_connection(self, client, conn, parent): + uid = self.page.names.uid(conn) + if uid in self.uids: + return + pre = conn.pre_obj + if isinstance(pre, nengo.ensemble.Neurons): + pre = pre.ensemble + post = conn.post_obj + if isinstance(post, nengo.connection.LearningRule): + post = post.connection.post + if isinstance(post, nengo.base.ObjView): + post = post.obj + if isinstance(post, nengo.ensemble.Neurons): + post = post.ensemble + pre = self.page.names.uid(pre) + post = self.page.names.uid(post) + self.uids[uid] = conn + pres = self.get_parents(pre)[:-1] + posts = self.get_parents(post)[:-1] + info = dict(uid=uid, pre=pres, post=posts, type='conn', parent=parent) + client.write_text(json.dumps(info)) + + @bind("netgraph.request_update") + def update_client(self, initialize=False): + if initialize: + self.client.send("toolbar.filename", filename=self.filename) + self.components.create() + + # When first attaching, send the pan and zoom + # TODO: update + + # pan = self.config.cfg[self.context.model].pos + # pan = (0, 0) if pan is None else pan + # zoom = self.config.cfg[self.context.model].size + # zoom = 1.0 if zoom is None else zoom[0] + # self.client.send("netgraph.pan", pan=pan) + # self.client.send("netgraph.zoom", zoom=zoom) diff --git a/nengo_gui/page.py b/nengo_gui/page.py index 46372269..0ba58c1e 100644 --- a/nengo_gui/page.py +++ b/nengo_gui/page.py @@ -1,497 +1,92 @@ -import importlib -import json import logging import os -import socket import threading -import time -import traceback -import nengo +from nengo_gui.client import bind, ExposedToClient +from nengo_gui.components import Voltage # TODO: remove hack! +from nengo_gui.editor import AceEditor +from nengo_gui.netgraph import NetGraph +from nengo_gui.simcontrol import SimControl -import nengo_gui -import nengo_gui.user_action -import nengo_gui.config +logger = logging.getLogger(__name__) -class PageSettings(object): - __slots__ = ['backend', 'editor_class', 'filename_cfg'] - - def __init__( - self, filename_cfg=None, backend='nengo', - editor_class=nengo_gui.components.AceEditor): - self.filename_cfg = filename_cfg - self.backend = backend - self.editor_class = editor_class - - -class Page(object): +class Page(ExposedToClient): """A handler for a single page of the nengo_gui. Parameters ---------- - - gui : nengo_gui.GUI - The main GUI - filename : str - The filename to open. If this is the same as gui.filename - then it will use the existing gui.model and gui.locals - (if available). Otherwise, the file will be executed to generate - the model - settings : PageSettings - Configures page behaviour (editor, backend, etc) - reset_cfg : bool, optional - If True, the existing .cfg file will be erased + editor_class : class, optional (Default: `.AceEditor`) """ - # Some Simulators can only have one instance running at a time - singleton_sims = dict(nengo_spinnaker=None) - - def __init__(self, gui, filename, settings, reset_cfg=False): - self.gui = gui - self.settings = settings - - self.code = None # the code for the model - self.model = None # the nengo.Network - self.locals = None # the locals() dictionary after executing - self.last_good_locals = None # the locals dict for the last time - # this script was run without errors - self.error = None # any error message generated - self.stdout = '' # text sent to stdout during execution - - self.changed = False # has the model been changed? - self.finished = False # should this Page be shut down - self._sim = None # the current nengo.Simulator - self.rebuild = False # should the model be rebuilt - self.sims_to_close = [] # list of sims that should be closed - - self.code = None # the source code currently displayed - self.error = None # any execute or build error - self.stdout = '' # text printed during execute+build - - self.undo_stack = [] - self.redo_stack = [] + def __init__(self, client, context, editor_class=AceEditor): + super(Page, self).__init__(client) - # placeholders for attributes that will be created by self.load() - self.name_finder = None # NameFinder from nengo objects to text - self.default_labels = None # dict of names to use for unlabelled objs - self.config = None # nengo_gui.Config for storing layout - self.components = None # list of Components - self.uid_prefix_counter = None # used for generating uids for components - self.component_uids = None # mapping from Components to text + self.client = client + self.editor = editor_class(self.client) + self.simcontrol = SimControl(self.client, backend=context.backend) + self.simcontrol.backend = context.backend + self.netgraph = NetGraph( + self.client, context.filename, context.filename_cfg) - self.config_save_needed = False - self.config_save_time = None # time of last config file save - self.config_save_period = 2.0 # minimum time between saves + self.client.bind("page.save", self.save) self.lock = threading.Lock() - # use the default filename if none is given - if filename is None: - self.filename = gui.model_context.filename - else: - try: - self.filename = os.path.relpath(filename) - except ValueError: - # happens on Windows if filename is on a different - # drive than the current directory - self.filename = filename - - # determine the .cfg filename - if self.settings.filename_cfg is None: - self.filename_cfg = self.filename + '.cfg' - else: - self.filename_cfg = self.settings.filename_cfg - - if reset_cfg: - self.clear_config() - - self.load() - - self.net_graph = self.get_component(nengo_gui.components.NetGraph) - self.editor = self.get_component(self.settings.editor_class) - - # build and run the model in a separate thread - t = threading.Thread(target=self.runner) - t.daemon = True - t.start() - - @property - def sim(self): - return self._sim - - @sim.setter - def sim(self, value): - if hasattr(self._sim, 'close'): - self.sims_to_close.append(self._sim) - self._sim = value - - def get_component(self, component_class): - for c in self.components: - if isinstance(c, component_class): - return c - return None - - def clear_config(self): - if os.path.isfile(self.filename_cfg): - os.remove(self.filename_cfg) - - def load(self): - """Load the model and initialize everything""" - if self.filename == self.gui.model_context.filename: - # if we're on the default filenaem, just load it from the GUI - self.model = self.gui.model_context.model - if self.gui.model_context.locals is None: - self.locals = None - else: - self.locals = self.gui.model_context.locals.copy() - else: - self.model = None - self.locals = None - - # if we still don't have a locals dictionary, then run the script - if self.locals is None: - try: - with open(self.filename) as f: - code = f.read() - except IOError: - code = '' - - self.execute(code) - - if self.model is None: - self.model = nengo.Network() - self.locals['model'] = self.model - - # figure out good names for objects - self.name_finder = nengo_gui.NameFinder(self.locals, self.model) - self.default_labels = self.name_finder.known_name - - # load the .cfg file - self.config = self.load_config() - self.config_save_needed = False - self.config_save_time = None # time of last config file save - - self.uid_prefix_counter = {} - - self.create_components() - - def create_components(self): - """Generate the actual Components from the Templates""" - #TODO: change the name of this - self.components = [] - self.component_uids = {} - for name, obj in self.locals.items(): - if isinstance(obj, nengo_gui.components.Component): - self.component_uids[obj] = name - self.gui.component_uids[id(obj)] = obj - self.components.append(obj) - - # this ensures NetGraph, AceEditor, and SimControl are first - self.components.sort(key=lambda x: x.component_order) - - def add_component(self, component): - """Add a new Component to an existing Page.""" - self.gui.component_uids[id(component)] = component - uid = self.get_uid(component) - component.attach(self, self.config[component], uid=uid) - self.components.append(component) - - def execute(self, code): - """Run the given code to generate self.model and self.locals. - - The code will be stored in self.code, any output to stdout will - be a string as self.stdout, and any error will be in self.error. - """ - code_locals = {} - code_locals['nengo_gui'] = nengo_gui - code_locals['__file__'] = self.filename - - self.code = code - self.error = None - self.stdout = '' - - exec_env = nengo_gui.exec_env.ExecutionEnvironment(self.filename) - try: - with exec_env: - compiled = compile(code, nengo_gui.exec_env.compiled_filename, - 'exec') - exec(compiled, code_locals) - except nengo_gui.exec_env.StartedSimulatorException: - line = nengo_gui.exec_env.determine_line_number() - exec_env.stdout.write('Warning: Simulators cannot be manually' - ' run inside nengo_gui (line %d)\n' % line) - except nengo_gui.exec_env.StartedGUIException: - line = nengo_gui.exec_env.determine_line_number() - exec_env.stdout.write('Warning: nengo_gui cannot be run inside' - ' nengo_gui (line %d)\n' % line) - except: - line = nengo_gui.exec_env.determine_line_number() - self.error = dict(trace=traceback.format_exc(), line=line) - self.stdout = exec_env.stdout.getvalue() - - # make sure we've defined a nengo.Network - model = code_locals.get('model', None) - if not isinstance(model, nengo.Network): - if self.error is None: - line = len(code.split('\n')) - self.error = dict(trace='must declare a nengo.Network ' - 'called "model"', line=line) - model = None - - self.model = model - self.locals = code_locals - if self.error is None: - self.last_good_locals = code_locals - - def load_config(self): - """Load the .cfg file""" - config = nengo_gui.config.Config() - self.locals['nengo_gui'] = nengo_gui - self.locals['_viz_config'] = config - fname = self.filename_cfg - if os.path.exists(fname): - with open(fname) as f: - config_code = f.readlines() - for line in config_code: - try: - exec(line, self.locals) - except Exception: - # FIXME - #if self.gui.interactive: - logging.debug('error parsing config: %s', line) - - # make sure the required Components exist - if '_viz_sim_control' not in self.locals: - c = nengo_gui.components.SimControl() - self.locals['_viz_sim_control'] = c - if '_viz_net_graph' not in self.locals: - c = nengo_gui.components.NetGraph() - self.locals['_viz_net_graph'] = c - # FIXME general editor - if '_viz_ace_editor' not in self.locals: - c = self.settings.editor_class() - # c = nengo_gui.components.AceEditor() - self.locals['_viz_ace_editor'] = c - - if self.model is not None: - if config[self.model].pos is None: - config[self.model].pos = (0, 0) - if config[self.model].size is None: - config[self.model].size = (1.0, 1.0) - - for k, v in self.locals.items(): - if isinstance(v, nengo_gui.components.Component): - self.default_labels[v] = k - v.attach(page=self, config=config[v], uid=k) - - return config - - def save_config(self, lazy=False, force=False): - """Write the .cfg file to disk. - - Parameters - ---------- - lazy : bool - If True, then only save if it has been more than config_save_time - since the last save and if config_save_needed - force : bool - If True, then always save right now - """ - if not force and not self.config_save_needed: - return - - now_time = time.time() - if not force and lazy and self.config_save_time is not None: - if (now_time - self.config_save_time) < self.config_save_period: - return - - with self.lock: - self.config_save_time = now_time - self.config_save_needed = False - try: - with open(self.filename_cfg, 'w') as f: - f.write(self.config.dumps(uids=self.default_labels)) - except IOError: - print("Could not save %s; permission denied" % - self.filename_cfg) - - def modified_config(self): - """Set a flag that the config file should be saved.""" - self.config_save_needed = True - - def create_javascript(self): - """Generate the javascript for the current model and layout.""" - if self.filename is not None: - fn = json.dumps(self.filename) - webpage_title_js = ';document.title = %s;' % fn - else: - webpage_title_js = '' - - assert isinstance(self.components[0], nengo_gui.components.SimControl) - - component_js = '\n'.join([c.javascript() for c in self.components]) - component_js += webpage_title_js - if not self.gui.model_context.writeable: - component_js += "$('#Open_file_button').addClass('deactivated');" - return component_js - - def get_label(self, obj, default_labels=None): - """Return a readable label for an object. - - If the object has a .label set, this will be used. Otherwise, it - uses default_labels, which thanks to the NameFinder will be legal - Python code for referring to the object given the current locals() - dictionary ("model.ensembles[1]" or "ens" or "model.buffer.state". - If it has to use default_labels, it will only use the last part of the - label (after the last "."). This avoids redundancy in nested displays. - """ - if default_labels is None: - default_labels = self.default_labels - label = obj.label - if label is None: - label = default_labels.get(obj, None) - # We should never ask for the label for something that can't be - # found in the default_labels. If this does happen, something - # has gone wrong. Note that this was often a symptom of the - # dreaded 'pop' bug that causes hassles during the summer school. - # Hopefully the reorganization of the code into Page and GUI - # (from Viz and VizSim) has dealt with this problem. - assert label is not None - if '.' in label: - label = label.rsplit('.', 1)[1] - if label is None: - label = repr(obj) - return label - - def get_uid(self, obj, default_labels=None): - """Return a unique identifier for an object. - - This should be the value given by the NameFinder. - """ - if default_labels is None: - default_labels = self.default_labels - uid = default_labels.get(obj, None) - if uid is None: - # TODO: do we ever need to fall back on this case? This should - # only happen if something goes wrong. - uid = repr(obj) - return uid - - def finish(self): - """Shut down this page.""" - if not self.finished: - self.finished = True - self.gui.remove_page(self) - - def generate_uid(self, obj, prefix): - """Make a new unique identifier for an object. - - This is used for new Components being created (so they can have - a unique identifier in the .cfg file). - """ - index = self.uid_prefix_counter.get(prefix, 0) - uid = '%s%d' % (prefix, index) - while uid in self.locals: - index += 1 - uid = '%s%d' % (prefix, index) - self.uid_prefix_counter[prefix] = index + 1 - - self.locals[uid] = obj - self.default_labels[obj] = uid - - def remove_uid(self, uid): - """Remove a generated uid (for when a component is removed).""" - if uid in self.locals: - obj = self.locals[uid] - del self.locals[uid] - del self.default_labels[obj] - else: - print('WARNING: remove_uid called on unknown uid: %s' % uid) - - def remove_component(self, component): - """Remove a component from the layout.""" - del self.gui.component_uids[id(component)] - self.remove_uid(component.uid) - self.components.remove(component) - - def config_change(self, component, new_cfg, old_cfg): - act = nengo_gui.user_action.ConfigAction(self, - component=component, - new_cfg=new_cfg, - old_cfg=old_cfg) - self.undo_stack.append([act]) - - def remove_graph(self, component): - act = nengo_gui.user_action.RemoveGraph(self.net_graph, - component) - self.undo_stack.append([act]) - + @bind("page.build") def build(self): - """Build the model.""" + """Build the network.""" # use the lock to make sure only one Simulator is building at a time # TODO: should there be a master lock in the GUI? with self.lock: - self.building = True - - # modify the model for the various Components - for c in self.components: - c.add_nengo_objects(self) - - # determine the backend to use - backend = importlib.import_module(self.settings.backend) - # if only one Simulator is allowed at a time, finish the old one - old_sim = Page.singleton_sims.get(self.settings.backend, None) - if old_sim is not None and old_sim is not self: - old_sim.sim = None - old_sim.finished = True - - exec_env = nengo_gui.exec_env.ExecutionEnvironment(self.filename, - allow_sim=True) - # build the simulation - try: - with exec_env: - self.sim = backend.Simulator(self.model) - except: - line = nengo_gui.exec_env.determine_line_number() - self.error = dict(trace=traceback.format_exc(), line=line) + self.netgraph.add_nengo_objects() + self.simcontrol.add_nengo_objects(self.netgraph.model) + # TODO: Remove hack! + del self.simcontrol.voltage_comps[:] + for c in self.netgraph.components: + if isinstance(c, Voltage): + self.simcontrol.voltage_comps.append(c) + self.simcontrol.build(self.netgraph.model, self.netgraph.filename) + self.netgraph.remove_nengo_objects() + self.simcontrol.remove_nengo_objects(self.netgraph.model) + + @bind("page.ready") + def ready(self): + pass + + @bind("page.save") + def save(self, filename=None, force=False): + filename = self.netgraph.filename if filename is None else filename + rename = filename != self.netgraph.filename + + if rename and os.path.exists(filename): + self.editor.send_filename(filename, "Could not rename to %s: " + "file already exists." % (filename,)) + return - self.stdout += exec_env.stdout.getvalue() + if rename: + # TODO: update page => netgraph, new structure + self.page.filename_cfg = filename + '.cfg' + self.page.save_config(force=True) + self.page.filename = filename - if self.sim is not None: - if self.settings.backend in Page.singleton_sims: - Page.singleton_sims[self.settings.backend] = self + try: + if self.editor.code is not None: + with open(filename, 'w') as f: + f.write(self.editor.code) - # remove the temporary components added for visualization - for c in self.components: - c.remove_nengo_objects(self) - # TODO: add checks to make sure everything's been removed + if rename: + self.editor.send_filename(filename) - self.building = False - self.rebuild = False + except IOError: + self.editor.send_filename( + filename, "Could not save %s: permission denied" % + (filename,)) - def runner(self): - """Separate thread for running the simulation itself.""" - # run the simulation - while not self.finished: - if self.sim is None: - time.sleep(0.01) - else: - try: - if hasattr(self.sim, 'max_steps'): - # this is only for the nengo_spinnaker simulation - self.sim.run_steps(self.sim.max_steps) - else: - self.sim.step() - except Exception as err: - if self.finished: - return - line = nengo_gui.exec_env.determine_line_number() - self.error = dict(trace=traceback.format_exc(), line=line) - self.sim = None - while self.sims_to_close: - self.sims_to_close.pop().close() + # TODO: why this? Can we just not? + # self.netgraph.reload(self.editor.code) - if self.rebuild: - self.build() - self.sim = None + def shutdown(self): + # TODO: call shutdown methods on these instead? + self.simcontrol.simthread.stop() + self.netgraph.filethread.stop() diff --git a/nengo_gui/password.py b/nengo_gui/password.py deleted file mode 100644 index 24f5f821..00000000 --- a/nengo_gui/password.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Password hashing functions replicating bcrypt API.""" - -from __future__ import print_function - -import binascii -from getpass import getpass -import hashlib -import os - - -def gensalt(size=16): - return binascii.hexlify(os.urandom(size)) - -def hashpw(password, salt, algorithm='sha1'): - h = hashlib.new(algorithm) - h.update(password) - h.update(salt) - return algorithm + ':' + salt + ':' + h.hexdigest() - -def checkpw(password, hashed): - algorithm, salt, _ = hashed.split(':') - return hashpw(password, salt, algorithm) == hashed - -def prompt_pw(): - while True: - p0 = getpass("Enter password: ") - p1 = getpass("Enter password: ") - if p0 == p1: - return p0 - print("Passwords do not match. Please try again.") diff --git a/nengo_gui/paths.py b/nengo_gui/paths.py new file mode 100644 index 00000000..54f944e3 --- /dev/null +++ b/nengo_gui/paths.py @@ -0,0 +1,4 @@ +import os + +rootdir = os.path.dirname(os.path.realpath(__file__)) +examples = os.path.join(rootdir, "examples") diff --git a/nengo_gui/server.py b/nengo_gui/server.py deleted file mode 100644 index 221aac13..00000000 --- a/nengo_gui/server.py +++ /dev/null @@ -1,516 +0,0 @@ -"""HTTP and WebSocket server implementation.""" - -import base64 -import errno -import hashlib -import logging -import socket -import struct -import ssl -import sys -import threading -import traceback - -try: - from http import server - from http.cookies import SimpleCookie - import socketserver - from urllib.parse import parse_qs, urlparse -except ImportError: # Python 2.7 - import BaseHTTPServer as server - from Cookie import SimpleCookie - import SocketServer as socketserver - from urlparse import parse_qs, urlparse - - -logger = logging.getLogger(__name__) - - -WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' - - -class SocketClosedError(IOError): - pass - - -class HttpError(Exception): - def __init__(self, code, msg, headers=(), data=None): - super(HttpError, self).__init__(msg) - self.code = code - self.msg = msg - self.headers = headers - if data is None: - data = b'

' + bytes(self.code) + b'

' + msg.encode( - 'utf-8') + b'

' - self.data = data - - def to_response(self): - return HtmlResponse(self.data, code=self.code, headers=self.headers) - - -class BadRequest(HttpError): - def __init__(self): - super(BadRequest, self).__init__(400, 'Bad request') - - -class Forbidden(HttpError): - def __init__(self): - super(Forbidden, self).__init__(403, 'Forbidden') - - -class InvalidResource(HttpError): - def __init__(self, path): - super(InvalidResource, self).__init__(404, 'Invalid resource: ' + path) - - -class UpgradeRequired(HttpError): - def __init__(self, headers): - super(UpgradeRequired, self).__init__(426, 'Upgrade required', headers) - - -class InternalServerError(HttpError): - def __init__(self, msg): - super(InternalServerError, self).__init__( - 500, 'Internal server error', data=msg.encode('utf-8')) - - -class HttpResponse(object): - def __init__(self, data, mimetype='text/html', code=200, headers=()): - self.data = data - self.mimetype = mimetype - self.code = code - self.headers = headers - - def send(self, request): - request.send_response(self.code) - request.send_header('Content-type', self.mimetype) - if hasattr(request, 'flush_headers'): - request.flush_headers() - request.wfile.write(request.cookie.output().encode('utf-8')) - request.wfile.write(b'\r\n') - for header in self.headers: - request.send_header(*header) - request.end_headers() - request.wfile.write(self.data) - - -class HttpRedirect(HttpResponse): - def __init__( - self, location, data=b'', mimetype='text/html', code=303, - headers=()): - super(HttpRedirect, self).__init__( - data=data, mimetype=mimetype, code=code, - headers=headers + (('Location', location),)) - self.location = location - - -class HtmlResponse(HttpResponse): - def __init__(self, body, code=200, headers=()): - data = b'' + body + b'' - super(HtmlResponse, self).__init__(data, code=code, headers=headers) - - -class ManagedThreadHttpServer(socketserver.ThreadingMixIn, server.HTTPServer): - """Threaded HTTP and WebSocket server that keeps track of its connections - to allow a proper shutdown.""" - - daemon_threads = True # this ensures all spawned threads exit - - def __init__(self, *args, **kwargs): - server.HTTPServer.__init__(self, *args, **kwargs) - - # keep track of open threads, so we can close them when we exit - self._requests = [] - self._websockets = [] - - self._shutting_down = False - - @property - def requests(self): - return self._requests[:] - - @property - def websockets(self): - return self._websockets[:] - - def create_websocket(self, socket): - ws = WebSocket(socket) - self._websockets.append(ws) - return ws - - def process_request_thread(self, request, client_address): - thread = threading.current_thread() - self._requests.append((thread, request)) - socketserver.ThreadingMixIn.process_request_thread( - self, request, client_address) - self._requests.remove((thread, request)) - - def handle_error(self, request, client_address): - exc_type, exc_value, _ = sys.exc_info() - if (exc_type is socket.error and - exc_value.args[0] in - [errno.EPIPE, errno.EBADF, errno.ECONNRESET]): - return # Probably caused by a server shutdown - else: - logger.exception("Server error.") - server.HTTPServer.handle_error(self, request, client_address) - - def shutdown(self): - if self._shutting_down: - return - self._shutting_down = True - - for ws in self.websockets: - ws.close() - for _, request in self.requests: - self.shutdown_request(request) - - server.HTTPServer.shutdown(self) - - def wait_for_shutdown(self, timeout=None): - """Wait for all request threads to finish. - - Parameters - ---------- - timeout : float, optional - Maximum time in seconds to wait for each thread to finish. - """ - for thread, _ in self.requests: - if thread.is_alive(): - thread.join(timeout) - - -class HttpWsRequestHandler(server.BaseHTTPRequestHandler): - """Base class for request handler that can handle normal and websocket - requests. - - `http_commands` and `ws_commands` are dictionaries mapping resource names - (with leading '/') to function names (as string) in this class. These - functions do not take any arguments except for `self`. All required data - is defined as attributes on the instance. In addition to the attributes - provided by the `BaseHTTPRequestHandler` the resource name is provided as - `resource`, the parsed query string (as dictionary) as `query` and combined - query string and post fields as `db`. In a websocket command handler - function `ws` provides access to the websocket. - - If no handler function for a resource was defined, the `http_default` and - `ws_default` functions will be used. - """ - http_commands = {} - ws_commands = {} - - def __init__(self, *args, **kwargs): - self.resource = None - self.query = None - self.db = {} - self.cookie = SimpleCookie() - self.ws = None - server.BaseHTTPRequestHandler.__init__(self, *args, **kwargs) - - def do_POST(self): - data = self.rfile.read( - int(self.headers['Content-Length'])).decode('ascii') - - if 'multipart/form-data' in self.headers['Content-Type']: - raise NotImplementedError() # TODO - else: - self.db = {k: v[0] for k, v in parse_qs(data).items()} - - self.do_GET() - - def do_GET(self): - parsed = urlparse(self.path) - self.resource = parsed.path - self.query = parse_qs(parsed.query) - self.db.update( - {k: v[0] for k, v in self.query.items() if k not in self.db}) - if 'Cookie' in self.headers: - self.cookie.load(self.headers['Cookie']) - - try: - connection = self.headers.get('Connection', 'close').lower() - if 'upgrade' in connection: - self.handle_upgrade() - else: - self.http_GET() - except HttpError as err: - logger.warning( - 'Error response (%i): %s', err.code, err.msg, exc_info=True) - err.to_response().send(self) - except Exception as err: - logger.exception('Error response') - err = InternalServerError( - '
' + traceback.format_exc() + '
') - err.to_response().send(self) - - def http_GET(self): - command = self._get_command(self.http_commands, self.resource) - if command is None: - response = self.http_default() - else: - response = getattr(self, command)() - response.send(self) - - def http_default(self): - raise InvalidResource(self.path) - - def handle_upgrade(self): - upgrade = self.headers.get('Upgrade').lower() - if upgrade == 'websocket': - self.upgrade_to_ws() - else: - raise BadRequest() - - def get_expected_origins(self): - raise NotImplementedError() - - def upgrade_to_ws(self): - response = '''HTTP/1.1 101 Switching Protocols -Upgrade: websocket -Connection: Upgrade -Sec-WebSocket-Accept: {sec} - -''' - valid_srv_addrs = self.get_expected_origins() - - try: - origin = urlparse(self.headers['Origin']) - assert origin.netloc.lower() in valid_srv_addrs - except KeyError: - raise Forbidden() - except AssertionError: - raise Forbidden() - - try: - host = self.headers['Host'].lower() - assert host in valid_srv_addrs - key = self.headers['Sec-WebSocket-Key'] - assert len(base64.b64decode(key)) == 16 - except KeyError: - raise BadRequest() - except AssertionError: - raise BadRequest() - - if self.headers['Sec-WebSocket-Version'] != '13': - raise UpgradeRequired(['Sec-WebSocket-Version: 13']) - - sec = base64.b64encode(hashlib.sha1( - (key + WS_MAGIC).encode('ascii')).digest()).decode('ascii') - _sendall(self.request, response.format(sec=sec).encode('utf-8')) - - self.ws = self.server.create_websocket(self.request) - self.ws.set_blocking(False) - - command = self._get_command(self.ws_commands, self.resource) - if command is None: - self.ws_default() - else: - getattr(self, command)() - self.ws.close() - - def ws_default(self): - raise InvalidResource(self.path) - - @classmethod - def _get_command(cls, commands, path): - if not path.startswith('/'): - path = '/' + path - - while len(path) > 0: - if path in commands: - return commands[path] - path = path.rsplit('/', 1)[0] - if '/' in commands: - return commands['/'] - return None - - -class WebSocket(object): - ST_OPEN, ST_CLOSING, ST_CLOSED = range(3) - - def __init__(self, socket): - self.socket = socket - self._buf = bytearray([]) - self.state = self.ST_OPEN - - def set_timeout(self, timeout): - self.socket.settimeout(timeout) - - def set_blocking(self, flag): - self.socket.setblocking(flag) - - def _read(self): - try: - self._buf = self._buf + bytearray(self.socket.recv(512)) - except ssl.SSLError as e: - if e.errno == 2: - # Corresponds to SSLWantReadError which only exists in Python - # 2.7.9+ and 3.3+. - pass - else: - raise - except socket.error as e: - if e.errno in [errno.EDEADLK, errno.EAGAIN, 10035]: - # no data available - pass - elif e.errno == errno.EBADF: - raise SocketClosedError("Cannot read from closed socket.") - else: - raise - - def read_frame(self): - try: - self._read() - frame, size = WebSocketFrame.parse(self._buf) - self._buf = self._buf[size:] - if not self._handle_frame(frame): - return frame - except ValueError: - return None - except socket.timeout: - return None - - - def _handle_frame(self, frame): - if frame.opcode == WebSocketFrame.OP_CLOSE: - if self.state not in [self.ST_CLOSING, self.ST_CLOSED]: - self.close() - raise SocketClosedError("Websocket has been closed") - elif frame.opcode == WebSocketFrame.OP_PING: - if self.state == self.ST_OPEN: - pong = WebSocketFrame( - fin=1, rsv=0, opcode=WebSocketFrame.OP_PONG, mask=0, - data=frame.data) - _sendall(self.socket, pong.pack()) - return True - elif frame.opcode == WebSocketFrame.OP_PONG: - return True - else: - return False - - def close(self): - if self.state not in [self.ST_CLOSING, self.ST_CLOSED]: - self.state = self.ST_CLOSING - close_frame = WebSocketFrame( - fin=1, rsv=0, opcode=WebSocketFrame.OP_CLOSE, mask=0, data=b'') - try: - _sendall(self.socket, close_frame.pack()) - except socket.error as err: - if err.errno in [errno.EPIPE, errno.EBADF]: - pass - - def write_frame(self, frame): - if self.state != self.ST_OPEN: - raise SocketClosedError("Connection not open.") - - try: - _sendall(self.socket, frame.pack()) - except socket.error as e: - if e.errno == errno.EPIPE: # Broken pipe - raise SocketClosedError("Cannot write to socket.") - else: - raise - - def write_text(self, text): - self.write_frame(WebSocketFrame.create_text_frame(text)) - - def write_binary(self, data): - self.write_frame(WebSocketFrame.create_binary_frame(data)) - - -def _sendall(socket, data): - bytes_sent = 0 - while bytes_sent < len(data): - bytes_sent += socket.send(data[bytes_sent:]) - - - -class WebSocketFrame(object): - __slots__ = ['fin', 'rsv', 'opcode', 'mask', 'data'] - - OP_CONT = 0x0 - OP_TEXT = 0x1 - OP_BIN = 0x2 - OP_CLOSE = 0x8 - OP_PING = 0x9 - OP_PONG = 0xA - - def __init__(self, fin, rsv, opcode, mask, data): - self.fin = fin - self.rsv = rsv - self.opcode = opcode - self.mask = mask - self.data = data - - @classmethod - def parse(cls, data): - try: - offset = 0 - - fin = (data[0] >> 7) & 0x1 - rsv = (data[0] >> 4) & 0x07 - opcode = data[0] & 0x0F - masked = (data[1] >> 7) & 0x01 - datalen = data[1] & 0x7F - mask = b'\x00\x00\x00\x00' - - offset += 2 - - if datalen == 126: - datalen = cls._to_int(data[offset:offset+2]) - offset += 2 - elif datalen == 127: - datalen = cls._to_int(data[offset:offset+8]) - offset += 8 - - if masked: - mask = data[offset:offset+4] - offset += 4 - - size = offset + datalen - masked_data = data[offset:size] - if len(masked_data) < datalen: - raise IndexError() - unmasked_data = [masked_data[i] ^ mask[i % 4] - for i in range(len(masked_data))] - data = bytearray(unmasked_data) - if opcode == cls.OP_TEXT: - data = data.decode('ascii') - - - return cls(fin, rsv, opcode, mask, data), size - except IndexError: - raise ValueError('Frame incomplete.') - - @classmethod - def _to_int(cls, data): - value = 0 - for b in data: - value = (value << 8) + b - return value - - def pack(self): - code = (self.fin & 0x01) << 7 - code |= (self.rsv & 0x07) << 4 - code |= self.opcode & 0x0F - - datalen = len(self.data) - mask_bit = ((self.mask != 0) & 0x01) << 7 - if datalen < 126: - header = struct.pack('!BB', code, datalen | mask_bit) - elif datalen <= 0xFFFF: - header = struct.pack('!BBH', code, 126 | mask_bit, datalen) - else: - header = struct.pack('!BBQ', code, 127 | mask_bit, datalen) - - data = self.data - - return header + data - - @classmethod - def create_text_frame(cls, text, mask=0): - return cls(1, 0, cls.OP_TEXT, mask, text.encode('utf-8')) - - @classmethod - def create_binary_frame(cls, data, mask=0): - return cls(1, 0, cls.OP_BIN, mask, data) diff --git a/nengo_gui/server/__init__.py b/nengo_gui/server/__init__.py new file mode 100644 index 00000000..7b33226e --- /dev/null +++ b/nengo_gui/server/__init__.py @@ -0,0 +1,31 @@ +from .auth import ( + AuthenticatedHttpRequestHandler, + RequireAuthentication, +) +from .exceptions import ( + BadRequest, + Forbidden, + HttpError, + InternalServerError, + InvalidResource, + ServerShutdown, + SocketClosedError, + UpgradeRequired, +) +from .http import ( + HtmlResponse, + HttpRedirect, + HttpRequestHandler, + HttpResponse, + ManagedThreadHttpServer, +) +from .session import ( + Session, + SessionManager, +) +from .ws import ( + AuthenticatedHttpWsRequestHandler, + ManagedThreadHttpWsServer, + WebSocket, + WebSocketFrame, +) diff --git a/nengo_gui/server/auth.py b/nengo_gui/server/auth.py new file mode 100644 index 00000000..52c0ec79 --- /dev/null +++ b/nengo_gui/server/auth.py @@ -0,0 +1,87 @@ +from __future__ import print_function + +import binascii +from getpass import getpass +import hashlib +import os + +from .exceptions import SessionExpiredError +from .http import HttpRedirect, HttpRequestHandler + + +def gensalt(size=16): + return binascii.hexlify(os.urandom(size)) + + +def hashpw(password, salt, algorithm='sha1'): + h = hashlib.new(algorithm) + h.update(password) + h.update(salt) + return algorithm + ':' + salt + ':' + h.hexdigest() + + +def checkpw(password, hashed): + algorithm, salt, _ = hashed.split(':') + return hashpw(password, salt, algorithm) == hashed + + +def prompt_pw(): + while True: + p0 = getpass("Enter password: ") + p1 = getpass("Enter password: ") + if p0 == p1: + return p0 + print("Passwords do not match. Please try again.") + + +class RequireAuthentication(object): + def __init__(self, login_route): + self.login_route = login_route + + def __call__(self, fn): + def auth_checked(inst): + session = inst.get_session() + has_password = inst.server.settings.password_hash is not None + if has_password and not session.authenticated: + return HttpRedirect(self.login_route) + return fn(inst) + return auth_checked + + +class AuthenticatedHttpRequestHandler(HttpRequestHandler): + + def get_expected_origins(self): + session = self.get_session() + has_password = self.server.settings.password_hash is not None + origins = [] + if not has_password: + origins.append('localhost:' + str(self.server.server_port)) + if self.server.server_port in [80, 443]: + origins.append('localhost') + elif session.login_host is not None: + return [session.login_host] + return origins + + def get_session(self): + try: + session_id = self.cookie['_session_id'].value + session = self.server.sessions[session_id] + except KeyError: + session_id, session = self.server.sessions.new_session( + self.request) + except SessionExpiredError: + session_id, session = self.server.sessions.new_session( + self.request) + + self.cookie['_session_id'] = session_id + return session + + def checkpw(self): + session = self.get_session() + pw = self.db['pw'] + + if 'pw' in self.db and checkpw(pw, self.server.settings.password_hash): + session.authenticated = True + session.login_host = self.headers.get('host', None) + + return session diff --git a/nengo_gui/server/exceptions.py b/nengo_gui/server/exceptions.py new file mode 100644 index 00000000..a91bb1cb --- /dev/null +++ b/nengo_gui/server/exceptions.py @@ -0,0 +1,48 @@ +class SocketClosedError(IOError): + pass + + +class HttpError(Exception): + def __init__(self, code, msg, headers=(), data=None): + super(HttpError, self).__init__(msg) + self.code = code + self.msg = msg + self.headers = headers + if data is None: + data = b'

' + bytes(self.code) + b'

' + msg.encode( + 'utf-8') + b'

' + self.data = data + + +class BadRequest(HttpError): + def __init__(self): + super(BadRequest, self).__init__(400, 'Bad request') + + +class Forbidden(HttpError): + def __init__(self): + super(Forbidden, self).__init__(403, 'Forbidden') + + +class InvalidResource(HttpError): + def __init__(self, path): + super(InvalidResource, self).__init__(404, 'Invalid resource: ' + path) + + +class UpgradeRequired(HttpError): + def __init__(self, headers): + super(UpgradeRequired, self).__init__(426, 'Upgrade required', headers) + + +class InternalServerError(HttpError): + def __init__(self, msg): + super(InternalServerError, self).__init__( + 500, 'Internal server error', data=msg.encode('utf-8')) + + +class SessionExpiredError(Exception): + pass + + +class ServerShutdown(Exception): + """Causes the server to shutdown when raised.""" diff --git a/nengo_gui/server/http.py b/nengo_gui/server/http.py new file mode 100644 index 00000000..6b499fbb --- /dev/null +++ b/nengo_gui/server/http.py @@ -0,0 +1,205 @@ +import errno +import logging +import socket +import sys +import threading +import traceback +import warnings + +from .exceptions import ( + BadRequest, + HttpError, + InternalServerError, + InvalidResource, +) +from .session import SessionManager +from ..compat import parse_qs, server, SimpleCookie, socketserver, urlparse + +logger = logging.getLogger(__name__) + + +def to_response(error): + return HtmlResponse(error.data, code=error.code, headers=error.headers) + + +class HttpResponse(object): + def __init__(self, data, mimetype='text/html', code=200, headers=()): + self.data = data + self.mimetype = mimetype + self.code = code + self.headers = headers + + def send(self, request): + request.send_response(self.code) + if self.mimetype is not None: + request.send_header('Content-type', self.mimetype) + if hasattr(request, 'flush_headers'): + request.flush_headers() + request.wfile.write(request.cookie.output().encode('utf-8')) + request.wfile.write(b'\r\n') + for header in self.headers: + request.send_header(*header) + request.end_headers() + if self.data is not None: + request.wfile.write(self.data) + + +class HttpRedirect(HttpResponse): + def __init__(self, location, + data=b'', mimetype='text/html', code=303, headers=()): + super(HttpRedirect, self).__init__( + data=data, mimetype=mimetype, code=code, + headers=headers + (('Location', location),)) + self.location = location + + +class HtmlResponse(HttpResponse): + def __init__(self, body, code=200, headers=()): + data = b'' + body + b'' + super(HtmlResponse, self).__init__(data, code=code, headers=headers) + + +class ManagedThreadHttpServer(socketserver.ThreadingMixIn, server.HTTPServer): + """Threaded HTTP server. + + Unlike the base server, this keeps track of its connections + to allow a proper shutdown. + """ + + daemon_threads = True # this ensures all spawned threads exit + + def __init__(self, *args, **kwargs): + server.HTTPServer.__init__(self, *args, **kwargs) + + self.sessions = SessionManager() + # keep track of open threads, so we can close them when we exit + self._requests = [] + + self._shutting_down = False + + @property + def requests(self): + return self._requests[:] + + def process_request_thread(self, request, client_address): + thread = threading.current_thread() + self._requests.append((thread, request)) + socketserver.ThreadingMixIn.process_request_thread( + self, request, client_address) + self._requests.remove((thread, request)) + + def handle_error(self, request, client_address): + exc_type, exc_value, _ = sys.exc_info() + if (exc_type is socket.error and + exc_value.args[0] in + [errno.EPIPE, errno.EBADF, errno.ECONNRESET]): + return # Probably caused by a server shutdown + else: + logger.exception("Server error.") + server.HTTPServer.handle_error(self, request, client_address) + + def _shutdown(self): + for _, request in self.requests: + self.shutdown_request(request) + server.HTTPServer.shutdown(self) + + def shutdown(self): + if self._shutting_down: + return + self._shutting_down = True + self._shutdown() + + def wait_for_shutdown(self, timeout=None): + """Wait for all request threads to finish. + + Parameters + ---------- + timeout : float, optional + Maximum time in seconds to wait for each thread to finish. + """ + for thread, _ in self.requests: + if thread.is_alive(): + thread.join(timeout) + + +class HttpRequestHandler(server.BaseHTTPRequestHandler): + + _http_routes = {} + + def __init__(self, *args, **kwargs): + self.route = None + self.query = None + self.db = {} + self.cookie = SimpleCookie() + server.BaseHTTPRequestHandler.__init__(self, *args, **kwargs) + + def do_POST(self): + data = self.rfile.read( + int(self.headers['Content-Length'])).decode('ascii') + + if 'multipart/form-data' in self.headers['Content-Type']: + raise NotImplementedError() # TODO + else: + self.db = {k: v[0] for k, v in parse_qs(data).items()} + + self.do_GET() + + def do_GET(self): + parsed = urlparse(self.path) + self.route = parsed.path + self.query = parse_qs(parsed.query) + self.db.update( + {k: v[0] for k, v in self.query.items() if k not in self.db}) + if 'Cookie' in self.headers: + self.cookie.load(self.headers['Cookie']) + + try: + connection = self.headers.get('Connection', 'close').lower() + if 'upgrade' in connection: + self.handle_upgrade() + else: + self.http_GET() + except HttpError as err: + logger.warning( + 'Error response (%i): %s', err.code, err.msg, exc_info=True) + to_response(err).send(self) + except Exception as err: + logger.exception('Error response') + err = InternalServerError( + '
' + traceback.format_exc() + '
') + to_response(err).send(self) + + def http_GET(self): + endpoint = self._get_endpoint(self._http_routes, self.route) + if endpoint is None: + raise InvalidResource(self.path) + response = endpoint(self) # TODO: pass in appropriate stuff + response.send(self) + + def handle_upgrade(self): + raise BadRequest() + + def get_expected_origins(self): + raise NotImplementedError() + + @classmethod + def _get_endpoint(cls, routes, route): + if not route.startswith('/'): + route = '/' + route + + while len(route) > 0: + if route in routes: + return routes[route] + route = route.rsplit('/', 1)[0] + if '/' in routes: + return routes['/'] + return None + + @classmethod + def http_route(cls, route): + def _http_route(endpoint): + if route in cls._http_routes: + warnings.warn("HTTP route %r was overwritten" % (route,)) + cls._http_routes[route] = endpoint + return endpoint + return _http_route diff --git a/nengo_gui/server/session.py b/nengo_gui/server/session.py new file mode 100644 index 00000000..2fa62a91 --- /dev/null +++ b/nengo_gui/server/session.py @@ -0,0 +1,57 @@ +import hashlib +import logging +import os +import time + +from .exceptions import SessionExpiredError + +logger = logging.getLogger(__name__) + + +class Session(object): + __slots__ = ('creation_time', 'authenticated', 'login_host') + + def __init__(self): + self.creation_time = time.time() + self.authenticated = False + self.login_host = None + + +class SessionManager(object): + def __init__(self, time_to_live=60 * 60 * 24 * 30): + self.time_to_live = time_to_live + self._sessions = {} + + def __getitem__(self, session_id): + session = self._sessions.get(session_id, None) + if (session is None or + session.creation_time + self.time_to_live < time.time()): + del self._sessions[session_id] + raise SessionExpiredError() + return session + + def __len__(self): + return len(self._sessions) + + def new_session(self, request): + session_id = self._new_session_id(request) + session = Session() + self._sessions[session_id] = session + return session_id, session + + def _new_session_id(self, request): + try: + peer = request.getpeername() # not supported on some systems + except: + logger.warning( + "Cannot get peer name. Sessions will not be tied to client.", + exc_info=True) + peer = '' + + session_id = hashlib.sha1() + session_id.update(os.urandom(16)) + for elem in peer: + if isinstance(elem, str): + elem = elem.encode('utf-8') + session_id.update(bytes(elem)) + return session_id.hexdigest() diff --git a/nengo_gui/server/ws.py b/nengo_gui/server/ws.py new file mode 100644 index 00000000..078247a1 --- /dev/null +++ b/nengo_gui/server/ws.py @@ -0,0 +1,316 @@ +import base64 +import errno +import hashlib +import select +import socket +import ssl +import struct +import warnings + +from .auth import AuthenticatedHttpRequestHandler +from .exceptions import ( + BadRequest, Forbidden, InvalidResource, SocketClosedError, UpgradeRequired) +from .http import ManagedThreadHttpServer +from ..compat import urlparse + +WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + + +def _sendall(socket, data): + bytes_sent = 0 + while bytes_sent < len(data): + _, ready, _ = select.select([], [socket], []) + assert socket in ready, "Socket not ready" + bytes_sent += socket.send(data[bytes_sent:]) + + +class WebSocketFrame(object): + __slots__ = ('fin', 'rsv', 'opcode', 'mask', 'data') + + OP_CONT = 0x0 + OP_TEXT = 0x1 + OP_BIN = 0x2 + OP_CLOSE = 0x8 + OP_PING = 0x9 + OP_PONG = 0xA + + def __init__(self, fin, rsv, opcode, mask, data): + self.fin = fin + self.rsv = rsv + self.opcode = opcode + self.mask = mask + self.data = data + + @classmethod + def parse(cls, data): + try: + offset = 0 + + fin = (data[0] >> 7) & 0x1 + rsv = (data[0] >> 4) & 0x07 + opcode = data[0] & 0x0F + masked = (data[1] >> 7) & 0x01 + datalen = data[1] & 0x7F + mask = b'\x00\x00\x00\x00' + + offset += 2 + + if datalen == 126: + datalen = cls._to_int(data[offset:offset+2]) + offset += 2 + elif datalen == 127: + datalen = cls._to_int(data[offset:offset+8]) + offset += 8 + + if masked: + mask = data[offset:offset+4] + offset += 4 + + size = offset + datalen + masked_data = data[offset:size] + if len(masked_data) < datalen: + raise IndexError() + unmasked_data = [masked_data[i] ^ mask[i % 4] + for i in range(len(masked_data))] + data = bytearray(unmasked_data) + if opcode == cls.OP_TEXT: + data = data.decode('ascii') + + return cls(fin, rsv, opcode, mask, data), size + except IndexError: + raise ValueError('Frame incomplete.') + + @classmethod + def _to_int(cls, data): + value = 0 + for b in data: + value = (value << 8) + b + return value + + def pack(self): + code = (self.fin & 0x01) << 7 + code |= (self.rsv & 0x07) << 4 + code |= self.opcode & 0x0F + + datalen = len(self.data) + mask_bit = ((self.mask != 0) & 0x01) << 7 + if datalen < 126: + header = struct.pack('!BB', code, datalen | mask_bit) + elif datalen <= 0xFFFF: + header = struct.pack('!BBH', code, 126 | mask_bit, datalen) + else: + header = struct.pack('!BBQ', code, 127 | mask_bit, datalen) + + data = self.data + + return header + data + + @classmethod + def create_text_frame(cls, text, mask=0): + return cls(1, 0, cls.OP_TEXT, mask, text.encode('utf-8')) + + @classmethod + def create_binary_frame(cls, data, mask=0): + return cls(1, 0, cls.OP_BIN, mask, data) + + +class WebSocket(object): + ST_OPEN, ST_CLOSING, ST_CLOSED = range(3) + + def __init__(self, socket): + self.socket = socket + self._buf = bytearray([]) + self.state = self.ST_OPEN + + def set_timeout(self, timeout): + self.socket.settimeout(timeout) + + def set_blocking(self, flag): + self.socket.setblocking(flag) + + def _read(self): + try: + self._buf = self._buf + bytearray(self.socket.recv(512)) + except ssl.SSLError as e: + if e.errno == 2: + # Corresponds to SSLWantReadError which only exists in Python + # 2.7.9+ and 3.3+. + pass + else: + raise + except socket.error as e: + if e.errno in [errno.EDEADLK, errno.EAGAIN, 10035]: + # no data available + pass + elif e.errno == errno.EBADF: + raise SocketClosedError("Cannot read from closed socket.") + else: + raise + + def read_frame(self): + try: + self._read() + frame, size = WebSocketFrame.parse(self._buf) + self._buf = self._buf[size:] + if not self._handle_frame(frame): + return frame + except ValueError: + return None + except socket.timeout: + return None + + def _handle_frame(self, frame): + if frame.opcode == WebSocketFrame.OP_CLOSE: + if self.state not in [self.ST_CLOSING, self.ST_CLOSED]: + self.close() + raise SocketClosedError("Websocket has been closed") + elif frame.opcode == WebSocketFrame.OP_PING: + if self.state == self.ST_OPEN: + pong = WebSocketFrame( + fin=1, rsv=0, opcode=WebSocketFrame.OP_PONG, mask=0, + data=frame.data) + _sendall(self.socket, pong.pack()) + return True + elif frame.opcode == WebSocketFrame.OP_PONG: + return True + else: + return False + + def close(self): + if self.state not in [self.ST_CLOSING, self.ST_CLOSED]: + self.state = self.ST_CLOSING + close_frame = WebSocketFrame( + fin=1, rsv=0, opcode=WebSocketFrame.OP_CLOSE, mask=0, data=b'') + try: + _sendall(self.socket, close_frame.pack()) + except socket.error as err: + if err.errno in [errno.EPIPE, errno.EBADF]: + pass + + def write_frame(self, frame): + if self.state != self.ST_OPEN: + raise SocketClosedError("Connection not open.") + + try: + _sendall(self.socket, frame.pack()) + except socket.error as e: + if e.errno == errno.EPIPE: # Broken pipe + raise SocketClosedError("Cannot write to socket.") + else: + raise + + def write_text(self, text): + self.write_frame(WebSocketFrame.create_text_frame(text)) + + def write_binary(self, data): + self.write_frame(WebSocketFrame.create_binary_frame(data)) + + +class ManagedThreadHttpWsServer(ManagedThreadHttpServer): + """Threaded HTTP and WebSocket server.""" + + def __init__(self, *args, **kwargs): + ManagedThreadHttpServer.__init__(self, *args, **kwargs) + self._websockets = [] + + @property + def websockets(self): + return self._websockets[:] + + def create_websocket(self, socket): + ws = WebSocket(socket) + self._websockets.append(ws) + return ws + + def _shutdown(self): + for ws in self.websockets: + ws.close() + ManagedThreadHttpServer._shutdown(self) + + +class AuthenticatedHttpWsRequestHandler(AuthenticatedHttpRequestHandler): + """Base class for request handler for normal and websocket requests. + + ``http_commands`` and ``ws_commands`` are dictionaries mapping resource + names (with leading '/') to function names (as string) in this class. + These functions do not take any arguments except for `self`. All required + data is defined as attributes on the instance. In addition to the + attributes provided by the `.BaseHTTPRequestHandler`, the resource name is + provided as ``resource``, the parsed query string (as dictionary) as + ``query`` and combined query string and post fields as ``db``. + In a websocket command handler function ``ws`` provides access + to the websocket. + + If no handler function for a resource was defined, the ``http_default`` and + ``ws_default`` functions will be used. + """ + + _ws_routes = {} + + def __init__(self, *args, **kwargs): + self.ws = None + AuthenticatedHttpRequestHandler.__init__(self, *args, **kwargs) + + def handle_upgrade(self): + upgrade = self.headers.get('Upgrade').lower() + if upgrade == 'websocket': + self.upgrade_to_ws() + + def upgrade_to_ws(self): + response = "\r\n".join([ + "HTTP/1.1 101 Switching Protocols", + "Upgrade: websocket", + "Connection: Upgrade", + "Sec-WebSocket-Accept: {sec}", + "", + "" + ]) + + valid_srv_addrs = self.get_expected_origins() + + try: + origin = urlparse(self.headers['Origin']) + assert origin.netloc.lower() in valid_srv_addrs + except KeyError: + raise Forbidden() + except AssertionError: + raise Forbidden() + + try: + host = self.headers['Host'].lower() + assert host in valid_srv_addrs + key = self.headers['Sec-WebSocket-Key'] + assert len(base64.b64decode(key)) == 16 + except KeyError: + raise BadRequest() + except AssertionError: + raise BadRequest() + + if self.headers['Sec-WebSocket-Version'] != '13': + raise UpgradeRequired(['Sec-WebSocket-Version: 13']) + + sec = base64.b64encode(hashlib.sha1( + (key + WS_MAGIC).encode('ascii')).digest()).decode('ascii') + _sendall(self.request, response.format(sec=sec).encode('utf-8')) + + self.ws = self.server.create_websocket(self.request) + self.ws.set_blocking(False) + + endpoint = self._get_endpoint(self._ws_routes, self.route) + if endpoint is None: + self.ws_default() + else: + endpoint(self) + self.ws.close() + + def ws_default(self): + raise InvalidResource(self.path) + + @classmethod + def ws_route(cls, route): + def _ws_route(endpoint): + if route in cls._ws_routes: + warnings.warn("Websocket route %r was overwritten" % (route,)) + cls._ws_routes[route] = endpoint + return endpoint + return _ws_route diff --git a/nengo_gui/simcontrol.py b/nengo_gui/simcontrol.py new file mode 100644 index 00000000..e740f8fb --- /dev/null +++ b/nengo_gui/simcontrol.py @@ -0,0 +1,278 @@ +import importlib +import time +import timeit +import traceback + +import nengo +import numpy as np + +from nengo_gui import exec_env +from nengo_gui.client import bind, ExposedToClient +from nengo_gui.threads import ControlledThread + + +class SimControl(ExposedToClient): + """Controls the simulation. + + Also instantiates and communicates with the SimControl and the Toolbar + on the JavaScript side, which includes the task of back-end selection. + """ + + RATE_TAU = 0.5 + + # TODO: shown_time=0.5, kept_time=4.0, + def __init__(self, client, dt=0.001, backend="nengo"): + super(SimControl, self).__init__(client) + self._dt = dt + # self.shown_time = shown_time + # self.kept_time = kept_time + self.backend = backend + + self.node = None + self.last_time = None + + self.paused = True + self.rate = 0.0 + + self.skipped = 1 + self.time = 0.0 + self.target_rate = 1.0 # desired speed of simulation + self.target_scale = None # desired proportion of full speed + self.delay_time = 0.0 # amount of delay per time step + self.rate_proportion = 1.0 # current proportion of full speed + self.sleep_offset = 0.0 # difference from actual sleep time + + # TODO: really need a better way to do this + self.voltage_comps = [] + + self._sim = None + self.simthread = ControlledThread(self._step) + self.simthread.pause() + self.simthread.start() + + # TODO: Make sure this is handled + # if self.page.changed: + # self.paused = True + # self.page.sim = None + # self.page.changed = False + + @property + @bind("simcontrol.get_backend") + def backend(self): + return self._backend + + @backend.setter + @bind("simcontrol.set_backend") + def backend(self, backend): + self._backend = backend + # self.client.dispatch("page.rebuild") # ??? also reset ??? + + @property + @bind("simcontrol.get_dt") + def dt(self): + return self._dt + + @property + def sim(self): + return self._sim + + @sim.setter + def sim(self, value): + self.simthread.pause() + if self._sim is not None and self._sim is not value: + self._sim.close() + self._sim = value + + @property + def status(self): + return self._status + + @status.setter + def status(self, val): + self._status = val + self.send_status() + + def add_nengo_objects(self, network): + with network: + self.node = nengo.Node(self.control, size_out=0) + + def attach(self, fast_client): + self.fast_client = fast_client + + def remove_nengo_objects(self, network): + network.nodes.remove(self.node) + + # TODO: This logic should be part of _step, not something injected + # into the simulation. Pausing the thread is already possible, + # but only used when changing the simulator. Ideally we'd have the + # _step control the throttling as well. + def control(self, t): + """Node embedded in the model to control simulation progression. + + Sleeps while the simulation is paused. + """ + + # TODO: this is a hack too far! + for v in self.voltage_comps: + # This should happen once per timestep... + assert len(self._sim.data.raw[v.probe]) == 1 + + data = self._sim.data.raw[v.probe][0] + assert data.shape == (v.n_neurons,) + del self._sim.data.raw[v.probe][:] # clear the data + v.fast_client.send(np.hstack([t, data])) + # OK hack over + + actual_dt = t - self.time + self.time = t + + now = timeit.default_timer() + if self.last_time is not None: + dt = now - self.last_time + if dt == 0: + self.skipped += 1 + else: + rate = actual_dt * self.skipped / dt + decay = np.exp(-dt / self.RATE_TAU) + self.rate *= decay + self.rate += (1 - decay) * rate + self.skipped = 1 + + if actual_dt > 0: + # compute current proportion of full speed + self.rate_proportion = 1.0 - ( + (self.rate * self.delay_time) / actual_dt) + + self.fast_client.send(np.array( + [self.time, self.rate, self.rate_proportion], dtype=np.float64)) + + # if we have a desired proportion, use it to control delay_time + # Note that we need last_time to not be None so that we have a + # valid dt value. + if self.target_scale is not None and self.last_time is not None: + s = self.target_scale + if s <= 0: + self.delay_time = 0.5 + else: + self.delay_time = (1.0/s - s) * (dt - self.delay_time) + + # if we have a desired rate, do a simple P-controller to get there + if self.target_rate is not None: + rate_error = self.rate - self.target_rate + delta = rate_error * 0.0000002 + self.delay_time += delta + + self.delay_time = np.clip(self.delay_time, 0, 0.5) + if self.delay_time > 0: + self.sleep(self.delay_time) + + self.last_time = now + + # Sleeps to prevent the simulation from advancing + # while the simulation is paused + while self.paused and self._sim is not None: + time.sleep(0.01) + self.last_time = None + + def _set_stream(self, name, output, line=None): + self.client.dispatch("editor.%s" % (name,), output=output, line=line) + + def _step(self): + try: + if hasattr(self._sim, 'max_steps'): + # this is only for the nengo_spinnaker simulation + self._sim.run_steps(self._sim.max_steps) + else: + self._sim.step() + except Exception as err: + self.status = 'build_error' + self._set_stream("stderr", traceback.format_exc()) + self.sim = None + + def build(self, network, filename): + # Remove the current simulator + self.sim = None + + # Build the simulation + Simulator = importlib.import_module(self.backend).Simulator + + sim = None + env = exec_env.ExecutionEnvironment(filename, allow_sim=True) + try: + with env: + # TODO: make it possible to pass args to the simulator + sim = Simulator(network, dt=self.dt) + except Exception: + self._set_stream("stderr", traceback.format_exc()) + self._set_stream("stdout", env.stdout.getvalue()) + self.sim = sim + + def send_status(self): + self.client.send("simcontrol.status", status=self.status) + + @bind("simcontrol.pause") + def pause(self): + self.paused = True + self.status = "paused" + self.simthread.pause() + + @bind("simcontrol.config") + def config(self): + self.client.send("simcontrol.config", + sims=[exec_env.discover_backends()], + current=self.backend) + # TODO: Move to typescript + # client.write_text('confignengo.toolbar.config_modal_show();') + # def backend_options_html(self): + # items = [] + # for module in exec_env.discover_backends(): + # if module == self.page.settings.backend: + # selected = ' selected' + # else: + # selected = '' + # item = '' % (selected, module) + # items.append(item) + # return ''.join(items) + + @bind("simcontrol.play") + def play(self): + if self._sim is None: + self.status = 'building' + self.client.dispatch("page.build") + self.paused = False + self.status = "running" + self.simthread.play() + + @bind("simcontrol.reset") + def reset(self): + self.paused = True + self.simthread.pause() + self.sim = None + self.time = 0 + self.rate = 0 + self.rate_proportion = 1.0 + self.fast_client.send(np.array( + [self.time, self.rate, self.rate_proportion], dtype=np.float64)) + + @bind("simcontrol.target_scale") + def target_scale(self, target): + self.target_scale = float(target) + self.target_rate = None + + def sleep(self, delay_time): + """Attempt to sleep for an amount of time without a busy loop. + + This keeps track of the difference between the requested time.sleep() + time and the actual amount of time slept, and then subtracts that + difference from future smart_sleep calls. This should give an + overall consistent sleep() time even if the actual sleep() time + is inaccurate. + """ + t = delay_time + self.sleep_offset + if t >= 0: + start = timeit.default_timer() + time.sleep(t) + end = timeit.default_timer() + self.sleep_offset += delay_time - (end - start) + else: + self.sleep_offset += delay_time diff --git a/nengo_gui/static/ace.css b/nengo_gui/static/ace.css deleted file mode 100644 index f157b0e7..00000000 --- a/nengo_gui/static/ace.css +++ /dev/null @@ -1,50 +0,0 @@ -#rightpane { - -webkit-flex: 0 0 auto; - flex: 0 0 auto; - - display: -webkit-flex; - display: flex; - -webkit-flex-direction: column; - flex-direction: column; -} - -#editor { - border-left:5px solid #606060; - flex: 1 1 auto; -} - -#console { - border-top:5px solid #606060; - border-left:5px solid #606060; - padding: 0.5em; - border-radius: 0px; - overflow: auto; - background: white; - -webkit-flex: 0 0 auto; - flex: 0 0 auto; - -webkit-user-select: text; - -khtml-user-select: text; - -moz-user-select: text; - -ms-user-select: text; - user-select: text; - display: block; -} - -#console pre { - padding: 0px; - margin: 0px; - background: none; - border: 0px; - /* Use same font-family as Ace editor */ - font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace; - white-space: pre-wrap; -} - -#console_stdout { - color: #1c73b3; -} - -#console_error { - color: #d65e00; - font-weight: 600; -} diff --git a/nengo_gui/static/ace.js b/nengo_gui/static/ace.js deleted file mode 100644 index 07ca9b8a..00000000 --- a/nengo_gui/static/ace.js +++ /dev/null @@ -1,263 +0,0 @@ -/** - * Code Editor - * @constructor - * - * @param {string} uid - A unique identifier - * @param {dict} args - A set of constructor arguments (see Nengo.Component) - * Ace function is written into HTML by server and called when the - * page is loaded. - */ - - -Nengo.disable_editor = function() { - $('#Toggle_ace').css('display', 'none'); - $('#Save_file').css('display', 'none'); - $('#Font_increase').css('display', 'none'); - $('#Font_decrease').css('display', 'none'); -} - -Nengo.Ace = function (uid, args) { - this.AceRange = ace.require('ace/range').Range; - if (uid[0] === '<') { - console.log("invalid uid for Ace: " + uid); - } - var self = this; - this.min_width = 50; - this.max_width = $(window).width() - 100; - - this.ws = Nengo.create_websocket(uid); - this.ws.onmessage = function(event) {self.on_message(event);} - - this.current_code = ''; - var code_div = document.createElement('div') - code_div.id = 'editor' - $('#rightpane').append(code_div); - this.editor = ace.edit('editor') - this.editor.getSession().setMode("ace/mode/python"); - this.editor.gotoLine(1); - this.marker = null; - - this.console = document.createElement('div'); - this.console.id = 'console'; - $('#rightpane').append(this.console); - this.console_height = Nengo.config.console_height; - this.console_stdout = document.createElement('pre'); - this.console_error = document.createElement('pre'); - this.console_stdout.id = 'console_stdout'; - this.console_error.id = 'console_error'; - this.console.appendChild(this.console_stdout); - this.console.appendChild(this.console_error); - $('#console').height(this.console_height); - - this.save_disabled = true; - this.update_trigger = true; // if an update of the model from the code editor is allowed - this.auto_update = true; // automatically update the model based on the text - - //Setup the button to toggle the code editor - $('#Toggle_ace').on('click', function(){self.toggle_shown();}); - $('#Save_file').on('click', function(){self.save_file();}); - $('#Font_increase').on('click', function(){self.font_size += 1;}); - $('#Font_decrease').on('click', function(){self.font_size -= 1;}); - - this.schedule_updates(); - - Object.defineProperty(this, 'width', { - get: function() { - return Nengo.config.editor_width; - }, - set: function(val) { - val = Math.max(Math.min(val, this.max_width), this.min_width); - $('#rightpane').width(val); - Nengo.config.editor_width = val; - } - - }); - - Object.defineProperty(this, 'hidden', { - get: function() { - return Nengo.config.hide_editor; - }, - set: function(val) { - Nengo.config.hide_editor = val; - if (val) { - this.hide_editor(); - } else { - this.show_editor(); - } - } - }); - - Object.defineProperty(this, 'font_size', { - get: function() { - return Nengo.config.editor_font_size; - }, - set: function(val) { - val = Math.max(val, 6); - this.editor.setFontSize(val); - Nengo.config.editor_font_size = val; - } - }); - - // automatically update the model based on the text - Object.defineProperty(this, 'auto_update', { - get: function() { - return Nengo.config.auto_update; - }, - set: function(val) { - this.update_trigger = val; - Nengo.config.auto_update = val; - } - }); - - this.width = Nengo.config.editor_width; - this.hidden = Nengo.config.hide_editor; - this.font_size = Nengo.config.editor_font_size; - this.auto_update = Nengo.config.auto_update; - this.redraw(); - - $(window).on('resize', function() {self.on_resize();}); - interact('#editor') - .resizable({ - edges: { left: true, right: false, bottom: false, top: false} - }) - .on('resizemove', function (event) { - self.width -= event.deltaRect.left; - self.redraw(); - }); - - interact('#console') - .resizable({ - edges: { left: true, right: false, bottom: false, top: true} - }) - .on('resizemove', function (event) { - var max = $('#rightpane').height() - 40; - var min = 20; - - self.console_height -= event.deltaRect.top; - - self.console_height = Nengo.clip(self.console_height, min, max); - $('#console').height(self.console_height); - - self.width -= event.deltaRect.left; - self.redraw(); - }) - .on('resizeend', function (event) { - Nengo.config.console_height = self.console_height; - }); -} - -//Send changes to the code to server every 100ms -Nengo.Ace.prototype.schedule_updates = function () { - var self = this; - setInterval(function () { - var editor_code = self.editor.getValue(); - if (editor_code != self.current_code) { - if (self.update_trigger) { - self.update_trigger = self.auto_update; - self.ws.send(JSON.stringify({code:editor_code, save:false})); - self.current_code = editor_code; - self.enable_save(); - $('#Sync_editor_button').addClass('disabled'); - } else { - // Visual indication that the code is different than the model displayed - $('#Sync_editor_button').removeClass('disabled'); - } - } - }, 100) -} - -Nengo.Ace.prototype.save_file = function () { - if (!($('#Save_file').hasClass('disabled'))) { - var editor_code = this.editor.getValue(); - this.ws.send(JSON.stringify({code:editor_code, save:true})); - this.disable_save(); - } -} - -Nengo.Ace.prototype.enable_save = function () { - $('#Save_file').removeClass('disabled'); -} - -Nengo.Ace.prototype.disable_save = function () { - $('#Save_file').addClass('disabled'); -} - -Nengo.Ace.prototype.on_message = function (event) { - var msg = JSON.parse(event.data) - if (msg.code !== undefined) { - this.editor.setValue(msg.code); - this.current_code = msg.code; - this.editor.gotoLine(1); - this.redraw(); - this.disable_save(); - } else if (msg.error === null) { - if (this.marker !== null) { - this.editor.getSession().removeMarker(this.marker); - this.marker = null; - this.editor.getSession().clearAnnotations(); - } - $(this.console_stdout).text(msg.stdout); - $(this.console_error).text(''); - this.console.scrollTop = this.console.scrollHeight; - } else if (msg.filename !== undefined) { - if (msg.valid) { - $('#filename')[0].innerHTML = msg.filename; - // update the URL so reload and bookmarks work as expected - history.pushState({}, msg.filename, '/?filename=' + msg.filename); - } else { - alert(msg.error); - } - } else if (msg.error !== undefined) { - var line = msg.error.line; - this.marker = this.editor.getSession() - .addMarker(new this.AceRange(line - 1, 0, line - 1, 10), - 'highlight', 'fullLine', true); - this.editor.getSession().setAnnotations([{ - row: line - 1, - type: 'error', - text: msg.short_msg, - }]); - $(this.console_stdout).text(msg.stdout); - $(this.console_error).text(msg.error.trace); - this.console.scrollTop = this.console.scrollHeight; - } else { - console.log(msg); - } -} - -Nengo.Ace.prototype.on_resize = function() { - this.max_width = $(window).width() - 100; - if (this.width > this.max_width) { - this.width = this.max_width; - } - this.redraw(); -} - -Nengo.Ace.prototype.show_editor = function () { - var editor = document.getElementById('rightpane'); - editor.style.display = 'flex'; - this.redraw(); -} - -Nengo.Ace.prototype.hide_editor = function () { - var editor = document.getElementById('rightpane'); - editor.style.display = 'none'; - this.redraw(); -} - -Nengo.Ace.prototype.toggle_shown = function () { - if (this.hidden) { - this.hidden = false; - } else { - this.hidden = true; - } - this.redraw(); -} - -Nengo.Ace.prototype.redraw = function () { - this.editor.resize(); - if (Nengo.netgraph !== undefined){ - Nengo.netgraph.on_resize(); - } - viewport.on_resize(); -} diff --git a/nengo_gui/static/components/2d_axes.js b/nengo_gui/static/components/2d_axes.js deleted file mode 100644 index ec8d990f..00000000 --- a/nengo_gui/static/components/2d_axes.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Basic 2d axes set. - * @constructor - * - * @param {DOMElement} parent - the element to add this component to - * @param {float} args.width - the width of the axes (in pixels) - * @param {float} args.height - the height of the axes (in pixels) - * @param {float} args.min_value - minimum value on y-axis - * @param {float} args.max_value - maximum value on y-axis - */ - -Nengo.Axes2D = function(parent, args) { - var self = this; - - this.max_y_width = 100; - - /** draw the plot as an SVG */ - this.svg = d3.select(parent).append('svg') - .attr('width', '100%') - .attr('height', '100%'); - - /** scales for mapping x and y values to pixels */ - this.scale_x = d3.scale.linear(); - this.scale_y = d3.scale.linear(); - this.scale_y.domain([args.min_value, args.max_value]); - - /** spacing between the graph and the outside edges (in pixels) */ - this.set_axes_geometry(args.width, args.height); - - /** define the x-axis */ - this.axis_x = d3.svg.axis() - .scale(this.scale_x) - .orient("bottom") - .ticks(2); - - this.axis_x_g = this.svg.append("g") - .attr("class", "axis axis_x unselectable") - .call(this.axis_x); - - /** define the y-axis */ - this.axis_y = d3.svg.axis() - .scale(this.scale_y) - .orient("left") - .tickValues([args.min_value, args.max_value]); - - this.axis_y_g = this.svg.append("g") - .attr("class", "axis axis_y unselectable") - .call(this.axis_y); -}; - -Nengo.Axes2D.prototype.set_axes_geometry = function(width, height) { - scale = parseFloat($('#main').css('font-size')); - this.width = width; - this.height = height; - this.ax_left = this.max_y_width; - this.ax_right = width - 1.75 * scale; - this.ax_bottom = height - 1.75 * scale; - this.ax_top = 1.75 * scale; - - this.tick_size = 0.4 * scale; - this.tick_padding = 0.2 * scale; -}; - -/** - * Adjust the graph layout due to changed size - */ -Nengo.Axes2D.prototype.on_resize = function(width, height) { - if (width < this.minWidth) { - width = this.minWidth; - } - if (height < this.minHeight) { - height = this.minHeight; - }; - this.set_axes_geometry(width, height); - - this.scale_x.range([this.ax_left, this.ax_right]); - this.scale_y.range([this.ax_bottom, this.ax_top]); - - //Adjust positions of x axis on resize - this.axis_x - .tickPadding(this.tick_padding) - .outerTickSize(this.tick_size, this.tick_size); - this.axis_y - .tickPadding(this.tick_padding) - .outerTickSize(this.tick_size, this.tick_size); - - this.axis_x_g.attr("transform", "translate(0," + this.ax_bottom + ")"); - this.axis_x_g.call(this.axis_x); - this.axis_y_g.attr("transform", "translate(" + this.ax_left + ", 0)"); - this.axis_y_g.call(this.axis_y); -}; - -Nengo.Axes2D.prototype.fit_ticks = function(parent){ - var self = this; - setTimeout(function(){ - var ticks = $(parent.div).find('.tick'); - var max_w = 0; - for (var i = 0; i < ticks.length; i++) { - var w = ticks[i].getBBox().width; - if (w > max_w) { - max_w = w; - } - } - self.max_y_width = max_w; - self.set_axes_geometry(); - self.on_resize(parent.width, parent.height); - }, 1) -} diff --git a/nengo_gui/static/components/component.css b/nengo_gui/static/components/component.css new file mode 100644 index 00000000..17ad22b3 --- /dev/null +++ b/nengo_gui/static/components/component.css @@ -0,0 +1,31 @@ +svg.netgraph { + text { + cursor: default; + + &.component-label { + dominant-baseline: text-after-edge; + text-anchor: middle; + } + + &:active { + cursor: move; + } + } + + rect.overlay { + fill: transparent; + } + + g.widgets > g:hover { + rect.overlay { + stroke: gray(40%) { + width: 1; + }; + } + } + + rect.view { + fill: #d9edf7; + stroke: #ccc; + } +} diff --git a/nengo_gui/static/components/component.js b/nengo_gui/static/components/component.js deleted file mode 100644 index c02b908b..00000000 --- a/nengo_gui/static/components/component.js +++ /dev/null @@ -1,307 +0,0 @@ -/** - * Base class for interactive visualization - * Components (value/raster/XY plots, sliders, etc...) will inherit from - * this class. - * - * @constructor - * - * @param {dict} args - A set of constructor arguments, including: - * @param {DOMElement} args.parent - the element to add this component to - * @param {float} args.x - the left side of the component (in pixels) - * @param {float} args.y - the top of the component (in pixels) - * @param {float} args.width - the width of the component (in pixels) - * @param {float} args.height - the height of the component (in pixels) - * @param {boolean} args.label_visible - whether the label should be shown - * @param {int} args.id - the id of the server-side component to connect to - * - * Component is inherited by specific component - * class prototypes (ie. Slider, Value) - * - */ -Nengo.Component = function(parent, args) { - var self = this; - - this.viewport = viewport; - - /** Create the div for the component and position it */ - this.div = document.createElement('div'); - - /** Prevent interact from messing up cursor */ - interact(this.div).styleCursor(true); - - this.x = args.x; - this.y = args.y; - this.w = args.width; - this.h = args.height; - - this.redraw_size(); - this.redraw_pos(); - - this.div.style.position = 'absolute'; - this.div.classList.add('graph'); - parent.appendChild(this.div); - this.parent = parent; - - this.label = document.createElement('div'); - this.label.classList.add('label', 'unselectable'); - this.label.innerHTML = args.label.replace('<', '<').replace('>', '>'); - this.label.style.position = 'fixed'; - this.label.style.width = args.width; - this.label.style.height = '2em'; - this.label_visible = true; - this.div.appendChild(this.label); - if (args.label_visible === false) { - this.hide_label(); - } - - self.minWidth = 2; - self.minHeight = 2; - - /** Move element to be drawn on top when clicked on */ - - this.div.onmousedown = function() { - this.style.zIndex = Nengo.next_zindex(); - }; - - this.div.ontouchstart = this.div.onmousedown; - - /** Allow element to be dragged */ - interact(this.div) - .draggable({ - inertia: true, - onstart: function () { - self.menu.hide_any(); - }, - onmove: function (event) { - var target = event.target; - - self.x = self.x + event.dx / (self.viewport.w * self.viewport.scale); - self.y = self.y + event.dy / (self.viewport.h * self.viewport.scale); - - self.redraw_pos(); - - }, - onend: function (event) { - self.save_layout(); - } - }) - - /** Allow element to be resized */ - interact(this.div) - .resizable({ - edges: { left: true, top: true, right: true, bottom: true } - }) - .on('resizestart', function (event) { - self.menu.hide_any(); - }) - .on('resizemove', function(event) { - var target = event.target; - var newWidth = event.rect.width; - var newHeight = event.rect.height; - var dx = event.deltaRect.left ; - var dy = event.deltaRect.top ; - var dz = event.deltaRect.right; - var da = event.deltaRect.bottom; - - self.x += (dx + dz) / 2 / (viewport.w * viewport.scale); - self.y += (dy + da) / 2 / (viewport.h * viewport.scale); - - self.w = newWidth / (viewport.w * viewport.scale) / 2; - self.h = newHeight / (viewport.h * viewport.scale) / 2; - - self.on_resize(newWidth, newHeight); - self.redraw_size(); - self.redraw_pos(); - }) - .on('resizeend', function(event) { - self.save_layout(); - }); - - /** Open a WebSocket to the server */ - this.uid = args.uid; - if (this.uid != undefined) { - this.ws = Nengo.create_websocket(this.uid); - this.ws.onmessage = function(event) {self.on_message(event);} - } - - /** flag whether there is a scheduled update that hasn't happened yet */ - this.pending_update = false; - - this.menu = new Nengo.Menu(self.parent); - interact(this.div) - .on('hold', function(event) { //change to 'tap' for right click - if (event.button == 0) { - if (self.menu.visible_any()) { - self.menu.hide_any(); - } else { - self.menu.show(event.clientX, event.clientY, - self.generate_menu()); - } - event.stopPropagation(); - } - }) - .on('tap', function(event) { //get rid of menus when clicking off - if (event.button == 0) { - if (self.menu.visible_any()) { - self.menu.hide_any(); - } - } - }); - $(this.div).bind('contextmenu', function(event) { - event.preventDefault(); - event.stopPropagation(); - if (self.menu.visible_any()) { - self.menu.hide_any(); - } else { - self.menu.show(event.clientX, event.clientY, - self.generate_menu()); - } - }); - - Nengo.Component.components.push(this); -}; - -Nengo.Component.components = []; -Nengo.Component.save_components = function() { - for (var index in Nengo.Component.components) { - Nengo.Component.components[index].save_layout(); - } -}; - -/** - * Method to be called when Component is resized - */ -Nengo.Component.prototype.on_resize = function(width, height) {}; - -/** - * Method to be called when Component received a WebSocket message - */ -Nengo.Component.prototype.on_message = function(event) {}; - - -Nengo.Component.prototype.generate_menu = function() { - var self = this; - var items = []; - if (this.label_visible) { - items.push(['Hide label', function() { - self.hide_label(); - self.save_layout(); - }]); - } else { - items.push(['Show label', function() { - self.show_label(); - self.save_layout(); - }]); - } - items.push(['Remove', function() {self.remove();}]); - return items; -}; - -Nengo.Component.prototype.remove = function(undo_flag, notify_server) { - undo_flag = typeof undo_flag !== 'undefined' ? undo_flag : false; - notify_server = typeof notify_server !== 'undefined' ? notify_server : true; - - if (notify_server) { - if (undo_flag === true) { - this.ws.send('remove_undo'); - } else { - this.ws.send('remove'); - } - } - this.parent.removeChild(this.div); - var index = Nengo.Component.components.indexOf(this); - Nengo.Component.components.splice(index, 1); -} - - -/** - * Schedule update() to be called in the near future. If update() is already - * scheduled, then do nothing. This is meant to limit how fast update() is - * called in the case that we are changing the data faster than whatever - * processing is needed in update() - */ -Nengo.Component.prototype.schedule_update = function(event) { - if (this.pending_update == false) { - this.pending_update = true; - var self = this; - window.setTimeout( - function() { - self.pending_update = false; - self.update() - }, 10); - } -} - -/** - * Do any visual updating that is needed due to changes in the underlying data - */ -Nengo.Component.prototype.update = function(event) { -} - -Nengo.Component.prototype.hide_label = function(event) { - if (this.label_visible) { - this.label.style.display = 'none'; - this.label_visible = false; - } -} - -Nengo.Component.prototype.show_label = function(event) { - if (!this.label_visible) { - this.label.style.display = 'inline'; - this.label_visible = true; - } -} - - -Nengo.Component.prototype.layout_info = function () { - var info = {}; - info.x = this.x; - info.y = this.y; - info.width = this.w; - info.height = this.h; - info.label_visible = this.label_visible; - return info; -} - -Nengo.Component.prototype.save_layout = function () { - var info = this.layout_info(); - this.ws.send('config:' + JSON.stringify(info)); -} - -Nengo.Component.prototype.update_layout = function (config) { - this.w = config.width; - this.h = config.height; - this.x = config.x; - this.y = config.y; - - this.redraw_size(); - this.redraw_pos(); - this.on_resize(this.get_screen_width(), this.get_screen_height()); - - if (config.label_visible === true) { - this.show_label(); - } else { - this.hide_label(); - } -} - - -Nengo.Component.prototype.redraw_size = function () { - this.width = this.viewport.w * this.w * this.viewport.scale * 2; - this.height = this.viewport.h * this.h * this.viewport.scale * 2; - this.div.style.width = this.width; - this.div.style.height = this.height; -}; - -Nengo.Component.prototype.redraw_pos = function () { - var x = (this.x + this.viewport.x - this.w) * this.viewport.w * this.viewport.scale; - var y = (this.y + this.viewport.y - this.h) * this.viewport.h * this.viewport.scale; - Nengo.set_transform(this.div, x, y); -}; - -Nengo.Component.prototype.get_screen_width = function () { - return this.viewport.w * this.w * this.viewport.scale * 2 -}; -Nengo.Component.prototype.get_screen_height = function () { - return this.viewport.h * this.h * this.viewport.scale * 2 -}; diff --git a/nengo_gui/static/components/component.ts b/nengo_gui/static/components/component.ts new file mode 100644 index 00000000..218e15cc --- /dev/null +++ b/nengo_gui/static/components/component.ts @@ -0,0 +1,297 @@ +import * as interact from "interactjs"; +import { h } from "maquette"; + +import "./component.css"; + +import { config } from "../config"; +import { Menu } from "../menu"; +import { NetGraph } from "../netgraph/main"; +import { Network } from "./network"; +import { AxesView, LegendView, PlotView } from "./plot"; +import { Position } from "./position"; +import { Connection, FastServerConnection } from "../server"; +import * as utils from "../utils"; + +export abstract class Component { + interactRoot; + menu: Menu; + uid: string; + view: ComponentView; + + protected server: Connection; + + protected static resizeDefaults = { + edges: { bottom: true, left: true, right: true, top: true }, + invert: "none", + margin: 10, + restrictSize: { + min: { height: 25, width: 25 } + } + }; + + constructor( + server: Connection, + uid: string, + view: ComponentView, + label: string, + pos: Position, + labelVisible: boolean = true + ) { + this.server = server; + this.uid = uid; + this.view = view; + this.view.pos = [pos.left, pos.top]; + this.view.scale = [pos.width, pos.height]; + this.label = label; + this.labelVisible = labelVisible; + + this.menu = new Menu(); + this.addMenuItems(); + + // TODO: nesting + // if (parent !== null) { + // this.view.parent.children.push(this); + // } + + this.interactRoot = interact(this.view.overlay); + + // TODO: Dicuss: previously, only plots had inertia. Should they all? + this.interactRoot.draggable({ inertia: true }); + this.interactRoot.on("dragmove", event => { + const [left, top] = this.view.pos; + this.view.pos = [left + event.dx, top + event.dy]; + }); + this.interactRoot.on("dragstart", () => { + Menu.hideShown(); + }); + this.interactRoot.on("dragend", () => { + // TODO: do something to update config, communicate to server? + // this.syncWithView(); + }); + + // --- Menu + const toggleMenu = event => { + if (Menu.shown !== null) { + Menu.hideShown(); + } else { + this.menu.show(event.clientX, event.clientY); + } + event.stopPropagation(); + }; + this.interactRoot.on("hold", event => { + if (event.button === 0) { + toggleMenu(event); + } + }); + // this.interactRoot.on("tap doubletap", (event) => { + // Menu.hideShown(); + // }); + this.interactRoot.on("contextmenu", event => { + event.preventDefault(); + toggleMenu(event); + }); + + const resizeOptions = this.resizeOptions; + if (resizeOptions != null) { + this.interactRoot.resizable(this.resizeOptions); + this.interactRoot.on("resizestart", event => { + Menu.hideShown(); + }); + this.interactRoot.on("resizemove", event => { + const dRect = event.deltaRect; + const [left, top] = this.view.pos; + const [width, height] = this.view.scale; + this.view.pos = [left + dRect.left, top + dRect.top]; + this.view.scale = [width + dRect.width, height + dRect.height]; + }); + this.interactRoot.on("resizeend", event => { + // TODO: turn this into an actual function call + // this.ng.notify("posSize", { + // height: this.view.height, + // uid: this.uid, + // width: this.view.width, + // x: this.view.x, + // y: this.view.y, + // }); + }); + } + } + + get label(): string { + return this.view.label; + } + + set label(val: string) { + this.view.label = val; + } + + get labelVisible(): boolean { + return this.view.labelVisible; + } + + set labelVisible(val: boolean) { + this.view.labelVisible = val; + } + + get pos(): [number, number] { + return this.view.pos; + } + + set pos(val: [number, number]) { + this.view.pos = val; + } + + get resizeOptions(): any { + // Note: return null to make not resizable + return Component.resizeDefaults; + } + + addMenuItems() {} + + // TODO: rename to createComponent? + createGraph(graphType, args = null) { + // tslint:disable-line + // TODO: get nested implemented this + // const w = this.nestedWidth; + // const h = this.nestedHeight; + // const pos = this.view.screenLocation; + // const w = this.view.width; + // const h = this.view.height; + // TODO: implement an interface for this and rename it + // const info: any = { + // height: this.ng.viewPort.fromScreenY(100), + // type: graphType, + // uid: this.uid, + // width: this.ng.viewPort.fromScreenX(100), + // x: this.ng.viewPort.fromScreenX(pos[0]) + // - this.ng.viewPort.shiftX(w), + // y: this.ng.viewPort.fromScreenY(pos[1]) + // - this.ng.viewPort.shiftY(h), + // }; + // if (args !== null) { + // info.args = args; + // } + // if (info.type === "Slider") { + // info.width /= 2; + // } + // TODO: change this to an actual function call + // this.ng.notify("createGraph", info); + } + + ondomadd() { + this.view.ondomadd(); + } + + onnetadd(network: Network) { + this.interactRoot.draggable({ + restrict: { restriction: network.view.root } + }); + if (this.resizeOptions != null) { + this.interactRoot.resizable({ + restrict: { restriction: network.view.root } + }); + } + } + + onnetgraphadd(netgraph: NetGraph) {} + + scale(factor: number) { + const [left, top] = this.view.pos; + this.view.pos = [left * factor, top * factor]; + const [width, height] = this.view.scale; + this.view.scale = [width * factor, height * factor]; + } +} + +export abstract class ComponentView { + body: SVGGElement; + overlay: SVGRectElement; + root: SVGGElement; + + protected _label: SVGTextElement; + + constructor() { + const node = h("g", { transform: "translate(0,0)" }, [ + h("text.component-label", { transform: "translate(0,0)" }), + h("rect.overlay") + ]); + + // Create the SVG group to hold this item's shape and it's label + this.root = utils.domCreateSVG(node) as SVGGElement; + this.overlay = this.root.querySelector(".overlay") as SVGRectElement; + this._label = this.root.querySelector("text") as SVGTextElement; + } + + get centerPos(): [number, number] { + const [width, height] = this.overlayScale; + const [left, top] = this.pos; + return [left + width * 0.5, top + height * 0.5]; + } + + get label(): string { + return this._label.textContent; + } + + set label(val: string) { + this._label.textContent = val; + } + + get labelVisible(): boolean { + return this._label.style.display === ""; + } + + set labelVisible(val: boolean) { + if (val) { + this._label.style.display = ""; + } else { + this._label.style.display = "none"; + } + } + + get overlayScale(): [number, number] { + return [ + Number(this.overlay.getAttribute("width")), + Number(this.overlay.getAttribute("height")) + ]; + } + + set overlayScale(val: [number, number]) { + this.overlay.setAttribute("width", `${val[0]}`); + this.overlay.setAttribute("height", `${val[1]}`); + this.updateLabel(); + } + + get pos(): [number, number] { + return utils.getTranslate(this.root); + } + + set pos(val: [number, number]) { + utils.setTranslate(this.root, val[0], val[1]); + this.updateLabel(); + } + + get scale(): [number, number] { + return this.overlayScale; + } + + set scale(val: [number, number]) { + this.overlayScale = val; + } + + protected updateLabel() { + const [width, height] = this.overlayScale; + utils.setTranslate( + this._label, + width * 0.5, + height + this._label.getBBox().height + ); + } + + ondomadd() { + // Since label position depends on actual size, reposition once added + this.scale = this.scale; + + // Ensure that overlay is on top + this.root.appendChild(this.overlay); + } +} diff --git a/nengo_gui/static/components/connection.css b/nengo_gui/static/components/connection.css new file mode 100644 index 00000000..74ec383e --- /dev/null +++ b/nengo_gui/static/components/connection.css @@ -0,0 +1,13 @@ +svg.netgraph g.connection { + line, path { + fill: none; + stroke: black { + width: 2; + } + } + + path.arrow { + fill: black; + stroke: none; + } +} diff --git a/nengo_gui/static/components/connection.ts b/nengo_gui/static/components/connection.ts new file mode 100644 index 00000000..9b18e229 --- /dev/null +++ b/nengo_gui/static/components/connection.ts @@ -0,0 +1,289 @@ +import { VNode, dom, h } from "maquette"; + +import "./connection.css"; + +import { Component } from "./component"; +import * as utils from "../utils"; + +// Note: connections are not registered in the component registry because +// they're handled differently by the netgraph; see ../netgraph.ts + +export abstract class ComponentConnection { + view: ConnectionView | RecurrentConnectionView; + + get visible(): boolean { + return this.view.visible; + } + + set visible(val: boolean) { + this.view.visible = val; + } + + abstract syncWithComponents(); +} + +export class FeedforwardConnection extends ComponentConnection { + readonly pre: Component; + readonly post: Component; + view: ConnectionView; + + constructor(pre, post) { + super(); + console.assert(this.pre !== this.post); + this.pre = pre; + this.post = post; + this.view = new ConnectionView(); + this.syncWithComponents(); + + this.pre.interactRoot.on("dragmove resizemove", () => { + this.view.startPos = this.pre.view.centerPos; + }); + this.post.interactRoot.on("dragmove resizemove", () => { + this.view.endPos = this.post.view.centerPos; + }); + } + + syncWithComponents() { + this.view.startPos = this.pre.view.centerPos; + this.view.endPos = this.post.view.centerPos; + } + + // constructor(ng, info, minimap, miniConn) { + // Flag to indicate this Connection has been deleted + // this.removed = false; + + // The actual NetGraphItem currently connected to/from + // this.pre = null; + // this.post = null; + + // this.minimap = minimap; + // this.miniConn = miniConn; + // if (!minimap) { + // this.gConns = ng.gConns; + // this.objects = ng.svgObjects; + // } else { + // this.gConns = ng.gConnsMini; + // this.objects = ng.minimapObjects; + // } + + // The uids for the pre and post items in the connection. + + // The lists start with the ideal target item, followed by the parent + // of that item, and its parent, and so on. If the first item on the + // this does not exist (due to it being inside a collapsed network), + // the connection will look for the next item on the list, and so on + // until it finds one that does exist. + // this.pres = info.pre; + // this.posts = info.post; + + // this.recurrent = this.pres[0] === this.posts[0]; + + // Figure out the best available items to connect to + // this.setPre(this.findPre()); + // this.setPost(this.findPost()); + + // Determine parent and add to parent's children list + // if (info.parent === null) { + // this.parent = null; + // } else { + // this.parent = this.objects[info.parent]; + // if (!minimap) { + // this.parent.childConnections.push(this); + // } + // } + + // Create the line and its arrowhead marker + // this.g = ng.createSVGElement("g"); + + // this.createLine(); + + // this.redraw(); + + // this.gConns.appendChild(this.g); + // } +} + +export class RecurrentConnection extends ComponentConnection { + readonly component: Component; + view: RecurrentConnectionView; + + constructor(component) { + super(); + this.component = component; + this.view = new RecurrentConnectionView(); + this.syncWithComponents(); + + this.component.interactRoot.on("dragmove resizemove", () => + this.syncWithComponents() + ); + } + + syncWithComponents() { + this.view.width = this.component.view.scale[0] * 1.4; + this.view.pos = this.component.view.centerPos; + } +} + +function arrowhead(rotate: number = 0): VNode { + return h("path.arrow", { + d: "M 10,0 L -5,-5 -5,5 z", + transform: `translate(0,0) rotate(${rotate})` + }); +} + +export class ConnectionView { + static arrowLocation = 0.6; + + arrow: SVGPathElement; + line: SVGLineElement; + root: SVGGElement; + + constructor() { + const node = h("g.connection", [ + h("line", { x1: "0", x2: "10", y1: "0", y2: "10" }), + arrowhead() + ]); + this.root = utils.domCreateSVG(node) as SVGGElement; + this.arrow = this.root.querySelector("path.arrow") as SVGPathElement; + this.line = this.root.querySelector("line") as SVGLineElement; + } + + get endPos(): [number, number] { + return [ + Number(this.line.getAttribute("x2")), + Number(this.line.getAttribute("y2")) + ]; + } + + set endPos(val: [number, number]) { + this.line.setAttribute("x2", `${val[0]}`); + this.line.setAttribute("y2", `${val[1]}`); + this.syncArrowWithLine(); + } + + get startPos(): [number, number] { + return [ + Number(this.line.getAttribute("x1")), + Number(this.line.getAttribute("y1")) + ]; + } + + set startPos(val: [number, number]) { + this.line.setAttribute("x1", `${val[0]}`); + this.line.setAttribute("y1", `${val[1]}`); + this.syncArrowWithLine(); + } + + get visible(): boolean { + return this.root.style.display !== "none"; + } + + set visible(val: boolean) { + if (val) { + this.root.style.display = null; + } else { + this.root.style.display = "none"; + } + } + + private syncArrowWithLine() { + const start = this.startPos; + const end = this.endPos; + utils.setTranslate( + this.arrow, + utils.lerp(start[0], end[0], ConnectionView.arrowLocation), + utils.lerp(start[1], end[1], ConnectionView.arrowLocation) + ); + // TODO: would be nice to do this in one step, but ok for now + const angle = utils.angle(start[0], end[0], start[1], end[1]); + const transform = this.arrow.getAttribute("transform"); + this.arrow.setAttribute("transform", `${transform} rotate(${angle})`); + } +} + +export class RecurrentConnectionView { + static arrowRotation = 171; // In degrees + + arrow: SVGGElement; + path: SVGGElement; + root: SVGGElement; + + private _width: number = 1.0; + + constructor() { + const node = h( + "g.connection.recurrent", + { + transform: "translate(0,0)" + }, + [ + h("path", { d: "" }), + arrowhead(RecurrentConnectionView.arrowRotation) + ] + ); + + this.root = utils.domCreateSVG(node) as SVGGElement; + this.path = this.root.firstChild as SVGGElement; + this.arrow = this.root.querySelector("path.arrow") as SVGPathElement; + } + + get pos(): [number, number] { + return utils.getTranslate(this.root); + } + + set pos(val: [number, number]) { + const [w, h] = [this.width, this.height]; + const r = RecurrentConnectionView.arrowRotation; + utils.setTranslate(this.root, val[0] + w * 0.15, val[1] - h * 1.1); + this.arrow.setAttribute( + "transform", + utils.singleline` + translate(${-w * 0.13},${h * 0.1165}) + rotate(${r - Math.max(30 - h, 0) * 0.8}) + ` + ); + } + + get height(): number { + // Note: aspect ratio is 1 : 0.675 + return this._width * 0.675; + } + + get width(): number { + return this._width; + } + + set width(val: number) { + const w = val; + // x goes into the negative because we set the position to be + // the center of the object + const d = utils.singleline` + M${w * -0.3663},${w * 0.59656} + C${w * -0.4493},${w * 0.5397} + ${w * -0.5},${w * 0.4645} + ${w * -0.5},${w * 0.3819} + ${w * -0.5},${w * 0.2083} + ${w * -0.27615},${w * 0.0676} + 0,${w * 0.0676} + S${w * 0.5},${w * 0.2083} + ${w * 0.5},${w * 0.3819} + C${w * 0.5},${w * 0.5156} + ${w * 0.367},${w * 0.63} + ${w * 0.18},${w * 0.675} + `; + this.path.setAttribute("d", d); + this._width = val; + } + + get visible(): boolean { + return this.root.style.display !== "none"; + } + + set visible(val: boolean) { + if (val) { + this.root.style.display = null; + } else { + this.root.style.display = "none"; + } + } +} diff --git a/nengo_gui/static/components/ensemble.css b/nengo_gui/static/components/ensemble.css new file mode 100644 index 00000000..07eebfa2 --- /dev/null +++ b/nengo_gui/static/components/ensemble.css @@ -0,0 +1,4 @@ +svg.netgraph g.ensemble { + fill: gray(80%); + stroke: gray(50%); +} diff --git a/nengo_gui/static/components/ensemble.ts b/nengo_gui/static/components/ensemble.ts new file mode 100644 index 00000000..8b3267b6 --- /dev/null +++ b/nengo_gui/static/components/ensemble.ts @@ -0,0 +1,162 @@ +import * as interact from "interactjs"; +import { VNode, dom, h } from "maquette"; + +import "./ensemble.css"; + +import { Component, ComponentView } from "./component"; +import { Position } from "./position"; +import { registerComponent } from "./registry"; +import { Connection } from "../server"; +import * as utils from "../utils"; + +export class Ensemble extends Component { + dimensions: number; + view: EnsembleView; + + constructor({ + server, + uid, + label, + pos, + dimensions, + labelVisible = true + }: { + server: Connection; + uid: string; + label: string; + pos: Position; + dimensions: number; + labelVisible?: boolean; + }) { + super(server, uid, new EnsembleView(), label, pos, labelVisible); + this.dimensions = dimensions; + + // Override resizemove to reposition while resizing + this.interactRoot.events.resizemove[0] = event => { + const dRect = event.deltaRect; + const edges = event.edges; + + let [left, top] = this.view.pos; + if (edges.top && !edges.right && !edges.left) { + left += dRect.left * 0.5; + } else if (edges.bottom && !edges.right && !edges.left) { + left -= dRect.right * 0.5; + } else { + left += dRect.left; + } + if (edges.right && !edges.top && !edges.bottom) { + top -= dRect.bottom * 0.5; + } else if (edges.left && !edges.top && !edges.bottom) { + top += dRect.top * 0.5; + } else { + top += dRect.top; + } + this.view.pos = [left, top]; + + const [width, height] = this.view.scale; + this.view.scale = [width + dRect.width, height + dRect.height]; + }; + } + + get resizeOptions(): any { + const options: any = {}; + for (const option in Component.resizeDefaults) { + options[option] = Component.resizeDefaults[option]; + } + options.preserveAspectRatio = true; + return options; + } + + addMenuItems() { + this.menu.addAction("Value", () => { + this.createGraph("Value"); + }); + this.menu.addAction( + "XY-value", + () => { + this.createGraph("XYValue"); + }, + () => this.dimensions > 1 + ); + this.menu.addAction("Spikes", () => { + this.createGraph("Raster"); + }); + this.menu.addAction("Voltages", () => { + this.createGraph("Voltage"); + }); + this.menu.addAction("Firing pattern", () => { + this.createGraph("SpikeGrid"); + }); + this.menu.addAction("Details ...", () => { + // TODO + // this.createModal(); + }); + } +} + +export class EnsembleView extends ComponentView { + circles: Array; + + // Width and height when g.ensemble transform is scale(1,1) + static baseWidth = 34.547; + static baseHeight = 35.404; + static heightToWidth = EnsembleView.baseWidth / EnsembleView.baseHeight; + + constructor() { + super(); + const r = "4.843"; + const node = h("g.ensemble", { transform: "scale(1,1)" }, [ + h("circle", { cx: r, cy: "10.52", r: r, "stroke-width": "1" }), + h("circle", { + cx: "16.186", + cy: "17.874", + r: r, + "stroke-width": "1" + }), + h("circle", { + cx: "21.012", + cy: "30.561", + r: r, + "stroke-width": "1" + }), + h("circle", { + cx: "29.704", + cy: "17.23", + r: r, + "stroke-width": "1" + }), + h("circle", { + cx: "5.647", + cy: "26.414", + r: r, + "stroke-width": "1" + }), + h("circle", { cx: "19.894", cy: r, r: r, "stroke-width": "1" }) + ]); + this.body = utils.domCreateSVG(node) as SVGGElement; + this.root.appendChild(this.body); + + // Convert NodeList to array + this.circles = utils.toArray(this.body.childNodes); + } + + get scale(): [number, number] { + return this.overlayScale; + } + + set scale(val: [number, number]) { + // Ensembles should keep the same aspect ratio; if we get something else, + // we'll use the larger of the width and height as height, and scale + // the width appropriately. + const height = val[1]; + const width = EnsembleView.heightToWidth * height; + const strokeWidth = `${EnsembleView.baseWidth / width}`; + utils.setScale(this.body, height / EnsembleView.baseHeight); + this.circles.forEach(circle => { + circle.setAttribute("stroke-width", strokeWidth); + }); + this.overlayScale = [width, height]; + } +} + +registerComponent("ensemble", Ensemble); diff --git a/nengo_gui/static/components/htmlview.js b/nengo_gui/static/components/htmlview.js deleted file mode 100644 index 89ce505d..00000000 --- a/nengo_gui/static/components/htmlview.js +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Arbitrary HTML display taking input from a Node - * See nengo_gui/examples/basics/html.py for example usage - * @constructor - * - * @param {DOMElement} parent - the element to add this component to - * @param {Nengo.SimControl} sim - the simulation controller - * @param {dict} args - A set of constructor arguments (see Nengo.Component) - */ - -Nengo.HTMLView = function(parent, sim, args) { - Nengo.Component.call(this, parent, args); - var self = this; - - this.sim = sim; - - this.pdiv = document.createElement('div'); - this.pdiv.style.width = '100%'; - this.pdiv.style.height = '100%'; - Nengo.set_transform(this.pdiv, 0, 0); - this.pdiv.style.position = 'fixed'; - this.pdiv.classList.add('htmlview'); - this.div.appendChild(this.pdiv); - - /** for storing the accumulated data */ - this.data_store = new Nengo.DataStore(1, this.sim, 0); - - /** call schedule_update whenever the time is adjusted in the SimControl */ - this.sim.div.addEventListener('adjust_time', - function(e) {self.schedule_update();}, false); - - this.on_resize(this.get_screen_width(), this.get_screen_height()); - - - -}; -Nengo.HTMLView.prototype = Object.create(Nengo.Component.prototype); -Nengo.HTMLView.prototype.constructor = Nengo.Pointer; - - -/** - * Receive new line data from the server - */ -Nengo.HTMLView.prototype.on_message = function(event) { - var data = event.data.split(" ", 1); - var time = parseFloat(data[0]); - - var msg = event.data.substring(data[0].length + 1); - - this.data_store.push([time, msg]); - this.schedule_update(); -} - -/** - * Redraw the lines and axis due to changed data - */ -Nengo.HTMLView.prototype.update = function() { - /** let the data store clear out old values */ - this.data_store.update(); - - var data = this.data_store.get_last_data()[0]; - - if (data === undefined) { - data = ''; - } - - this.pdiv.innerHTML = data; - -}; - -/** - * Adjust the graph layout due to changed size - */ -Nengo.HTMLView.prototype.on_resize = function(width, height) { - if (width < this.minWidth) { - width = this.minWidth; - } - if (height < this.minHeight) { - height = this.minHeight; - }; - - this.width = width; - this.height = height; - //this.div.style.width = width; - //this.div.style.height = height; - - this.label.style.width = width; - - this.update(); -}; diff --git a/nengo_gui/static/components/htmlview.ts b/nengo_gui/static/components/htmlview.ts new file mode 100644 index 00000000..23d1c6f5 --- /dev/null +++ b/nengo_gui/static/components/htmlview.ts @@ -0,0 +1,125 @@ +/** + * Arbitrary HTML display taking input from a Node + * See nengo_gui/examples/basics/html.py for example usage. + * + * @constructor + * @param {DOMElement} parent - the element to add this component to + * @param {SimControl} sim - the simulation controller + * @param {dict} args - A set of constructor arguments (see Component) + */ + +import { DataStore } from "../datastore"; +// import * as utils from "../utils"; +import { registerComponent } from "./registry"; +import { Connection } from "../server"; +import { Position } from "./position"; +import { ValueView } from "./value"; +import { Widget } from "./widget"; + +export class HTMLView extends Widget { + dataStore; + pdiv; + sim; + view: ValueView; + + constructor({ + server, + uid, + label, + pos, + dimensions, + synapse, + labelVisible = true + }: { + server: Connection; + uid: string; + label: string; + pos: Position; + dimensions: number; + synapse: number; + labelVisible?: boolean; + }) { + super( + server, + uid, + new ValueView(), + label, + pos, + dimensions, + synapse, + labelVisible + ); + + // TODO: all of this really + + this.pdiv = document.createElement("div"); + this.pdiv.style.width = "100%"; + this.pdiv.style.height = "100%"; + // utils.setTransform(this.pdiv, 0, 0); + this.pdiv.style.position = "fixed"; + this.pdiv.classList.add("htmlview"); + // this.div.appendChild(this.pdiv); + + // For storing the accumulated data. + this.dataStore = new DataStore(1, 0); + + // Call scheduleUpdate whenever the time is adjusted in the SimControl + window.addEventListener("TimeSlider.moveShown", e => { + // this.scheduleUpdate(); + }); + + // this.onresize( + // this.viewPort.scaleWidth(this.w), + // this.viewPort.scaleHeight(this.h) + // ); + } + + /** + * Receive new line data from the server + */ + onMessage(event) { + const data = event.data.split(" ", 1); + const time = parseFloat(data[0]); + + const msg = event.data.substring(data[0].length + 1); + + this.dataStore.push([time, msg]); + // this.scheduleUpdate(null); + } + + /** + * Redraw the lines and axis due to changed data + */ + update() { + // Let the data store clear out old values + this.dataStore.update(); + + let data = this.dataStore.getLastData()[0]; + + if (data === undefined) { + data = ""; + } + + this.pdiv.innerHTML = data; + } + + /** + * Adjust the graph layout due to changed size + */ + onresize(width, height) { + // if (width < this.minWidth) { + // width = this.minWidth; + // } + // if (height < this.minHeight) { + // height = this.minHeight; + // } + + // this.width = width; + // this.height = height; + // this.label.style.width = width; + + this.update(); + } +} + +registerComponent("html_view", HTMLView); diff --git a/nengo_gui/static/components/image.js b/nengo_gui/static/components/image.js deleted file mode 100644 index fd173820..00000000 --- a/nengo_gui/static/components/image.js +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Shows an image or pixel grid over time - * @constructor - * - * @param {DOMElement} parent - the element to add this component to - * @param {Nengo.SimControl} sim - the simulation controller - * @param {dict} args - A set of constructor arguments (see Nengo.Component) - * @param {int} args.n_lines - number of decoded values - * @param {float} args.miny - minimum value on y-axis - * @param {float} args.maxy - maximum value on y-axis - */ - -Nengo.Image = function(parent, sim, args) { - var self = this; - - Nengo.Component.call(self, parent, args); - self.sim = sim; - self.display_time = args.display_time; - self.pixels_x = args.pixels_x; - self.pixels_y = args.pixels_y; - self.n_pixels = self.pixels_x * self.pixels_y; - - /** for storing the accumulated data */ - self.data_store = new Nengo.DataStore(self.n_pixels, self.sim, 0); - - /** draw the plot as an SVG */ - self.svg = d3.select(self.div).append('svg') - .attr('width', '100%') - .attr('height', '100%') - .attr('style', [ - 'padding-top:', '2em', - ].join("")); - - /** call schedule_update whenever the time is adjusted in the SimControl */ - self.sim.div.addEventListener('adjust_time', - function(e) {self.schedule_update();}, false); - - /** create the image */ - self.image = self.svg.append("image") - .attr("x", 0) - .attr("y", 0) - .attr("width", "100%") - .attr("height", "100%") - .attr("style", [ - "image-rendering: -webkit-optimize-contrast;", - "image-rendering: -moz-crisp-edges;", - "image-rendering: pixelated;" - ].join("")); - - self.canvas = document.createElement('CANVAS'); - self.canvas.width = self.pixels_x; - self.canvas.height = self.pixels_y; - - self.on_resize(this.get_screen_width(), this.get_screen_height()); - -}; -Nengo.Image.prototype = Object.create(Nengo.Component.prototype); -Nengo.Image.prototype.constructor = Nengo.Image; - -/** - * Receive new line data from the server - */ -Nengo.Image.prototype.on_message = function(event) { - var data = new Uint8Array(event.data); - var msg_size = this.n_pixels + 4; - - for (var i = 0; i < data.length; i += msg_size) { - var time_data = new Float32Array(event.data.slice(i, i + 4)); - data = Array.prototype.slice.call(data, i + 3, i + msg_size); - data[0] = time_data[0]; - this.data_store.push(data); - } - this.schedule_update(); -} - -/** - * Redraw the lines and axis due to changed data - */ -Nengo.Image.prototype.update = function() { - var self = this; - - /** let the data store clear out old values */ - self.data_store.update(); - - var data = self.data_store.get_last_data(); - var ctx = self.canvas.getContext("2d"); - var imgData = ctx.getImageData(0, 0, self.pixels_x, self.pixels_y); - for (var i = 0; i < self.n_pixels; i++) { - imgData.data[4*i + 0] = data[i]; - imgData.data[4*i + 1] = data[i]; - imgData.data[4*i + 2] = data[i]; - imgData.data[4*i + 3] = 255; - } - ctx.putImageData(imgData, 0, 0); - var dataURL = self.canvas.toDataURL("image/png"); - - self.image.attr("xlink:href", dataURL); -}; - -/** - * Adjust the graph layout due to changed size - */ -Nengo.Image.prototype.on_resize = function(width, height) { - var self = this; - if (width < self.minWidth) { - width = self.minWidth; - } - if (height < self.minHeight) { - height = self.minHeight; - }; - - self.svg - .attr("width", width) - .attr("height", height); - - self.update(); - - self.label.style.width = width; - - self.width = width; - self.height = height; - self.div.style.width = width; - self.div.style.height = height; -}; diff --git a/nengo_gui/static/components/image.ts b/nengo_gui/static/components/image.ts new file mode 100644 index 00000000..108701e2 --- /dev/null +++ b/nengo_gui/static/components/image.ts @@ -0,0 +1,169 @@ +/** + * Shows an image or pixel grid over time. + * + * @constructor + * @param {DOMElement} parent - the element to add this component to + * @param {SimControl} sim - the simulation controller + * @param {dict} args - A set of constructor arguments (see Component) + * @param {int} args.n_lines - number of decoded values + * @param {float} args.miny - minimum value on y-axis + * @param {float} args.maxy - maximum value on y-axis + */ + +import * as d3 from "d3"; + +import { DataStore } from "../datastore"; +import { Plot } from "./plot"; +import { Position } from "./position"; +import { registerComponent } from "./registry"; +import { Connection } from "../server"; +import { ValueView } from "./value"; + +export class Image extends Plot { + canvas; + dataStore: DataStore; + displayTime; + image; + nPixels: number; + pixelsX: number; + pixelsY: number; + svg; + view: ValueView; + + constructor({ + server, + uid, + label, + pos, + dimensions, + synapse, + labelVisible = true, + xlim = [-0.5, 0], + ylim = [-1, 1] + }: { + server: Connection; + uid: string; + label: string, + pos: Position; + dimensions: number; + synapse: number; + labelVisible?: boolean; + xlim?: [number, number]; + ylim?: [number, number]; + }) { + super( + server, + uid, + new ValueView(), + label, + pos, + dimensions, + synapse, + labelVisible, + xlim, + ylim + ); + + // this.displayTime = displayTime; + this.pixelsX = xlim[1]; + this.pixelsY = ylim[1]; + this.nPixels = this.pixelsX * this.pixelsY; + + // For storing the accumulated data + this.dataStore = new DataStore(this.nPixels, 0); + + // Draw the plot as an SVG + this.svg = d3 + .select("this.div") + .append("svg") + .attr("width", "100%") + .attr("height", "100%") + .attr("style", ["padding-top:", "2em"].join("")); + + // Call schedule_update whenever the time is adjusted in the SimControl + window.addEventListener("TimeSlider.moveShown", e => { + // this.scheduleUpdate(); + }); + + // Create the image + this.image = this.svg + .append("image") + .attr("x", 0) + .attr("y", 0) + .attr("width", "100%") + .attr("height", "100%") + .attr( + "style", + [ + "image-rendering: -webkit-optimize-contrast;", + "image-rendering: -moz-crisp-edges;", + "image-rendering: pixelated;" + ].join("") + ); + + this.canvas = document.createElement("CANVAS"); + this.canvas.width = this.pixelsX; + this.canvas.height = this.pixelsY; + } + + /** + * Receive new line data from the server + */ + onMessage(event) { + let data = new Uint8Array(event.data); + const msgSize = this.nPixels + 4; + + for (let i = 0; i < data.length; i += msgSize) { + const timeData = new Float32Array(event.data.slice(i, i + 4)); + data = Array.prototype.slice.call(data, i + 3, i + msgSize); + data[0] = timeData[0]; + this.dataStore.add(Array.prototype.slice.call(data)); + } + // this.scheduleUpdate(); + } + + /** + * Redraw the lines and axis due to changed data + */ + update() { + // Let the data store clear out old values + // this.dataStore.update(); + // const data = this.dataStore.getLastData(); + // const ctx = this.canvas.getContext("2d"); + // const imgData = ctx.getImageData(0, 0, this.pixelsX, this.pixelsY); + // for (let i = 0; i < this.nPixels; i++) { + // imgData.data[4 * i + 0] = data[i]; + // imgData.data[4 * i + 1] = data[i]; + // imgData.data[4 * i + 2] = data[i]; + // imgData.data[4 * i + 3] = 255; + // } + // ctx.putImageData(imgData, 0, 0); + // const dataURL = this.canvas.toDataURL("image/png"); + // this.image.attr("xlink:href", dataURL); + } + + /** + * Adjust the graph layout due to changed size + */ + onresize(width, height) { + // if (width < this.minWidth) { + // width = this.minWidth; + // } + // if (height < this.minHeight) { + // height = this.minHeight; + // } + + this.svg.attr("width", width).attr("height", height); + + this.update(); + + // this.label.style.width = width; + + // this.width = width; + // this.height = height; + // this.div.style.width = width; + // this.div.style.height = height; + } +} + +registerComponent("spike_grid", Image); diff --git a/nengo_gui/static/components/netgraph.css b/nengo_gui/static/components/netgraph.css deleted file mode 100644 index 583b64b0..00000000 --- a/nengo_gui/static/components/netgraph.css +++ /dev/null @@ -1,65 +0,0 @@ -.netgraph { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -} - -.ensemble{ - stroke-width:1; - } - -g.node rect{ - stroke-width:1; - } - -g.net rect{ - stroke-width:1; - } - -.netgraph g text { - text-anchor: middle; - dominant-baseline: text-before-edge; - cursor: default !important; - } - -.netgraph g text:active { - cursor: move; -} - -.netgraph line { - stroke: black; - stroke-width: 2px; -} - -.minimap { - border-radius: 10px/60px; - height: 100%; - opacity: 0.85; - position: 'relative'; - width: 100%; -} - -.minimap line { - stroke: black; - stroke-width: 1px; -} - -rect.view{ - fill: #d9edf7; - stroke: #ccc; -} - -.recur { - stroke: black; - stroke-width: 2px; - fill: none; -} - -.netgraph g.passthrough ellipse { - stroke-width: 0px; - fill: black; -} - -.netgraph g.passthrough text { - text-anchor: middle; - dominant-baseline: text-before-edge; -} - diff --git a/nengo_gui/static/components/netgraph.js b/nengo_gui/static/components/netgraph.js deleted file mode 100644 index 8b50075c..00000000 --- a/nengo_gui/static/components/netgraph.js +++ /dev/null @@ -1,685 +0,0 @@ -/** - * Network diagram - * @constructor - * - * @param {DOMElement} parent - the element to add this component to - * @param {dict} args - A set of constructor arguments, including: - * @param {int} args.id - the id of the server-side NetGraph to connect to - * - * NetGraph constructor is written into HTML file from the python - * server and is run on page load. - */ -Nengo.NetGraph = function(parent, args) { - if (args.uid[0] === '<') { - console.log("invalid uid for NetGraph: " + args.uid); - } - this.offsetX = 0; // global x,y pan offset - this.offsetY = 0; - - var scale = 1.0; - Object.defineProperty(this, 'scale', { - // global scaling factor - get: function() { - return scale; - }, - set: function(val) { - if (val === scale) { return; } - scale = val; - this.update_fonts(); - this.redraw(); - - viewport.scale = scale; - viewport.redraw_all(); - } - - }); - - Object.defineProperty(this, 'zoom_fonts', { - // scale fonts when zooming - get: function() { - return Nengo.config.zoom_fonts; - }, - set: function(val) { - if (val === this.zoom_fonts) { return; } - Nengo.config.zoom_fonts = val; - this.update_fonts(); - } - }); - - Object.defineProperty(this, 'aspect_resize', { - //preserve aspect ratios on window resize - get: function() { - return Nengo.config.aspect_resize; - }, - set: function(val) { - if (val === this.aspect_resize) { return; } - Nengo.config.aspect_resize = val; - - } - - }); - - Object.defineProperty(this, 'font_size', { - get: function() { - return Nengo.config.font_size; - }, - set: function(val) { - if (val === this.font_size) { return; } - Nengo.config.font_size = val; - this.update_fonts(); - } - }); - - // Do networks have transparent backgrounds? - Object.defineProperty(this, 'transparent_nets', { - get: function() { - return Nengo.config.transparent_nets; - }, - set: function(val) { - if (val === this.transparent_nets) { return; } - Nengo.config.transparent_nets = val; - for (var key in this.svg_objects) { - var ngi = this.svg_objects[key]; - ngi.compute_fill(); - if (ngi.type === 'net' && ngi.expanded) { - ngi.shape.style["fill-opacity"] = val ? 0.0 : 1.0; - } - } - - } - }); - - this.svg_objects = {}; // dict of all Nengo.NetGraphItems, by uid - this.svg_conns = {}; // dict of all Nengo.NetGraphConnections, by uid - this.minimap_objects = {}; - this.minimap_conns = {}; - - this.mm_min_x = 0; - this.mm_max_x = 0; - this.mm_min_y = 0; - this.mm_max_y = 0; - - this.mm_scale = .1; - - this.in_zoom_delay = false; - - /** Since connections may go to items that do not exist yet (since they - * are inside a collapsed network), this dictionary keeps a list of - * connections to be notified when a particular item appears. The - * key in the dictionary is the uid of the nonexistent item, and the - * value is a list of Nengo.NetGraphConnections that should be notified - * when that item appears. */ - this.collapsed_conns = {}; - - /** create the master SVG element */ - this.svg = this.createSVGElement('svg'); - this.svg.classList.add('netgraph'); - this.svg.style.width = '100%'; - this.svg.id = 'netgraph'; - this.svg.style.height = '100%'; - this.svg.style.position = 'absolute'; - - interact(this.svg).styleCursor(false); - - Nengo.netgraph = this; - parent.appendChild(this.svg); - this.parent = parent; - - this.width = $(this.svg).width(); - this.height = $(this.svg).height(); - - this.tool_height = $(toolbar.toolbar).height(); - - /** three separate layers, so that expanded networks are at the back, - * then connection lines, and then other items (nodes, ensembles, and - * collapsed networks) are drawn on top. */ - this.g_networks = this.createSVGElement('g'); - this.svg.appendChild(this.g_networks); - this.g_conns = this.createSVGElement('g'); - this.svg.appendChild(this.g_conns); - this.g_items = this.createSVGElement('g'); - this.svg.appendChild(this.g_items); - - /** Reading netgraph.css file as text and embedding it within def tags, - * this is needed for saving the SVG plot to disk*/ - - /** load contents of the CSS file as string */ - var file = document.getElementById('netgraphcss'); - var css = Array.prototype.map.call(file.sheet.cssRules, function - css_text(x) {return x.cssText; } ).join('\n'); - - /** embed CSS code into SVG tag */ - var s = document.createElement('style'); - s.setAttribute('type', 'text/css'); - s.innerHTML = ""; - - var defs = document.createElement('defs'); - defs.appendChild(s); - - this.svg.insertBefore(defs, this.svg.firstChild); - - /** connect to server */ - this.ws = Nengo.create_websocket(args.uid); - this.ws.onmessage = function(event) {self.on_message(event);} - - /** respond to resize events */ - this.svg.addEventListener("resize", function() {self.on_resize();}); - window.addEventListener("resize", function() {self.on_resize();}); - - /** dragging the background pans the full area by changing offsetX,Y */ - var self = this; - - /** define cursor behaviour for background */ - interact(this.svg) - .on('mousedown', function() { - var cursor = document.documentElement.getAttribute('style'); - if (cursor !== null) { - if (cursor.match(/resize/) == null) { // don't change resize cursor - document.documentElement.setAttribute('style','cursor:move;'); - } - } - }) - .on('mouseup', function() { - document.documentElement.setAttribute('style','cursor:default;') - }); - - interact(this.svg) - .draggable({ - onstart: function() { - self.menu.hide_any(); - }, - onmove: function(event) { - self.offsetX += event.dx / self.get_scaled_width(); - self.offsetY += event.dy / self.get_scaled_height(); - for (var key in self.svg_objects) { - self.svg_objects[key].redraw_position(); - if (self.mm_display) { - self.minimap_objects[key].redraw_position(); - } - } - for (var key in self.svg_conns) { - self.svg_conns[key].redraw(); - } - - viewport.x = self.offsetX; - viewport.y = self.offsetY; - viewport.redraw_all(); - - self.scaleMiniMapViewBox(); - - }, - onend: function(event) { - /** let the server know what happened */ - self.notify({act:"pan", x:self.offsetX, y:self.offsetY}); - }}); - - /** scrollwheel on background zooms the full area by changing scale. - * Note that offsetX,Y are also changed to zoom into a particular - * point in the space */ - interact(document.getElementById('main')) - .on('click', function(event) { - $('.ace_text-input').blur(); - }) - .on('wheel', function(event) { - event.preventDefault(); - - self.menu.hide_any(); - var x = (event.clientX) / self.width - var y = (event.clientY - self.tool_height) / self.height; - - - switch (event.deltaMode) { - case 1: // DOM_DELTA_LINE - if (event.deltaY != 0) { - var delta = Math.log(1. + Math.abs(event.deltaY)) * 60; - if (event.deltaY < 0) { - delta *= -1; - } - } else { - var delta = 0; - } - break; - case 2: // DOM_DELTA_PAGE - // No idea what device would generate scrolling by a page - var delta = 0; - break; - case 0: // DOM_DELTA_PIXEL - default: // Assume pixel if unknown - var delta = event.deltaY; - break; - } - - var scale = 1. + Math.abs(delta) / 600.; - if (delta > 0) { - scale = 1. / scale; - } - - Nengo.Component.save_components(); - - var xx = x / self.scale - self.offsetX; - var yy = y / self.scale - self.offsetY; - self.offsetX = (self.offsetX + xx) / scale - xx; - self.offsetY = (self.offsetY + yy) / scale - yy; - - self.scale = scale * self.scale; - viewport.x = self.offsetX; - viewport.y = self.offsetY; - viewport.redraw_all(); - - self.scaleMiniMapViewBox(); - - self.redraw(); - - /** let the server know what happened */ - self.notify({act:"zoom", scale:self.scale, - x:self.offsetX, y:self.offsetY}); - }); - - this.menu = new Nengo.Menu(self.parent); - - //Determine when to pull up the menu - interact(this.svg) - .on('hold', function(event) { //change to 'tap' for right click - if (event.button == 0) { - if (self.menu.visible_any()) { - self.menu.hide_any(); - } else { - self.menu.show(event.clientX, event.clientY, - self.generate_menu()); - } - event.stopPropagation(); - } - }) - .on('tap', function(event) { //get rid of menus when clicking off - if (event.button == 0) { - if (self.menu.visible_any()) { - self.menu.hide_any(); - } - } - }); - - $(this.svg).bind('contextmenu', function(event) { - event.preventDefault(); - if (self.menu.visible_any()) { - self.menu.hide_any(); - } else { - self.menu.show(event.clientX, event.clientY, - self.generate_menu()); - } - }); - - this.create_minimap(); -}; - -Nengo.NetGraph.prototype.generate_menu = function() { - var self = this; - var items = []; - items.push(['Auto-layout', - function() {self.notify({act:"feedforward_layout", - uid:null});}]); - return items; - -} - -/** Event handler for received WebSocket messages */ -Nengo.NetGraph.prototype.on_message = function(event) { - data = JSON.parse(event.data); - if (data.type === 'net') { - this.create_object(data); - } else if (data.type === 'ens') { - this.create_object(data); - } else if (data.type === 'node') { - this.create_object(data); - } else if (data.type === 'conn') { - this.create_connection(data); - } else if (data.type === 'pan') { - this.set_offset(data.pan[0], data.pan[1]); - } else if (data.type === 'zoom') { - this.scale = data.zoom; - } else if (data.type === 'expand') { - var item = this.svg_objects[data.uid]; - item.expand(true,true) - } else if (data.type === 'collapse') { - var item = this.svg_objects[data.uid]; - item.collapse(true,true) - } else if (data.type === 'pos_size') { - var item = this.svg_objects[data.uid]; - item.x = data.pos[0]; - item.y = data.pos[1]; - item.width = data.size[0]; - item.height = data.size[1]; - - item.redraw(); - - this.scaleMiniMap(); - - } else if (data.type === 'config') { - // Anything about the config of a component has changed - var uid = data.uid; - for (var i = 0; i < Nengo.Component.components.length; i++) { - if (Nengo.Component.components[i].uid === uid) { - Nengo.Component.components[i].update_layout(data.config); - break; - } - } - } else if (data.type === 'js') { - eval(data.code); - } else if (data.type === 'rename') { - var item = this.svg_objects[data.uid]; - item.set_label(data.name); - - } else if (data.type === 'remove') { - var item = this.svg_objects[data.uid]; - if (item === undefined) { - item = this.svg_conns[data.uid]; - } - - item.remove(); - - } else if (data.type === 'reconnect') { - var conn = this.svg_conns[data.uid]; - conn.set_pres(data.pres); - conn.set_posts(data.posts); - conn.set_recurrent(data.pres[0] === data.posts[0]); - conn.redraw(); - - } else if (data.type === 'delete_graph') { - var uid = data.uid; - for (var i = 0; i < Nengo.Component.components.length; i++) { - if (Nengo.Component.components[i].uid === uid) { - Nengo.Component.components[i].remove(true, data.notify_server); - break; - } - } - } else { - console.log('invalid message'); - console.log(data); - } -}; - - -/** report an event back to the server */ -Nengo.NetGraph.prototype.notify = function(info) { - this.ws.send(JSON.stringify(info)); -} - -/** pan the screen (and redraw accordingly) */ -Nengo.NetGraph.prototype.set_offset = function(x, y) { - this.offsetX = x; - this.offsetY = y; - this.redraw(); - - viewport.x = x; - viewport.y = y; - viewport.redraw_all(); -} - - -Nengo.NetGraph.prototype.update_fonts = function() { - if (this.zoom_fonts) { - $('#main').css('font-size', 3 * this.scale * this.font_size/100 + 'em'); - } else { - $('#main').css('font-size', this.font_size/100 + 'em'); - } -} - -/** redraw all elements */ -Nengo.NetGraph.prototype.redraw = function() { - for (var key in this.svg_objects) { - this.svg_objects[key].redraw(); - } - for (var key in this.svg_conns) { - this.svg_conns[key].redraw(); - } -} - - -/** helper function for correctly creating SVG elements */ -Nengo.NetGraph.prototype.createSVGElement = function(tag) { - return document.createElementNS("http://www.w3.org/2000/svg", tag); -} - - -/** Create a new NetGraphItem - * if an existing NetGraphConnection is looking for this item, it will be - * notified */ -Nengo.NetGraph.prototype.create_object = function(info) { - var item_mini = new Nengo.NetGraphItem(this, info, true); - this.minimap_objects[info.uid] = item_mini; - - var item = new Nengo.NetGraphItem(this, info, false, item_mini); - this.svg_objects[info.uid] = item; - - this.detect_collapsed_conns(item.uid); - this.detect_collapsed_conns(item_mini.uid); - - this.scaleMiniMap(); -}; - - -/** create a new NetGraphConnection */ -Nengo.NetGraph.prototype.create_connection = function(info) { - var conn_mini = new Nengo.NetGraphConnection(this, info, true); - this.minimap_conns[info.uid] = conn_mini; - - var conn = new Nengo.NetGraphConnection(this, info, false, conn_mini); - this.svg_conns[info.uid] = conn; -}; - - -/** handler for resizing the full SVG */ -Nengo.NetGraph.prototype.on_resize = function(event) { - - var width = $(this.svg).width(); - var height = $(this.svg).height(); - - if (this.aspect_resize) { - for (var key in this.svg_objects) { - var item = this.svg_objects[key]; - if (item.depth == 1) { - var new_width = item.get_screen_width() / this.scale; - var new_height = item.get_screen_height() / this.scale; - item.width = new_width/(2*width); - item.height = new_height/(2*height); - } - } - } - - this.width = width; - this.height = height; - this.mm_width = $(this.minimap).width(); - this.mm_height = $(this.minimap).height(); - - this.redraw(); -}; - - -/** return the pixel width of the SVG times the current scale factor */ -Nengo.NetGraph.prototype.get_scaled_width = function() { - return this.width * this.scale; -} - - -/** return the pixel height of the SVG times the current scale factor */ -Nengo.NetGraph.prototype.get_scaled_height = function() { - return this.height * this.scale; -} - - -/** expand or collapse a network */ -Nengo.NetGraph.prototype.toggle_network = function(uid) { - var item = this.svg_objects[uid]; - if (item.expanded) { - item.collapse(true); - } else { - item.expand(); - } -} - - -/** register a NetGraphConnection with a target item that it is looking for - * This is a NetGraphItem that does not exist yet, because it is inside a - * collapsed network. When it does appear, NetGraph.detect_collapsed will - * handle notifying the NetGraphConnection. */ -Nengo.NetGraph.prototype.register_conn = function(conn, target) { - if (this.collapsed_conns[target] === undefined) { - this.collapsed_conns[target] = [conn]; - } else { - var index = this.collapsed_conns[target].indexOf(conn); - if (index === -1) { - this.collapsed_conns[target].push(conn); - } - } -} - - -/** if a NetGraphConnection is looking for an item with a particular uid, - * but that item does not exist yet (due to it being inside a collapsed - * network), then it is added to the collapsed_conns dicutionary. When - * an item is create, this function is used to see if any NetGraphConnections - * are waiting for it, and notifies them. */ -Nengo.NetGraph.prototype.detect_collapsed_conns = function(uid) { - var conns = this.collapsed_conns[uid]; - if (conns !== undefined) { - delete this.collapsed_conns[uid]; - for (var i in conns) { - var conn = conns[i]; - /** make sure the NetGraphConnection hasn't been removed since - * it started listening */ - if (!conn.removed) { - conn.set_pre(conn.find_pre()); - conn.set_post(conn.find_post()); - conn.redraw(); - } - } - } -} - -/** create a minimap */ -Nengo.NetGraph.prototype.create_minimap = function () { - var self = this; - - this.minimap_div = document.createElement('div'); - this.minimap_div.className = 'minimap'; - this.parent.appendChild(this.minimap_div); - - this.minimap = this.createSVGElement('svg'); - this.minimap.classList.add('minimap'); - this.minimap.id = 'minimap'; - this.minimap_div.appendChild(this.minimap); - - // box to show current view - this.view = this.createSVGElement('rect'); - this.view.classList.add('view'); - this.minimap.appendChild(this.view); - - this.g_networks_mini = this.createSVGElement('g'); - this.g_conns_mini = this.createSVGElement('g'); - this.g_items_mini = this.createSVGElement('g'); - // order these are appended is important for layering - this.minimap.appendChild(this.g_networks_mini); - this.minimap.appendChild(this.g_conns_mini); - this.minimap.appendChild(this.g_items_mini); - - this.mm_width = $(this.minimap).width(); - this.mm_height = $(this.minimap).height(); - - // default display minimap - this.mm_display = true; - this.toggleMiniMap(); -} - -Nengo.NetGraph.prototype.toggleMiniMap = function () { - if (this.mm_display == true) { - $('.minimap')[0].style.visibility = 'hidden'; - this.g_conns_mini.style.opacity = 0; - this.mm_display = false; - } else { - $('.minimap')[0].style.visibility = 'visible'; - this.g_conns_mini.style.opacity = 1; - this.mm_display = true ; - this.scaleMiniMap(); - } -} - -/** Calculate the minimap position offsets and scaling **/ -Nengo.NetGraph.prototype.scaleMiniMap = function () { - if (!this.mm_display) { return; } - - keys = Object.keys(this.svg_objects); - if (keys.length === 0) { - return; - } - - // TODO: Could also store the items at the four min max values - // and only compare against those, or check against all items - // in the lists when they move. Might be important for larger - // networks. - var first_item = true; - for (var key in this.svg_objects) { - item = this.svg_objects[key]; - // ignore anything inside a subnetwork - if (item.depth > 1) { - continue; - } - - var minmax_xy = item.getMinMaxXY(); - if (first_item == true) { - this.mm_min_x = minmax_xy[0]; - this.mm_max_x = minmax_xy[1]; - this.mm_min_y = minmax_xy[2]; - this.mm_max_y = minmax_xy[3]; - first_item = false; - continue; - } - - if (this.mm_min_x > minmax_xy[0]) { - this.mm_min_x = minmax_xy[0]; - } - if (this.mm_max_x < minmax_xy[1]) { - this.mm_max_x = minmax_xy[1]; - } - if (this.mm_min_y > minmax_xy[2]) { - this.mm_min_y = minmax_xy[2]; - } - if (this.mm_max_y < minmax_xy[3]) { - this.mm_max_y = minmax_xy[3]; - } - } - - this.mm_scale = 1 / Math.max(this.mm_max_x - this.mm_min_x, this.mm_max_y - this.mm_min_y); - - // give a bit of a border - this.mm_min_x -= this.mm_scale * .05; - this.mm_max_x += this.mm_scale * .05; - this.mm_min_y -= this.mm_scale * .05; - this.mm_max_y += this.mm_scale * .05; - // TODO: there is a better way to do this than recalculate - this.mm_scale = 1 / Math.max(this.mm_max_x - this.mm_min_x, this.mm_max_y - this.mm_min_y); - - this.redraw(); - this.scaleMiniMapViewBox(); -} - -/** Calculate which part of the map is being displayed on the - * main viewport and scale the viewbox to reflect that. */ -Nengo.NetGraph.prototype.scaleMiniMapViewBox = function () { - if (!this.mm_display) { return; } - - var mm_w = this.mm_width - var mm_h = this.mm_height - - var w = mm_w * this.mm_scale; - var h = mm_h * this.mm_scale; - - var disp_w = (this.mm_max_x - this.mm_min_x) * w; - var disp_h = (this.mm_max_y - this.mm_min_y) * h; - - var view_offsetX = -(this.mm_min_x + this.offsetX) * w + (mm_w - disp_w) / 2.; - var view_offsetY = -(this.mm_min_y + this.offsetY) * h + (mm_h - disp_h) / 2.; - - this.view.setAttributeNS(null, 'x', view_offsetX); - this.view.setAttributeNS(null, 'y', view_offsetY); - this.view.setAttribute('width', w / this.scale); - this.view.setAttribute('height', h / this.scale); -} diff --git a/nengo_gui/static/components/netgraph_conn.js b/nengo_gui/static/components/netgraph_conn.js deleted file mode 100644 index b137102c..00000000 --- a/nengo_gui/static/components/netgraph_conn.js +++ /dev/null @@ -1,374 +0,0 @@ -/** - * Network diagram connection line - * @constructor - * - * @param {Nengo.NetGraph} ng - The containing Nengo.NetGraph - * @param {dict} info - A set of constructor arguments, including: - * @param {string} info.uid - A unique identifier - * @param {string or null} info.parent - A containing NetGraphItem - * @param {array of strings} info.pre - uid to connect from and its parents - * @param {array of strings} info.post - uid to connect to and its parents - */ -Nengo.NetGraphConnection = function(ng, info, minimap, mini_conn) { - this.ng = ng; - this.uid = info.uid; - - /** flag to indicate this Connection has been deleted */ - this.removed = false; - - /** the actual NetGraphItem currently connected to/from */ - this.pre = null; - this.post = null; - - this.minimap = minimap; - this.mini_conn = mini_conn; - if (!minimap) { - this.g_conns = ng.g_conns; - this.objects = ng.svg_objects; - } else { - this.g_conns = ng.g_conns_mini; - this.objects = ng.minimap_objects; - } - - /** the uids for the pre and post items in the connection - * The lists start with the ideal target item, followed by the parent - * of that item, and its parent, and so on. If the first item on the - * this does not exist (due to it being inside a collapsed network), - * the connection will look for the next item on the list, and so on - * until it finds one that does exist. */ - this.pres = info.pre; - this.posts = info.post; - - this.recurrent = this.pres[0] === this.posts[0]; - - /** figure out the best available items to connect to */ - this.set_pre(this.find_pre()); - this.set_post(this.find_post()); - - /** determine parent and add to parent's children list */ - if (info.parent === null) { - this.parent = null; - } else { - this.parent = this.objects[info.parent]; - if (!minimap) { - this.parent.child_connections.push(this); - } - } - - /** create the line and its arrowhead marker */ - this.g = ng.createSVGElement('g'); - - this.create_line(); - - this.redraw(); - - this.g_conns.appendChild(this.g); -} - -Nengo.NetGraphConnection.prototype.set_recurrent = function(recurrent) { - if (this.recurrent === recurrent) { - return; - } - this.remove_line(); - this.recurrent = recurrent; - this.create_line(); -} - -Nengo.NetGraphConnection.prototype.create_line = function() { - if (this.recurrent) { - this.recurrent_ellipse = this.ng.createSVGElement('path'); - this.recurrent_ellipse.setAttribute('d', - "M6.451,28.748C2.448,26.041,0,22.413,0,18.425C0, \ - 10.051,10.801,3.262,24.125,3.262 \ - S48.25,10.051,48.25,18.425c0,6.453-6.412,11.964-15.45,14.153"); - this.recurrent_ellipse.setAttribute('class','recur'); - this.g.appendChild(this.recurrent_ellipse); - - this.marker = this.ng.createSVGElement('path'); - this.g.appendChild(this.marker); - - if (this.minimap == false) { - this.marker.setAttribute('d', "M 6.5 0 L 0 5.0 L 7.5 8.0 z"); - } else { - this.marker.setAttribute('d', "M 4 0 L 0 2 L 4 4 z"); - } - - } else { - this.line = this.ng.createSVGElement('line'); - this.g.appendChild(this.line); - this.marker = this.ng.createSVGElement('path'); - if (this.minimap == false) { - this.marker.setAttribute('d', "M 10 0 L -5 -5 L -5 5 z"); - } else { - this.marker.setAttribute('d', "M 3 0 L -2.5 -2.5 L -2.5 2.5 z"); - } - this.g.appendChild(this.marker); - } -} - -Nengo.NetGraphConnection.prototype.remove_line = function() { - if (this.recurrent) { - this.g.removeChild(this.recurrent_ellipse); - this.g.removeChild(this.marker); - this.recurrent_ellipse = undefined; - this.marker = undefined; - } else { - this.g.removeChild(this.line); - this.g.removeChild(this.marker); - this.line = undefined; - this.marker = undefined; - } -} - - - -/** set the item connecting from */ -Nengo.NetGraphConnection.prototype.set_pre = function(pre) { - if (this.pre !== null) { - /** if we're currently connected, disconnect */ - var index = this.pre.conn_out.indexOf(this); - if (index === -1) { - console.log('error removing in set_pre'); - } - this.pre.conn_out.splice(index, 1); - } - this.pre = pre; - if (this.pre !== null) { - /** add myself to pre's output connections list */ - this.pre.conn_out.push(this); - } -} - - -/** set the item connecting to */ -Nengo.NetGraphConnection.prototype.set_post = function(post) { - if (this.post !== null) { - /** if we're currently connected, disconnect */ - var index = this.post.conn_in.indexOf(this); - if (index === -1) { - console.log('error removing in set_pre'); - } - this.post.conn_in.splice(index, 1); - } - this.post = post; - if (this.post !== null) { - /** add myself to post's input connections list */ - this.post.conn_in.push(this); - } -} - - -/** determine the best available item to connect from */ -Nengo.NetGraphConnection.prototype.find_pre = function() { - for (var i in this.pres) { - var pre = this.objects[this.pres[i]]; - if (pre !== undefined) { - return pre; - } else { - /** register to be notified if a better match occurs */ - this.ng.register_conn(this, this.pres[i]); - } - } - return null; -} - - -/** determine the best available item to connect to */ -Nengo.NetGraphConnection.prototype.find_post = function() { - for (var i in this.posts) { - var post = this.objects[this.posts[i]]; - if (post !== undefined) { - return post; - } else { - /** register to be notified if a better match occurs */ - this.ng.register_conn(this, this.posts[i]); - } - } - return null; -} - -Nengo.NetGraphConnection.prototype.set_pres = function(pres) { - this.pres = pres; - this.set_pre(this.find_pre()); - - if (!this.minimap) { - this.mini_conn.set_pres(pres); - } -} -Nengo.NetGraphConnection.prototype.set_posts = function(posts) { - this.posts = posts; - this.set_post(this.find_post()); - - if (!this.minimap) { - this.mini_conn.set_posts(posts); - } -} - - -/** remove this connection */ -Nengo.NetGraphConnection.prototype.remove = function() { - if (!this.minimap && this.parent !== null) { - var index = this.parent.child_connections.indexOf(this); - if (index === -1) { - console.log('error removing in remove'); - } - this.parent.child_connections.splice(index, 1); - } - - if (this.pre != null) { - var index = this.pre.conn_out.indexOf(this); - if (index === -1) { - console.log('error removing from conn_out'); - } - this.pre.conn_out.splice(index, 1); - } - - if (this.post != null) { - var index = this.post.conn_in.indexOf(this); - if (index === -1) { - console.log('error removing from conn_in'); - } - this.post.conn_in.splice(index, 1); - } - - - this.g_conns.removeChild(this.g); - this.removed = true; - - delete this.ng.svg_conns[this.uid]; - - if (!this.minimap) { - this.mini_conn.remove(); - } -} - - -/** redraw the connection */ -Nengo.NetGraphConnection.prototype.redraw = function() { - if (this.pre === null || this.post === null) { - if (this.line !== undefined) { - this.line.setAttribute('visibility', 'hidden'); - } - this.marker.setAttribute('visibility', 'hidden'); - return; - } else { - if (this.line !== undefined) { - this.line.setAttribute('visibility', 'visible'); - } - this.marker.setAttribute('visibility', 'visible'); - } - var pre_pos = this.pre.get_screen_location(); - - if (this.recurrent) { - var item = this.objects[this.pres[0]]; - if (item === undefined) { - this.marker.setAttribute('visibility', 'hidden'); - this.recurrent_ellipse.setAttribute('visibility', 'hidden'); - } else { - this.marker.setAttribute('visibility', 'visible'); - this.recurrent_ellipse.setAttribute('visibility', 'visible'); - var width = item.get_displayed_size()[0]; - var height = item.get_displayed_size()[1]; - - var scale = item.shape.getAttribute('transform'); - var scale_value = parseFloat(scale.split(/[()]+/)[1]); - - if (this.minimap == false) { - this.recurrent_ellipse.setAttribute('style','stroke-width:' + - 2/scale_value+';'); - } else { - this.recurrent_ellipse.setAttribute('style','stroke-width:' + - 1/scale_value+';'); - } - - var ex = pre_pos[0] - scale_value*17.5; - var ey = pre_pos[1] - height - scale_value*36; - - this.recurrent_ellipse.setAttribute('transform', - 'translate(' + ex + ',' + ey + ')' + scale); - - var mx = pre_pos[0]-1; - if (this.minimap == false) { - var my = pre_pos[1] - height - scale_value*32.15 - 5; - } else { - var my = pre_pos[1] - height - scale_value*32 - 2; - } - this.marker.setAttribute('transform', - 'translate(' + mx + ',' + my + ')'); - } - } else { - var post_pos = this.post.get_screen_location(); - this.line.setAttribute('x1', pre_pos[0]); - this.line.setAttribute('y1', pre_pos[1]); - this.line.setAttribute('x2', post_pos[0]); - this.line.setAttribute('y2', post_pos[1]); - - var angle = Math.atan2(post_pos[1] - pre_pos[1], //angle between objects - post_pos[0] - pre_pos[0]); - - var w1 = this.pre.get_screen_width(); - var h1 = this.pre.get_screen_height(); - var w2 = this.post.get_screen_width(); - var h2 = this.post.get_screen_height(); - - a1 = Math.atan2(h1,w1); - a2 = Math.atan2(h2,w2); - - var pre_length = this.intersect_length(angle, a1, w1, h1); - var post_to_pre_angle = angle - Math.PI; - if (post_to_pre_angle < -Math.PI) {post_to_pre_angle+=2*Math.PI;} - var post_length = this.intersect_length(post_to_pre_angle, a2, w2, h2); - - var mx = (pre_pos[0]+pre_length[0]) * 0.4 - + (post_pos[0]+post_length[0]) * 0.6; - var my = (pre_pos[1]+pre_length[1]) * 0.4 - + (post_pos[1]+post_length[1]) * 0.6; - - //Check to make sure the marker doesn't go past either endpoint - vec1 = [post_pos[0]-pre_pos[0], post_pos[1]-pre_pos[1]]; - vec2 = [mx-pre_pos[0], my-pre_pos[1]]; - dot_prod = (vec1[0]*vec2[0] + vec1[1]*vec2[1]) - / (vec1[0]*vec1[0]+vec1[1]*vec1[1]); - - if (dot_prod < 0) { - mx = pre_pos[0]; - my = pre_pos[1]; - } else if (dot_prod>1){ - mx = post_pos[0]; - my = post_pos[1]; - } - angle = 180 / Math.PI * angle; - this.marker.setAttribute('transform', - 'translate(' + mx + ',' + my + ') rotate('+ angle +')'); - } - - if (!this.minimap && this.ng.mm_display) { - this.mini_conn.redraw(); - } -} -/**Function to determine the length of an intersection line through a rectangle - ** theta - the angle of the line - ** alpha - the angle between zero and the top right corner of the object - **/ -Nengo.NetGraphConnection.prototype.intersect_length = function(theta, alpha, width, height) { - var quad = 0; - var beta = 2*(Math.PI/2 - alpha); //angle between top corners - var h2 = (height/2)*(height/2); - var w2 = (width/2)*(width/2); - - if (theta >= -alpha && theta < alpha) { //1st quadrant - var x = width/2; - var y = width/2*Math.tan(theta); - } else if (theta >= alpha && theta < alpha + beta) { //2nd quadrant - var x = (height/2)/Math.tan(theta); - var y = height/2; - } else if (theta >= alpha + beta || theta < -(alpha + beta)) { //3rd quadrant - var x = -width/2; - var y = -width/2*Math.tan(theta); - } else { //4th quadrant - var x = -(height/2)/Math.tan(theta); - var y = -height/2; - } - - return [x,y]; -} diff --git a/nengo_gui/static/components/netgraph_item.js b/nengo_gui/static/components/netgraph_item.js deleted file mode 100644 index 63a239cd..00000000 --- a/nengo_gui/static/components/netgraph_item.js +++ /dev/null @@ -1,884 +0,0 @@ -/** - * Network diagram individual item (node) - * @constructor - * - * @param {Nengo.NetGraph} ng - The Nengo.NetGraph this Item is inside - * @param {dict} info - A dictionary of settings for the item, including: - * @param {float array} info.pos - x,y position - * @param {float array} info.size - half width, half height of item - * @param {string} info.type - one of ['net', 'ens', 'node'] - * @param {string} info.uid - unique identifier - * @param {string or null} info.parent - a NetGraphItem with .type=='net' - */ -Nengo.NetGraphItem = function(ng, info, minimap, mini_item) { - var self = this; - - this.ng = ng; - this.type = info.type; - this.uid = info.uid; - this.sp_targets = info.sp_targets; - this.default_output = info.default_output; - this.passthrough = info.passthrough; - this.fixed_width = null; - this.fixed_height = null; - this.dimensions = info.dimensions; - this.minimap = minimap; - this.html_node = info.html; - if (minimap == false) { - this.g_networks = ng.g_networks; - this.g_items = ng.g_items; - this.mini_item = mini_item; - } else { - this.g_networks = ng.g_networks_mini; - this.g_items = ng.g_items_mini; - } - - var width = info.size[0]; - Object.defineProperty(this, 'width', { - get: function() { - return width; - }, - set: function(val) { - width = val; - - if (!this.minimap) { - this.mini_item.width = val - } - } - }); - var height = info.size[1]; - Object.defineProperty(this, 'height', { - get: function() { - return height; - }, - set: function(val) { - height = val; - - if (!this.minimap) { - this.mini_item.height = val - } - } - }); - var x = info.pos[0]; - Object.defineProperty(this, 'x', { - get: function() { - return x; - }, - set: function(val) { - x = val; - - if (!this.minimap) { - this.mini_item.x = val - } - } - }); - var y = info.pos[1]; - Object.defineProperty(this, 'y', { - get: function() { - return y; - }, - set: function(val) { - y = val; - - if (!this.minimap) { - this.mini_item.y = val - } - } - }); - - /** if this is a network, the children list is the set of NetGraphItems - * and NetGraphConnections that are inside this network */ - this.children = []; - this.child_connections = []; - - // NetGraphConnections leading into and out of this item - this.conn_out = []; - this.conn_in = []; - - // minimum and maximum drawn size, in pixels - this.minWidth = 5; - this.minHeight = 5; - this.aspect = null; - - this.expanded = false; - - /** determine the parent NetGraphItem (if any) and the nested depth - * of this item */ - if (info.parent === null) { - this.parent = null; - this.depth = 1; - } else { - this.parent = ng.svg_objects[info.parent]; - this.depth = this.parent.depth + 1; - if (!minimap) { - this.parent.children.push(this); - } - } - - /** create the SVG group to hold this item */ - var g = this.ng.createSVGElement('g'); - this.g = g; - this.g_items.appendChild(g); - g.classList.add(this.type); - - this.area = this.ng.createSVGElement('rect'); - this.area.style.fill = 'transparent'; - - this.menu = new Nengo.Menu(this.ng.parent); - - // different types use different SVG elements for display - if (info.type === 'node') { - if (this.passthrough) { - this.shape = this.ng.createSVGElement('ellipse'); - if (this.minimap == false) { - this.fixed_width = 10; - this.fixed_height = 10; - } else { - this.fixed_width = 3; - this.fixed_height = 3; - } - this.g.classList.add('passthrough'); - } else { - this.shape = this.ng.createSVGElement('rect'); - } - } else if (info.type === 'net') { - this.shape = this.ng.createSVGElement('rect'); - } else if (info.type === 'ens') { - this.aspect = 1.; - this.shape = this.ensemble_svg(); - } else { - console.log("Unknown NetGraphItem type"); - console.log(item); - } - - this.compute_fill(); - - if (this.minimap == false) { - var label = this.ng.createSVGElement('text'); - this.label = label; - label.innerHTML = info.label; - g.appendChild(label); - }; - - g.appendChild(this.shape); - g.appendChild(this.area); - - this.redraw(); - - interact.margin(10); - - if (!this.minimap) { - // dragging an item to change its position - var uid = this.uid; - interact(g) - .draggable({ - onstart: function () { - self.menu.hide_any(); - self.move_to_front(); - }, - onmove: function(event) { - var w = self.ng.get_scaled_width(); - var h = self.ng.get_scaled_height(); - var item = self.ng.svg_objects[uid]; - var parent = item.parent; - while (parent !== null) { - w = w * parent.width * 2; - h = h * parent.height * 2; - parent = parent.parent; - } - item.x += event.dx / w; - item.y += event.dy / h; - item.redraw() - - if (self.depth === 1) { - self.ng.scaleMiniMap(); - } - }, - onend: function(event) { - var item = self.ng.svg_objects[uid]; - item.constrain_position(); - self.ng.notify({act:"pos", uid:uid, x:item.x, y:item.y}); - - item.redraw(); - }}); - - if (!this.passthrough) { - // dragging the edge of item to change its size - var tmp = this.shape - if(info.type === 'ens') { - tmp = $(this.shape.getElementsByClassName('mainCircle'))[0]; - } - interact(this.area) - .resizable({ - edges: { left: true, right: true, bottom: true, top: true }, - invert: this.type == 'ens' ? 'reposition' : 'none' - }) - .on('resizestart', function(event) { - self.menu.hide_any(); - }) - .on('resizemove', function(event) { - var item = self.ng.svg_objects[uid]; - var pos = item.get_screen_location(); - var h_scale = self.ng.get_scaled_width(); - var v_scale = self.ng.get_scaled_height(); - var parent = item.parent; - while (parent !== null) { - h_scale = h_scale * parent.width * 2; - v_scale = v_scale * parent.height * 2; - parent = parent.parent; - } - - if (self.aspect !== null) { - self.constrain_aspect(); - - var vertical_resize = event.edges.bottom || event.edges.top; - var horizontal_resize = event.edges.left || event.edges.right; - - var w = pos[0] - event.clientX + self.ng.offsetX; - var h = pos[1] - event.clientY + self.ng.offsetY; - - if (event.edges.right) { - w *= -1; - } - if (event.edges.bottom) { - h *= -1; - } - if (w < 0) { - w = 1; - } - if (h < 0) { - h = 1; - } - - var screen_w = item.width * h_scale; - var screen_h = item.height * v_scale; - - if (horizontal_resize && vertical_resize) { - var p = (screen_w * w + screen_h * h) / Math.sqrt( - screen_w * screen_w + screen_h * screen_h); - var norm = Math.sqrt(self.aspect * self.aspect + 1); - h = p / (self.aspect / norm); - w = p * (self.aspect / norm); - } else if (horizontal_resize) { - h = w / self.aspect; - } else { - w = h * self.aspect; - } - - var scaled_w = w / h_scale; - var scaled_h = h / v_scale; - - item.width = scaled_w; - item.height = scaled_h; - } else { - var dw = event.deltaRect.width / h_scale / 2; - var dh = event.deltaRect.height / v_scale / 2; - var offset_x = dw + event.deltaRect.left / h_scale; - var offset_y = dh + event.deltaRect.top / v_scale; - - item.width += dw; - item.height += dh; - item.x += offset_x; - item.y += offset_y; - } - - item.redraw(); - - if (self.depth === 1) { - self.ng.scaleMiniMap(); - } - }) - .on('resizeend', function(event) { - var item = self.ng.svg_objects[uid]; - item.constrain_position(); - item.redraw(); - self.ng.notify({act:"pos_size", uid:uid, - x:item.x, y:item.y, - width:item.width, height:item.height}); - }); - } - - // Determine when to pull up the menu - interact(this.g) - .on('hold', function(event) { // change to 'tap' for right click - if (event.button == 0) { - if (self.menu.visible_any()) { - self.menu.hide_any(); - } else { - self.menu.show(event.clientX, event.clientY, - self.generate_menu()); - } - event.stopPropagation(); - } - }) - .on('tap', function(event) { // get rid of menus when clicking off - if (event.button == 0) { - if (self.menu.visible_any()) { - self.menu.hide_any(); - } - } - }) - .on('doubletap', function(event) { // get rid of menus when clicking off - if (event.button == 0) { - if (self.menu.visible_any()) { - self.menu.hide_any(); - } else if (self.type === 'net') { - if (self.expanded) { - self.collapse(true); - } else { - self.expand(); - } - } - } - }); - $(this.g).bind('contextmenu', function(event) { - event.preventDefault(); - event.stopPropagation(); - if (self.menu.visible_any()) { - self.menu.hide_any(); - } else { - self.menu.show(event.clientX, event.clientY, - self.generate_menu()); - } - }); - - if (info.type === 'net') { - // if a network is flagged to expand on creation, then expand it - if (info.expanded) { - // Report to server but do not add to the undo stack - this.expand(true,true); - } - } - }; -}; - -Nengo.NetGraphItem.prototype.set_label = function(label) { - this.label.innerHTML = label; -} - -Nengo.NetGraphItem.prototype.move_to_front = function() { - this.g.parentNode.appendChild(this.g); - - for (var item in this.children) { - this.children[item].move_to_front(); - } -}; - -Nengo.NetGraphItem.prototype.generate_menu = function () { - var self = this; - var items = []; - if (this.type === 'net') { - if (this.expanded) { - items.push(['Collapse network', - function() {self.collapse(true);}]); - items.push(['Auto-layout', - function() {self.request_feedforward_layout();}]); - } else { - items.push(['Expand network', - function() {self.expand();}]); - } - if (this.default_output && this.sp_targets.length == 0) { - items.push(['Output Value', - function() {self.create_graph('Value');}]); - } - } - if (this.type == 'ens') { - items.push(['Value', function() {self.create_graph('Value');}]); - if (this.dimensions > 1) { - items.push(['XY-value', function() {self.create_graph('XYValue');}]); - } - items.push(['Spikes', function() {self.create_graph('Raster');}]); - items.push(['Voltages', function() {self.create_graph('Voltage');}]); - items.push(['Firing pattern', function() {self.create_graph('SpikeGrid');}]); - } - if (this.type == 'node') { - items.push(['Slider', function() {self.create_graph('Slider');}]); - if (this.dimensions > 0) { - items.push(['Value', function() {self.create_graph('Value');}]); - } - if (this.dimensions > 1) { - items.push(['XY-value', function() {self.create_graph('XYValue');}]); - } - if (this.html_node) { - items.push(['HTML', function() {self.create_graph('HTMLView');}]); - } - } - if (this.sp_targets.length > 0) { - items.push(['Semantic pointer cloud', - function() {self.create_graph('Pointer', self.sp_targets[0]);}]); - items.push(['Semantic pointer plot', - function() {self.create_graph('SpaSimilarity', self.sp_targets[0]);}]); - } - // TODO: Enable input and output value plots for basal ganglia network - items.push(['Details ...', function() {self.create_modal();}]); - return items; -}; - -Nengo.NetGraphItem.prototype.create_graph = function (type, args) { - var info = {}; - info.act = 'create_graph'; - info.type = type; - var w = this.get_nested_width(); - var h = this.get_nested_height(); - - var pos = this.get_screen_location(); - - info.x = pos[0] / (viewport.w * viewport.scale) - viewport.x + w; - info.y = pos[1] / (viewport.h * viewport.scale) - viewport.y + h; - - info.width = 100 / (viewport.w * viewport.scale); - info.height = 100 / (viewport.h * viewport.scale); - - if (info.type == 'Slider') { - info.width /= 2; - } - - info.uid = this.uid; - if (typeof(args) != 'undefined') { info.args = args; } - this.ng.notify(info); -}; - -Nengo.NetGraphItem.prototype.create_modal = function () { - var info = {}; - info.act = 'create_modal'; - info.uid = this.uid; - info.conn_in_uids = this.conn_in.map(function (c) { return c.uid; }); - info.conn_out_uids = this.conn_out.map(function (c) { return c.uid; }); - this.ng.notify(info); -} - -Nengo.NetGraphItem.prototype.request_feedforward_layout = function () { - this.ng.notify({act:"feedforward_layout", uid:this.uid}); -}; - -/** expand a collapsed network */ -Nengo.NetGraphItem.prototype.expand = function(rts, auto) { - // default to true if no parameter is specified - rts = typeof rts !== 'undefined' ? rts : true; - auto = typeof auto !== 'undefined' ? auto : false; - - this.g.classList.add('expanded'); - - if (!this.expanded) { - this.expanded = true; - if (this.ng.transparent_nets) { - this.shape.style["fill-opacity"] = 0.0; - } - this.g_items.removeChild(this.g); - this.g_networks.appendChild(this.g); - if (!this.minimap) { - this.mini_item.expand(rts, auto); - } - } else { - console.log("expanded a network that was already expanded"); - console.log(this); - } - - if (rts) { - if (auto) { - // Update the server, but do not place on the undo stack - this.ng.notify({act:"auto_expand", uid:this.uid}); - } else { - this.ng.notify({act:"expand", uid:this.uid}); - } - } -} - -Nengo.NetGraphItem.prototype.set_label_below = function(flag) { - if (flag && !this.label_below) { - var screen_h = this.get_screen_height(); - this.label.setAttribute('transform', 'translate(0, ' + (screen_h / 2) + ')'); - } else if (!flag && this.label_below) { - this.label.setAttribute('transform', ''); - } -} - - -/** collapse an expanded network */ -Nengo.NetGraphItem.prototype.collapse = function(report_to_server, auto) { - auto = typeof auto !== 'undefined' ? auto : false; - this.g.classList.remove('expanded'); - - // remove child NetGraphItems and NetGraphConnections - while (this.child_connections.length > 0) { - this.child_connections[0].remove(); - } - while (this.children.length > 0) { - this.children[0].remove(); - } - - if (this.expanded) { - this.expanded = false; - if (this.ng.transparent_nets) { - this.shape.style["fill-opacity"] = 1.0; - } - this.g_networks.removeChild(this.g); - this.g_items.appendChild(this.g); - if (!this.minimap) { - this.mini_item.collapse(report_to_server, auto); - } - } else { - console.log("collapsed a network that was already collapsed"); - console.log(this); - } - - if (report_to_server) { - if (auto) { - // Update the server, but do not place on the undo stack - this.ng.notify({act:"auto_collapse", uid:this.uid}); - } else { - this.ng.notify({act:"collapse", uid:this.uid}); - } - } -} - - -/** determine the fill color based on the depth */ -Nengo.NetGraphItem.prototype.compute_fill = function() { - var depth = this.ng.transparent_nets ? 1 : this.depth; - - if (!this.passthrough) { - var fill = Math.round(255 * Math.pow(0.8, depth)); - this.shape.style.fill = 'rgb(' + fill + ',' + fill + ',' + fill + ')'; - var stroke = Math.round(255 * Math.pow(0.8, depth + 2)); - this.shape.style.stroke = 'rgb(' + stroke + ',' + stroke + ',' + stroke + ')'; - } -} - - -/** remove the item from the graph */ -Nengo.NetGraphItem.prototype.remove = function() { - if (this.expanded) { - /** collapse the item, but don't tell the server since that would - * update the server's config */ - this.collapse(false); - } - - // remove the item from the parent's children list - if (!this.minimap && this.parent !== null) { - var index = this.parent.children.indexOf(this); - this.parent.children.splice(index, 1); - } - - delete this.ng.svg_objects[this.uid]; - - // update any connections into or out of this item - var conn_in = this.conn_in.slice(); - for (var i in conn_in) { - var conn = conn_in[i]; - conn.set_post(conn.find_post()); - conn.redraw(); - } - var conn_out = this.conn_out.slice(); - for (var i in conn_out) { - var conn = conn_out[i]; - conn.set_pre(conn.find_pre()); - conn.redraw(); - } - - // remove from the SVG - this.g_items.removeChild(this.g); - if (this.depth == 1) { - this.ng.scaleMiniMap(); - } - - if (!this.minimap) { - this.mini_item.remove(); - } -}; - -Nengo.NetGraphItem.prototype.constrain_aspect = function() { - this.size = this.get_displayed_size(); -}; - -Nengo.NetGraphItem.prototype.get_displayed_size = function() { - if (this.aspect !== null) { - var h_scale = this.ng.get_scaled_width(); - var v_scale = this.ng.get_scaled_height(); - var w = this.get_nested_width() * h_scale; - var h = this.get_nested_height() * v_scale; - - if (h * this.aspect < w) { - w = h * this.aspect; - } else if (w / this.aspect < h) { - h = w / this.aspect; - } - - return [w / h_scale, h / v_scale]; - } else { - return [this.width, this.height]; - } -}; - -Nengo.NetGraphItem.prototype.constrain_position = function() { - this.constrain_aspect(); - - if (this.parent !== null) { - this.width = Math.min(0.5, this.width); - this.height = Math.min(0.5, this.height); - - this.x = Math.min(this.x, 1.0-this.width); - this.x = Math.max(this.x, this.width); - - this.y = Math.min(this.y, 1.0-this.height); - this.y = Math.max(this.y, this.height); - } -}; - -Nengo.NetGraphItem.prototype.redraw_position = function() { - var screen = this.get_screen_location(); - - // update my position - this.g.setAttribute('transform', 'translate(' + screen[0] + ', ' + - screen[1] + ')'); -}; - -Nengo.NetGraphItem.prototype.redraw_children = function() { - // update any children's positions - for (var i in this.children) { - var item = this.children[i]; - item.redraw(); - } -}; - -Nengo.NetGraphItem.prototype.redraw_child_connections = function() { - // update any children's positions - for (var i in this.child_connections) { - var item = this.child_connections[i]; - item.redraw(); - } -}; - - -Nengo.NetGraphItem.prototype.redraw_connections = function() { - /** update any connections into and out of this */ - for (var i in this.conn_in) { - var item = this.conn_in[i]; - item.redraw(); - } - for (var i in this.conn_out) { - var item = this.conn_out[i]; - item.redraw(); - } -}; - -/** return the width of the item, taking into account parent widths */ -Nengo.NetGraphItem.prototype.get_nested_width = function() { - var w = this.width; - var parent = this.parent; - while (parent !== null) { - w *= parent.width * 2; - parent = parent.parent; - } - return w; -} - -/** return the height of the item, taking into account parent heights */ -Nengo.NetGraphItem.prototype.get_nested_height = function() { - var h = this.height; - var parent = this.parent; - while (parent !== null) { - h *= parent.height * 2; - parent = parent.parent; - } - return h; -} - -Nengo.NetGraphItem.prototype.redraw_size = function() { - var screen_w = this.get_screen_width(); - var screen_h = this.get_screen_height(); - - if (this.aspect !== null) { - if (screen_h * this.aspect < screen_w) { - screen_w = screen_h * this.aspect; - } else if (screen_w / this.aspect < screen_h) { - screen_h = screen_w / this.aspect; - } - } - - // the circle pattern isn't perfectly square, so make its area smaller - var area_w = this.type === 'ens' ? screen_w * 0.97 : screen_w; - var area_h = screen_h; - this.area.setAttribute('transform', - 'translate(-' + (area_w / 2) + ', -' + (area_h / 2) + ')'); - this.area.setAttribute('width', area_w); - this.area.setAttribute('height', area_h); - - if (this.type === 'ens') { - var scale = Math.sqrt(screen_h * screen_h + screen_w * screen_w) / Math.sqrt(2); - var r = 17.8; //TODO: Don't hardcode the size of the ensemble - this.shape.setAttribute('transform', 'scale(' + scale / 2 / r + ')'); - this.shape.style.setProperty('stroke-width', 20/scale); - } else if (this.passthrough) { - this.shape.setAttribute('rx', screen_w / 2); - this.shape.setAttribute('ry', screen_h / 2); - } else { - this.shape.setAttribute('transform', - 'translate(-' + (screen_w / 2) + ', -' + (screen_h / 2) + ')'); - this.shape.setAttribute('width', screen_w); - this.shape.setAttribute('height', screen_h); - if (this.type === 'node') { - var radius = Math.min(screen_w, screen_h); - // TODO: Don't hardcode .1 as the corner radius scale - this.shape.setAttribute('rx', radius*.1); - this.shape.setAttribute('ry', radius*.1); - } - } - - if (!this.minimap) { - this.label.setAttribute('transform', 'translate(0, ' + (screen_h / 2) + ')'); - }; -}; - -Nengo.NetGraphItem.prototype.get_screen_width = function() { - if (this.minimap && !this.ng.mm_display) { return 1; } - - if (this.fixed_width !== null) { - return this.fixed_width; - } - - if (!this.minimap) { - var w = this.ng.width; - var screen_w = this.get_nested_width() * w * this.ng.scale; - } else { - var w = this.ng.mm_width; - var screen_w = this.get_nested_width() * w * this.ng.mm_scale; - }; - - if (screen_w < this.minWidth) { - screen_w = this.minWidth; - } - - return screen_w * 2; -} - -Nengo.NetGraphItem.prototype.get_screen_height = function() { - if (this.minimap && !this.ng.mm_display) { return 1; } - - if (this.fixed_height !== null) { - return this.fixed_height; - } - - if (this.minimap == false) { - var h = this.ng.height; - var screen_h = this.get_nested_height() * h * this.ng.scale; - } else { - var h = this.ng.mm_height; - var screen_h = this.get_nested_height() * h * this.ng.mm_scale; - }; - - if (screen_h < this.minHeight) { - screen_h = this.minHeight; - } - - return screen_h * 2; -} - - -/** force a redraw of the item */ -Nengo.NetGraphItem.prototype.redraw = function() { - this.redraw_position(); - this.redraw_size(); - this.redraw_children(); - this.redraw_child_connections(); - this.redraw_connections(); - - if (!this.minimap && this.ng.mm_display) { - this.mini_item.redraw() - } -} - - -/** determine the pixel location of the centre of the item */ -Nengo.NetGraphItem.prototype.get_screen_location = function() { - // FIXME this should probably use this.ng.get_scaled_width and this.ng.get_scaled_height - if (this.minimap && !this.ng.mm_display) { return [1, 1]; } - - if (this.minimap == false) { - var w = this.ng.width * this.ng.scale; - var h = this.ng.height * this.ng.scale; - - var offsetX = this.ng.offsetX * w; - var offsetY = this.ng.offsetY * h; - } else { - var mm_w = this.ng.mm_width; - var mm_h = this.ng.mm_height; - - var w = mm_w * this.ng.mm_scale; - var h = mm_h * this.ng.mm_scale; - - var disp_w = (this.ng.mm_max_x - this.ng.mm_min_x) * w; - var disp_h = (this.ng.mm_max_y - this.ng.mm_min_y) * h; - - var offsetX = -this.ng.mm_min_x * w + (mm_w - disp_w) / 2.; - var offsetY = -this.ng.mm_min_y * h + (mm_h - disp_h) / 2.; - }; - - var dx = 0; - var dy = 0; - var parent = this.parent; - while (parent !== null) { - dx *= parent.width * 2; - dy *= parent.height * 2; - - dx += (parent.x - parent.width); - dy += (parent.y - parent.height); - parent = parent.parent; - } - dx *= w; - dy *= h; - - var ww = w; - var hh = h; - if (this.parent !== null) { - ww *= this.parent.get_nested_width() * 2; - hh *= this.parent.get_nested_height() * 2; - } - - return [this.x * ww + dx + offsetX, - this.y * hh + dy + offsetY]; -} - -/**Function for drawing ensemble svg*/ -Nengo.NetGraphItem.prototype.ensemble_svg = function() { - var shape = this.ng.createSVGElement('g'); - shape.setAttribute('class', 'ensemble'); - - var dx = -1.25; - var dy = 0.25; - - var circle = this.ng.createSVGElement('circle'); - this.setAttributes(circle, {'cx':-11.157 + dx,'cy':-7.481 + dy,'r':'4.843'}); - shape.appendChild(circle); - var circle = this.ng.createSVGElement('circle'); - this.setAttributes(circle, {'cx':0.186 + dx,'cy':-0.127 + dy,'r':'4.843'}); - shape.appendChild(circle); - var circle = this.ng.createSVGElement('circle'); - this.setAttributes(circle, {'cx':5.012 + dx,'cy':12.56 + dy,'r':'4.843'}); - shape.appendChild(circle); - var circle = this.ng.createSVGElement('circle'); - this.setAttributes(circle, {'cx':13.704 + dx,'cy':-0.771 + dy,'r':'4.843'}); - shape.appendChild(circle); - var circle = this.ng.createSVGElement('circle'); - this.setAttributes(circle, {'cx':-10.353 + dx,'cy':8.413 + dy,'r':'4.843'}); - shape.appendChild(circle); - var circle = this.ng.createSVGElement('circle'); - this.setAttributes(circle, {'cx':3.894 + dx,'cy':-13.158 + dy,'r':'4.843'}); - shape.appendChild(circle); - - return shape; -} -/** Helper function for setting attributions*/ -Nengo.NetGraphItem.prototype.setAttributes = function(el, attrs) { - for(var key in attrs) { - el.setAttribute(key, attrs[key]); - } -} - -Nengo.NetGraphItem.prototype.getMinMaxXY = function () { - min_x = this.x - this.width; - max_x = this.x + this.width; - min_y = this.y - this.height; - max_y = this.y + this.height; - return [min_x, max_x, min_y, max_y]; -} diff --git a/nengo_gui/static/components/network.css b/nengo_gui/static/components/network.css new file mode 100644 index 00000000..22d7a800 --- /dev/null +++ b/nengo_gui/static/components/network.css @@ -0,0 +1,6 @@ +svg g.network rect { + fill: gray(80%); + stroke: gray(50%) { + width: 1; + } +} diff --git a/nengo_gui/static/components/network.ts b/nengo_gui/static/components/network.ts new file mode 100644 index 00000000..94cbf97b --- /dev/null +++ b/nengo_gui/static/components/network.ts @@ -0,0 +1,221 @@ +import { VNode, dom, h } from "maquette"; + +import "./network.css"; + +import { Component, ComponentView } from "./component"; +import { config } from "../config"; +import { + ComponentConnection, + FeedforwardConnection, + RecurrentConnection +} from "./connection"; +import { Menu } from "../menu"; +import { NetGraph } from "../netgraph/main"; +import { Plot } from "./plot"; +import { Position } from "./position"; +import { Connection } from "../server"; +import * as utils from "../utils"; +import { registerComponent } from "./registry"; + +export class Network extends Component { + expanded: boolean; + // spTargets; // Vocab...? Subclass for SPA networks? + // defaultOutput; + gClass: string[]; + gNetworks: SVGElement; + view: NetworkView; + + protected _depth: number; + + constructor({ + server, + uid, + label, + pos, + labelVisible = true, + expanded = false, + depth = 0, + defaultOutput = null + }: { + server: Connection; + label: string; + uid: string; + pos: Position; + labelVisible?: boolean; + expanded?: boolean; + depth?: number; + defaultOutput?: string; + }) { + super(server, uid, new NetworkView(), label, pos, labelVisible); + + this.expanded = expanded; + this.depth = depth; + // this.defaultOutput = defaultOutput; + this.transparent = config.transparentNets; + + // Do in expanded or depth setter? + // this.computeFill(); + + document.addEventListener("nengoConfigChange", (event: CustomEvent) => { + const key = event.detail; + if (key === "transparentNets") { + this.transparent = config.transparentNets; + } + }); + } + + get depth(): number { + return this._depth; + } + + set depth(val: number) { + const fill = Math.round(255 * Math.pow(0.8, val)); + const stroke = Math.round(255 * Math.pow(0.8, val + 2)); + this.view.fill = `rgb(${fill},${fill},${fill})`; + this.view.stroke = `rgb(${stroke},${stroke},${stroke})`; + this._depth = val; + } + + get transparent(): boolean { + return this.view.transparent; + } + + set transparent(val: boolean) { + this.view.transparent = val; + } + + addMenuItems() { + // this.menu.addAction("Output Value", () => { + // this.createGraph("Value"); + // }, () => this.defaultOutput && this.spTargets.length === 0); + // this.menu.addAction("Semantic pointer cloud", () => { + // this.createGraph("Pointer", this.spTargets[0]); + // }, () => this.spTargets.length > 0); + // this.menu.addAction("Semantic pointer plot", () => { + // this.createGraph("SpaSimilarity", this.spTargets[0]); + // }, () => this.spTargets.length > 0); + this.menu.addAction("Details ...", () => { + // TODO + // this.createModal(); + }); + } + + /** + * Determine the fill color based on the depth. + */ + computeFill() { + // const depth = this.ng.transparentNets ? 1 : this.view.depth; + // TODO: depth + const depth = 1; + } + + onnetgraphadd(netgraph: NetGraph) { + this.menu.addAction( + "Collapse network", + () => { + netgraph.collapse(this); + }, + () => this.expanded + ); + this.menu.addAction( + "Auto-layout", + () => { + // TODO: server? + this.server.send("netgraph.autolayout"); + }, + () => this.expanded + ); + this.menu.addAction( + "Expand network", + () => { + netgraph.expand(this); + }, + () => !this.expanded + ); + + this.interactRoot.on("doubletap", event => { + // Get rid of menus when clicking off + if (event.button === 0) { + if (Menu.shown !== null) { + Menu.hideShown(); + } else { + if (this.expanded) { + netgraph.collapse(this); + } else { + netgraph.expand(this); + } + } + } + }); + + super.onnetgraphadd(netgraph); + } +} + +export class NetworkView extends ComponentView { + rect: SVGRectElement; + + constructor() { + super(); + const node = h("g.network", [ + h("rect", { + height: "50", + styles: { + fill: "rgb(0,0,0)", + "fill-opacity": "1.0", + stroke: "rgb(0,0,0)" + }, + width: "50", + x: "0", + y: "0" + }) + ]); + this.body = utils.domCreateSVG(node) as SVGGElement; + this.root.appendChild(this.body); + this.rect = this.body.firstChild as SVGRectElement; + } + + get fill(): string { + return this.rect.style.fill; + } + + set fill(val: string) { + this.rect.style.fill = val; + } + + get scale(): [number, number] { + return [ + Number(this.rect.getAttribute("width")), + Number(this.rect.getAttribute("height")) + ]; + } + + set scale(val: [number, number]) { + const [width, height] = val; + this.rect.setAttribute("width", `${width}`); + this.rect.setAttribute("height", `${height}`); + this.overlayScale = [width, height]; + } + + get stroke(): string { + return this.rect.style.stroke; + } + + set stroke(val: string) { + this.rect.style.stroke = val; + } + + get transparent(): boolean { + return this.rect.style.fillOpacity === "0"; + } + + set transparent(val: boolean) { + if (val) { + this.rect.style.fillOpacity = "0"; + } else { + this.rect.style.fillOpacity = "1"; + } + } +} + +registerComponent("network", Network); diff --git a/nengo_gui/static/components/node.css b/nengo_gui/static/components/node.css new file mode 100644 index 00000000..bb668133 --- /dev/null +++ b/nengo_gui/static/components/node.css @@ -0,0 +1,13 @@ +svg.netgraph { + g.node rect { + fill: gray(80%); + stroke: gray(50%) { + width: 1; + } + } + + g.passthrough circle { + fill: black; + stroke: none; + } +} diff --git a/nengo_gui/static/components/node.ts b/nengo_gui/static/components/node.ts new file mode 100644 index 00000000..92c12853 --- /dev/null +++ b/nengo_gui/static/components/node.ts @@ -0,0 +1,159 @@ +import { VNode, dom, h } from "maquette"; + +import "./node.css"; + +import { Component, ComponentView } from "./component"; +import { Position } from "./position"; +import { registerComponent } from "./registry"; +import { Connection } from "../server"; +import * as utils from "../utils"; + +export class PassthroughNode extends Component { + fixedHeight: number; + fixedWidth: number; + view: PassthroughNodeView; + + constructor({ + server, + uid, + label, + pos, + labelVisible = true + }: { + server: Connection; + uid: string; + label: string; + pos: Position; + labelVisible?: boolean; + }) { + super(server, uid, new PassthroughNodeView(), label, pos, labelVisible); + } + + get resizeOptions(): any { + return null; + } +} + +export class PassthroughNodeView extends ComponentView { + static width: number = 8; + static height: number = 8; + + constructor() { + super(); + const node = h("g.passthrough", [ + h("circle", { cx: "4", cy: "4", r: "4" }) + ]); + this.body = utils.domCreateSVG(node) as SVGGElement; + this.root.appendChild(this.body); + this.overlayScale = [ + PassthroughNodeView.width, + PassthroughNodeView.height + ]; + } + + get scale(): [number, number] { + return [PassthroughNodeView.width, PassthroughNodeView.height]; + } + + set scale(val: [number, number]) { + // Scale cannot be changed + } +} + +export class Node extends Component { + htmlNode; + dimensions: number; + view: NodeView; + + constructor({ + server, + uid, + label, + pos, + dimensions, + labelVisible = true + }: { + server: Connection; + uid: string; + label: string; + pos: Position; + dimensions: number; + labelVisible?: boolean; + }) { + super(server, uid, new NodeView(), label, pos, labelVisible); + this.dimensions = dimensions; + } + + addMenuItems() { + this.menu.addAction("Slider", () => { + this.createGraph("Slider"); + }); + this.menu.addAction( + "Value", + () => { + this.createGraph("Value"); + }, + () => this.dimensions > 0 + ); + this.menu.addAction( + "XY-value", + () => { + this.createGraph("XYValue"); + }, + () => this.dimensions > 1 + ); + this.menu.addAction( + "HTML", + () => { + this.createGraph("HTMLView"); + }, + () => this.htmlNode + ); + this.menu.addAction("Details ...", () => { + // TODO + // this.createModal(); + }); + } +} + +export class NodeView extends ComponentView { + static radiusScale: number = 0.1; + + rect: SVGRectElement; + + constructor() { + super(); + const node = h("g.node", [ + h("rect", { + height: "50", + rx: `${NodeView.radiusScale * 50}`, + ry: `${NodeView.radiusScale * 50}`, + width: "50", + x: "0", + y: "0" + }) + ]); + this.body = utils.domCreateSVG(node) as SVGGElement; + this.root.appendChild(this.body); + this.rect = this.body.firstChild as SVGRectElement; + } + + get scale(): [number, number] { + return [ + Number(this.rect.getAttribute("width")), + Number(this.rect.getAttribute("height")) + ]; + } + + set scale(val: [number, number]) { + const [width, height] = val; + const smaller = Math.min(width, height); + this.rect.setAttribute("width", `${width}`); + this.rect.setAttribute("height", `${height}`); + this.rect.setAttribute("rx", `${NodeView.radiusScale * smaller}`); + this.rect.setAttribute("ry", `${NodeView.radiusScale * smaller}`); + this.overlayScale = [width, height]; + } +} + +registerComponent("node", Node); diff --git a/nengo_gui/static/components/plot.css b/nengo_gui/static/components/plot.css new file mode 100644 index 00000000..d259f3cd --- /dev/null +++ b/nengo_gui/static/components/plot.css @@ -0,0 +1,111 @@ +svg.netgraph { + + /* .pointer span { */ + /* display: block; */ + /* margin: 0; */ + /* padding: 0; */ + /* text-align: center; */ + /* } */ + + .axes { + &:hover { + border: 1px solid #888888; + } + + text { + cursor: default; + font-size: 0.8em; + + &:active { + cursor: move; + } + } + + .axis { + path { + stroke: black { + width: 1; + } + fill: none; + } + } + + .crosshairX { + text { + dominant-baseline: text-before-edge; + } + } + + .crosshairY { + text { + dominant-baseline: middle; + text-anchor: end; + } + } + } + + circle.last-point { + fill-opacity: 0.5; + } + + .crosshair { + line { + stroke: black { + width: 0.5; + } + } + text { + font-size: 80%; + pointer-events: none; + } + } + + .crosshairX { + text { + text-anchor: middle; + } + } + + .crosshairY { + text { + text-anchor: end; + } + } + + .legend-item { + text { + font-size: 80%; /* TODO: global for text? */ + &.label { + text-anchor: start; + } + &.value { + text-anchor: end; + } + } + } + + .line { + fill: none; + stroke-width: 1.5; + } + +} + +.warning-text { + background-color: white; + box: { + align: center; + pack: center; + } + border: 1px dashed #a94442; + color: #a94442; + display: flex; + margin: { + left: 20%; + right: auto; + } + position: absolute; + text-align: center; + top: 37%; + width: 60%; +} diff --git a/nengo_gui/static/components/plot.ts b/nengo_gui/static/components/plot.ts new file mode 100644 index 00000000..57872489 --- /dev/null +++ b/nengo_gui/static/components/plot.ts @@ -0,0 +1,621 @@ +import * as d3 from "d3"; +import { VNode, dom, h } from "maquette"; + +import "./plot.css"; + +import { ComponentView } from "./component"; +import { InputDialogView } from "../modal"; +import { Position } from "./position"; +import { Connection } from "../server"; +import * as utils from "../utils"; +import { Widget } from "./widget"; + +export class Axis { + axis: d3.svg.Axis; + g: d3.Selection; + scale: d3.scale.Linear; + + constructor(xy: "X" | "Y", g: SVGGElement, lim: [number, number]) { + this.scale = d3.scale.linear(); + this.axis = d3.svg.axis(); + this.axis.orient(xy === "X" ? "bottom" : "left"); + this.axis.scale(this.scale); + this.g = d3.select(g); + this.lim = lim; + } + + get lim(): [number, number] { + const lim = this.scale.domain() as [number, number]; + console.assert(lim.length === 2); + return lim; + } + + set lim(val: [number, number]) { + this.scale.domain(val); + this.axis.tickValues(val); + this.axis(this.g); + } + + get pixelLim(): [number, number] { + const scale = this.scale.range() as [number, number]; + console.assert(scale.length === 2); + return scale; + } + + set pixelLim(val: [number, number]) { + this.scale.range(val); + this.axis(this.g); + } + + get tickSize(): number { + return this.axis.outerTickSize(); + } + + set tickSize(val: number) { + // .tickPadding(val * 0.5) + this.axis.outerTickSize(val); + } + + isPixelValid(pixel: number) { + const lim = this.pixelLim; + if (lim[0] > lim[1]) { + return lim[1] <= pixel && pixel <= lim[0]; + } else { + return lim[0] <= pixel && pixel <= lim[1]; + } + } + + pixelAt(value: number) { + return this.scale(value); + } + + valueAt(pixel: number) { + return this.scale.invert(pixel); + } +} + +export class Axes { + // TODO: what should these actually be? + static minHeight: number = 20; + static minWidth: number = 20; + + x: Axis; + y: Axis; + view: AxesView; + + protected _height: number; + protected _width: number; + + // TODO: have left xtick disappear if too close to right xtick? + + // TODO: probably don't have width, height passed in? get from view? + constructor( + plotView: PlotView, + width, + height, + xlim: [number, number] = [-0.5, 0.0], + ylim: [number, number] = [-1, 1] + ) { + this.view = plotView.axes; + this._width = width; + this._height = height; + + // TODO: better initial values for x? + this.x = new Axis("X", this.view.x.g, xlim); + this.y = new Axis("Y", this.view.y.g, ylim); + + // Set up mouse handlers for crosshairs + plotView.overlay.addEventListener("mouseout", () => { + this.view.crosshair.visible = false; + }); + plotView.overlay.addEventListener("mousemove", (event: MouseEvent) => { + const pt = utils.dom2svg(plotView.root, event.x, event.y); + if (this.x.isPixelValid(pt.x) && this.y.isPixelValid(pt.y)) { + this.view.crosshair.pos = [pt.x, pt.y]; + this.view.crosshair.value = [ + this.x.valueAt(pt.x), + this.y.valueAt(pt.y) + ]; + this.view.crosshair.visible = true; + } else { + this.view.crosshair.visible = false; + } + }); + + // TODO: previosly, we hid on mouse wheel... should we? + // this.view.root.addEventListener("mousewheel", () => { + // this.view.crosshairPos = ; + // }); + } + + get height(): number { + return this._height; + } + + get padding(): [number, number] { + return [5, 5]; + } + + set scale(val: [number, number]) { + this._width = Math.max(Axes.minWidth, val[0]); + this._height = Math.max(Axes.minHeight, val[1]); + + const [xWidth, xHeight] = this.view.x.scale; + const [yWidth, yHeight] = this.view.y.scale; + + // TOOD: why 0 and not yWidth? + this.view.x.pos = [0, this._height - xHeight]; + this.x.pixelLim = [yWidth, this._width]; + this.view.y.pos = [yWidth, 0]; + this.y.pixelLim = [this._height - xHeight, 0]; + this.view.crosshair.scale = [this._width, this._height - xHeight]; + } + + get width(): number { + return this._width; + } + + ondomadd() { + this.scale = [this._width, this._height]; + const yWidth = this.view.y.scale[0]; + this.view.crosshair.offset = [0, yWidth]; + this.x.tickSize = 0.4 * yWidth; + this.y.tickSize = 0.4 * yWidth; + } +} + +export abstract class Plot extends Widget { + axes: Axes; + view: PlotView; + + constructor( + server: Connection, + uid: string, + view: PlotView, + label: string, + pos: Position, + dimensions: number, + synapse: number, + labelVisible: boolean = true, + xlim: [number, number] = [-0.5, 0], + ylim: [number, number] = [-1, 1] + ) { + super(server, uid, view, label, pos, dimensions, synapse, labelVisible); + this.synapse = synapse; + this.view.dimensions = dimensions; + + this.addAxes(pos.width, pos.height, xlim, ylim); + + this.interactRoot.on("resizemove", event => { + // Resizing the view happens in the superclass; we update axes here + this.axes.scale = this.view.scale; + this.syncWithDataStore(); + }); + + window.addEventListener( + "TimeSlider.timeShown", + utils.throttle((event: CustomEvent) => { + this.xlim = [ + event.detail.timeShown - event.detail.shownWidth, + event.detail.timeShown + ]; + }, 20) // Update once every 20 ms + ); + window.addEventListener("SimControl.reset", e => { + this.reset(); + }); + } + + get legendLabels(): string[] { + return this.view.legendLabels; + } + + set legendLabels(val: string[]) { + this.view.legendLabels = val; + } + + get legendVisible(): boolean { + return this.view.legendVisible; + } + + set legendVisible(val: boolean) { + this.view.legendVisible = val; + } + + get xlim(): [number, number] { + return this.axes.x.lim; + } + + set xlim(val: [number, number]) { + this.axes.x.lim = val; + this.syncWithDataStore(); + } + + get ylim(): [number, number] { + return this.axes.y.lim; + } + + set ylim(val: [number, number]) { + this.axes.y.lim = val; + this.syncWithDataStore(); + } + + addAxes(width, height, xlim, ylim) { + this.axes = new Axes(this.view, width, height, xlim, ylim); + } + + addMenuItems() { + this.menu.addAction( + "Hide legend", + () => { + this.legendVisible = false; + }, + () => this.legendVisible + ); + this.menu.addAction( + "Show legend", + () => { + this.legendVisible = true; + }, + () => !this.legendVisible + ); + // TODO: give the legend its own context menu + this.menu.addAction( + "Set legend labels", + () => { + this.askLegend(); + }, + () => this.legendVisible + ); + this.menu.addSeparator(); + super.addMenuItems(); + } + + askLegend() { + const modal = new InputDialogView("Legend labels", "New value"); + modal.title = "Enter comma separated legend labels"; + modal.ok.addEventListener("click", () => { + const labelCSV = modal.input.value; + // No validation to do. + // Empty entries assumed to be indication to skip modification. + // Long strings okay. + // Excissive entries get ignored. + // TODO: Allow escaping of commas + if (labelCSV !== null && labelCSV !== "") { + this.legendLabels = labelCSV.split(",").map(s => s.trim()); + } + $(modal).modal("hide"); + }); + utils.handleTabs(modal); + $(modal.root).on("hidden.bs.modal", () => { + document.body.removeChild(modal.root); + }); + document.body.appendChild(modal.root); + modal.show(); + } + + ondomadd() { + super.ondomadd(); + this.axes.ondomadd(); + } + + scale(factor: number) { + super.scale(factor); + this.axes.scale = this.view.scale; + this.syncWithDataStore(); + } +} + +export class CrosshairView { + x: SVGGElement; + xLine: SVGLineElement; + xText: SVGTextElement; + y: SVGGElement; + yLine: SVGLineElement; + yText: SVGTextElement; + + constructor() { + const crosshair = (xy: "X" | "Y") => + h(`g.crosshair.crosshair${xy}`, { styles: { display: "none" } }, [ + h("line", { x1: "0", x2: "0", y1: "0", y2: "0" }), + h("text", { x: "0", y: "0" }, ["0.000"]) + ]); + this.x = utils.domCreateSVG(crosshair("X")) as SVGGElement; + this.y = utils.domCreateSVG(crosshair("Y")) as SVGGElement; + this.xLine = this.x.querySelector("line"); + this.xText = this.x.querySelector("text"); + this.yLine = this.y.querySelector("line"); + this.yText = this.y.querySelector("text"); + } + + get offset(): [number, number] { + return [ + Number(this.xLine.getAttribute("y1")), + Number(this.yLine.getAttribute("x1")) + ]; + } + + set offset(val: [number, number]) { + this.xLine.setAttribute("y1", String(val[0])); + this.yLine.setAttribute("x1", String(val[1])); + this.yText.setAttribute("x", String(val[1])); + } + + get pos(): [number, number] { + return [ + Number(this.xLine.getAttribute("x1")), + Number(this.yLine.getAttribute("y1")) + ]; + } + + set pos(val: [number, number]) { + this.xLine.setAttribute("x1", String(val[0])); + this.xLine.setAttribute("x2", String(val[0])); + this.yLine.setAttribute("y1", String(val[1])); + this.yLine.setAttribute("y2", String(val[1])); + this.xText.setAttribute("x", String(val[0])); + this.yText.setAttribute("y", String(val[1])); + } + + get scale(): [number, number] { + return [ + Number(this.yLine.getAttribute("x2")), + Number(this.xLine.getAttribute("y2")) + ]; + } + + set scale(val: [number, number]) { + this.yLine.setAttribute("x2", String(val[0])); + this.xLine.setAttribute("y2", String(val[1])); + this.xText.setAttribute("y", String(val[1])); + } + + get value(): [number, number] { + return [Number(this.xText.textContent), Number(this.yText.textContent)]; + } + + set value(val: [number, number]) { + this.xText.textContent = val[0].toFixed(3); + this.yText.textContent = val[1].toFixed(3); + } + + get visible(): boolean { + return this.x.style.display !== "none"; + } + + set visible(val: boolean) { + if (val) { + this.x.style.display = ""; + this.y.style.display = ""; + } else { + this.x.style.display = "none"; + this.y.style.display = "none"; + } + } +} + +export class AxisView { + g: SVGGElement; + orientation: "horizontal" | "vertical"; + + constructor(xy: "X" | "Y") { + const node = h(`g.axis.axis${xy}.unselectable`, { + transform: "translate(0,0)" + }); + this.g = utils.domCreateSVG(node) as SVGGElement; + this.orientation = xy === "X" ? "horizontal" : "vertical"; + } + + get pos(): [number, number] { + return utils.getTranslate(this.g); + } + + set pos(val: [number, number]) { + utils.setTranslate(this.g, val[0], val[1]); + } + + get scale(): [number, number] { + const bbox = this.g.getBBox(); + return [bbox.width, bbox.height]; + } +} + +export class AxesView { + x: AxisView; + y: AxisView; + crosshair: CrosshairView; + root: SVGGElement; + + constructor() { + const node = h("g.axes"); + this.root = utils.domCreateSVG(node) as SVGGElement; + this.x = new AxisView("X"); + this.y = new AxisView("Y"); + this.root.appendChild(this.x.g); + this.root.appendChild(this.y.g); + this.crosshair = new CrosshairView(); + this.root.appendChild(this.crosshair.x); + this.root.appendChild(this.crosshair.y); + } + + get fontSize(): number { + // TODO: Number doesn't work here so we use parseFloat, but feels bad + return parseFloat(window.getComputedStyle(this.root)["font-size"]); + } + + get scale(): [number, number] { + const [xWidth, xHeight] = this.x.scale; + const [yWidth, yHeight] = this.y.scale; + return [xWidth + yWidth, xHeight + yHeight]; + } +} + +export class LegendView { + colors: string[]; + root: SVGGElement; + + private _labels: SVGTextElement[] = []; + private _legendItems: SVGGElement[] = []; + private _values: SVGTextElement[] = []; + + constructor(colors: string[]) { + this.colors = colors; + const dimensions = this.colors.length; + const node = h("g.legend", { transform: "translate(0,0)" }); + this.root = utils.domCreateSVG(node) as SVGGElement; + this.numLabels = this.colors.length; + this.labels = []; + } + + get labels(): string[] { + return this._labels.map(label => label.textContent); + } + + set labels(val: string[]) { + this._labels.forEach((label, i) => { + if (i >= val.length) { + label.textContent = `Dimension ${i + 1}`; + } else { + label.textContent = val[i]; + } + }); + } + + get numLabels(): number { + return this._legendItems.length; + } + + set numLabels(val: number) { + while (this._legendItems.length - val < 0) { + this.addLabel(); + } + while (this._legendItems.length - val > 0) { + this.removeLabel(); + } + } + + get pos(): [number, number] { + return utils.getTranslate(this.root); + } + + set pos(val: [number, number]) { + utils.setTranslate(this.root, val[0], val[1]); + } + + get valuesVisible(): boolean { + return ( + this._values.length > 0 && this._values[0].style.display !== "none" + ); + } + + set valuesVisible(val: boolean) { + this._values.forEach(value => { + value.style.display = val ? null : "none"; + }); + } + + set values(val: number[]) { + console.assert(val.length === this.numLabels); + this._values.forEach((value, i) => { + value.textContent = val[i].toFixed(2); + }); + } + + private addLabel() { + const i = this._legendItems.length; + const node = h("g.legend-item", [ + h("rect", { + fill: this.colors[i % this.colors.length], + height: "10", + width: "10", + y: `${i * 20}` + }), + h("text.legend-label", { + x: "15", + y: `${i * 20 + 9}` + }), + h("text.legend-value", { + styles: { display: "none" }, // Hide by default + y: `${i * 20 + 9}` + }) + ]); + const legendItem = utils.domCreateSVG(node) as SVGGElement; + this.root.appendChild(legendItem); + this._legendItems.push(legendItem); + this._labels.push(legendItem.querySelector( + "text.legend-label" + ) as SVGTextElement); + this._values.push(legendItem.querySelector( + "text.legend-value" + ) as SVGTextElement); + } + + private removeLabel() { + const legendItem = this._legendItems.pop(); + this._labels.pop(); + this._values.pop(); + if (legendItem != null) { + this.root.removeChild(legendItem); + } + } +} + +export abstract class PlotView extends ComponentView { + axes: AxesView; + colors: string[] = []; + legend: LegendView; + plot: SVGGElement; + + constructor() { + super(); + this.axes = new AxesView(); + this.legend = new LegendView(this.colors); + const node = h("g.plot"); + this.body = utils.domCreateSVG(node) as SVGGElement; + this.body.appendChild(this.axes.root); + this.body.appendChild(this.legend.root); + this.root.appendChild(this.body); + } + + get dimensions(): number { + return this.colors.length; + } + + set dimensions(val: number) { + this.colors = utils.makeColors(val); + } + + get legendLabels(): string[] { + return this.legend.labels; + } + + set legendLabels(val: string[]) { + this.legend.labels = val; + } + + get legendVisible(): boolean { + return this.body.contains(this.legend.root); + } + + set legendVisible(val: boolean) { + if (val) { + this.body.appendChild(this.legend.root); + } else { + this.body.removeChild(this.legend.root); + } + } + + get scale(): [number, number] { + return this.overlayScale; + } + + set scale(val: [number, number]) { + const [width, height] = val; + this.overlayScale = [width, height]; + this.legend.pos = [width + 2, 0]; + } + + protected updateLabel() { + utils.setTranslate(this._label, this.overlayScale[0] * 0.5, 0); + } +} diff --git a/nengo_gui/static/components/pointer.css b/nengo_gui/static/components/pointer.css deleted file mode 100644 index 97799f87..00000000 --- a/nengo_gui/static/components/pointer.css +++ /dev/null @@ -1,6 +0,0 @@ -.pointer span { - display:block; - margin:0px; - padding:0px; - text-align: center; -} \ No newline at end of file diff --git a/nengo_gui/static/components/pointer.js b/nengo_gui/static/components/pointer.js deleted file mode 100644 index 30e93e24..00000000 --- a/nengo_gui/static/components/pointer.js +++ /dev/null @@ -1,243 +0,0 @@ -/** - * Decoded semantic pointer display - * @constructor - * - * @param {DOMElement} parent - the element to add this component to - * @param {Nengo.SimControl} sim - the simulation controller - * @param {dict} args - A set of constructor arguments (see Nengo.Component) - * - * Pointer constructor is called by python server when a user requests a plot - * or when the config file is making graphs. Server request is handled in - * netgraph.js {.on_message} function. - */ - -Nengo.Pointer = function(parent, sim, args) { - Nengo.Component.call(this, parent, args); - var self = this; - - this.sim = sim; - this.pointer_status = false; - - this.pdiv = document.createElement('div'); - this.pdiv.style.width = args.width; - this.pdiv.style.height = args.height; - Nengo.set_transform(this.pdiv, 0, 25); - this.pdiv.style.position = 'fixed'; - this.pdiv.classList.add('pointer'); - this.div.appendChild(this.pdiv); - - this.show_pairs = args.show_pairs; - - /** for storing the accumulated data */ - this.data_store = new Nengo.DataStore(1, this.sim, 0); - - /** call schedule_update whenever the time is adjusted in the SimControl */ - this.sim.div.addEventListener('adjust_time', - function(e) {self.schedule_update();}, false); - - /** call reset whenever the simulation is reset */ - this.sim.div.addEventListener('sim_reset', - function(e) {self.reset();}, false); - - this.on_resize(this.get_screen_width(), this.get_screen_height()); - - this.fixed_value = ''; - var self = this; - - this.div.addEventListener("mouseup", - function(event) { - // for some reason 'tap' doesn't seem to work here while the - // simulation is running, so I'm doing the timing myself - var now = new Date().getTime() / 1000; - if (now - self.mouse_down_time > 0.1) { - return; - } - if (event.button == 0) { - if (self.menu.visible) { - self.menu.hide(); - } else { - self.menu.show(event.clientX, event.clientY, - self.generate_menu()); - } - } - } - ); - - this.div.addEventListener("mousedown", - function(event) { - self.mouse_down_time = new Date().getTime() / 1000; - } - ); - - - -}; -Nengo.Pointer.prototype = Object.create(Nengo.Component.prototype); -Nengo.Pointer.prototype.constructor = Nengo.Pointer; - -Nengo.Pointer.prototype.generate_menu = function() { - var self = this; - var items = []; - items.push(['Set value...', function() {self.set_value();}]); - if (this.show_pairs) { - items.push(['Hide pairs', function() {self.set_show_pairs(false);}]); - } else { - items.push(['Show pairs', function() {self.set_show_pairs(true);}]); - } - - // add the parent's menu items to this - // TODO: is this really the best way to call the parent's generate_menu()? - return $.merge(items, Nengo.Component.prototype.generate_menu.call(this)); -}; - -Nengo.Pointer.prototype.set_show_pairs = function(value) { - if (this.show_pairs !== value) { - this.show_pairs = value; - this.save_layout(); - } -}; - -Nengo.Pointer.prototype.set_value = function() { - var self = this; - Nengo.modal.title('Enter a Semantic Pointer value...'); - Nengo.modal.single_input_body('Pointer', 'New value'); - Nengo.modal.footer('ok_cancel', function(e) { - var value = $('#singleInput').val(); - var modal = $('#myModalForm').data('bs.validator'); - - modal.validate(); - if (modal.hasErrors() || modal.isIncomplete()) { - return; - } - if ((value === null) || (value === '')) { - value = ':empty:'; - } - self.fixed_value = value; - self.ws.send(value); - $('#OK').attr('data-dismiss', 'modal'); - }); - var $form = $('#myModalForm').validator({ - custom: { - my_validator: function($item) { - var ptr = $item.val(); - if (ptr === null) { - ptr = ''; - } - self.ws.send(':check only:' + ptr); - return self.pointer_status; - } - } - }); - - $('#singleInput').attr('data-error', 'Invalid semantic ' + - 'pointer expression. Semantic pointers themselves must start with ' + - 'a capital letter. Expressions can include mathematical operators ' + - 'such as +, * (circular convolution), and ~ (pseudo-inverse). ' + - 'E.g., (A+~(B*C)*2)*0.5 would be a valid semantic pointer expression.'); - - Nengo.modal.show(); -} - -/** - * Receive new line data from the server - */ -Nengo.Pointer.prototype.on_message = function(event) { - data = event.data.split(" "); - - if (data[0].substring(0,11) == "bad_pointer") { - this.pointer_status = false; - return; - } else if (data[0].substring(0,12) == "good_pointer") { - this.pointer_status = true; - return; - } - - var time = parseFloat(data[0]); - - var items = data[1].split(";"); - this.data_store.push([time, items]); - this.schedule_update(); -} - -/** - * Redraw the lines and axis due to changed data - */ -Nengo.Pointer.prototype.update = function() { - /** let the data store clear out old values */ - this.data_store.update(); - - var data = this.data_store.get_last_data()[0]; - - while(this.pdiv.firstChild) { - this.pdiv.removeChild(this.pdiv.firstChild); - } - this.pdiv.style.width = this.width; - this.pdiv.style.height = this.height; - - if (data === undefined) { - return; - } - - var total_size = 0; - - var items = []; - - // display the text in proportion to similarity - for (var i=0; i < data.length; i++) { - var size = parseFloat(data[i].substring(0,4)); - var span = document.createElement('span'); - span.innerHTML = data[i].substring(4); - this.pdiv.appendChild(span); - total_size += size; - var c = Math.floor(255 - 255 * size); - if (c<0) c = 0; - if (c>255) c = 255; - span.style.color = 'rgb('+c+','+c+','+c+')'; - items.push(span); - } - - var scale = this.height / total_size * 0.6; - - for (var i=0; i < data.length; i++) { - var size = parseFloat(data[i].substring(0,4)); - items[i].style.fontSize = '' + (size * scale) + 'px' - } -}; - -/** - * Adjust the graph layout due to changed size - */ -Nengo.Pointer.prototype.on_resize = function(width, height) { - if (width < this.minWidth) { - width = this.minWidth; - } - if (height < this.minHeight) { - height = this.minHeight; - }; - - this.width = width; - this.height = height; - this.div.style.width = width; - this.div.style.height = height; - - this.label.style.width = width; - - this.update(); -}; - - -Nengo.Pointer.prototype.layout_info = function () { - var info = Nengo.Component.prototype.layout_info.call(this); - info.show_pairs = this.show_pairs; - return info; -} - -Nengo.Pointer.prototype.update_layout = function (config) { - this.show_pairs = config.show_pairs; - Nengo.Component.prototype.update_layout.call(this, config); -} - -Nengo.Pointer.prototype.reset = function(event) { - this.data_store.reset(); - this.schedule_update(); -} diff --git a/nengo_gui/static/components/position.ts b/nengo_gui/static/components/position.ts new file mode 100644 index 00000000..0f96d8e1 --- /dev/null +++ b/nengo_gui/static/components/position.ts @@ -0,0 +1,18 @@ +export class Position { + left: number; + top: number; + width: number | null; + height: number | null; + + constructor( + left: number = 0, + top: number = 0, + width: number = null, + height: number = null + ) { + this.left = left; + this.top = top; + this.width = width; + this.height = height; + } +} diff --git a/nengo_gui/static/components/raster.css b/nengo_gui/static/components/raster.css deleted file mode 100644 index f2fe88d4..00000000 --- a/nengo_gui/static/components/raster.css +++ /dev/null @@ -1,5 +0,0 @@ -.graph .spikes { - fill: none; - stroke: black; - stroke-width: 1.5px; -} diff --git a/nengo_gui/static/components/raster.js b/nengo_gui/static/components/raster.js deleted file mode 100644 index 169baa3a..00000000 --- a/nengo_gui/static/components/raster.js +++ /dev/null @@ -1,183 +0,0 @@ -/** - * Raster plot showing spike events over time - * @constructor - * - * @param {DOMElement} parent - the element to add this component to - * @param {Nengo.SimControl} sim - the simulation controller - * @param {dict} args - A set of constructor arguments (see Nengo.Component) - * @param {int} args.n_neurons - number of neurons - * - * Raster constructor is called by python server when a user requests a plot - * or when the config file is making graphs. Server request is handled in - * netgraph.js {.on_message} function. - */ -Nengo.Raster = function(parent, sim, args) { - Nengo.Component.call(this, parent, args); - var self = this; - this.n_neurons = args.n_neurons || 1; - this.sim = sim; - - /** for storing the accumulated data */ - this.data_store = new Nengo.DataStore(1, this.sim, 0); - - this.axes2d = new Nengo.TimeAxes(this.div, args); - this.axes2d.scale_y.domain([0, args.n_neurons]); - - - /** call schedule_update whenever the time is adjusted in the SimControl */ - this.sim.div.addEventListener('adjust_time', - function(e) {self.schedule_update();}, false); - - /** call reset whenever the simulation is reset */ - this.sim.div.addEventListener('sim_reset', - function(e) {self.reset();}, false); - - /** create the lines on the plots */ - var line = d3.svg.line() - .x(function(d, i) {return self.axes2d.scale_x(times[i]);}) - .y(function(d) {return self.axes2d.scale_y(d);}) - this.path = this.axes2d.svg.append("g").selectAll('path') - .data(this.data_store.data); - - this.path.enter().append('path') - .attr('class', 'line') - .style('stroke', Nengo.make_colors(1)); - - this.update(); - this.on_resize(this.get_screen_width(), this.get_screen_height()); - this.axes2d.axis_y.tickValues([0, args.n_neurons]); - this.axes2d.fit_ticks(this); -}; -Nengo.Raster.prototype = Object.create(Nengo.Component.prototype); -Nengo.Raster.prototype.constructor = Nengo.Raster; - -/** - * Receive new line data from the server - */ -Nengo.Raster.prototype.on_message = function(event) { - var time = new Float32Array(event.data, 0, 1); - var data = new Int16Array(event.data, 4); - this.data_store.push([time[0], data]); - this.schedule_update(); -} - -Nengo.Raster.prototype.set_n_neurons = function(n_neurons) { - this.n_neurons = n_neurons; - this.axes2d.scale_y.domain([0, n_neurons]); - this.axes2d.axis_y.tickValues([0, n_neurons]); - this.ws.send('n_neurons:' + n_neurons); -} - - -/** - * Redraw the lines and axis due to changed data - */ -Nengo.Raster.prototype.update = function() { - /** let the data store clear out old values */ - this.data_store.update(); - - /** determine visible range from the Nengo.SimControl */ - var t1 = this.sim.time_slider.first_shown_time; - var t2 = t1 + this.sim.time_slider.shown_time; - - this.axes2d.set_time_range(t1, t2); - - /** update the lines */ - var shown_data = this.data_store.get_shown_data(); - - var path = []; - for (var i = 0; i < shown_data[0].length; i++) { - var t = this.axes2d.scale_x( - this.data_store.times[ - this.data_store.first_shown_index + i]); - - for (var j = 0; j < shown_data[0][i].length; j++) { - var y1 = this.axes2d.scale_y(shown_data[0][i][j]); - var y2 = this.axes2d.scale_y(shown_data[0][i][j]+1); - path.push('M ' + t + ' ' + y1 + 'V' + y2); - } - } - this.path.attr("d", path.join("")); -}; - -/** - * Adjust the graph layout due to changed size - */ -Nengo.Raster.prototype.on_resize = function(width, height) { - if (width < this.minWidth) { - width = this.minWidth; - } - if (height < this.minHeight) { - height = this.minHeight; - }; - - this.axes2d.on_resize(width, height); - - this.update(); - - this.label.style.width = width; - - this.width = width; - this.height = height; - this.div.style.width = width; - this.div.style.height= height; -}; - -Nengo.Raster.prototype.reset = function(event) { - this.data_store.reset(); - this.schedule_update(); -} - -Nengo.Raster.prototype.generate_menu = function() { - var self = this; - var items = []; - items.push(['Set # neurons...', function() {self.set_neuron_count();}]); - - return $.merge(items, Nengo.Component.prototype.generate_menu.call(this)); -}; - -Nengo.Raster.prototype.set_neuron_count = function() { - var count = this.n_neurons; - var self = this; - Nengo.modal.title('Set number of neurons...'); - Nengo.modal.single_input_body(count, 'Number of neurons'); - Nengo.modal.footer('ok_cancel', function(e) { - var new_count = $('#singleInput').val(); - var modal = $('#myModalForm').data('bs.validator'); - modal.validate(); - if (modal.hasErrors() || modal.isIncomplete()) { - return; - } - if (new_count !== null) { - new_count = parseInt(new_count); - self.set_n_neurons(new_count); - self.axes2d.fit_ticks(self); - } - $('#OK').attr('data-dismiss', 'modal'); - }); - var $form = $('#myModalForm').validator({ - custom: { - my_validator: function($item) { - var num = $item.val(); - var valid = false; - if ($.isNumeric(num)) { - num = Number(num); - if (num >= 0 && Number.isInteger(num)) { - valid = true; - } - } - return valid; - } - }, - }); - - $('#singleInput').attr('data-error', 'Input should be a positive integer'); - - Nengo.modal.show(); - $('#OK').on('click', function() { - var w = $(self.div).width(); - var h = $(self.div).height(); - self.on_resize(w, h); - }) -} - diff --git a/nengo_gui/static/components/raster.ts b/nengo_gui/static/components/raster.ts new file mode 100644 index 00000000..a3dc3189 --- /dev/null +++ b/nengo_gui/static/components/raster.ts @@ -0,0 +1,158 @@ +/** + * Raster plot showing spike events over time. + * + * @constructor + * @param {DOMElement} parent - the element to add this component to + * @param {SimControl} sim - the simulation controller + * @param {dict} args - A set of constructor arguments (see Component) + * @param {int} args.n_neurons - number of neurons + * + * Raster constructor is called by python server when a user requests a plot + * or when the config file is making graphs. Server request is handled in + * netgraph.js {.on_message} function. + */ + +import * as d3 from "d3"; +import * as $ from "jquery"; + +import { VNode, dom, h } from "maquette"; + +import { DataStore } from "../datastore"; +import { InputDialogView } from "../modal"; +import { Axes, Plot, PlotView } from "./plot"; +import { Position } from "./position"; +import { registerComponent } from "./registry"; +import { Connection } from "../server"; +import * as utils from "../utils"; + +export class Raster extends Plot { + view: RasterView; + + protected _nNeurons: number; + + constructor({ + server, + uid, + label, + pos, + synapse, + nNeurons, + labelVisible = true, + xlim = [-0.5, 0], + }: { + server: Connection; + uid: string; + label: string; + pos: Position; + synapse: number; + nNeurons: number; + labelVisible?: boolean; + xlim?: [number, number]; + }) { + super( + server, + uid, + new RasterView, + label, + pos, + nNeurons, + synapse, + labelVisible, + xlim, + [0, nNeurons] + ); + this.nNeurons = nNeurons; + } + + get nNeurons(): number { + return this._nNeurons; + } + + set nNeurons(val: number) { + this._nNeurons = val; + this.ylim = [0, this.nNeurons]; + } + + addMenuItems() { + this.menu.addAction("Set number of neurons...", () => { + this.askNNeurons(); + }); + this.menu.addSeparator(); + super.addMenuItems(); + } + + askNNeurons() { + const modal = new InputDialogView( + `${this.nNeurons}`, + "Number of neurons", + "Input should be a positive integer" + ); + modal.title = "Set number of neurons..."; + modal.ok.addEventListener("click", () => { + const validator = $(modal).data("bs.validator"); + validator.validate(); + if (validator.hasErrors() || validator.isIncomplete()) { + return; + } + if (modal.input.value !== null) { + const newCount = parseInt(modal.input.value, 10); + this.nNeurons = newCount; + } + $(modal).modal("hide"); + }); + utils.handleTabs(modal); + + $(modal).validator({ + custom: { + ngvalidator: item => { + return utils.isInt(item.value); + } + } + }); + $(modal.root).on("hidden.bs.modal", () => { + document.body.removeChild(modal.root); + }); + document.body.appendChild(modal.root); + modal.show(); + } + + /** + * Redraw the lines and axis due to changed data. + */ + syncWithDataStore = utils.throttle(() => { + const [tStart, tEnd] = this.xlim; + const shownData = this.datastore.timeSlice(tStart, tEnd); + + const path = []; + if (shownData[0] != null) { + shownData.forEach(row => { + const t = this.axes.x.pixelAt(row[0]); + // TODO: figure out what this should be (what is data?) + row.slice(1).forEach(y => { + const y1 = this.axes.y.pixelAt(y); + const y2 = this.axes.y.pixelAt(y + 1); + path.push(`M ${t} ${y1}V${y2}`); + }); + }); + } + this.view.line = path.join(""); + }, 20); +} + +export class RasterView extends PlotView { + // All the lines are implemented as a single path element + path: SVGPathElement; + + constructor() { + super(); + const node = h("path.line", {stroke: this.colors[0]}); + this.path = utils.domCreateSVG(node) as SVGPathElement; + this.body.appendChild(this.path); + } + + set line(val: string) { + this.path.setAttribute("d", val); + } +} + +registerComponent("raster", Raster); diff --git a/nengo_gui/static/components/registry.ts b/nengo_gui/static/components/registry.ts new file mode 100644 index 00000000..a22ccd69 --- /dev/null +++ b/nengo_gui/static/components/registry.ts @@ -0,0 +1,14 @@ +import { Component } from "./component"; +import { Connection } from "../server"; + +type C = new (argobj) => Component; + +export const ComponentRegistry: {[name: string]: C} = {}; + +export function registerComponent(name: string, compClass: C) { + ComponentRegistry[name] = compClass; +} + +export function createComponent(name, argobj: any) { + return new ComponentRegistry[name](argobj); +} diff --git a/nengo_gui/static/components/slider.css b/nengo_gui/static/components/slider.css index 8d29c712..053d61aa 100644 --- a/nengo_gui/static/components/slider.css +++ b/nengo_gui/static/components/slider.css @@ -1,11 +1,66 @@ -.guideline{ - background-color: #ddd; - z-index: -1; - - border: 1px solid #888; - border-radius: 4px; +svg.netgraph { + + g.control { + + rect.guideline { + fill: #ddd; + pointer-events: none; + rx: 4; + rx: 4; + stroke: #888 { + width: 1; + } + } + + + g.handle { + + rect { + fill: #fff; + stroke: #666 { + width: 1; + }; + rx: 4; + ry: 4; + } + + &:hover { + cursor: pointer; + + rect { + fill: #e6e6e6; + } + } + + &.invalid { + rect { + fill: salmon; + } + } + + input { + background-color: transparent; + border: none; + cursor: text; + line-height: normal; + outline: none; + text-align: center; + } + + text { + alignment-baseline: central; + cursor: pointer; + text-anchor: middle; + } + } + } } -input#value_in_field { - cursor: text !important; + +.guideline { + background-color: #ddd; + border: 1px solid #888 { + radius: 4px; + } + z-index: -1; } diff --git a/nengo_gui/static/components/slider.js b/nengo_gui/static/components/slider.js deleted file mode 100644 index 994a4d1e..00000000 --- a/nengo_gui/static/components/slider.js +++ /dev/null @@ -1,346 +0,0 @@ -/** - * A slider object with 1+ handles to adjust Node values - * @constructor - * - * @param {DOMElement} parent - the element to add this component to - * @param {Nengo.SimControl} sim - the simulation controller - * @params {dict} args - a set of constructor arguments (see Nengo.Component) - * @params {int} args.n_sliders - the number of sliders to show - * - * Slider constructor is called by python server when a user requests a slider - * or when the config file is making sliders. Server request is handled in - * netgraph.js {.on_message} function. - */ -Nengo.Slider = function(parent, sim, args) { - Nengo.Component.call(this, parent, args); - var self = this; - this.sim = sim; - - //Check if user is filling in a number into a slider - this.filling_slider_value = false; - this.n_sliders = args.n_sliders; - - this.data_store = null; - - this.notify_msgs = []; - // TODO: get rid of the immediate parameter once the websocket delay - // fix is merged in (#160) - this.immediate_notify = true; - - this.set_axes_geometry(this.width, this.height); - - this.minHeight = 40; - - this.group = document.createElement('div'); - this.group.style.height = this.slider_height; - this.group.style.marginTop = this.ax_top; - this.group.style.whiteSpace = 'nowrap'; - this.group.position = 'relative'; - this.div.appendChild(this.group); - - /** make the sliders */ - // the value to use when releasing from user control - this.reset_value = args.start_value; - // the value to use when restarting the simulation from beginning - this.start_value = args.start_value; - - this.sliders = []; - for (var i = 0; i < args.n_sliders; i++) { - // Creating a SliderControl object for every slider handle required - var slider = new Nengo.SliderControl(args.min_value, args.max_value); - slider.container.style.width = (100 / args.n_sliders) + '%'; - slider.display_value(args.start_value[i]); - slider.index = i; - slider.fixed = false; - - slider.on('change', function(event) { - event.target.fixed = true; - self.send_value(event.target.index, event.value); - }).on('changestart', function(event) { - self.menu.hide_any(); - for (var i in this.sliders) { - if (this.sliders[i] !== event.target) { - this.sliders[i].deactivate_type_mode(); - } - } - }); - - this.group.appendChild(slider.container); - this.sliders.push(slider); - } - - /** call schedule_update whenever the time is adjusted in the SimControl */ - this.sim.div.addEventListener('adjust_time', - function(e) {self.schedule_update();}, false); - - this.sim.div.addEventListener('sim_reset', - function(e) {self.on_sim_reset();}, false); - - this.on_resize(this.get_screen_width(), this.get_screen_height()); -}; -Nengo.Slider.prototype = Object.create(Nengo.Component.prototype); -Nengo.Slider.prototype.constructor = Nengo.Slider; - -Nengo.Slider.prototype.set_axes_geometry = function(width, height) { - this.width = width; - this.height = height; - scale = parseFloat($('#main').css('font-size')); - this.border_size = 1; - this.ax_top = 1.75 * scale; - this.slider_height = this.height - this.ax_top; -}; - -Nengo.Slider.prototype.send_value = function(slider_index, value) { - console.assert(typeof slider_index == 'number'); - console.assert(typeof value == 'number'); - - if (this.immediate_notify) { - this.ws.send(slider_index + ',' + value); - } else { - this.notify(slider_index + ',' + value); - } - this.sim.time_slider.jump_to_end(); -}; - -Nengo.Slider.prototype.on_sim_reset = function(event) { - // release slider position and reset it - for (var i = 0; i < this.sliders.length; i++) { - this.notify('' + i + ',reset'); - this.sliders[i].display_value(this.start_value[i]); - this.sliders[i].fixed = false; - } -}; - -/** - * Receive new line data from the server - */ -Nengo.Slider.prototype.on_message = function(event) { - var data = new Float32Array(event.data); - if (this.data_store === null) { - this.data_store = new Nengo.DataStore(this.sliders.length, this.sim, 0); - } - this.reset_value = []; - for (var i = 0; i < this.sliders.length; i++) { - this.reset_value.push(data[i + 1]); - - if (this.sliders[i].fixed) { - data[i + 1] = this.sliders[i].value; - } - } - this.data_store.push(data); - - this.schedule_update(); -} - - -/** - * update visual display based when component is resized - */ -Nengo.Slider.prototype.on_resize = function(width, height) { - console.assert(typeof width == 'number'); - console.assert(typeof height == 'number'); - - if (width < this.minWidth) { - width = this.minWidth; - } - if (height < this.minHeight) { - height = this.minHeight; - }; - - this.set_axes_geometry(width, height); - - this.group.style.height = height - this.ax_top - 2 * this.border_size; - this.group.style.marginTop = this.ax_top; - - var N = this.sliders.length; - for (var i in this.sliders) { - this.sliders[i].on_resize(); - } - - this.label.style.width = this.width; - this.div.style.width = this.width; - this.div.style.height= this.height; -}; - - -Nengo.Slider.prototype.generate_menu = function() { - var self = this; - var items = []; - items.push(['Set range...', function() {self.set_range();}]); - items.push(['Set value...', function() {self.user_value();}]); - items.push(['Reset value', function() {self.user_reset_value();}]); - - // add the parent's menu items to this - // TODO: is this really the best way to call the parent's generate_menu()? - return $.merge(items, Nengo.Component.prototype.generate_menu.call(this)); -}; - -/** report an event back to the server */ -Nengo.Slider.prototype.notify = function(info) { - this.notify_msgs.push(info); - - // only send one message at a time - // TODO: find a better way to figure out when it's safe to send - // another message, rather than just waiting 1ms.... - if (this.notify_msgs.length == 1) { - var self = this; - window.setTimeout(function() { - self.send_notify_msg(); - }, 50); - } -} - -/** send exactly one message back to server - * and schedule the next message to be sent, if any - */ -Nengo.Slider.prototype.send_notify_msg = function() { - msg = this.notify_msgs[0]; - this.ws.send(msg); - if (this.notify_msgs.length > 1) { - var self = this; - window.setTimeout(function() { - self.send_notify_msg(); - }, 50); - } - this.notify_msgs.splice(0, 1); -} - -Nengo.Slider.prototype.update = function() { - /** let the data store clear out old values */ - if (this.data_store !== null) { - this.data_store.update(); - - var data = this.data_store.get_last_data(); - - for (var i=0; i< this.sliders.length; i++) { - if (!this.data_store.is_at_end() || !this.sliders[i].fixed) { - this.sliders[i].display_value(data[i]); - } - } - } -} - -Nengo.Slider.prototype.user_value = function () { - var self = this; - - //First build the prompt string - var prompt_string = ''; - for (var i = 0; i < this.sliders.length; i++){ - prompt_string = prompt_string + this.sliders[i].value.toFixed(2); - if (i != this.sliders.length - 1) { - prompt_string = prompt_string + ", "; - } - } - Nengo.modal.title('Set slider value(s)...'); - Nengo.modal.single_input_body(prompt_string, 'New value(s)'); - Nengo.modal.footer('ok_cancel', function(e) { - var new_value = $('#singleInput').val(); - var modal = $('#myModalForm').data('bs.validator'); - - modal.validate(); - if (modal.hasErrors() || modal.isIncomplete()) { - return; - } - self.immediate_notify = false; - if (new_value !== null) { - new_value = new_value.split(','); - //Update the sliders one at a time - for (var i = 0; i < self.sliders.length; i++){ - self.sliders[i].fixed = true; - self.sliders[i].set_value(parseFloat(new_value[i])); - } - } - self.immediate_notify = true; - $('#OK').attr('data-dismiss', 'modal'); - }); - - var $form = $('#myModalForm').validator({ - custom: { - my_validator: function($item) { - var nums = $item.val().split(','); - if (nums.length != self.sliders.length) { - return false; - } - for (var i=0; i,".'); - Nengo.modal.show(); -} - -Nengo.Slider.prototype.layout_info = function () { - var info = Nengo.Component.prototype.layout_info.call(this); - info.width = info.width; - info.min_value = this.sliders[0].scale.domain()[1]; - info.max_value = this.sliders[0].scale.domain()[0]; - return info; -}; - -Nengo.Slider.prototype.update_layout = function (config) { - //FIXME: this has to be backwards to work. Something fishy must be going on - for (var i in this.sliders) { - this.sliders[i].set_range(config.min_value, config.max_value); - } - Nengo.Component.prototype.update_layout.call(this, config); -} \ No newline at end of file diff --git a/nengo_gui/static/components/slider.ts b/nengo_gui/static/components/slider.ts new file mode 100644 index 00000000..1111012f --- /dev/null +++ b/nengo_gui/static/components/slider.ts @@ -0,0 +1,550 @@ +/** + * A slider object with 1+ handles to adjust Node values + * + * @constructor + * @param {DOMElement} parent - the element to add this component to + * @param {SimControl} sim - the simulation controller + * @param {dict} args - a set of constructor arguments (see Component) + * @param {int} args.n_sliders - the number of sliders to show + * + * Slider constructor is called by python server when a user requests a slider + * or when the config file is making sliders. Server request is handled in + * netgraph.js {.on_message} function. + */ + +import * as d3 from "d3"; +import * as $ from "jquery"; +import * as interact from "interactjs"; +import { dom, h, VNode } from "maquette"; + +import "./slider.css"; + +import { ComponentView } from "./component"; +import { Menu } from "../menu"; +import * as utils from "../utils"; +import { InputDialogView } from "../modal"; +import { ValueView } from "./value"; +import { Position } from "./position"; +import { registerComponent } from "./registry"; +import { Connection } from "../server"; +import { Widget } from "./widget"; + +export class Slider extends Widget { + // The value to use when releasing from user control + lastReceived: number[]; + sliderHeight: number; + // The value to use when restarting the simulation from beginning + startValue: number[]; + userValue: number[] = []; + view: SliderView; + + constructor({ + server, + uid, + label, + pos, + dimensions, + synapse, + labelVisible = true, + startValue = [], + lim = [-1, 1] + }: { + server: Connection; + uid: string; + label: string; + pos: Position; + dimensions: number; + synapse: number; + labelVisible?: boolean; + startValue?: number[]; + lim?: [number, number]; + }) { + super( + server, + uid, + new SliderView(dimensions), + label, + pos, + dimensions, + synapse, + labelVisible + ); + this.view.lim = lim; + while (startValue.length < dimensions) { + startValue.push(0); + } + + this.startValue = startValue; + this.lastReceived = startValue.slice(); + for (let i = 0; i < startValue.length; i++) { + this.userValue.push(NaN); + // Set before installing the change handler + this.view.controls[i].value = startValue[i]; + } + + window.addEventListener("SimControl.reset", e => { + this.reset(); + }); + + this.fastServer.bind((data: ArrayBuffer) => { + this.add(new Float64Array(data)); + }); + + // Set up events for each slider control + this.view.controls.forEach((control, i) => { + const interactHandle = interact(control.handle); + interactHandle.draggable({}); + interactHandle.on("tap", event => { + control.enableManualEntry(); + }); + interactHandle.on("dragmove", event => { + let [_, py] = utils.getTranslate(control.handle); + py += event.dy; + control.value = control.toValue(py); + control.root.dispatchEvent(new Event("slidercontrol.changed")); + }); + + control.root.addEventListener("slidercontrol.changed", event => { + this.userValue[i] = control.value; + this.fastServer.send(Float64Array.from(this.userValue)); + }); + }); + } + + addMenuItems() { + this.menu.addAction("Set range...", () => { + this.askLim(); + }); + this.menu.addAction("Set value...", () => { + this.askValue(); + }); + this.menu.addAction("Reset value", () => { + this.forgetUserChanges(); + }); + this.menu.addSeparator(); + super.addMenuItems(); + } + + askLim() { + const lim = this.view.lim; + const modal = new InputDialogView( + `[${lim[0]}, ${lim[1]}]`, + "New range", + "Input should be in the form ','." + ); + modal.title = "Set slider range..."; + modal.ok.addEventListener("click", () => { + const validator = $(modal).data("bs.validator"); + validator.validate(); + if (validator.hasErrors() || validator.isIncomplete()) { + return; + } + if (modal.input.value !== null) { + const newLim = modal.input.value.split(","); + console.assert(newLim.length === 2); + this.view.lim = [parseFloat(newLim[0]), parseFloat(newLim[1])]; + } + $(modal).modal("hide"); + }); + utils.handleTabs(modal); + + $(modal).validator({ + custom: { + ngvalidator: item => { + const nums = item.value.split(","); + let valid = false; + if (utils.isNum(nums[0]) && utils.isNum(nums[1])) { + if (Number(nums[0]) < Number(nums[1])) { + // Two numbers, 1st less than 2nd + valid = true; + } + } + return nums.length === 2 && valid; + } + } + }); + $(modal.root).on("hidden.bs.modal", () => { + document.body.removeChild(modal.root); + }); + document.body.appendChild(modal.root); + modal.show(); + } + + askValue() { + const plural = this.dimensions > 1 ? "s" : ""; + const prompt = this.view.controls + .map(control => control.value.toFixed(2)) + .join(", "); + const modal = new InputDialogView( + prompt, + `New value${plural}`, + "Input should be one value for each slider, separated by commas." + ); + modal.title = `Set slider value${plural}...`; + modal.ok.addEventListener("click", () => { + const validator = $(modal).data("bs.validator"); + validator.validate(); + if (validator.hasErrors() || validator.isIncomplete()) { + return; + } + if (modal.input.value !== null) { + const newValue = modal.input.value.split(",").map(parseFloat); + // Update the sliders one at a time + this.view.controls.forEach((control, i) => { + this.userValue[i] = newValue[i]; + control.value = newValue[i]; + control.root.dispatchEvent(new Event("slidercontrol.changed")); + }); + } + $(modal).modal("hide"); + }); + utils.handleTabs(modal); + + $(modal).validator({ + custom: { + ngvalidator: item => { + const nums = item.value.split(","); + return ( + nums.length === this.view.controls.length && + nums.every(num => { + return utils.isNum(num); + }) + ); + } + } + }); + $(modal.root).on("hidden.bs.modal", () => { + document.body.removeChild(modal.root); + }); + document.body.appendChild(modal.root); + modal.show(); + } + + forgetUserChanges() { + this.view.controls.forEach((control, i) => { + control.value = this.lastReceived[i]; + this.userValue[i] = NaN; + }); + } + + reset() { + super.reset(); + this.view.controls.forEach((control, i) => { + control.value = this.lastReceived[i]; + }); + } + + syncWithDataStore = utils.throttle(() => { + const data = this.datastore.data[this.datastore.data.length - 1].slice(1); + this.lastReceived = data; + for (let i = 0; i < this.lastReceived.length; i++) { + if (isNaN(this.userValue[i])) { + this.view.controls[i].value = data[i]; + } + } + }, 20); +} + +export class SliderView extends ComponentView { + static padAround = 4; + static padBetween = 6; + + controls: SliderControlView[] = []; + group: HTMLDivElement; + + constructor(nControls: number) { + super(); + + const node = h("g.slider"); + this.body = utils.domCreateSVG(node) as SVGGElement; + this.root.appendChild(this.body); + + for (let i = 0; i < nControls; i++) { + this.addSliderControl(); + } + } + + get lim(): [number, number] { + return this.controls[0].lim; + } + + set lim(val: [number, number]) { + this.controls.forEach(control => { + control.lim = val; + }); + } + + get scale(): [number, number] { + return this.overlayScale; + } + + set scale(val: [number, number]) { + const [width, height] = val; + const totalPadding = + (this.controls.length - 1) * SliderView.padBetween + + SliderView.padAround * 2; + const eachWidth = Math.max( + (width - totalPadding) / this.controls.length, + 0 + ); + + this.controls.forEach((control, i) => { + control.scale = [eachWidth, height]; + control.pos = [ + SliderView.padAround + (eachWidth + SliderView.padBetween) * i, + 0 + ]; + }); + this.overlayScale = [width, height]; + } + + addSliderControl() { + const control = new SliderControlView(); + this.controls.push(control); + this.body.appendChild(control.root); + // Resize + this.scale = this.scale; + } + + removeSliderControl(index: number = null) { + if (index == null) { + index = this.controls.length - 1; + } + this.body.removeChild(this.controls[index].root); + this.controls.splice(index, 1); + // Resize + this.scale = this.scale; + } + + ondomadd() { + super.ondomadd(); + // Usually overlay goes on top, but for interaction controls go on top + this.root.appendChild(this.body); + } +} + +/** + * A SliderControl object which creates a single guideline + handle within + * a slider object. + * + * @constructor + * @param {int} min - The minimum value the handle can take + * @param {int} max - the maximum value the handle can take + * + * SliderControl is called within the Slider constructor for each + * handle that is needed. + */ +export class SliderControlView { + static guidelineWidth = 7; + static handlePad = 2; + + guideline: SVGRectElement; + handle: SVGGElement; + handleRect: SVGGElement; + handleText: SVGGElement; + manualEntry: boolean = false; + root: SVGGElement; + + private _lim: [number, number] = [0, 0]; + private _value: number = 0; + + constructor() { + const node = h("g.control", { transform: "translate(0,0)" }, [ + h("rect.guideline", { + height: "0", + width: `${SliderControlView.guidelineWidth}`, + x: "0" + }), + h("g.handle", { transform: "translate(0,0)" }, [ + h("rect", { + height: "0", + width: "0" + }), + h("text", { x: "0", y: "0" }, ["0.00"]) + ]) + ]); + + this.root = utils.domCreateSVG(node) as SVGGElement; + this.guideline = this.root.querySelector( + ".guideline" + ) as SVGRectElement; + this.handle = this.root.querySelector(".handle") as SVGGElement; + this.handleRect = this.handle.querySelector("rect") as SVGRectElement; + this.handleText = this.handle.querySelector("text") as SVGTextElement; + } + + get lim(): [number, number] { + return this._lim; + } + + set lim(val: [number, number]) { + this._lim = val; + this.update(); + } + + get pos(): [number, number] { + return utils.getTranslate(this.root); + } + + set pos(val: [number, number]) { + utils.setTranslate(this.root, val[0], val[1]); + } + + get scale(): [number, number] { + return [ + Number(this.handleRect.getAttribute("width")), + Number(this.guideline.getAttribute("height")) + ]; + } + + set scale(val: [number, number]) { + this.guideline.setAttribute("height", `${val[1]}`); + this.guideline.setAttribute( + "x", + `${(val[0] - SliderControlView.guidelineWidth) * 0.5}` + ); + + const textHeight = this.handleText.getBBox().height; + this.handleRect.setAttribute("width", `${val[0]}`); + this.handleText.setAttribute("x", `${val[0] * 0.5}`); + this.handleText.setAttribute( + "y", + `${SliderControlView.handlePad + textHeight * 0.5}` + ); + this.handleRect.setAttribute( + "height", + `${textHeight + SliderControlView.handlePad * 2}` + ); + this.update(); + } + + get value(): number { + return this._value; + } + + set value(val: number) { + val = utils.clip(val, this._lim[0], this._lim[1]); + if (val !== this._value) { + this._value = val; + this.handleText.textContent = this._value.toFixed(2); + this.update(); + } + } + + enableManualEntry() { + if (this.manualEntry) { + console.warn("Manual entry is already enabled"); + return; + } + this.manualEntry = true; + + // Hide SVG text + this.handleText.setAttribute("visibility", "hidden"); + + // Render an HTML input inside the handle + const foreign = utils.domCreateSVG(h("foreignObject")); + const form = h( + "body", + { + xmlns: "http://www.w3.org/1999/xhtml" + }, + [ + h("input", { + styles: { + fontSize: window.getComputedStyle(this.handleText) + .fontSize, + width: `${this.handleRect.getAttribute("width")}px` + }, + type: "text" + }) + ] + ); + foreign.appendChild(dom.create(form).domNode); + this.handle.appendChild(foreign); + + const disable = () => { + if (!this.manualEntry) { + console.warn("Disable called twice"); + return; + } + this.manualEntry = false; + this.handle.classList.remove("invalid"); + // Remove HTML input inside the handle + this.handle.removeChild(this.handle.lastChild); + // Show SVG text again + this.handleText.setAttribute("visibility", null); + }; + + const input = this.handle.querySelector("input") as HTMLInputElement; + input.value = this.value.toFixed(2); + input.focus(); + input.select(); + input.addEventListener("input", () => { + if (this.isValid(input.value)) { + this.handle.classList.remove("invalid"); + } else { + this.handle.classList.add("invalid"); + } + }); + input.addEventListener("keydown", event => { + const enterKey = 13; + const escKey = 27; + event.stopPropagation(); + if (event.which === enterKey) { + event.preventDefault(); + if (this.isValid(input.value)) { + this.value = parseFloat(input.value); + this.root.dispatchEvent(new Event("slidercontrol.changed")); + disable(); + } + } else if (event.which === escKey) { + disable(); + } + }); + input.addEventListener("blur", event => { + // TODO: Should we set the value when blurring? I think yes... + if (this.isValid(input.value)) { + this.value = parseFloat(input.value); + this.root.dispatchEvent(new Event("slidercontrol.changed")); + } + if (this.manualEntry) { + disable(); + } + }); + } + + isValid(val: number | string) { + if (typeof val === "string") { + val = parseFloat(val); + } + return val >= this._lim[0] && val <= this._lim[1]; + } + + toPixels(val: number) { + const [vMin, vMax] = this._lim; + const pRange = + Number(this.guideline.getAttribute("height")) - + Number(this.handleRect.getAttribute("height")); + + const vRange = vMax - vMin; + return pRange - (val - vMin) / vRange * pRange; + } + + toValue(pixels: number) { + const [vMin, vMax] = this._lim; + const pRange = + Number(this.guideline.getAttribute("height")) - + Number(this.handleRect.getAttribute("height")); + + return (pRange - pixels) / pRange * (vMax - vMin) + vMin; + } + + private update = utils.throttle(() => { + const pixels = this.toPixels(this._value); + if (!isNaN(pixels)) { + utils.setTranslate(this.handle, 0, this.toPixels(this._value)); + } + }, 10); +} + +registerComponent("slider", Slider); diff --git a/nengo_gui/static/components/slidercontrol.js b/nengo_gui/static/components/slidercontrol.js deleted file mode 100644 index 5b3d270c..00000000 --- a/nengo_gui/static/components/slidercontrol.js +++ /dev/null @@ -1,211 +0,0 @@ -/** - * A SliderControl object which creates a single guideline + handle within - * a slider object - * @constructor - * - * @params {int} min - The minimum value the handle can take - * @params {int} max - the maximum value the handle can take - * - * SliderControl is called within the Slider constructor for each - * handle that is needed. - */ -Nengo.SliderControl = function(min, max) { - var self = this; - - this.min = min; - this.max = max; - - this.value = 0 - this.type_mode = false; - - this.border_width = 1 - - this.scale = d3.scale.linear(); - this.scale.domain([max, min]); - - // TODO move CSS to CSS file - this.container = document.createElement('div'); - this.container.style.display = 'inline-block'; - this.container.style.position = 'relative'; - this.container.style.height = '100%'; - this.container.style.padding = '0.75em 0'; - - this.guideline = document.createElement('div'); - this.guideline.classList.add('guideline'); - this.guideline.style.width = '0.5em'; - this.guideline.style.height = '100%'; - this.guideline.style.margin = 'auto'; - $(this.guideline).on('mousedown', function(event) { - self.set_value(self.value); - }) - this.container.appendChild(this.guideline); - - - this.handle = document.createElement('div'); - this.handle.classList.add('btn'); - this.handle.classList.add('btn-default'); - this.handle.innerHTML = 'n/a'; - this.handle.style.position = 'absolute'; - this.handle.style.height = '1.5em'; - this.handle.style.marginTop = '0.75em'; - this.handle.style.width = '95%'; - this.handle.style.fontSize = 'inherit'; - this.handle.style.padding = '0.1em 0'; - this.handle.style.borderWidth = this.border_width + 'px'; - this.handle.style.borderColor = '#666'; - this.handle.style.left = '2.5%'; - this.handle.style.transform = 'translate(0, -50%)'; - this.update_handle_pos(0); - this.container.appendChild(this.handle); - - interact(this.handle) - .draggable({ - onstart: function () { - self.dispatch('changestart', {'target': this}); - self.deactivate_type_mode(); - self._drag_y = self.get_handle_pos(); - }, - onmove: function (event) { - var target = event.target; - self._drag_y += event.dy; - - self.scale.range([0, self.guideline.clientHeight]); - self.set_value(self.scale.invert(self._drag_y)) - }, - onend: function (event) { - self.dispatch('changeend', {'target': this}); - } - }); - - interact(this.handle) - .on('tap', function(event) { - self.activate_type_mode(); - event.stopPropagation(); - }).on('keydown', function(event) { self.handle_keypress(event); }); - - this.listeners = {}; -}; - -Nengo.SliderControl.prototype.on = function(type, fn) { - this.listeners[type] = fn; - return this; -} - -Nengo.SliderControl.prototype.dispatch = function(type, ev) { - if (type in this.listeners) { - this.listeners[type].call(this, ev); - } -} - -Nengo.SliderControl.prototype.set_range = function(min, max) { - this.min = min; - this.max = max; - this.scale.domain([max, min]); - this.set_value(this.value); - this.on_resize(); -}; - -Nengo.SliderControl.prototype.display_value = function(value) { - if (value < this.min) { - value = this.min; - } - if (value > this.max) { - value = this.max; - } - - this.value = value; - - this.update_handle_pos(value); - this.update_value_text(value); -} - -Nengo.SliderControl.prototype.set_value = function(value) { - var old_value = this.value; - this.display_value(value); - this.dispatch('change', {'target': this, 'value': this.value}); -}; - -Nengo.SliderControl.prototype.activate_type_mode = function() { - if (this.type_mode) { - return; - } - - var self = this; - - this.dispatch('changestart', {'target': this}); - - this.type_mode = true; - - this.handle.innerHTML = ''; - var elem = this.handle.querySelector('#value_in_field') - elem.value = this.format_value(this.value); - elem.focus(); - elem.select(); - elem.style.width = '100%'; - elem.style.textAlign = 'center'; - elem.style.backgroundColor = 'transparent'; - $(elem).on('input', function (event) { - if (Nengo.is_num(elem.value)) { - self.handle.style.backgroundColor = ''; - } else { - self.handle.style.backgroundColor = 'salmon'; - } - }).on('blur', function (event) { - self.deactivate_type_mode(); - }); -}; - -Nengo.SliderControl.prototype.deactivate_type_mode = function(event) { - if (!this.type_mode) { - return; - } - - this.dispatch('changeend', {'target': this}); - - this.type_mode = false; - - $(this.handle).off('keydown'); - this.handle.style.backgroundColor = ''; - this.handle.innerHTML = this.format_value(this.value); -}; - -Nengo.SliderControl.prototype.handle_keypress = function(event) { - if (!this.type_mode) { - return; - } - - var enter_keycode = 13; - var esc_keycode = 27; - var key = event.which; - - if (key == enter_keycode) { - var input = this.handle.querySelector('#value_in_field').value; - if (Nengo.is_num(input)) { - this.deactivate_type_mode(); - this.set_value(parseFloat(input)); - } - } else if (key == esc_keycode) { - this.deactivate_type_mode(); - } -}; - -Nengo.SliderControl.prototype.update_handle_pos = function(value) { - this.handle.style.top = this.scale(value) + this.border_width; -}; - -Nengo.SliderControl.prototype.get_handle_pos = function() { - return parseFloat(this.handle.style.top) - this.border_width; -}; - -Nengo.SliderControl.prototype.update_value_text = function(value) { - this.handle.innerHTML = this.format_value(value); -}; - -Nengo.SliderControl.prototype.format_value = function(value) { - return value.toFixed(2); -} - -Nengo.SliderControl.prototype.on_resize = function() { - this.scale.range([0, this.guideline.clientHeight]); - this.update_handle_pos(this.value); -}; diff --git a/nengo_gui/static/components/spa.ts b/nengo_gui/static/components/spa.ts new file mode 100644 index 00000000..c7b2ae77 --- /dev/null +++ b/nengo_gui/static/components/spa.ts @@ -0,0 +1,355 @@ +/** + * Decoded semantic pointer display. + * + * @constructor + * @param {DOMElement} parent - the element to add this component to + * @param {SimControl} sim - the simulation controller + * @param {dict} args - A set of constructor arguments (see Component) + * + * Pointer constructor is called by python server when a user requests a plot + * or when the config file is making graphs. Server request is handled in + * netgraph.js {.on_message} function. + */ + +import * as $ from "jquery"; +import * as d3 from "d3"; +import { VNode, dom, h } from "maquette"; + +import { Component, ComponentView } from "./component"; +import { DataStore } from "../datastore"; +import { Menu } from "../menu"; +import { InputDialogView } from "../modal"; +import { Position } from "./position"; +import { registerComponent } from "./registry"; +import { Connection } from "../server"; +import * as utils from "../utils"; +import { Value } from "./value"; +import { Widget } from "./widget"; + +export class SpaPointer extends Widget { + view: SpaPointerView; + + protected _fixedValue: string = null; + protected _showPairs: boolean; + + constructor({ + server, + uid, + label, + pos, + dimensions, + synapse, + labelVisible = true, + showPairs = false + }: { + server: Connection; + uid: string; + label: string; + pos: Position; + dimensions: number; + synapse: number; + labelVisible?: boolean; + showPairs?: boolean; + }) { + super( + server, + uid, + new SpaPointerView(), + label, + pos, + dimensions, + synapse, + labelVisible + ); + // TOOD: set this.numItems + this.showPairs = showPairs; + } + + get fixedValue(): string | null { + return this._fixedValue; + } + + set fixedValue(val: string | null) { + this._fixedValue = val; + this.syncWithDataStore; + } + + get showPairs(): boolean { + return this._showPairs; + } + + set showPairs(val: boolean) { + if (this._showPairs !== val) { + this._showPairs = val; + // Notify server? + // this.saveLayout(); + } + } + + addMenuItems() { + this.menu.addAction("Set pointer...", () => { + this.askValue(); + }); + this.menu.addAction( + "Hide pairs", + () => { + this.showPairs = false; + }, + () => this._showPairs + ); + this.menu.addAction( + "Show pairs", + () => { + this.showPairs = true; + }, + () => !this._showPairs + ); + this.menu.addSeparator(); + super.addMenuItems(); + } + + // TODO: handle bad pointer errors (?) + + askValue() { + const modal = new InputDialogView( + "Pointer", + "New value", + "Invalid semantic pointer expression. " + + "Semantic pointers must start with a capital letter. " + + "Expressions can include mathematical operators such as +, " + + "* (circular convolution), and ~ (pseudo-inverse). E.g., " + + "(A + ~(B * C) * 2) * 0.5 would be a valid semantic pointer " + + "expression." + ); + modal.title = "Enter a Semantic Pointer ..."; + modal.ok.addEventListener("click", () => { + const validator = $(modal).data("bs.validator"); + validator.validate(); + if (validator.hasErrors() || validator.isIncomplete()) { + return; + } + + let value = modal.input.value; + if (value === null || value === "") { + value = ":empty:"; + } + this.fixedValue = value; + // this.ws.send(value); + $(modal).modal("hide"); + }); + utils.handleTabs(modal); + + $(modal).validator({ + custom: { + ngvalidator: item => { + let ptr = item.value; + if (ptr === null) { + ptr = ""; + } + // this.ws.send(":check only:" + ptr); + } + } + }); + $(modal.root).on("hidden.bs.modal", () => { + document.body.removeChild(modal.root); + }); + document.body.appendChild(modal.root); + modal.show(); + } + + /** + * Redraw the lines and axis due to changed data. + */ + syncWithDataStore = utils.throttle(() => { + const data = this.datastore.at(this.currentTime); + if (data != null) { + this.view.values = data; + } + }, 20); +} + +export class SpaPointerView extends ComponentView { + root: SVGGElement; + + private _items: Array = []; + private _values: Array; + + constructor() { + super(); + const node = h("g.widget"); + this.root = utils.domCreateSVG(node) as SVGGElement; + } + + get labels(): Array { + return this._items.map(item => item.textContent); + } + + set labels(val: Array) { + console.assert(val.length === this.numItems); + this._items.forEach((item, i) => { + item.textContent = val[i]; + }); + } + + get numItems(): number { + return this._items.length; + } + + set numItems(val: number) { + while (this._items.length - val < 0) { + this.addItem(); + } + while (this._items.length - val > 0) { + this.removeItem(); + } + } + + get scale(): [number, number] { + return this.overlayScale; + } + + set scale(val: [number, number]) { + const [width, height] = val; + this.overlayScale = [width, height]; + } + + get values(): Array { + return this._values; + } + + set values(val: Array) { + console.assert(val.length === this.numItems); + const height = this.scale[1]; + const total = val.reduce((a, b) => a + b, 0); + + let y = 0; + this._items.forEach((item, i) => { + item.setAttribute("y", `${y}`); + + const hex = utils.clip(val[i] * 255, 0, 255); + item.setAttribute("stroke", `rgb(${hex},${hex},${hex})`); + + const itemHeight = val[i] / total * height; + item.setAttribute("font-size", `${itemHeight}`); + y += itemHeight; + }); + + // Keep these around so we resize + this._values = val; + } + + private addItem() { + const width = this.scale[0]; + const i = this._items.length; + const node = h("text.pointer", { + "font-size": "12", + stroke: "rgb(255, 255, 255)", + x: `${width * 0.5}`, + y: `${i * 12}` + }); + const item = utils.domCreateSVG(node) as SVGGElement; + this.root.appendChild(item); + this._items.push(item); + } + + private removeItem() { + const item = this._items.pop(); + if (item != null) { + this.root.removeChild(item); + } + } +} + +registerComponent("spa_pointer", SpaPointer); + +/** + * Line graph showing semantic pointer decoded values over time. + * + * @constructor + * @param {DOMElement} parent - the element to add this component to + * @param {SimControl} sim - the simulation controller + * @param {dict} args - A set of constructor arguments (see Component) + * @param {int} args.nLines - number of decoded values + */ + +export class SpaSimilarity extends Value { + constructor({ + server, + uid, + label, + pos, + dimensions, + synapse, + labelVisible = true, + xlim = [-0.5, 0], + ylim = [-1, 1] + }: { + server: Connection; + uid: string; + label: string; + pos: Position; + dimensions: number; + synapse: number; + labelVisible?: boolean; + xlim?: [number, number]; + ylim?: [number, number]; + }) { + super({ + server, + uid, + label, + pos, + dimensions, + synapse, + labelVisible, + xlim, + ylim + }); + this.view.legend.valuesVisible = true; + } + + // Copy from Pointer + // set showPairs(value) { + // if (this._showPairs !== value) { + // this._showPairs = value; + // this.saveLayout(); + // this.ws.send(value); + // } + // } + + resetLegendAndData(newLabels) { + // Clear the database and create a new one since dimensions have changed + this.datastore.reset(); + this.datastore.dims = newLabels.length; + this.view.legend.numLabels = newLabels.length; + this.legendLabels = newLabels; + } + + /** + * Handle websocket messages. + * + * There are three types of messages that can be received: + * - a legend needs to be updated + * - the data has been updated + * - showPairs has been toggled + * This calls the method associated to handling the type of message. + */ + // onMessage(event) { + // const data = JSON.parse(event.data); + // const funcName = data.shift(); + // this[funcName](data); + // } + + addMenuItems() { + // this.menu.addAction("Hide pairs", () => { + // this.showPairs = false; + // }, () => this.showPairs); + // this.menu.addAction("Show pairs", () => { + // this.showPairs = true; + // }, () => !this.showPairs); + this.menu.addSeparator(); + super.addMenuItems(); + } +} + +registerComponent("spa_similarity", SpaSimilarity); diff --git a/nengo_gui/static/components/spa_similarity.css b/nengo_gui/static/components/spa_similarity.css deleted file mode 100644 index 5fcbe139..00000000 --- a/nengo_gui/static/components/spa_similarity.css +++ /dev/null @@ -1,12 +0,0 @@ -.graph .line { - fill: none; - stroke: black; - stroke-width: 1.5px; -} - -.legend { - padding: 5px; - font: 10px sans-serif; - position: absolute; - display: inline; -} diff --git a/nengo_gui/static/components/spa_similarity.js b/nengo_gui/static/components/spa_similarity.js deleted file mode 100644 index 16a78f51..00000000 --- a/nengo_gui/static/components/spa_similarity.js +++ /dev/null @@ -1,223 +0,0 @@ -/** - * Line graph showing semantic pointer decoded values over time - * @constructor - * - * @param {DOMElement} parent - the element to add this component to - * @param {Nengo.SimControl} sim - the simulation controller - * @param {dict} args - A set of constructor arguments (see Nengo.Component) - * @param {int} args.n_lines - number of decoded values - */ - -Nengo.SpaSimilarity = function(parent, sim, args) { - Nengo.Value.call(this, parent, sim, args); - - this.synapse = args.synapse; - this.data_store = new Nengo.GrowableDataStore(this.n_lines, this.sim, this.synapse); - this.show_pairs = false; - - var self = this; - - this.colors = Nengo.make_colors(6); - this.color_func = function(d, i) {return self.colors[i % 6]}; - - this.line.defined(function(d) { return !isNaN(d)}); - - // create the legend from label args - this.legend_labels = args.pointer_labels; - this.legend = document.createElement('div'); - this.legend.classList.add('legend', 'unselectable'); - this.div.appendChild(this.legend); - this.legend_svg = Nengo.draw_legend(this.legend, args.pointer_labels, this.color_func, this.uid); -}; - -Nengo.SpaSimilarity.prototype = Object.create(Nengo.Value.prototype); -Nengo.SpaSimilarity.prototype.constructor = Nengo.SpaSimilarity; - - -Nengo.SpaSimilarity.prototype.reset_legend_and_data = function(new_labels){ - // clear the database and create a new one since the dimensions have changed - this.data_store = new Nengo.GrowableDataStore(new_labels.length, this.sim, this.synapse); - - // delete the legend's children - while(this.legend.lastChild){ - this.legend.removeChild(this.legend.lastChild); - } - this.legend_svg = d3.select(this.legend).append("svg").attr("id", "legend"+this.uid); - - // redraw all the legends if they exist - this.legend_labels = []; - if(new_labels[0] != ""){ - this.update_legend(new_labels); - } - - this.update(); - -} - -Nengo.SpaSimilarity.prototype.data_msg = function(push_data){ - - var data_dims = push_data.length - 1; - - // TODO: Move this check inside datastore? - if(data_dims > this.data_store.dims){ - this.data_store.dims = data_dims; - this.n_lines = data_dims; - } - - this.data_store.push(push_data); - this.schedule_update(); -}; - -Nengo.SpaSimilarity.prototype.update_legend = function(new_labels){ - - var self = this; - this.legend_labels = this.legend_labels.concat(new_labels); - - // expand the height of the svg, where "20" is around the height of the font - this.legend_svg.attr("height", 20 * this.legend_labels.length); - - - // Data join - var recs = this.legend_svg.selectAll("rect").data(this.legend_labels); - var legend_labels = this.legend_svg.selectAll(".legend-label").data(this.legend_labels); - var val_texts = this.legend_svg.selectAll(".val").data(this.legend_labels); - // enter to append remaining lines - recs.enter() - .append("rect") - .attr("x", 0) - .attr("y", function(d, i){ return i * 20;}) - .attr("width", 10) - .attr("height", 10) - .style("fill", this.color_func); - - legend_labels.enter().append("text") - .attr("x", 15) - .attr("y", function(d, i){ return i * 20 + 9;}) - .attr("class", "legend-label") - .html(function(d, i) { - return self.legend_labels[i]; - }); - - // expand the width of the svg of the longest string - var label_list = $("#legend"+this.uid+" .legend-label").toArray(); - var longest_label = Math.max.apply(Math, label_list.map(function(o){return o.getBBox().width;})); - // "50" is for the similarity measure that is around three characters wide - var svg_right_edge = longest_label + 50; - this.legend_svg.attr("width", svg_right_edge); - - val_texts.attr("x", svg_right_edge) - .attr("y", function(d, i){ return i * 20 + 9;}); - val_texts.enter().append("text") - .attr("x", svg_right_edge) - .attr("y", function(d, i){ return i * 20 + 9;}) - .attr("text-anchor","end") - .attr("class", "val"); -}; - -/* there are three types of messages that can be received: - - a legend needs to be updated - - the data has been updated - - show_pairs has been toggled - this calls the method associated to handling the type of message -*/ -Nengo.SpaSimilarity.prototype.on_message = function(event) { - var data = JSON.parse(event.data); - var func_name = data.shift(); - this[func_name](data); -}; - -/** - * Redraw the lines and axis due to changed data - */ -Nengo.SpaSimilarity.prototype.update = function() { - /** let the data store clear out old values */ - this.data_store.update(); - - /** determine visible range from the Nengo.SimControl */ - var t1 = this.sim.time_slider.first_shown_time; - var t2 = t1 + this.sim.time_slider.shown_time; - - this.axes2d.set_time_range(t1, t2); - - /** update the lines */ - var self = this; - var shown_data = this.data_store.get_shown_data(); - // Data join - this.path = this.axes2d.svg.selectAll(".line").data(shown_data); - // update - this.path.attr('d', self.line); - // enter to append remaining lines - this.path.enter() - .append('path') - .attr('class', 'line') - .style('stroke', this.color_func) - .attr('d', self.line); - // remove any lines that aren't needed anymore - this.path.exit().remove(); - - /* update the legend text */ - if(this.legend_svg && shown_data[0].length !== 0){ - // get the most recent similarity - var latest_simi = []; - for(var i = 0; i < shown_data.length; i++){ - latest_simi.push(shown_data[i][shown_data[i].length - 1]); - } - - // update the text in the legend - var texts = this.legend_svg.selectAll(".val").data(this.legend_labels); - - texts.html(function(d, i) { - var sign = ''; - if(latest_simi[i] < 0){ - sign = "−"; - } - return sign + Math.abs(latest_simi[i]).toFixed(2); - }); - } - -}; - -Nengo.SpaSimilarity.prototype.generate_menu = function() { - var self = this; - var items = []; - items.push(['Set range...', function() {self.set_range();}]); - - if (this.show_pairs) { - items.push(['Hide pairs', function() {self.set_show_pairs(false);}]); - } else { - items.push(['Show pairs', function() {self.set_show_pairs(true);}]); - } - - // add the parent's menu items to this - return $.merge(items, Nengo.Component.prototype.generate_menu.call(this)); -}; - -Nengo.SpaSimilarity.prototype.set_show_pairs = function(value) { - if (this.show_pairs !== value) { - this.show_pairs = value; - this.save_layout(); - this.ws.send(value); - } -}; - -Nengo.SpaSimilarity.prototype.layout_info = function () { - var info = Nengo.Component.prototype.layout_info.call(this); - info.show_pairs = this.show_pairs; - info.min_value = this.axes2d.scale_y.domain()[0]; - info.max_value = this.axes2d.scale_y.domain()[1]; - return info; -} - -Nengo.SpaSimilarity.prototype.update_layout = function(config) { - this.update_range(config.min_value, config.max_value); - this.show_pairs = config.show_pairs; - Nengo.Component.prototype.update_layout.call(this, config); -} - -Nengo.SpaSimilarity.prototype.reset = function () { - // ask for a legend update - this.ws.send("reset_legend"); -} - -// TODO: should I remove the ability to set range? -// Or limit it to something intuitive diff --git a/nengo_gui/static/components/tests/plot.test.ts b/nengo_gui/static/components/tests/plot.test.ts new file mode 100644 index 00000000..be0ef835 --- /dev/null +++ b/nengo_gui/static/components/tests/plot.test.ts @@ -0,0 +1,93 @@ +/** + * Test the Axes class. + */ + +import { dom, h } from "maquette"; +import * as test from "tape"; + +import * as fixtures from "../../tests/fixtures"; + +import { Axes } from "../plot"; + +// function axesNode() { +// return h("div", [h("svg", {height: "100%", width: "100%"}, [ +// xAxis(), +// yAxis(), +// ])]); +// } + +// function xAxis() { +// return h("g.axis.axisX.unselectable", [ +// xTick(0.0), +// xTick(0.5), +// xTick(1.0), +// h("path.domain", {d: "M0,6V0H1V6" }), +// ]); +// } + +// function xTick(x) { +// return h("g.tick", { +// styles: {opacity: "1"}, +// transform: "translate(" + x + ",0)", +// }, [ +// h("line", {x2: "0", y2: "6"}), +// h("text", { +// dy: ".71em", +// style: "text-anchor: middle;", +// x: "0", +// y: "9", +// }, [x.toFixed(1)]), +// ]); +// } + +// function yAxis() { +// return h("g.axis.axisY.unselectable", [ +// yTick(0.0), +// yTick(1.0), +// h("path.domain", {d: "M-6,0H0V1H-6"}), +// ]); +// } + +// function yTick(y) { +// return h("g.tick", { +// style: "opacity: 1;", +// transform: "translate(0," + y + ")", +// }, [ +// h("line", {x2: "-6", y2: "0"}), +// h("text", { +// dy: ".32em", +// style: "text-anchor: end;", +// x: "-9", +// y: "0", +// }, [y.toFixed(1)]), +// ]); +// } + +// test("Axes.setAxesGeometry", assert => { +// const document = new fixtures.Document(assert); +// const div = document.document.createElement("div"); + +// const axes = new Axes(div, { +// "height": 100, +// "maxValue": 1, +// "minValue": 0, +// "width": 100, +// }); + +// // assert.equal(div.innerHTML, dom.create(axesNode()).domNode.innerHTML); +// assert.ok(div.isEqualNode(dom.create(axesNode()).domNode)); +// assert.equal(axes.width, 100); +// assert.equal(axes.height, 100); +// assert.equal(axes.axLeft, 100); +// assert.equal(axes.axRight, 0); +// assert.equal(axes.axBottom, 0); +// assert.equal(axes.axTop, 0); +// assert.equal(axes.tickSize, 0); +// assert.equal(axes.tickPadding, 0); + +// axes.setAxesGeometry(50, 50); + +// assert.equal(div.innerHTML, ""); + +// fixtures.teardown(assert, document); +// }); diff --git a/nengo_gui/static/components/time_axes.js b/nengo_gui/static/components/time_axes.js deleted file mode 100644 index c1d36b3f..00000000 --- a/nengo_gui/static/components/time_axes.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * 2d axes set with the horizontal axis being a time axis. -* @constructor - * - * @param {DOMElement} parent - the element to add this component to - * @param {dict} args - A set of constructor arguments (see Nengo.Axes2D) - * - * Called by a specific component when it requires an axes set (with the - * x-axis showing current model time). - */ - -Nengo.TimeAxes = function(parent, args) { - Nengo.Axes2D.call(this, parent, args); - var self = this; - this.display_time = args.display_time; - - - this.axis_x.ticks(0); - - this.axis_time_end =this.svg.append("text") - .text("Time: NULL") - .attr('class', 'graph_text unselectable')[0][0]; - this.axis_time_start =this.svg.append("text") - .text("Time: NULL") - .attr('class','graph_text unselectable')[0][0]; - - if (this.display_time == false) { - this.axis_time_start.style.display = 'none'; - this.axis_time_end.style.display = 'none'; - } -}; -Nengo.TimeAxes.prototype = Object.create(Nengo.Axes2D.prototype); -Nengo.TimeAxes.prototype.constructor = Nengo.TimeAxes; - -Nengo.TimeAxes.prototype.set_time_range = function(start, end) { - this.scale_x.domain([start, end]); - this.axis_time_start.textContent = start.toFixed(3); - this.axis_time_end.textContent = end.toFixed(3); - this.axis_x_g.call(this.axis_x); -}; - -Nengo.TimeAxes.prototype.on_resize = function(width, height) { - Nengo.Axes2D.prototype.on_resize.call(this, width, height); - - scale = parseFloat($('#main').css('font-size')); - var suppression_width = 6 * scale; - var text_offset = 1.2 * scale; - - if (width < suppression_width || this.display_time == false){ - this.axis_time_start.style.display = 'none'; - } else { - this.axis_time_start.style.display = 'block'; - } - - this.axis_time_start.setAttribute('y', this.ax_bottom + text_offset); - this.axis_time_start.setAttribute('x', this.ax_left - text_offset); - this.axis_time_end.setAttribute('y', this.ax_bottom + text_offset); - this.axis_time_end.setAttribute('x', this.ax_right - text_offset); -}; diff --git a/nengo_gui/static/components/value.css b/nengo_gui/static/components/value.css deleted file mode 100644 index c771a492..00000000 --- a/nengo_gui/static/components/value.css +++ /dev/null @@ -1,5 +0,0 @@ -.graph .line { - fill: none; - stroke: black; - stroke-width: 1.5px; -} diff --git a/nengo_gui/static/components/value.js b/nengo_gui/static/components/value.js deleted file mode 100644 index 64c85e5b..00000000 --- a/nengo_gui/static/components/value.js +++ /dev/null @@ -1,430 +0,0 @@ -/** - * - * Line graph showing decoded values over time - * @constructor - * - * @param {DOMElement} parent - the element to add this component to - * @param {Nengo.SimControl} sim - the simulation controller - * @param {dict} args - A set of constructor arguments (see Nengo.Component) - * @param {int} args.n_lines - number of decoded values - * @param {float} args.min_value - minimum value on y-axis - * @param {float} args.max_value - maximum value on y-axis - * - * Value constructor is called by python server when a user requests a plot - * or when the config file is making graphs. Server request is handled in - * netgraph.js {.on_message} function. - */ - -Nengo.Value = function(parent, sim, args) { - Nengo.Component.call(this, parent, args); - var self = this; - this.n_lines = args.n_lines || 1; - this.sim = sim; - this.display_time = args.display_time; - this.synapse = args.synapse; - - /** for storing the accumulated data */ - this.data_store = new Nengo.DataStore(this.n_lines, this.sim, 0.0); - - this.axes2d = new Nengo.TimeAxes(this.div, args); - - /** call schedule_update whenever the time is adjusted in the SimControl */ - this.sim.div.addEventListener('adjust_time', - function(e) {self.schedule_update();}, false); - - /** call reset whenever the simulation is reset */ - this.sim.div.addEventListener('sim_reset', - function(e) {self.reset();}, false); - - /** create the lines on the plots */ - this.line = d3.svg.line() - .x(function(d, i) { - return self.axes2d.scale_x( - self.data_store.times[i + self.data_store.first_shown_index]); - }) - .y(function(d) {return self.axes2d.scale_y(d);}) - this.path = this.axes2d.svg.append("g").selectAll('path') - .data(this.data_store.data); - - this.colors = Nengo.make_colors(this.n_lines); - this.path.enter() - .append('path') - .attr('class', 'line') - .style('stroke', function(d, i) {return self.colors[i];}); - - // Flag for whether or not update code should be changing the crosshair - // Both zooming and the simulator time changing cause an update, but the crosshair - // should only update when the time is changing - this.crosshair_updates = false; - - // Keep track of mouse position TODO: fix this to be not required - this.crosshair_mouse = [0,0]; - - this.crosshair_g = this.axes2d.svg.append('g') - .attr('class', 'crosshair'); - - // TODO: put the crosshair properties in CSS - this.crosshair_g.append('line') - .attr('id', 'crosshairX') - .attr('stroke', 'black') - .attr('stroke-width', '0.5px'); - - this.crosshair_g.append('line') - .attr('id', 'crosshairY') - .attr('stroke', 'black') - .attr('stroke-width', '0.5px'); - - // TODO: have the fonts and colour set appropriately - this.crosshair_g.append('text') - .attr('id', 'crosshairXtext') - .style('text-anchor', 'middle') - .attr('class', 'graph_text'); - - this.crosshair_g.append('text') - .attr('id', 'crosshairYtext') - .style('text-anchor', 'end') - .attr('class', 'graph_text'); - - this.axes2d.svg - .on('mouseover', function() { - var mouse = d3.mouse(this); - self.crosshair_updates = true; - self.crosshair_g.style('display', null); - self.cross_hair_mouse = [mouse[0], mouse[1]]; - }) - .on('mouseout', function() { - var mouse = d3.mouse(this); - self.crosshair_updates = false; - self.crosshair_g.style('display', 'none'); - self.cross_hair_mouse = [mouse[0], mouse[1]]; - }) - .on('mousemove', function() { - var mouse = d3.mouse(this); - self.crosshair_updates = true; - self.cross_hair_mouse = [mouse[0], mouse[1]]; - self.update_crosshair(mouse); - }) - .on('mousewheel', function() { - // Hide the crosshair when zooming, until a better option comes along - self.crosshair_updates = false; - self.crosshair_g.style('display', 'none'); - }); - - this.update(); - this.on_resize(this.get_screen_width(), this.get_screen_height()); - this.axes2d.axis_y.tickValues([args.min_value, args.max_value]); - this.axes2d.fit_ticks(this); - - this.colors = Nengo.make_colors(6); - this.color_func = function(d, i) {return self.colors[i % 6]}; - this.legend = document.createElement('div'); - this.legend.classList.add('legend'); - this.div.appendChild(this.legend); - - this.legend_labels = args.legend_labels || []; - if (this.legend_labels.length !== this.n_lines) { - // fill up the array with temporary labels - for (var i=this.legend_labels.length; i this.axes2d.ax_left && x < this.axes2d.ax_right && y > this.axes2d.ax_top && y < this.axes2d.ax_bottom) { - this.crosshair_g.style('display', null); - - this.crosshair_g.select('#crosshairX') - .attr('x1', x) - .attr('y1', this.axes2d.ax_top) - .attr('x2', x) - .attr('y2', this.axes2d.ax_bottom); - - this.crosshair_g.select('#crosshairY') - .attr('x1', this.axes2d.ax_left) - .attr('y1', y) - .attr('x2', this.axes2d.ax_right) - .attr('y2', y); - - this.crosshair_g.select('#crosshairXtext') - .attr('x', x - 2) - .attr('y', this.axes2d.ax_bottom + 17) //TODO: don't use magic numbers - .text(function () { - return Math.round(self.axes2d.scale_x.invert(x) * 100) / 100; - }); - - this.crosshair_g.select('#crosshairYtext') - .attr('x', this.axes2d.ax_left - 3) - .attr('y', y + 3) - .text(function () { - return Math.round(self.axes2d.scale_y.invert(y) * 100) / 100; - }); - } else { - this.crosshair_g.style('display', 'none'); - } -}; - -/** - * Receive new line data from the server - */ -Nengo.Value.prototype.on_message = function(event) { - var data = new Float32Array(event.data); - data = Array.prototype.slice.call(data); - var size = this.n_lines + 1; - /** since multiple data packets can be sent with a single event, - make sure to process all the packets */ - while (data.length >= size) { - this.data_store.push(data.slice(0, size)); - data = data.slice(size); - } - if (data.length > 0) { - console.log('extra data: ' + data.length); - } - this.schedule_update(); -}; - -/** - * Redraw the lines and axis due to changed data - */ -Nengo.Value.prototype.update = function() { - /** let the data store clear out old values */ - this.data_store.update(); - - /** determine visible range from the Nengo.SimControl */ - var t1 = this.sim.time_slider.first_shown_time; - var t2 = t1 + this.sim.time_slider.shown_time; - - this.axes2d.set_time_range(t1, t2); - - /** update the lines */ - var self = this; - var shown_data = this.data_store.get_shown_data(); - - this.path.data(shown_data) - .attr('d', self.line); - - //** Update the crosshair text if the mouse is on top */ - if (this.crosshair_updates) { - this.update_crosshair(this.cross_hair_mouse); - } -}; - -/** - * Adjust the graph layout due to changed size - */ -Nengo.Value.prototype.on_resize = function(width, height) { - if (width < this.minWidth) { - width = this.minWidth; - } - if (height < this.minHeight) { - height = this.minHeight; - }; - - this.axes2d.on_resize(width, height); - - this.update(); - - this.label.style.width = width; - - this.width = width; - this.height = height; - this.div.style.width = width; - this.div.style.height= height; -}; - -Nengo.Value.prototype.generate_menu = function() { - var self = this; - var items = []; - items.push(['Set range...', function() {self.set_range();}]); - items.push(['Set synapse...', function() {self.set_synapse_dialog();}]); - - if (this.show_legend) { - items.push(['Hide legend', function() {self.set_show_legend(false);}]); - } else { - items.push(['Show legend', function() {self.set_show_legend(true);}]); - } - - // TODO: give the legend it's own context menu - items.push(['Set legend labels', function () {self.set_legend_labels();}]) - - // add the parent's menu items to this - return $.merge(items, Nengo.Component.prototype.generate_menu.call(this)); -}; - -Nengo.Value.prototype.set_show_legend = function(value){ - if (this.show_legend !== value) { - this.show_legend = value; - this.save_layout(); - - if (this.show_legend === true) { - Nengo.draw_legend(this.legend, this.legend_labels.slice(0, this.n_lines), this.color_func); - } else { - // delete the legend's children - while (this.legend.lastChild) { - this.legend.removeChild(this.legend.lastChild); - } - } - } -} - -Nengo.Value.prototype.set_legend_labels = function() { - var self = this; - - Nengo.modal.title('Enter comma seperated legend label values'); - Nengo.modal.single_input_body('Legend label', 'New value'); - Nengo.modal.footer('ok_cancel', function(e) { - var label_csv = $('#singleInput').val(); - var modal = $('#myModalForm').data('bs.validator'); - - // No validation to do. - // Empty entries assumed to be indication to skip modification - // Long strings okay - // Excissive entries get ignored - // TODO: Allow escaping of commas - if ((label_csv !== null) && (label_csv !== '')) { - labels = label_csv.split(','); - - for (var i=0; i,".'); - Nengo.modal.show(); - $('#OK').on('click', function () { - var w = $(self.div).width(); - var h = $(self.div).height(); - self.on_resize(w, h); - }) -} - -Nengo.Value.prototype.update_range = function(min, max) { - this.axes2d.scale_y.domain([min, max]); - this.axes2d.axis_y_g.call(this.axes2d.axis_y); -} - -Nengo.Value.prototype.reset = function(event) { - this.data_store.reset(); - this.schedule_update(); -} - -Nengo.Value.prototype.set_synapse_dialog = function() { - var self = this; - Nengo.modal.title('Set synaptic filter...'); - Nengo.modal.single_input_body(this.synapse, - 'Filter time constant (in seconds)'); - Nengo.modal.footer('ok_cancel', function (e) { - var new_synapse = $('#singleInput').val(); - var modal = $('#myModalForm').data('bs.validator'); - modal.validate(); - if (modal.hasErrors() || modal.isIncomplete()) { - return; - } - if (new_synapse !== null) { - new_synapse = parseFloat(new_synapse); - if (new_synapse === self.synapse) { - return; - } - self.synapse = new_synapse; - self.ws.send('synapse:' + self.synapse); - } - $('#OK').attr('data-dismiss', 'modal'); - }); - var $form = $('#myModalForm').validator({ - custom: { - my_validator: function($item) { - var num = $item.val(); - if ($.isNumeric(num)) { - num = Number(num); - if (num >= 0) { - return true; - } - } - return false; - } - }, - }); - $('#singleInput').attr('data-error', 'should be a non-negative number'); - Nengo.modal.show(); -} diff --git a/nengo_gui/static/components/value.ts b/nengo_gui/static/components/value.ts new file mode 100644 index 00000000..0b257210 --- /dev/null +++ b/nengo_gui/static/components/value.ts @@ -0,0 +1,187 @@ +/** + * Line graph showing decoded values over time + * + * Value constructor is called by python server when a user requests a plot + * or when the config file is making graphs. Server request is handled in + * netgraph.js {.on_message} function. + * + * @constructor + * @param {DOMElement} parent - the element to add this component to + * @param {SimControl} sim - the simulation controller + * @param {dict} args - A set of constructor arguments (see Component) + * @param {int} args.n_lines - number of decoded values + * @param {float} args.min_value - minimum value on y-axis + * @param {float} args.max_value - maximum value on y-axis + */ + +import * as d3 from "d3"; +import * as $ from "jquery"; +import { VNode, dom, h } from "maquette"; + +import { Axes, Plot } from "./plot"; +import { InputDialogView } from "../modal"; +import { PlotView } from "./plot"; +import { Position } from "./position"; +import { registerComponent } from "./registry"; +import { Connection } from "../server"; +import * as utils from "../utils"; + +export class Value extends Plot { + lines: Array>>; + view: ValueView; + + constructor({ + server, + uid, + label, + pos, + dimensions, + synapse, + labelVisible = true, + xlim = [-0.5, 0], + ylim = [-1, 1] + }: { + server: Connection; + uid: string; + label: string; + pos: Position; + dimensions: number; + synapse: number; + labelVisible?: boolean; + xlim?: [number, number]; + ylim?: [number, number]; + }) { + super( + server, + uid, + new ValueView(), + label, + pos, + dimensions, + synapse, + labelVisible, + xlim, + ylim + ); + + // Create the lines on the plots + this.view.numLines = dimensions; + this.lines = utils.emptyArray(dimensions).map((_, i) => + d3.svg + .line() + .x(d => this.axes.x.pixelAt(d[0])) + .y(d => this.axes.y.pixelAt(d[i + 1])) + .defined(d => d[i + 1] != null) + ); + + this.fastServer.bind((data: ArrayBuffer) => { + this.add(new Float64Array(data)); + }); + } + + addMenuItems() { + this.menu.addAction("Set y-limits...", () => { + this.askYlim(); + }); + super.addMenuItems(); + } + + askYlim() { + const modal = new InputDialogView( + String(this.ylim), + "New y-limits", + "Input should be in the form ','." + ); + modal.title = "Set y-limits..."; + modal.ok.addEventListener("click", () => { + const validator = $(modal).data("bs.validator"); + validator.validate(); + if (validator.hasErrors() || validator.isIncomplete()) { + return; + } + if (modal.input.value !== null) { + const newRange = modal.input.value.split(","); + const min = parseFloat(newRange[0]); + const max = parseFloat(newRange[1]); + this.ylim = [min, max]; + } + $(modal).modal("hide"); + }); + utils.handleTabs(modal); + + $(modal).validator({ + custom: { + ngvalidator: item => { + const nums = item.value.split(","); + let valid = false; + if (utils.isNum(nums[0]) && utils.isNum(nums[1])) { + // Two numbers, 1st less than 2nd + if (Number(nums[0]) < Number(nums[1])) { + valid = true; + } + } + return nums.length === 2 && valid; + } + } + }); + $(modal.root).on("hidden.bs.modal", () => { + document.body.removeChild(modal.root); + }); + document.body.appendChild(modal.root); + modal.show(); + } + + syncWithDataStore = utils.throttle(() => { + // Update the lines + const [tStart, tEnd] = this.xlim; + + const shownData = this.datastore.timeSlice(tStart, tEnd); + if (shownData[0] != null) { + this.view.lines = this.lines.map(line => line(shownData)); + if (this.legendVisible && this.view.legend.valuesVisible) { + const last = shownData[shownData.length - 1]; + this.view.legend.values = last.slice(1); + } + } + }, 10); +} + +export class ValueView extends PlotView { + paths: Array = []; + + set lines(val: Array) { + this.paths.forEach((path, i) => { + path.setAttribute("d", val[i]); + }); + } + + get numLines(): number { + return this.paths.length; + } + + set numLines(val: number) { + while (this.paths.length - val < 0) { + this.addPath(); + } + while (this.paths.length - val > 0) { + this.removePath(); + } + } + + private addPath() { + const i = this.paths.length; + const node = h("path.line", { stroke: this.colors[i] }); + const path = utils.domCreateSVG(node) as SVGPathElement; + this.paths.push(path); + this.body.appendChild(path); + } + + private removePath() { + const path = this.paths.pop(); + if (path != null) { + this.body.removeChild(path); + } + } +} + +registerComponent("value", Value); diff --git a/nengo_gui/static/components/widget.ts b/nengo_gui/static/components/widget.ts new file mode 100644 index 00000000..3878c772 --- /dev/null +++ b/nengo_gui/static/components/widget.ts @@ -0,0 +1,145 @@ +import { Component, ComponentView } from "./component"; +import { DataStore, TypedArray } from "../datastore"; +import { InputDialogView } from "../modal"; +import { Position } from "./position"; +import { + Connection, + FastConnection, + FastServerConnection, + MockFastConnection, + MockConnection +} from "../server"; +import * as utils from "../utils"; + +export abstract class Widget extends Component { + currentTime: number = 0.0; + datastore: DataStore; + synapse: number; + + protected fastServer: FastConnection; + + constructor( + server: Connection, + uid: string, + view: ComponentView, + label: string, + pos: Position, + dimensions: number, + synapse: number, + labelVisible: boolean = true + ) { + super(server, uid, view, label, pos, labelVisible); + this.synapse = synapse; + this.datastore = new DataStore(dimensions, 0.0); + if (server instanceof MockConnection) { + // If server is mocked, mock the fast connection too + this.fastServer = new MockFastConnection(this.uid); + } else { + this.fastServer = new FastServerConnection(this.uid); + } + + window.addEventListener( + "TimeSlider.moveShown", + utils.throttle((e: CustomEvent) => { + this.currentTime = e.detail.shownTime[1]; + }, 50) // Update once every 50 ms + ); + } + + get dimensions(): number { + return this.datastore.dims; + } + + set dimensions(val: number) { + console.warn(`Changing dimensionality of ${this.uid}`); + this.datastore.dims = val; + } + + /** + * Receive new line data from the server. + */ + add(data: number[] | TypedArray) { + // TODO: handle this in the websocket code + if (data.length !== this.dimensions + 1) { + console.error( + `Got data with ${data.length - 1} dimensions; ` + + `should be ${this.dimensions} dimensions.` + ); + } else { + this.datastore.add(data); + this.syncWithDataStore(); + } + } + + addMenuItems() { + this.menu.addAction("Set synapse...", () => { + this.askSynapse(); + }); + this.menu.addAction( + "Hide label", + () => { + this.labelVisible = false; + // see component.interactRoot.on("dragend resizeend") + // this.saveLayout(); + }, + () => this.labelVisible + ); + this.menu.addAction( + "Show label", + () => { + this.labelVisible = true; + // see component.interactRoot.on("dragend resizeend") + // this.saveLayout(); + }, + () => !this.labelVisible + ); + // TODO: attachNetGraph + // this.menu.addAction("Remove", () => { this.remove(); }); + + super.addMenuItems(); + } + + askSynapse() { + const modal = new InputDialogView( + String(this.synapse), + "Synaptic filter time constant (in seconds)", + "Input should be a non-negative number" + ); + modal.title = "Set synaptic filter..."; + modal.ok.addEventListener("click", () => { + const validator = $(modal).data("bs.validator"); + validator.validate(); + if (validator.hasErrors() || validator.isIncomplete()) { + return; + } + if (modal.input.value !== null) { + const newSynapse = parseFloat(modal.input.value); + if (newSynapse !== this.synapse) { + this.synapse = newSynapse; + // this.ws.send("synapse:" + this.synapse); + } + } + $(modal).modal("hide"); + }); + utils.handleTabs(modal); + + $(modal).validator({ + custom: { + ngvalidator: item => { + return utils.isNum(item.value) && Number(item.value) >= 0; + } + } + }); + $(modal.root).on("hidden.bs.modal", () => { + document.body.removeChild(modal.root); + }); + document.body.appendChild(modal.root); + modal.show(); + } + + reset() { + this.datastore.reset(); + } + + syncWithDataStore: () => void; +} diff --git a/nengo_gui/static/components/xy_axes.js b/nengo_gui/static/components/xy_axes.js deleted file mode 100644 index 899b009c..00000000 --- a/nengo_gui/static/components/xy_axes.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Basic 2d axes set. - * @constructor - * - * @param {float} args.width - the width of the axes (in pixels) - * @param {float} args.height - the height of the axes (in pixels) - * @param {float} args.min_value - minimum value on y-axis - * @param {float} args.max_value - maximum value on y-axis - */ - -Nengo.XYAxes = function(parent, args) { - Nengo.Axes2D.call(this, parent, args); - - this.scale_x.domain([args.min_value, args.max_value]); - this.axis_x.tickValues([args.min_value, args.max_value]); - this.axis_x.ticks(this.axis_y.ticks()[0]); - - this.min_val = args.min_value; - this.max_val = args.max_value; -}; - -Nengo.XYAxes.prototype = Object.create(Nengo.Axes2D.prototype); -Nengo.XYAxes.prototype.constructor = Nengo.XYAxes; - -/** - * Adjust the graph layout due to changed size - */ -Nengo.XYAxes.prototype.on_resize = function(width, height) { - Nengo.Axes2D.prototype.on_resize.call(this, width, height); - - var x_offset = this.ax_bottom - this.min_val / (this.max_val - this.min_val) * (this.ax_top - this.ax_bottom); - var y_offset = this.ax_left - this.min_val / (this.max_val - this.min_val) * (this.ax_right - this.ax_left); - - this.axis_x_g.attr("transform", "translate(0," + x_offset + ")"); - this.axis_x_g.call(this.axis_x); - this.axis_y_g.attr("transform", "translate(" + y_offset + ", 0)"); - this.axis_y_g.call(this.axis_y); -}; diff --git a/nengo_gui/static/components/xyvalue.css b/nengo_gui/static/components/xyvalue.css deleted file mode 100644 index 14132fe8..00000000 --- a/nengo_gui/static/components/xyvalue.css +++ /dev/null @@ -1,14 +0,0 @@ -.warning-text { - color:#a94442; - position: absolute; - text-align: center; - margin-left: 20%; - margin-right: auto; - width:60%; - top:37%; - display:box; - box-align:center; - box-pack:center; - border: 1px dashed #a94442; - background-color: white; -} diff --git a/nengo_gui/static/components/xyvalue.js b/nengo_gui/static/components/xyvalue.js deleted file mode 100644 index dbc6e38b..00000000 --- a/nengo_gui/static/components/xyvalue.js +++ /dev/null @@ -1,283 +0,0 @@ -/** - * Line graph showing decoded values over time - * @constructor - * - * @param {DOMElement} parent - the element to add this component to - * @param {Nengo.SimControl} sim - the simulation controller - * @param {dict} args - A set of constructor arguments (see Nengo.Component) - * @param {int} args.n_lines - number of decoded values - * @param {float} args.min_value - minimum value on x-axis and y-axis - * @param {float} args.max_value - maximum value on x-axis and y-axis - * @param {Nengo.SimControl} args.sim - the simulation controller - * - * XYValue constructor is called by python server when a user requests a plot - * or when the config file is making graphs. Server request is handled in - * netgraph.js {.on_message} function. - */ - -Nengo.XYValue = function(parent, sim, args) { - Nengo.Component.call(this, parent, args); - var self = this; - - this.n_lines = args.n_lines || 1; - this.sim = sim; - - /** for storing the accumulated data */ - this.data_store = new Nengo.DataStore(this.n_lines, this.sim, 0); - - this.axes2d = new Nengo.XYAxes(this.div, args); - - // the two indices of the multi-dimensional data to display - this.index_x = args.index_x; - this.index_y = args.index_y; - - /** call schedule_update whenever the time is adjusted in the SimControl */ - this.sim.div.addEventListener('adjust_time', - function(e) {self.schedule_update();}, false); - - /** call reset whenever the simulation is reset */ - this.sim.div.addEventListener('sim_reset', - function(e) {self.reset();}, false); - - /** create the lines on the plots */ - var line = d3.svg.line() - .x(function(d, i) {return self.axes2d.scale_x(self.data_store.data[this.index_x][i]);}) - .y(function(d) {return self.axe2d.scale_y(d);}); - this.path = this.axes2d.svg.append("g").selectAll('path') - .data([this.data_store.data[this.index_y]]); - this.path.enter().append('path') - .attr('class', 'line') - .style('stroke', Nengo.make_colors(1)); - - /** create a circle to track the most recent data */ - this.recent_circle = this.axes2d.svg.append("circle") - .attr("r", this.get_circle_radius()) - .attr('cx', this.axes2d.scale_x(0)) - .attr('cy', this.axes2d.scale_y(0)) - .style("fill", Nengo.make_colors(1)[0]) - .style('fill-opacity', 0); - - this.invalid_dims = false; - - this.axes2d.fit_ticks(this); - this.on_resize(this.get_screen_width(), this.get_screen_height()); -}; -Nengo.XYValue.prototype = Object.create(Nengo.Component.prototype); -Nengo.XYValue.prototype.constructor = Nengo.Value; - -/** - * Receive new line data from the server - */ -Nengo.XYValue.prototype.on_message = function(event) { - var data = new Float32Array(event.data); - this.data_store.push(data); - this.schedule_update(); -} - -/** - * Redraw the lines and axis due to changed data - */ -Nengo.XYValue.prototype.update = function() { - var self = this; - - /** let the data store clear out old values */ - this.data_store.update(); - - /** update the lines if there is data with valid dimensions */ - var good_idx = self.index_x < self.n_lines && self.index_y < self.n_lines - if (good_idx) { - var shown_data = this.data_store.get_shown_data(); - - /** update the lines */ - var line = d3.svg.line() - .x(function(d, i) { - return self.axes2d.scale_x( - shown_data[self.index_x][i]); - }) - .y(function(d) {return self.axes2d.scale_y(d);}); - this.path.data([shown_data[this.index_y]]) - .attr('d', line); - - var last_index = shown_data[self.index_x].length - 1; - if(last_index >= 0){ - - /** update the circle if there is valid data */ - this.recent_circle.attr('cx', self.axes2d.scale_x(shown_data[self.index_x][last_index])) - .attr('cy', self.axes2d.scale_y(shown_data[self.index_y][last_index])) - .style('fill-opacity', 0.5); - } - - /** if switching from invalids dimensions to valid dimensions, remove - the label */ - if (this.invalid_dims === true) { - this.div.removeChild(this.warning_text); - this.invalid_dims = false; - } - - } else if (this.invalid_dims == false) { - this.invalid_dims = true; - - // create the HTML text element - this.warning_text = document.createElement('div'); - this.div.appendChild(this.warning_text); - this.warning_text.className = "warning-text"; - this.warning_text.innerHTML = "Change
Dimension
Indices"; - } - -}; - -/** - * Adjust the graph layout due to changed size - */ -Nengo.XYValue.prototype.on_resize = function(width, height) { - this.axes2d.on_resize(width, height); - - this.update(); - - this.label.style.width = width; - this.width = width; - this.height = height; - this.div.style.width = width; - this.div.style.height = height; - this.recent_circle.attr("r", this.get_circle_radius()); -}; - -Nengo.XYValue.prototype.get_circle_radius = function() { - return Math.min(this.width, this.height) / 30; -} - -Nengo.XYValue.prototype.generate_menu = function() { - var self = this; - var items = []; - items.push(['Set range...', function() {self.set_range();}]); - items.push(['Set X, Y indices...', function() {self.set_indices();}]); - - // add the parent's menu items to this - return $.merge(items, Nengo.Component.prototype.generate_menu.call(this)); -}; - - -Nengo.XYValue.prototype.layout_info = function () { - var info = Nengo.Component.prototype.layout_info.call(this); - info.min_value = this.axes2d.scale_y.domain()[0]; - info.max_value = this.axes2d.scale_y.domain()[1]; - info.index_x = this.index_x; - info.index_y = this.index_y; - return info; -} - -Nengo.XYValue.prototype.update_layout = function (config) { - this.update_indices(config.index_x, config.index_y); - this.update_range(config.min_value, config.max_value); - Nengo.Component.prototype.update_layout.call(this, config); -} - -Nengo.XYValue.prototype.set_range = function() { - var range = this.axes2d.scale_y.domain(); - var self = this; - Nengo.modal.title('Set graph range...'); - Nengo.modal.single_input_body(range, 'New range'); - Nengo.modal.footer('ok_cancel', function(e) { - var new_range = $('#singleInput').val(); - var modal = $('#myModalForm').data('bs.validator'); - - modal.validate(); - if (modal.hasErrors() || modal.isIncomplete()) { - return; - } - if (new_range !== null) { - new_range = new_range.split(','); - var min = parseFloat(new_range[0]); - var max = parseFloat(new_range[1]); - self.update_range(min, max); - self.update(); - self.save_layout(); - } - $('#OK').attr('data-dismiss', 'modal'); - }); - var $form = $('#myModalForm').validator({ - custom: { - my_validator: function($item) { - var nums = $item.val().split(','); - var valid = false; - if ($.isNumeric(nums[0]) && $.isNumeric(nums[1])) { - // Two numbers, 1st less than 2nd - // The axes must intersect at 0 - var ordered = Number(nums[0]) < Number(nums[1]); - var zeroed = Number(nums[0]) * Number(nums[1]) <= 0; - if (ordered && zeroed) { - valid = true; - } - } - return (nums.length == 2 && valid); - } - } - }); - - $('#singleInput').attr('data-error', 'Input should be in the form ' + - '"," and the axes must cross at zero.'); - Nengo.modal.show(); -} - -Nengo.XYValue.prototype.update_range = function(min, max) { - this.axes2d.min_val = min; - this.axes2d.max_val = max; - this.axes2d.scale_x.domain([min, max]); - this.axes2d.scale_y.domain([min, max]); - this.axes2d.axis_x.tickValues([min, max]); - this.axes2d.axis_y.tickValues([min, max]); - this.axes2d.axis_y_g.call(this.axes2d.axis_y); - this.axes2d.axis_x_g.call(this.axes2d.axis_x); - this.on_resize(this.get_screen_width(), this.get_screen_height()); -} - -Nengo.XYValue.prototype.set_indices = function() { - var self = this; - Nengo.modal.title('Set X and Y indices...'); - Nengo.modal.single_input_body([this.index_x,this.index_y], 'New indices'); - Nengo.modal.footer('ok_cancel', function(e) { - var new_indices = $('#singleInput').val(); - var modal = $('#myModalForm').data('bs.validator'); - - modal.validate(); - if (modal.hasErrors() || modal.isIncomplete()) { - return; - } - if (new_indices !== null) { - new_indices = new_indices.split(','); - self.update_indices(parseInt(new_indices[0]), - parseInt(new_indices[1])); - self.save_layout(); - } - $('#OK').attr('data-dismiss', 'modal'); - }); - var $form = $('#myModalForm').validator({ - custom: { - my_validator: function($item) { - var nums = $item.val().split(','); - return ((parseInt(Number(nums[0])) == nums[0]) && - (parseInt(Number(nums[1])) == nums[1]) && - (nums.length == 2) && - (Number(nums[1])=0) && - (Number(nums[0])=0)); - } - } - }); - - $('#singleInput').attr('data-error', 'Input should be two positive ' + - 'integers in the form ",". ' + - 'Dimensions are zero indexed.'); - - Nengo.modal.show(); -} - -Nengo.XYValue.prototype.update_indices = function(index_x, index_y) { - this.index_x = index_x; - this.index_y = index_y; - this.update(); -} - -Nengo.XYValue.prototype.reset = function(event) { - this.data_store.reset(); - this.schedule_update(); -} diff --git a/nengo_gui/static/components/xyvalue.ts b/nengo_gui/static/components/xyvalue.ts new file mode 100644 index 00000000..5999d3f1 --- /dev/null +++ b/nengo_gui/static/components/xyvalue.ts @@ -0,0 +1,270 @@ +import * as d3 from "d3"; +import * as $ from "jquery"; +import { VNode, dom, h } from "maquette"; + +import { ComponentView } from "./component"; +import { DataStore } from "../datastore"; +import { InputDialogView } from "../modal"; +import { Axes, Plot, PlotView } from "./plot"; +import { Position } from "./position"; +import { registerComponent } from "./registry"; +import { Connection } from "../server"; +import * as utils from "../utils"; + +export class XYAxes extends Axes { + get padding(): [number, number] { + return [5, 5]; + } + + set scale(val: [number, number]) { + this._width = Math.max(Axes.minWidth, val[0]); + this._height = Math.max(Axes.minHeight, val[1]); + + const [xWidth, xHeight] = this.view.x.scale; + const [yWidth, yHeight] = this.view.y.scale; + + this.x.pixelLim = [0, this._width]; + this.y.pixelLim = [this._height, 0]; + this.view.x.pos = [0, utils.clip(this.y.pixelAt(0), 0, this._height)]; + this.view.y.pos = [ + utils.clip(this.x.pixelAt(0), yWidth, this._width), + 0 + ]; + this.view.crosshair.scale = [this._width, this._height]; + } +} + +export class XYValue extends Plot { + line: d3.svg.Line>; + view: XYValueView; + + protected _index: [number, number] = [0, 1]; + + constructor({ + server, + uid, + label, + pos, + dimensions, + synapse, + labelVisible = true, + index = [0, 1], + xlim = [-1, 1], + ylim = [-1, 1] + }: { + server: Connection; + uid: string; + label: string; + pos: Position; + dimensions: number; + synapse: number; + labelVisible?: boolean; + index?: [number, number]; + xlim?: [number, number]; + ylim?: [number, number]; + }) { + super( + server, + uid, + new XYValueView(), + label, + pos, + dimensions, + synapse, + labelVisible, + xlim, + ylim + ); + this.index = index; + this.line = d3.svg.line(); + this.line.x(d => this.axes.x.pixelAt(d[this._index[0] + 1])); + this.line.y(d => this.axes.y.pixelAt(d[this._index[1] + 1])); + } + + get index(): [number, number] { + return this._index; + } + + set index(val: [number, number]) { + if (val[0] >= this.dimensions || val[1] >= this.dimensions) { + console.error(`Index not in ${this.dimensions} dimensions`); + } else { + this._index = val; + this.syncWithDataStore(); + } + } + + addAxes(width, height, xlim, ylim) { + this.axes = new XYAxes(this.view, width, height, xlim, ylim); + } + + addMenuItems() { + this.menu.addAction("Set X, Y limits...", () => { + this.askLim(); + }); + this.menu.addAction("Set X, Y indices...", () => { + this.askIndices(); + }); + this.menu.addSeparator(); + super.addMenuItems(); + } + + askLim() { + const lim = this.axes.y.lim; + const modal = new InputDialogView( + `${lim[0]},${lim[1]}`, + "New limits", + "Input should be in the " + + "form ',' and the axes must cross at zero." + ); + modal.title = "Set X, Y limits..."; + modal.ok.addEventListener("click", () => { + const validator = $(modal).data("bs.validator"); + validator.validate(); + if (validator.hasErrors() || validator.isIncomplete()) { + return; + } + if (modal.input.value !== null) { + const newRange = modal.input.value.split(","); + const min = parseFloat(newRange[0]); + const max = parseFloat(newRange[1]); + this.xlim = [min, max]; + this.ylim = [min, max]; + } + $(modal).modal("hide"); + }); + utils.handleTabs(modal); + + $(modal).validator({ + custom: { + ngvalidator: item => { + const nums = item.value.split(","); + let valid = false; + if (utils.isNum(nums[0]) && utils.isNum(nums[1])) { + // Two numbers, 1st less than 2nd. + // The axes must intersect at 0. + const ordered = Number(nums[0]) < Number(nums[1]); + const zeroed = Number(nums[0]) * Number(nums[1]) <= 0; + if (ordered && zeroed) { + valid = true; + } + } + return nums.length === 2 && valid; + } + } + }); + $(modal.root).on("hidden.bs.modal", () => { + document.body.removeChild(modal.root); + }); + document.body.appendChild(modal.root); + modal.show(); + } + + askIndices() { + const modal = new InputDialogView( + `${this.index[0]},${this.index[1]}`, + "New indices", + "Input should be two positive integers in the form " + + "','. Dimensions are zero indexed." + ); + modal.title = "Set X, Y indices..."; + modal.ok.addEventListener("click", () => { + const validator = $(modal).data("bs.validator"); + validator.validate(); + if (validator.hasErrors() || validator.isIncomplete()) { + return; + } + if (modal.input.value !== null) { + const newIndices = modal.input.value.split(","); + this.index = [ + parseInt(newIndices[0], 10), + parseInt(newIndices[1], 10) + ]; + } + $(modal).modal("hide"); + }); + utils.handleTabs(modal); + + $(modal).validator({ + custom: { + ngvalidator: item => { + const nums = item.value.split(",").map(Number); + return ( + parseInt(nums[0], 10) === nums[0] && + parseInt(nums[1], 10) === nums[1] && + nums.length === 2 && + (Number(nums[1]) < this.dimensions && + Number(nums[1]) >= 0) && + (Number(nums[0]) < this.dimensions && + Number(nums[0]) >= 0) + ); + } + } + }); + $(modal.root).on("hidden.bs.modal", () => { + document.body.removeChild(modal.root); + }); + document.body.appendChild(modal.root); + modal.show(); + } + + /** + * Redraw the lines and axis due to changed data. + */ + syncWithDataStore = utils.throttle(() => { + // Update the lines + const [tStart, tEnd] = this.xlim; + const shownData = this.datastore.timeSlice(tStart, tEnd); + if (shownData[0] != null) { + this.view.line = this.line(shownData); + } + }, 20); +} + +export class XYValueView extends PlotView { + circle: SVGCircleElement; + path: SVGPathElement; + + constructor() { + super(); // Dimensions always 1 + const pathNode = h("path.line", { stroke: this.colors[0] }); + const circleNode = h("circle.last-point", { + cx: "0", + cy: "0", + fill: this.colors[0], + r: "0" + }); + this.path = utils.domCreateSVG(pathNode) as SVGPathElement; + this.body.appendChild(this.path); + this.circle = utils.domCreateSVG(circleNode) as SVGCircleElement; + } + + set line(val: string) { + this.path.setAttribute("d", val); + if (!this.body.contains(this.circle)) { + this.body.appendChild(this.circle); + } + // Parse the "d" attribute to get the last x, y coordinate + const commands = val.split(/(?=[LMC])/); + const last = commands[commands.length - 1]; + const lastNums = last + .replace(/[lmcz]/gi, "") + .split(",") + .map(Number); + this.circle.setAttribute("cx", `${lastNums[0]}`); + this.circle.setAttribute("cy", `${lastNums[1]}`); + } + + get scale(): [number, number] { + return this.overlayScale; + } + + set scale(val: [number, number]) { + const [width, height] = val; + this.overlayScale = [width, height]; + this.legend.pos = [width + 2, 0]; + this.circle.setAttribute("r", `${Math.min(width, height) / 30}`); + } +} + +registerComponent("xy_value", XYValue); diff --git a/nengo_gui/static/config.js b/nengo_gui/static/config.js deleted file mode 100644 index 4f405657..00000000 --- a/nengo_gui/static/config.js +++ /dev/null @@ -1,47 +0,0 @@ -Nengo.Config = function(parent, args) { - var self = this; - - define_option = function(key, default_val) { - var type = typeof(default_val); - Object.defineProperty(self, key, { - get: function() { - var val = localStorage.getItem("ng." + key) || default_val; - if (type === "boolean") { - return val === "true" || val === true; - } else if (type === "number") { - return Number(val); - } else { - return val; - } - }, - set: function(val) { - return localStorage.setItem("ng." + key, val); - }, - enumerable: true - }); - }; - - // General options accessible through Configuration Options - define_option("transparent_nets", false); - define_option("aspect_resize", false); - define_option("zoom_fonts", false); - define_option("font_size", 100); - define_option("scriptdir", "."); - - // Ace editor options - define_option("hide_editor", false); - define_option("editor_width", 580); - define_option("editor_font_size", 12); - define_option("auto_update", true); - define_option("console_height", 100); -}; - -Nengo.Config.prototype.restore_defaults = function() { - for (var option in this) { - if (this.hasOwnProperty(option)) { - localStorage.removeItem("ng." + option); - } - } -} - -Nengo.config = new Nengo.Config(); diff --git a/nengo_gui/static/config.ts b/nengo_gui/static/config.ts new file mode 100644 index 00000000..9f8c2838 --- /dev/null +++ b/nengo_gui/static/config.ts @@ -0,0 +1,486 @@ +import { VNode, dom, h } from "maquette"; +import { ModalView } from "./modal"; + +/** + * A class that takes the place of localStorage if it doesn't exist. + * + * Note that this does not aim to implements the whole localStorage spec; + * it only implements what Config uses. The values set in this object will + * only exist for the current session and will not persist across sessions. + */ +class MockLocalStorage implements Storage { + [key: string]: any; + items: { [key: string]: string } = {}; + + get length(): number { + return Object.keys(this.items).length; + } + + clear() { + this.items = {}; + } + + getItem(name: string): string { + return this.items[name]; + } + + key(index: number) { + return Object.keys(this.items)[index]; + } + + removeItem(name: string): void { + delete this.items[name]; + } + + setItem(name: string, val: any): void { + this.items[name] = String(val); + } +} + +class Config { + storage: Storage; + + constructor() { + if (typeof localStorage === "undefined" || localStorage === null) { + this.storage = new MockLocalStorage(); + } else { + this.storage = localStorage; + } + } + + get aspectResize(): boolean { + return this.getBool("aspectResize", false); + } + + set aspectResize(val: boolean) { + this.setAny("aspectResize", val); + } + + get autoUpdate(): boolean { + return this.getBool("autoUpdate", true); + } + + set autoUpdate(val: boolean) { + this.setAny("autoUpdate", val); + } + + get backend(): string { + return this.getString("backend", "nengo"); + } + + set backend(val: string) { + this.setAny("backend", val); + } + + get consoleHeight(): number { + return this.getNumber("consoleHeight", 100); + } + + set consoleHeight(val: number) { + this.setAny("consoleHeight", val); + } + + get editorFontSize(): number { + return this.getNumber("editorFontSize", 12); + } + + set editorFontSize(val: number) { + this.setAny("editorFontSize", val); + } + + get editorWidth(): number { + return this.getNumber("editorWidth", 580); + } + + set editorWidth(val: number) { + this.setAny("editorWidth", val); + } + + get fontPercent(): number { + return this.getNumber("fontPercent", 100); + } + + set fontPercent(val: number) { + this.setAny("fontPercent", val); + } + + get hideEditor(): boolean { + return this.getBool("hideEditor", false); + } + + set hideEditor(val: boolean) { + this.setAny("hideEditor", val); + } + + get scriptdir(): string { + return this.getString("scriptdir", "."); + } + + set scriptdir(val: string) { + this.setAny("scriptdir", val); + } + + get transparentNets(): boolean { + return this.getBool("transparentNets", false); + } + + set transparentNets(val: boolean) { + this.setAny("transparentNets", val); + } + + get zoomFonts(): boolean { + return this.getBool("zoomFonts", false); + } + + set zoomFonts(val: boolean) { + this.setAny("zoomFonts", val); + } + + restoreDefaults() { + Object.keys(this).forEach(option => { + this.storage.removeItem("ng." + option); + }); + } + + private getBool(key: string, defaultVal: boolean = null) { + const val = this.getString(key) || defaultVal; + return val === "true" || val === true; + } + + private getNumber(key: string, defaultVal: number = null) { + return Number(this.getString(key) || defaultVal); + } + + private getString(key: string, defaultVal: string = null) { + return this.storage.getItem(`ng.${key}`) || defaultVal; + } + + private setAny(key: string, val: any) { + if (this.getString(key) !== String(val)) { + this.storage.setItem(`ng.${key}`, val); + document.dispatchEvent( + new CustomEvent("nengoConfigChange", { + detail: key + }) + ); + } else { + console.log(`'${key}' already set to ${val}`); + } + } +} + +export const config = new Config(); + +// The following classes deal with the rendering of config items in the UI + +export class ConfigItem { + help: string; + key: string; + label: string; + update: (event: Event) => void; + + constructor( + key: string, + label: string, + update: (event: Event) => void = null, + help: string = null + ) { + this.help = help; + this.key = key; + this.label = label; + this.update = update; + if (this.update === null) { + this.update = (event: Event) => { + this.defaultUpdate(event); + }; + } + } + + defaultUpdate(event: Event) { + const el = event.target as HTMLElement; + const eltype = el.getAttribute("type"); + if (el instanceof HTMLInputElement && eltype === "checkbox") { + config[this.key] = el.checked; + } else if ( + el instanceof HTMLInputElement || + el instanceof HTMLSelectElement + ) { + config[this.key] = el.value; + } + } + + setView(element: HTMLInputElement | HTMLSelectElement) { + element.value = config[this.key]; + } +} + +export class CheckboxItem extends ConfigItem { + setView(element: HTMLInputElement | HTMLSelectElement) { + if (element instanceof HTMLInputElement) { + element.checked = config[this.key]; + } + } +} + +export class ComboboxItem extends ConfigItem { + options: string[]; + + constructor( + key: string, + label: string, + options: string[], + update: (event: Event) => void = null, + help: string = null + ) { + super(key, label, update, help); + this.options = options; + } +} + +export class TextItem extends ConfigItem { + attributes: any; + + constructor( + key: string, + label: string, + update: (event: Event) => void = null, + help: string = null, + attributes: any = {} + ) { + super(key, label, update, help); + this.attributes = attributes; + this.attributes["type"] = "text"; + } +} + +export class NumberItem extends TextItem { + unit: string; + + constructor( + key: string, + label: string, + unit: string = "", + update: (event: Event) => void = null, + help: string = null, + attributes: any = {} + ) { + super(key, label, update, help, attributes); + this.unit = unit; + this.attributes["type"] = "number"; + } +} + +export const configItems = [ + new NumberItem( + "fontPercent", + "Font size", + "%", + null, + "As a percentage of base size", + { + "data-error": "Must be within 20–999 percent base size", + max: 999, + maxlength: 3, + min: 20, + required: true, + step: 1 + } + ), + new CheckboxItem("zoomFonts", "Scale text when zooming"), + new CheckboxItem( + "aspectResize", + "Fix aspect ratio of elements on canvas resize" + ), + new CheckboxItem( + "autoUpdate", + "Automatically synchronize model with editor", + event => { + const el = event.target as HTMLInputElement; + config.autoUpdate = el.checked; + // Also modify editor.updateTrigger? + } + ), + new CheckboxItem("transparentNets", "Expanded networks are transparent"), + new TextItem( + "scriptdir", + "Script directory", + null, + "Enter a full absolute path, or '.' to use the current directory.", + { + placeholder: "Current directory" + } + ), + new ComboboxItem("backend", "Select backend", ["nengo"]) + // TODO: this.sim.simulatorOptions +]; + +export class ConfigDialog { + saved: any = {}; + view: ConfigDialogView = new ConfigDialogView(configItems); + + constructor() { + this.view.ok.addEventListener("click", () => { + const validator = $(this.view.form).data("bs.validator"); + validator.validate(); + if (validator.hasErrors() || validator.isIncomplete()) { + return; + } + // Set the data-dismiss attribute and let event propagate + $(this.view).modal("hide"); + }); + + this.view.cancel.addEventListener("click", () => { + // Roll back any changes + Object.keys(this.saved).forEach(option => { + if (config[option] !== this.saved[option]) { + config[option] = this.saved[option]; + } + }); + }); + + this.view.configItems.forEach((configItem, i) => { + // Allow the enter key to submit on text/number inputs + const inputType = configItem.getAttribute("type"); + if (inputType === "text" || inputType === "number") { + configItem.addEventListener( + "keydown", + (event: KeyboardEvent) => { + if (event.which === 13) { + event.preventDefault(); + this.view.ok.click(); + } + } + ); + } + // All inputs get updated live + configItem.addEventListener("change", event => { + const validator = $(this.view.form).data("bs.validator"); + validator.validate(); + if (!validator.hasErrors()) { + configItems[i].update(event); + } + }); + }); + + $(this.view.form).validator(); + } + + show() { + // Save values from before showing the modal for restoring after cancel + for (const option in config) { + const vType = typeof config[option]; + if ( + vType === "number" || + vType === "boolean" || + vType === "string" + ) { + this.saved[option] = config[option]; + } + } + + // Set values as of current config state + configItems.forEach((configItem, i) => { + configItem.setView(this.view.configItems[i]); + }); + const validator = $(this.view.form).data("bs.validator"); + validator.validate(); + this.view.show(); + } +} + +export class ConfigDialogView extends ModalView { + cancel: HTMLButtonElement; + configItems: (HTMLInputElement | HTMLSelectElement)[]; + form: HTMLFormElement; + ok: HTMLButtonElement; + + constructor(configItems: ConfigItem[]) { + super(); + + const checkboxGroup = (configItem: CheckboxItem) => { + return h("div.form-group.checkbox", [ + h("label.control-label", [ + h("input", { + type: "checkbox" + }), + configItem.label + ]), + h("span.help-block.with-errors", [configItem.help]) + ]); + }; + + const comboboxGroup = (configItem: ComboboxItem) => { + return h("div.form-group", [ + h("label.control-label", [ + configItem.label, + h("select.form-control", configItem.options) + ]), + h("span.help-block.with-errors", [configItem.help]) + ]); + }; + + const numberGroup = (configItem: NumberItem) => { + return h("div.form-group", [ + h("label.control-label", [ + configItem.label, + h("div.input-group.col-xs-3", [ + h("input.form-control", configItem.attributes), + h("span.input-group-addon", [configItem.unit]) + ]) + ]), + h("span.help-block.with-errors", [configItem.help]) + ]); + }; + + const textGroup = (configItem: TextItem) => { + return h("div.form-group", [ + h("label.control-label", [ + configItem.label, + h("input.form-control", configItem.attributes) + ]), + h("span.help-block.with-errors", [configItem.help]) + ]); + }; + + const node = h("form.form-horizontal", [ + configItems.map(configItem => { + if (configItem instanceof CheckboxItem) { + return checkboxGroup(configItem); + } else if (configItem instanceof ComboboxItem) { + return comboboxGroup(configItem); + } else if (configItem instanceof NumberItem) { + // Must check NumberItem first as it extends TextItem + return numberGroup(configItem); + } else if (configItem instanceof TextItem) { + return textGroup(configItem); + } else { + throw new TypeError("ConfigItem not recognized."); + } + }) + ]); + + this.form = dom.create(node).domNode as HTMLFormElement; + this.body.appendChild(this.form); + + this.title = "Configure options"; + this.ok = this.addFooterButton("OK"); + this.cancel = this.addCloseButton("Cancel"); + + const groups = this.body.getElementsByClassName("form-group"); + this.configItems = []; + for (let i = 0; i < groups.length; i++) { + this.configItems[i] = groups[i].querySelector("input"); + if (this.configItems[i] === null) { + this.configItems[i] = groups[i].querySelector("select"); + } + console.assert(this.configItems[i] !== null); + } + } + + show() { + $(this.root).modal("show"); + $(this.root).on("shown.bs.modal", () => { + this.configItems[0].focus(); + }); + } +} diff --git a/nengo_gui/static/data_to_csv.js b/nengo_gui/static/data_to_csv.js deleted file mode 100644 index 08ae8d4d..00000000 --- a/nengo_gui/static/data_to_csv.js +++ /dev/null @@ -1,68 +0,0 @@ -/** - * A function that returns simulation data as a csv, only saves data which is present in graphs. - * As well it only saves the data in the datastore, which is based on the - * amount of time kept in the simulation. - * - * @param {Nengo.Component.components} data_set: A list of the graph items in the simulation - * - */ - - -var data_to_csv = function(data_set){ - - var values = []; - var dim_values = []; - var times = []; - var csv = []; - var csv_string = ""; - - var data_set = data_set.filter(function(data){ - return (data.constructor === Nengo.Value || data.constructor === Nengo.XYValue) - }); - - //Extracts all the values from the data_set variable - for(var x = 0; x < data_set.length; x++){ - values.push([]); - for(var y = 0; y < data_set[x].data_store.data.length; y++){ - values[x].push(data_set[x].data_store.data[y]); - } - - } - //Grabs all the time steps - times = data_set[0].data_store.times; - - //Headers for the csv file - csv.push(["Graph Name"]); - csv.push(["Times"]); - - //Adds ensemble name and appropirate number of spaces to the header - for(var x = 0; x < data_set.length; x++){ - csv[0].push(data_set[x].label.innerHTML); - for(var z = 0; z < values[x].length-1; z++){ - csv[0].push([]); - } - - } - for(var x = 0; x < values.length; x++){ - for(var y = 0; y < values[x].length; y++){ - csv[1].push("Dimension"+(y+1)); - } - } - - //Puts the data at each time step into a row in the csv - for (var x = 0; x < times.length; x++){ - temp_arr = [times[x]] - for(var y = 0; y < values.length; y++){ - for(var z = 0; z < values[y].length; z++){ - temp_arr.push(values[y][z][x]) - } - } - csv.push(temp_arr); - } - - //Turns the array into a CSV string - csv.forEach(function(elem,index){csv[index]=elem.join(",");}); - csv_string = csv.join("\n"); - return csv_string; - -} diff --git a/nengo_gui/static/datastore.js b/nengo_gui/static/datastore.js deleted file mode 100644 index 9231e1fd..00000000 --- a/nengo_gui/static/datastore.js +++ /dev/null @@ -1,357 +0,0 @@ -/** - * Storage of a set of data points and associated times with a fixed - * number of dimensions. - * @constructor - * - * @param {int} dims - number of data points per time - * @param {Nengo.SimControl} sim - the simulation controller - * @param {float} synapse - the filter to apply to the data - */ - -Nengo.DataStore = function(dims, sim, synapse) { - this.synapse = synapse; /** TODO: get from Nengo.SimControl */ - this.sim = sim; - this.times = []; - this.data = []; - for (var i=0; i < dims; i++) { - this.data.push([]); - } -} - -/** - * Add a set of data. - * @param {array} row - dims+1 data points, with time as the first one - */ -Nengo.DataStore.prototype.push = function(row) { - /** if you get data out of order, wipe out the later data */ - if (row[0] < this.times[this.times.length - 1]) { - var index = 0; - while (this.times[index] < row[0]) { - index += 1; - } - - var dims = this.data.length; - this.times.splice(index, this.times.length); - for (var i=0; i < this.data.length; i++) { - this.data[i].splice(index, this.data[i].length); - } - } - - - /** compute lowpass filter (value = value*decay + new_value*(1-decay) */ - var decay = 0.0; - if ((this.times.length != 0) && (this.synapse > 0)) { - var dt = row[0] - this.times[this.times.length - 1]; - decay = Math.exp(-dt / this.synapse); - } - - - /** put filtered values into data array */ - for (var i = 0; i < this.data.length; i++) { - if (decay == 0.0) { - this.data[i].push(row[i + 1]); - } else { - this.data[i].push(row[i + 1] * (1-decay) + - this.data[i][this.data[i].length - 1] * decay); - } - } - /** store the time as well */ - this.times.push(row[0]); -}; - - -/** - * Reset the data storage. This will clear current data so there is - * nothing to display on a reset event. - */ -Nengo.DataStore.prototype.reset = function() { - var index = 0; - this.times.splice(index, this.times.length); - for (var i=0; i < this.data.length; i++) { - this.data[i].splice(index, this.data[i].length); - } -} - -/** - * update the data storage. This should be call periodically (before visual - * updates, but not necessarily after every push()). Removes old data outside - * the storage limit set by the Nengo.SimControl. - */ -Nengo.DataStore.prototype.update = function() { - /** figure out how many extra values we have (values whose time stamp is - * outside the range to keep) - */ - var extra = 0; - // how much has the most recent time exceeded how much is allowed to be kept - var limit = this.sim.time_slider.last_time - - this.sim.time_slider.kept_time; - while (this.times[extra] < limit) { - extra += 1; - } - - /** remove the extra data */ - if (extra > 0) { - this.times = this.times.slice(extra); - for (var i = 0; i < this.data.length; i++) { - this.data[i] = this.data[i].slice(extra); - } - } -} - -/** - * Return just the data that is to be shown - */ -Nengo.DataStore.prototype.get_shown_data = function() { - /* determine time range */ - var t1 = this.sim.time_slider.first_shown_time; - var t2 = t1 + this.sim.time_slider.shown_time; - - /* find the corresponding index values */ - var index = 0; - while (this.times[index] < t1) { - index += 1; - } - var last_index = index; - while (this.times[last_index] < t2 && last_index < this.times.length) { - last_index += 1; - } - this.first_shown_index = index; - - /** return the visible slice of the data */ - var shown = []; - for (var i = 0; i < this.data.length; i++) { - shown.push(this.data[i].slice(index, last_index)); - } - return shown; -} - -Nengo.DataStore.prototype.is_at_end = function() { - var ts = this.sim.time_slider; - return (ts.last_time < ts.first_shown_time + ts.shown_time + 1e-9); -} - -Nengo.DataStore.prototype.get_last_data = function() { - /* determine time range */ - var t1 = this.sim.time_slider.first_shown_time; - var t2 = t1 + this.sim.time_slider.shown_time; - - /* find the corresponding index values */ - var last_index = 0; - while (this.times[last_index] < t2 && last_index < this.times.length - 1) { - last_index += 1; - } - - /** return the visible slice of the data */ - var shown = []; - for (var i = 0; i < this.data.length; i++) { - shown.push(this.data[i][last_index]); - } - return shown; -} - -/** - * Storage of a set of data points and associated times with an increasable - * number of dimensions. - * @constructor - * - * @param {int} dims - number of data points per time - * @param {Nengo.SimControl} sim - the simulation controller - * @param {float} synapse - the filter to apply to the data - */ - -Nengo.GrowableDataStore = function(dims, sim, synapse){ - Nengo.DataStore.call(this, dims, sim, synapse); - this._dims = dims; - - Object.defineProperty(this, "dims", { - get: function(){ - return this._dims; - }, - set: function(dim_val){ - // throw a bunch of errors if bad things happen - // assuming you can only grow dims and not shrink them - if(this._dims < dim_val){ - for (var i=0; i < dim_val - this._dims; i++) { - this.data.push([]); - } - } else if(this._dims > dim_val) { - throw "can't decrease size of datastore"; - } - this._dims = dim_val; - } - }); - -} - -Nengo.GrowableDataStore.prototype = Object.create(Nengo.DataStore.prototype); -Nengo.GrowableDataStore.prototype.constructor = Nengo.GrowableDataStore; - -Nengo.GrowableDataStore.prototype.get_offset = function(){ - var offset = []; - offset.push(0); - - for (var i = 1; i < this._dims; i++){ - if(this.data[i] === undefined){ - offset.push(this.data[0].length); - } else { - offset.push(this.data[0].length - this.data[i].length); - } - } - - return offset -} - - -/** - * Add a set of data. - * @param {array} row - dims+1 data points, with time as the first one - */ -Nengo.GrowableDataStore.prototype.push = function(row) { - /** get the offsets */ - var offset = this.get_offset(); - - /** if you get data out of order, wipe out the later data */ - if (row[0] < this.times[this.times.length - 1]) { - var index = 0; - while (this.times[index] < row[0]) { - index += 1; - } - - this.times.splice(index, this.times.length); - for (var i=0; i < this._dims; i++) { - if(index - offset[i] >= 0){ - this.data[i].splice(index - offset[i], this.data[i].length); - } - } - } - - - /** compute lowpass filter (value = value*decay + new_value*(1-decay) */ - var decay = 0.0; - if ((this.times.length != 0) && (this.synapse > 0)) { - var dt = row[0] - this.times[this.times.length - 1]; - decay = Math.exp(-dt / this.synapse); - } - - - /** put filtered values into data array */ - for (var i = 0; i < this._dims; i++) { - if (decay == 0.0 || this.data[i].length === 0) { - this.data[i].push(row[i + 1]); - } else { - this.data[i].push(row[i + 1] * (1-decay) + - this.data[i][this.data[i].length - 1] * decay); - } - } - /** store the time as well */ - this.times.push(row[0]); -}; - -/* reset dimensions before resetting the datastore */ -Nengo.GrowableDataStore.prototype.reset = function() { - console.log("resetting growable"); - this._dims = 1; - Nengo.DataStore.call(this, this._dims, this.sim, this.synapse); -} -/** - * update the data storage. This should be call periodically (before visual - * updates, but not necessarily after every push()). Removes old data outside - * the storage limit set by the Nengo.SimControl. - */ -Nengo.GrowableDataStore.prototype.update = function() { - /** figure out how many extra values we have (values whose time stamp is - * outside the range to keep) - */ - var offset = this.get_offset(); - var extra = 0; - var limit = this.sim.time_slider.last_time - - this.sim.time_slider.kept_time; - while (this.times[extra] < limit) { - extra += 1; - } - - /** remove the extra data */ - if (extra > 0) { - this.times = this.times.slice(extra); - for (var i = 0; i < this.data.length; i++) { - if(extra - offset[i] >= 0){ - this.data[i] = this.data[i].slice(extra - offset[i]); - } - } - } -} - -/** - * Return just the data that is to be shown - */ -Nengo.GrowableDataStore.prototype.get_shown_data = function() { - var offset = this.get_offset(); - /* determine time range */ - var t1 = this.sim.time_slider.first_shown_time; - var t2 = t1 + this.sim.time_slider.shown_time; - - /* find the corresponding index values */ - var index = 0; - while (this.times[index] < t1) { - index += 1; - } - // logically, you should start the search for the - var last_index = index; - while (this.times[last_index] < t2 && last_index < this.times.length) { - last_index += 1; - } - this.first_shown_index = index; - - /** return the visible slice of the data */ - var shown = []; - var nan_number = 0; - var slice_start = 0; - for (var i = 0; i < this._dims; i++) { - - if(last_index > offset[i] && offset[i] !== 0){ - - if(index < offset[i]){ - nan_number = offset[i] - index; - slice_start = 0; - } else { - nan_number = 0; - slice_start = index - offset[i]; - } - - shown.push( - Array.apply(null, Array(nan_number)).map(function(){return "NaN"}).concat( - this.data[i].slice(slice_start, last_index - offset[i]) - ) - ); - - } else { - - shown.push(this.data[i].slice(index, last_index)); - - } - } - - return shown; -} - -Nengo.GrowableDataStore.prototype.get_last_data = function() { - var offset = this.get_offset(); - /* determine time range */ - var t1 = this.sim.time_slider.first_shown_time; - var t2 = t1 + this.sim.time_slider.shown_time; - - /* find the corresponding index values */ - var last_index = 0; - while (this.times[last_index] < t2 && last_index < this.times.length - 1) { - last_index += 1; - } - - /** return the visible slice of the data */ - var shown = []; - for (var i = 0; i < this._dims; i++) { - if(last_index - offset[i] >= 0){ - shown.push(this.data[i][last_index - offset[i]]); - } - } - return shown; -} diff --git a/nengo_gui/static/datastore.ts b/nengo_gui/static/datastore.ts new file mode 100644 index 00000000..9f0bcb3f --- /dev/null +++ b/nengo_gui/static/datastore.ts @@ -0,0 +1,183 @@ +import * as utils from "./utils"; + +export type TypedArray = + | Uint8Array + | Int8Array + | Uint8Array + | Uint8ClampedArray + | Int16Array + | Uint16Array + | Int32Array + | Uint32Array + | Float32Array + | Float64Array; + +/** + * Storage of a set of data points and associated times with a fixed + * number of dimensions. + * + * @constructor + * @param {int} dims - number of data points per time + * @param {SimControl} sim - the simulation controller + * @param {float} synapse - the filter to apply to the data + */ +export class DataStore { + data: number[][] = []; + times: number[] = []; + synapse: number; + + protected _dims: number; + + constructor(dims: number, synapse: number) { + this._dims = dims; + this.synapse = synapse; // TODO: get from SimControl + + // Listen for updates to the TimeSlider + window.addEventListener( + "TimeSlider.addTime", + utils.throttle((event: CustomEvent) => { + // How much has the most recent time exceeded how much is kept? + const limit = event.detail.timeCurrent - event.detail.keptTime; + const extra = DataStore.nearestIndex(this.times, limit); + + // Remove the extra data + if (extra > 0) { + this.remove(0, extra); + } + }, 200) // Only update once per 200 ms + ); + } + + get dims(): number { + return this._dims; + } + + set dims(val: number) { + const newDims = val - this._dims; + + if (newDims > 0) { + const nulls = utils.emptyArray(newDims).map(() => null); + this.data = this.data.map(row => row.concat(nulls)); + } else if (newDims < 0) { + console.warn(`Removed ${Math.abs(newDims)} dimension(s).`); + // + 2 because time is dim 0, and end is not included + this.data = this.data.map(row => row.slice(0, val + 2)); + } + this._dims = val; + } + + get length(): number { + return this.data.length; + } + + /** + * Returns the index in `array` at `element`, or before it. + * + * This is helpful for determining where to insert an element, + * and for finding indices between two elements. + * + * This method uses binary search to be much faster than the naive + * approach of iterating through the array in order. + * Note that this assumes that the array is sorted, but that is also true + * if you were to search through it linearly. + * + * Returns 0 if element is less than all elements in array. + */ + static nearestIndex(array: number[], element: number) { + let [low, high] = [0, array.length]; + + while (high > low) { + // Note: | 0 is a faster Math.floor + const ix = ((high + low) / 2) | 0; + + if (array[ix] <= element) { + if (array[ix + 1] > element || ix + 1 === array.length) { + return ix; + } else { + low = ix + 1; + } + } else { + high = ix; + } + } + console.assert(low === 0 && high === 0); + return 0; + } + + /** + * Add a row of data. + * + * @param {array} row - dims+1 data points, with time as the first one + */ + add(row: number[] | TypedArray) { + console.assert(row.length - 1 === this.dims); + const time = row[0]; + // If we get data out of order, wipe out the later data + if (time < this.times[this.times.length - 1]) { + this.remove(DataStore.nearestIndex(this.times, time)); + } + + // Compute lowpass filter (value = value*decay + newValue*(1-decay) + let decay = 0.0; + if (this.times.length > 0 && this.synapse > 0) { + const dt = time - this.times[this.times.length - 1]; + decay = Math.exp(-dt / this.synapse); + } + + // Filter new data + const newdata = [time]; + const lastdata = this.data[this.data.length - 1]; + for (let i = 1; i < row.length; i++) { + if (lastdata == null || lastdata[i] == null || decay <= 0.0) { + newdata.push(row[i]); + } else { + newdata.push(row[i] * (1 - decay) + lastdata[i] * decay); + } + } + + this.data.push(newdata); + // Also keep a separate times list (for fast lookups) + this.times.push(time); + } + + at(time: number) { + return this.data[DataStore.nearestIndex(this.times, time)]; + } + + /** + * Reset the data storage. + * + * This will clear current data so there is + * nothing to display on a reset event. + */ + reset() { + this.remove(0); + } + + remove(start: number, deleteCount?: number) { + // TODO: try to remove this weird if statement + if (deleteCount == null) { + this.data.splice(start); + this.times.splice(start); + } else { + this.data.splice(start, deleteCount); + this.times.splice(start, deleteCount); + } + } + + slice(beginIndex: number, endIndex?: number) { + if (endIndex == null) { + return this.data.slice(beginIndex); + } else { + return this.data.slice(beginIndex, endIndex); + } + } + + timeSlice(beginTime: number, endTime?: number) { + const beginIndex = DataStore.nearestIndex(this.times, beginTime); + const endIndex = endTime + ? DataStore.nearestIndex(this.times, endTime) + 1 + : undefined; + return this.slice(beginIndex, endIndex); + } +} diff --git a/nengo_gui/static/debug/items.ts b/nengo_gui/static/debug/items.ts new file mode 100644 index 00000000..ec12cc14 --- /dev/null +++ b/nengo_gui/static/debug/items.ts @@ -0,0 +1,190 @@ +import { MockConnection } from "../server"; + +// main +import { ConfigDialog, configItems } from "../config"; +import { Editor } from "../editor"; +import { HotkeyManager } from "../hotkeys"; +import { Menu } from "../menu"; +import { NetGraph } from "../netgraph/main"; +import { Sidebar } from "../sidebar"; +import { SimControl } from "../sim-control"; +import { Toolbar } from "../toolbar"; + +// views +import { ConfigDialogView } from "../config"; +import { NetGraphView } from "../netgraph/view"; +import { EditorView } from "../editor"; +import { HotkeysDialogView } from "../hotkeys"; +import { MenuView } from "../menu"; +import { AlertDialogView, InputDialogView, ModalView } from "../modal"; +import { SidebarView } from "../sidebar"; +import { SimControlView } from "../sim-control"; +import { ToolbarView } from "../toolbar"; + +// components +import { Ensemble } from "../components/ensemble"; +import { Network } from "../components/network"; +import { Node, PassthroughNode } from "../components/node"; +import { Position } from "../components/position"; +import { Raster } from "../components/raster"; +import { Slider } from "../components/slider"; +import { Value } from "../components/value"; +import { XYValue } from "../components/xyvalue"; + +export const listeners = { + ConfigDialog: null +}; + +export const main = { + ConfigDialog: () => { + const cd = new ConfigDialog(); + if (this.listeners.ConfigDialog === null) { + this.listeners.ConfigDialog = (e: CustomEvent) => { + console.log(e.detail + " changed"); + }; + document.addEventListener( + "nengoConfigChange", + this.listeners.ConfigDialog + ); + } + cd.show(); + return cd; + }, + Editor: () => { + return new Editor(new MockConnection()); + }, + Menu: () => { + const menu = new Menu(); + menu.addAction("Action 1.1", () => console.log("Action 1.1")); + menu.addHeader("Hidden"); + menu.addAction("Hidden", () => false, () => false); + menu.addSeparator(); + menu.addAction("Action 2.1", () => console.log("Action 2.1")); + menu.show(0, 0); + return menu; + }, + Sidebar: () => new Sidebar(new MockConnection()), + SimControl: () => new SimControl(new MockConnection(), 4.0, 1.0), + Toolbar: () => { + const tb = new Toolbar(new MockConnection()); + tb.filename = "test.py"; + return tb; + } +}; + +export const view = { + AlertDialogView: () => { + const a = new AlertDialogView("Test text"); + a.show(); + return a; + }, + ConfigDialogView: () => { + const cd = new ConfigDialogView(configItems); + cd.show(); + return cd; + }, + EditorView: () => { + return new EditorView(); + }, + HotkeysDialogView: () => { + const m = new HotkeyManager(new MockConnection()); + m.add("Test ctrl", "a", { ctrl: true }, () => {}); + m.add("Test shift", "b", { shift: true }, () => {}); + m.add("Test both", "c", { ctrl: true, shift: true }, () => {}); + const hk = new HotkeysDialogView(m.hotkeys); + hk.show(); + return hk; + }, + InputDialogView: () => { + const i = new InputDialogView("0.5", "Test label"); + i.show(); + return i; + }, + MenuView: () => { + const menu = new MenuView(); + menu.addAction("Action 1"); + menu.addHeader("Subactions"); + menu.addAction("Action 1.1"); + menu.addAction("Action 1.2"); + menu.addSeparator(); + menu.addAction("Action 2.1"); + return menu; + }, + ModalView: () => { + const mv = new ModalView(); + mv.show(); + return mv; + }, + ToolbarView: () => new ToolbarView(), + SidebarView: () => new SidebarView(), + SimControlView: () => new SimControlView() +}; + +export const component = { + Ensemble: () => + new Ensemble({ + server: new MockConnection(), + uid: "Ensemble", + label: "Ensemble", + pos: new Position(20, 20, 50, 50), + dimensions: 1 + }), + Network: () => + new Network({ + server: new MockConnection(), + uid: "Network", + label: "Network", + pos: new Position(20, 20, 50, 50) + }), + Node: () => + new Node({ + server: new MockConnection(), + uid: "Node", + label: "Node", + pos: new Position(20, 20, 50, 50), + dimensions: 1 + }), + PassthroughNode: () => + new PassthroughNode({ + server: new MockConnection(), + uid: "Passthrough", + label: "Passthrough", + pos: new Position(20, 20) + }), + Raster: () => + new Raster({ + server: new MockConnection(), + uid: "Raster", + label: "Raster", + pos: new Position(20, 20, 100, 100), + nNeurons: 2, + synapse: 0.005 + }), + Slider: () => + new Slider({ + server: new MockConnection(), + uid: "Slider", + label: "Slider", + pos: new Position(20, 20, 100, 100), + dimensions: 2, + synapse: 0.005 + }), + Value: () => + new Value({ + server: new MockConnection(), + uid: "Value", + label: "Value", + pos: new Position(20, 20, 100, 100), + dimensions: 2, + synapse: 0.005 + }), + XYValue: () => + new XYValue({ + server: new MockConnection(), + uid: "XY Value", + label: "XY Value", + pos: new Position(20, 20, 100, 100), + dimensions: 2, + synapse: 0.005 + }) +}; diff --git a/nengo_gui/static/debug/main.ts b/nengo_gui/static/debug/main.ts new file mode 100644 index 00000000..ebbe07cb --- /dev/null +++ b/nengo_gui/static/debug/main.ts @@ -0,0 +1,181 @@ +/** + * Entry point into the Nengo debug application. + */ + +import "awesomplete/awesomplete.css"; +import * as Awesomplete from "awesomplete"; +import "bootstrap/dist/css/bootstrap.min.css"; +import "imports-loader?$=jquery,jQuery=jquery!bootstrap"; +import "imports-loader?$=jquery,jQuery=jquery!bootstrap-validator"; +import "imports-loader?$=jquery,jQuery=jquery!jquery-ui"; +import "imports-loader?$=jquery,jQuery=jquery!jqueryfiletree/src/jQueryFileTree"; + +import "../favicon.ico"; + +import * as items from "./items"; +import { DebugItem, NengoDebug, NengoWindow } from "../main"; +import { Network } from "../components/network"; +import { NetGraph } from "../netgraph/main"; +import { MockConnection } from "../server"; +import { DebugView } from "./view"; + +export class CommandHistory { + history: string[]; + label: string; + toSave = 0; + static autoSaveThreshold = 1; + static keyPrefix = "ngdebug.history"; + + constructor(label: string) { + this.label = label; + const fromStorage = localStorage.getItem(this.key); + if (fromStorage === null) { + this.history = []; + } else { + this.history = JSON.parse(fromStorage); + } + } + + get key(): string { + return CommandHistory.keyPrefix + "." + this.label; + } + + add(command: string) { + if (this.history.indexOf(command) < 0) { + this.history.push(command); + this.toSave += 1; + } + // We expect that save will be called manually, but just in case, + // we autosave once we have a certain number of new commands. + if (this.toSave > CommandHistory.autoSaveThreshold) { + this.save(); + } + } + + save() { + localStorage.setItem(this.key, JSON.stringify(this.history)); + this.toSave = 0; + } +} + +export class Debug { + nengoWindow: NengoWindow; + view: DebugView = new DebugView(); + + constructor() { + this.view.iframe.addEventListener("load", () => { + this.nengoWindow = this.view.iframe.contentWindow as NengoWindow; + + this.view.outline.onclick = () => { + this.nengoDebug.toggleOutline(); + }; + this.view.log.onclick = () => { + this.nengoDebug.toggleLog(); + }; + + const attach = (category: string) => { + const obj = items[category]; + Object.keys(obj).forEach(label => { + const clickable = this.view.register(category, label); + + clickable.onclick = () => { + const item = this.nengoDebug.add(category, label); + this.attachControlGroup(item); + this.nengoWindow.dispatchEvent(new Event("resize")); + }; + }); + }; + attach("main"); + attach("view"); + attach("component"); + }); + } + + get nengoDebug() { + return this.nengoWindow.nengoDebug; + } + + attachControlGroup(item: DebugItem) { + const group = this.view.addControlGroup(item.name); + if (item.category === "component") { + const connectButton = group.addConnectButton(); + connectButton.addEventListener("click", event => { + const ng = this.nengoDebug.netgraph; + const randomObj = + ng.components.components[ + Math.floor(Math.random() * ng.components.length) + ]; + console.log(`Connecting to ${randomObj.uid}`); + ng.connect(item.obj, randomObj); + }); + } + + // Add autocomplete for the text input + const inputHistory = new CommandHistory(item.name); + const autocomplete = new Awesomplete(group.input, { + list: inputHistory.history, + minChars: 1, + maxItems: 4 + }); + Object.getOwnPropertyNames(item.obj).forEach(key => { + inputHistory.add("obj." + key); + }); + + // Eval JS when pressing enter or clicking on eval button + const evalView = () => { + const js: string = group.input.value; + if (js !== "") { + const out = item.eval(js); + group.input.value = ""; + group.evalOutput.textContent = out; + inputHistory.add(js); + autocomplete.list = inputHistory.history; + } + }; + group.evalButton.addEventListener("click", () => { + evalView(); + }); + + // TODO: hijack console.log + // stackoverflow.com/questions/11403107/ + + group.input.addEventListener("keypress", event => { + if (event.key.toLowerCase() === "enter") { + evalView(); + return false; + } + }); + group.removeButton.addEventListener("click", event => { + inputHistory.save(); + this.nengoDebug.remove(item); + this.view.removeControlGroup(group); + }); + } +} + +if (typeof document !== "undefined") { + document.addEventListener("DOMContentLoaded", () => { + const debug = new Debug(); + document.body.appendChild(debug.view.root); + }); +} + +// TODO + +// import { NetGraph } from "./netgraph"; + +// /* tslint:disable:no-console */ +// document.addEventListener("DOMContentLoaded", () => { +// const netg = new NetGraph("test"); +// document.body.appendChild(netg.view.root); +// netg.view.onResize(null); +// console.assert(netg.view.width !== 0); +// console.assert(netg.view.height !== 0); +// netg.createNode( +// {ng: netg, width: 0.2, height: 0.2, posX: 0.5, posY: 0.5, +// parent: null, uid: "node2"}, +// {miniItem: 1, label: "test_node"}, 1, null); +// console.log("stuff is loaded"); +// }); + +// obj.createNode({ng: obj, width: 0.2, height: 0.2, posX: 0.5, posY: 0.5, parent: null, uid: "node2"}, {miniItem: 1, label: "test_node"}, 1, null); diff --git a/nengo_gui/static/debug/view.css b/nengo_gui/static/debug/view.css new file mode 100644 index 00000000..492bb0a5 --- /dev/null +++ b/nengo_gui/static/debug/view.css @@ -0,0 +1,86 @@ +:root { + --light: gray(94%); + --dark: gray(60%); +} + +html, body { + height: 100%; + width: 100%; + margin: 0; +} + +.debug { + height: 100%; + width: 100%; + + iframe { + border: none; + height: calc(100% - 200px); /* 200px for div.debug-controls */ + width: 100%; + } + + &-controls { + background-color: var(--light); + border: 1px var(--dark) solid { + radius: 5px 5px 0 0; + } + bottom: 0; + display: flex; + height: 200px; + left: 0; + position: absolute; + width: 100%; + + input { + font-family: Menlo,Monaco,Consolas,"Courier New",monospace; + } + + .awesomplete { + display: block; + + .visually-hidden { + display: none; + } + + > ul { + font-family: Menlo,Monaco,Consolas,"Courier New",monospace; + top: 38px; + } + } + + .btn { + margin-bottom: 4px; + .caret { + margin-left: 4px; + } + } + + .btn-block + .btn-block { + margin-top: 0; + } + + .control-group { + display: flex; + border-right: 1px var(--dark) solid; + flex-direction: column; + padding: { + left: 8px; + right: 8px; + top: 8px; + } + + p { + display: inline-block; + } + + /* &.meta { */ + /* width: 60px; */ + /* } */ + + &.last { + border-right: none; + flex-grow: 1; + } + } + } +} diff --git a/nengo_gui/static/debug/view.ts b/nengo_gui/static/debug/view.ts new file mode 100644 index 00000000..dcde27bf --- /dev/null +++ b/nengo_gui/static/debug/view.ts @@ -0,0 +1,150 @@ +import { VNode, dom, h } from "maquette"; + +import "./view.css"; + +export class ControlGroupView { + evalButton: HTMLButtonElement; + evalOutput: HTMLElement; + input: HTMLInputElement; + removeButton: HTMLButtonElement; + root: HTMLDivElement; + + constructor(label: string) { + const node = h("div.control-group.last", [ + h("div", [ + h("p", [h("code", [`var obj = new ${label}(...);`])]), + h("button.btn.btn-xs.btn-default.pull-right#remove", [ + `Remove ${label}` + ]) + ]), + h("div.input-group", [ + h("input.form-control", { + spellcheck: false, + type: "text" + }), + h("span.input-group-btn", [ + h("button.btn.btn-default#eval", { type: "button" }, [ + "Eval JS" + ]) + ]) + ]), + h("div", [ + h("p", [h("code", [h("span.glyphicon.glyphicon-console")])]), + h("p", [h("code#output")]) + ]) + ]); + this.root = dom.create(node).domNode as HTMLDivElement; + this.input = this.root.querySelector("input") as HTMLInputElement; + this.evalButton = this.root.querySelector("#eval") as HTMLButtonElement; + this.evalOutput = this.root.querySelector("#output") as HTMLElement; + this.removeButton = this.root.querySelector( + "#remove" + ) as HTMLButtonElement; + } + + addConnectButton() { + const node = h("button.btn.btn-xs.btn-default.pull-right#connect", [ + "Connect to random object" + ]); + const button = dom.create(node).domNode as HTMLButtonElement; + this.root.firstChild.appendChild(button); + return button; + } +} + +export class DebugView { + iframe: HTMLIFrameElement; + log: HTMLButtonElement; + outline: HTMLButtonElement; + root: HTMLDivElement; + private controls: HTMLDivElement; + private menus: { [id: string]: HTMLDivElement }; + + constructor() { + const button = ( + id: string, + icon: string, + { active: active = false } = {} + ): VNode => { + let activeClass = ""; + if (active) { + activeClass = ".active"; + } + + return h( + `button.btn.btn-default.btn-block${activeClass}#${id}`, + { + autocomplete: "off", + "data-toggle": "button", + type: "button" + }, + [h(`span.glyphicon.glyphicon-${icon}`)] + ); + }; + const menu = (id: string, label: string = ""): VNode => { + let text = `Add ${label}`; + if (label === "") { + text = "Add"; // Remove the trailing space + } + return h(`div.dropup#${id}`, [ + h( + "button.btn.btn-default.btn-block.dropdown-toggle", + { + "data-toggle": "dropdown", + type: "button" + }, + [text, h("span.caret")] + ), + h("ul.dropdown-menu") + ]); + }; + + const node = h("div.debug", [ + h("div.debug-controls", [ + h("div.control-group", [ + button("outline", "th"), + button("log", "info-sign", { active: true }) + ]), + h("div.control-group", [ + menu("main"), + menu("view", "View"), + menu("component", "Component"), + ]) + ]), + h("iframe", { src: "nengo.html" }) + ]); + this.root = dom.create(node).domNode as HTMLDivElement; + this.iframe = this.root.querySelector("iframe") as HTMLIFrameElement; + this.controls = this.root.querySelector( + ".debug-controls" + ) as HTMLDivElement; + this.outline = this.controls.querySelector( + "#outline" + ) as HTMLButtonElement; + this.log = this.controls.querySelector("#log") as HTMLButtonElement; + this.menus = { + component: this.controls.querySelector( + "#component" + ) as HTMLDivElement, + main: this.controls.querySelector("#main") as HTMLDivElement, + view: this.controls.querySelector("#view") as HTMLDivElement + }; + } + + addControlGroup(label: string) { + const group = new ControlGroupView(label); + this.controls.appendChild(group.root); + return group; + } + + register(category: string, typeName: string) { + const node = h("li", [h("a", { href: "#" }, [typeName])]); + const root = dom.create(node).domNode; + this.menus[category].querySelector(".dropdown-menu").appendChild(root); + return root.querySelector("a") as HTMLAnchorElement; + } + + removeControlGroup(group: ControlGroupView) { + this.controls.removeChild(group.root); + } +} diff --git a/nengo_gui/static/details.ts b/nengo_gui/static/details.ts new file mode 100644 index 00000000..91d9b8ba --- /dev/null +++ b/nengo_gui/static/details.ts @@ -0,0 +1,499 @@ +import * as d3 from "d3"; +import * as $ from "jquery"; +import { VNode, dom, h } from "maquette"; + +import { config } from "./config"; +import { InputDialogView, ModalView } from "./modal"; +import * as utils from "./utils"; +import * as tooltips from "./tooltips"; + +export class DetailsDialogView extends ModalView { + contents: HTMLDivElement; + tabs: HTMLUListElement; + + constructor() { + super(); + + const tabs = h("ul.nav.nav-tabs"); + + const content = h("div.tab-content"); + + this.tabs = dom.create(tabs).domNode as HTMLUListElement; + this.contents = dom.create(content).domNode as HTMLDivElement; + this.body.appendChild(this.tabs); + this.body.appendChild(this.contents); + } + + addConnectionsTab(netgraphitem, conninfo) { + const tab = this.addTab("connections", "Connections"); + + const row = (objs, getConnOther, getConnConnUidList) => + objs.map(obj => { + // Get a handle to the object that we're connected to + const connOther = getConnOther(obj); + + // Make a row in the table + const tr = h("tr", [ + h( + "td", + (>[ + String(connOther.label.innerHTML) + ]).concat( + dropdown( + connOther.uid, + conninfo.objType[String(obj.uid)], + getConnConnUidList(obj) + ) + ) + ), + h("td", [conninfo.func[String(obj.uid)]]), + h("td", [ + conninfo.fan[String(obj.uid)], + conninfo.objType[String(obj.uid)] === "passthrough" + ? utils.bsTooltip(tooltips.Conn.fan_passthrough[0]) + : null + ]) + ]); + }); + + const dropdown = (uid, objType, connUidList) => { + const nodes: VNode[] = []; + if (connUidList.length > 1) { + const slug = String(connUidList[0]).replace(/[\.\[\]]/g, "_"); + + // Populate the list-group + for (let p = connUidList.length - 1; p >= 0; p--) { + let endpointIcon = "glyphicon glyphicon-triangle-right"; + let shadedOption = "shaded"; + let pathItem: string; + + const svgObjects: any = {}; + // svgObjects = this.netgraph.svgObjects + + if (connUidList[p] in svgObjects) { + // If the uid is in ng.svgObjects, use the obj's label + pathItem = svgObjects[connUidList[p]].label.innerHTML; + } else { + // Otherwise, use the object's uid (with brackets to + // indicate that the UI is unsure of the exact label) + pathItem = "(" + String(connUidList[p]) + ")"; + } + + if (uid === connUidList[p]) { + // Toggle the shading option when othersUid is reached + shadedOption = ""; + } + + if (p === 0) { + if (objType === "ens") { + endpointIcon = + "glyphicon glyphicon-option-horizontal"; + } else if (objType === "node") { + endpointIcon = "glyphicon glyphicon-stop"; + } else if (objType === "passthrough") { + endpointIcon = "glyphicon glyphicon-share-alt"; + } else if (objType === "net") { + endpointIcon = "glyphicon glyphicon-list-alt"; + } else { + endpointIcon = "glyphicon glyphicon-warning-sign"; + } + } + + nodes.push( + h("li.list-group-item." + shadedOption, [ + h("span." + endpointIcon), + pathItem + ]) + ); + } + + nodes.push( + h( + "a", + { + "aria-expanded": false, + "data-toggle": "collapse", + href: "#pathlist" + slug + }, + [utils.bsTooltip(tooltips.Conn.expand[0], "right")] + ) + ); + + nodes.push( + h("div.collapse#pathlist" + slug, [ + h("ul.list-group", [ + h("li.list-group-item.shaded", [ + h("span.glyphicon.glyphicon-home"), + "Model" + ]), + nodes + ]) + ]) + ); + } + + return nodes; + }; + + const connInObjs = netgraphitem.connIn; + if (connInObjs.length > 0) { + const section = h("h3", ["Incoming connections"]); + + const table = h("table.table.table-condensed", [ + h("tr", [ + h("th.conn-objs", [ + "Object", + utils.bsPopover( + "'Pre' object", + "This object plays the role of 'Pre' in the " + + "connection to this object.", + "top" + ) + ]), + h("th.conn-funcs", [ + "Function", + utils.bsPopover( + "Connection function", + "The function being computed across this " + + "connection (in vector space).", + "top" + ) + ]), + h("th.conn-fan", [ + "Fan in", + utils.bsPopover( + "Neuron fan-in", + "The number of incoming neural connections. " + + "In biological terms, this is the " + + "maximum number of synapses in the " + + "dendritic tree of a single neuron in " + + "this object, resulting from this " + + "connection. The total number of " + + "synapses would be the sum of the " + + "non-zero numbers in this column.", + "top" + ) + ]) + ]), + row(connInObjs, connObj => connObj.pre, connObj => connObj.pres) + ]); + tab.appendChild(dom.create(section).domNode); + tab.appendChild(dom.create(table).domNode); + } + + const connOutObjs = netgraphitem.connOut; + if (connOutObjs.length > 0) { + if (connInObjs.length > 0) { + tab.appendChild(dom.create(h("hr")).domNode); + } + const section = h("h3", ["Outgoing connections"]); + const table = h("table.table.table-condensed", [ + h("tr", [ + h("th.conn-objs", [ + "Object", + utils.bsPopover( + "'Post' object", + "This object plays the role of 'Post' in " + + "the connection from this object.", + "top" + ) + ]), + h("th.conn-funcs", [ + "Function", + utils.bsPopover( + "Connection function", + "The function being computed across this " + + "connection (in vector space).", + "top" + ) + ]), + h("th.conn-fan", [ + "Fan out", + utils.bsPopover( + "Neuron fan-out", + "The number of outgoing neural connections. " + + "In biological terms, this is the " + + "maximum number of synapses from axon " + + "terminals of a single neuron in this " + + "object, resulting from this " + + "connection. The total number of " + + "synapses would be the sum of the " + + "non-zero numbers in this column.", + "top" + ) + ]) + ]), + row( + connOutObjs, + connObj => connObj.post, + connObj => connObj.posts + ) + ]); + + tab.appendChild(dom.create(section).domNode); + tab.appendChild(dom.create(table).domNode); + } + + if (connInObjs.length === 0 && connOutObjs.length === 0) { + let warntext = "No connections to or from this object."; + if (netgraphitem.type === "net" && netgraphitem.expanded) { + warntext = + "Network is expanded. Please see individual " + + "objects for connection info."; + } + + const warn = utils.bsAlert("Warning: " + warntext, "warning"); + tab.appendChild(dom.create(warn).domNode); + } + + return tab; + } + + addParamTab(params, strings) { + const tab = this.addTab("params", "Parameters"); + + const node = h( + "dl.dl-horizontal", + [].concat.apply( + [], + params.map(param => [ + h("dt", [ + param[0], + utils.bsTooltip(param[0], strings[param[0]]) + ]), + h("dd", [param[1]]) + ]) + ) + ); + tab.appendChild(dom.create(node).domNode); + + return tab; + } + + addPlotsTab(plots) { + const tab = this.addTab("plots", "Plots"); + + // This indicates an error (usually no sim running) + if (typeof plots === "string") { + const err = utils.bsAlert("Error: " + plots, "danger"); + tab.appendChild(dom.create(err).domNode); + } else { + plots.forEach(plot => { + const section = h("h4", [plot.title]); + plot.warnings.forEach(warn => + utils.bsAlert("Warning: " + warn, "warning") + ); + if (plot.plot === "multiline") { + tab.appendChild( + lines(plot.x, plot.y, plot.xLabel, plot.yLabel) + ); + } else if (plot.plot !== "none") { + console.warn( + "Plot type " + + plot.plot + + " not understood or not implemented yet." + ); + } + }); + } + + return tab; + } + + addTab(id: string, label: string): HTMLDivElement { + const tab = h("li", [ + h("a", { href: "#" + id, "data-toggle": "tab" }, [label]) + ]); + + const node = h("div.tab-pane#" + id); + + const firstTab = this.tabs.childElementCount === 0; + + this.tabs.appendChild(dom.create(tab).domNode); + const content = dom.create(node).domNode as HTMLDivElement; + this.contents.appendChild(content); + return content; + } + + show() { + // If nothing set as active, set the first tab as active + if (this.tabs.querySelector(".active") === null) { + (this.tabs.firstChild).classList.add("active"); + (this.contents.firstChild).classList.add("active"); + } + + // Activate all tooltips and popovers + utils.bsActivateTooltips(this.body); + utils.bsActivatePopovers(this.body); + super.show(); + } +} + +export class EnsembleDialogView extends DetailsDialogView { + connections: HTMLDivElement; + params: HTMLDivElement; + plots: HTMLDivElement; + + constructor() { + super(); + + this.params = this.addParamTab(null, null); + this.plots = this.addPlotsTab(null); + this.connections = this.addConnectionsTab(null, null); + } +} + +export class NodeDialogView extends DetailsDialogView { + connections: HTMLDivElement; + params: HTMLDivElement; + plots: HTMLDivElement; + + constructor() { + super(); + + this.params = this.addParamTab(null, null); + this.plots = this.addPlotsTab(null); + this.connections = this.addConnectionsTab(null, null); + } +} + +export class NetworkDialogView extends DetailsDialogView { + connections: HTMLDivElement; + stats: HTMLDivElement; + + constructor() { + super(); + + this.stats = this.addStatisticsTab(null); + this.connections = this.addConnectionsTab(null, null); + } + + addStatisticsTab(stats): HTMLDivElement { + const tab = this.addTab("stats", "Statistics"); + + stats.forEach(stat => { + const section = h("h3", [stat.title]); + const table = h( + "table.table.table-condensed.table-hover", + stat.stats.map(statstat => + h("tr", [ + h("td.col-md-8", [statstat[0]]), + h("td.col-md-4", [statstat[1]]) + ]) + ) + ); + tab.appendChild(dom.create(section).domNode); + tab.appendChild(dom.create(table).domNode); + }); + + return tab; + } +} + +export function lines( + x: number[], + ys: number[][], + xLabel: string, + yLabel: string, + width: number = 500, + height: number = 220 +): SVGSVGElement { + const margin = { bottom: 50, left: 75, right: 0, top: 10 }; + const w = width - margin.left - margin.right; + const h = height - margin.bottom - margin.top; + const graphW = w + margin.left + margin.right; + const graphH = h + margin.bottom + margin.top; + const textOffset = 15; + + const scaleX = d3.scale + .linear() + .domain([x[0], x[x.length - 1]]) + .range([margin.left, w - margin.right]); + const scaleY = d3.scale + .linear() + .domain([ + d3.min(ys, y => { + return d3.min(y); + }) - 0.01, + d3.max(ys, y => { + return d3.max(y); + }) + 0.01 + ]) + .range([h + margin.top, margin.top]); + + // Create an SVG element with the desired dimensions and margin. + const svg = d3.select(document.createElementNS(d3.ns.prefix["svg"], "svg")); + const graph = svg.attr("width", graphW).attr("height", graphH); + + // Create the axes + const xAxis = d3.svg + .axis() + .scale(scaleX) + .orient("bottom") + .ticks(9); + graph + .append("g") + .attr("class", "axis axisX unselectable") + .attr("transform", "translate(0," + (h + margin.top) + ")") + .call(xAxis); + + const yAxisLeft = d3.svg + .axis() + .scale(scaleY) + .ticks(5) + .orient("left"); + graph + .append("g") + .attr("class", "axis axisY unselectable") + .attr("transform", "translate(" + margin.left + ",0)") + .call(yAxisLeft); + + // Label the axes + if (xLabel !== "") { + svg + .append("text") + .attr("class", "x label") + .attr("text-anchor", "middle") + .attr("x", graphW / 2) + .attr("y", textOffset + graphH - margin.bottom / 2) + .text(xLabel); + } + + if (yLabel !== "") { + svg + .append("text") + .attr("class", "y label") + .attr("text-anchor", "middle") + .attr("x", -graphH / 2) + .attr("y", -textOffset + margin.left / 2) + .attr("dy", ".75em") + .attr("transform", "rotate(-90)") + .text(yLabel); + } + + // Add the lines + const colors = utils.makeColors(ys.length); + + const line = d3.svg + .line() + .x((d, i) => { + return scaleX(x[i]); + }) + .y(d => { + return scaleY(d); + }); + + graph + .append("g") + .selectAll("path") + .data(ys) + .enter() + .append("path") + .attr("d", line) + .attr("class", "line") + .style("stroke", (d, i) => { + return colors[i]; + }); + + return svg.node() as SVGSVGElement; +} diff --git a/nengo_gui/static/lib/fonts/glyphicons-halflings-regular.woff2 b/nengo_gui/static/dist/448c34a56d699c29117adc64c43affeb.woff2 similarity index 100% rename from nengo_gui/static/lib/fonts/glyphicons-halflings-regular.woff2 rename to nengo_gui/static/dist/448c34a56d699c29117adc64c43affeb.woff2 diff --git a/nengo_gui/static/lib/fonts/glyphicons-halflings-regular.svg b/nengo_gui/static/dist/89889688147bd7575d6327160d64e760.svg similarity index 100% rename from nengo_gui/static/lib/fonts/glyphicons-halflings-regular.svg rename to nengo_gui/static/dist/89889688147bd7575d6327160d64e760.svg diff --git a/nengo_gui/static/lib/fonts/glyphicons-halflings-regular.ttf b/nengo_gui/static/dist/e18bbf611f2a2e43afc071aa2f4e1512.ttf similarity index 100% rename from nengo_gui/static/lib/fonts/glyphicons-halflings-regular.ttf rename to nengo_gui/static/dist/e18bbf611f2a2e43afc071aa2f4e1512.ttf diff --git a/nengo_gui/static/lib/fonts/glyphicons-halflings-regular.eot b/nengo_gui/static/dist/f4769f9bdb7466be65088239c12046d1.eot similarity index 100% rename from nengo_gui/static/lib/fonts/glyphicons-halflings-regular.eot rename to nengo_gui/static/dist/f4769f9bdb7466be65088239c12046d1.eot diff --git a/nengo_gui/static/lib/fonts/glyphicons-halflings-regular.woff b/nengo_gui/static/dist/fa2772327f55d8198301fdb8bcfc8158.woff similarity index 100% rename from nengo_gui/static/lib/fonts/glyphicons-halflings-regular.woff rename to nengo_gui/static/dist/fa2772327f55d8198301fdb8bcfc8158.woff diff --git a/nengo_gui/static/dist/favicon.ico b/nengo_gui/static/dist/favicon.ico new file mode 100644 index 00000000..e221c47d Binary files /dev/null and b/nengo_gui/static/dist/favicon.ico differ diff --git a/nengo_gui/static/dist/nengo.js b/nengo_gui/static/dist/nengo.js new file mode 100644 index 00000000..5d77cff7 --- /dev/null +++ b/nengo_gui/static/dist/nengo.js @@ -0,0 +1,62461 @@ +/******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // identity function for calling harmony imports with the correct context +/******/ __webpack_require__.i = function(value) { return value; }; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { +/******/ configurable: false, +/******/ enumerable: true, +/******/ get: getter +/******/ }); +/******/ } +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = "/static/dist/"; +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = 155); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/* no static exports found */ +/* all exports used */ +/*!*********************************!*\ + !*** ./~/jquery/dist/jquery.js ***! + \*********************************/ +/***/ (function(module, exports, __webpack_require__) { + +var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! + * jQuery JavaScript Library v2.2.4 + * http://jquery.com/ + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2016-05-20T17:23Z + */ + +(function( global, factory ) { + + if ( typeof module === "object" && typeof module.exports === "object" ) { + // For CommonJS and CommonJS-like environments where a proper `window` + // is present, execute the factory and get jQuery. + // For environments that do not have a `window` with a `document` + // (such as Node.js), expose a factory as module.exports. + // This accentuates the need for the creation of a real `window`. + // e.g. var jQuery = require("jquery")(window); + // See ticket #14549 for more info. + module.exports = global.document ? + factory( global, true ) : + function( w ) { + if ( !w.document ) { + throw new Error( "jQuery requires a window with a document" ); + } + return factory( w ); + }; + } else { + factory( global ); + } + +// Pass this if window is not defined yet +}(typeof window !== "undefined" ? window : this, function( window, noGlobal ) { + +// Support: Firefox 18+ +// Can't be in strict mode, several libs including ASP.NET trace +// the stack via arguments.caller.callee and Firefox dies if +// you try to trace through "use strict" call chains. (#13335) +//"use strict"; +var arr = []; + +var document = window.document; + +var slice = arr.slice; + +var concat = arr.concat; + +var push = arr.push; + +var indexOf = arr.indexOf; + +var class2type = {}; + +var toString = class2type.toString; + +var hasOwn = class2type.hasOwnProperty; + +var support = {}; + + + +var + version = "2.2.4", + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + + // The jQuery object is actually just the init constructor 'enhanced' + // Need init if jQuery is called (just allow error to be thrown if not included) + return new jQuery.fn.init( selector, context ); + }, + + // Support: Android<4.1 + // Make sure we trim BOM and NBSP + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([\da-z])/gi, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }; + +jQuery.fn = jQuery.prototype = { + + // The current version of jQuery being used + jquery: version, + + constructor: jQuery, + + // Start with an empty selector + selector: "", + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num != null ? + + // Return just the one element from the set + ( num < 0 ? this[ num + this.length ] : this[ num ] ) : + + // Return all the elements in a clean array + slice.call( this ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + ret.context = this.context; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + each: function( callback ) { + return jQuery.each( this, callback ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map( this, function( elem, i ) { + return callback.call( elem, i, elem ); + } ) ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[ j ] ] : [] ); + }, + + end: function() { + return this.prevObject || this.constructor(); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: arr.sort, + splice: arr.splice +}; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[ 0 ] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + + // Skip the boolean and the target + target = arguments[ i ] || {}; + i++; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction( target ) ) { + target = {}; + } + + // Extend jQuery itself if only one argument is passed + if ( i === length ) { + target = this; + i--; + } + + for ( ; i < length; i++ ) { + + // Only deal with non-null/undefined values + if ( ( options = arguments[ i ] ) != null ) { + + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject( copy ) || + ( copyIsArray = jQuery.isArray( copy ) ) ) ) { + + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray( src ) ? src : []; + + } else { + clone = src && jQuery.isPlainObject( src ) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend( { + + // Unique for each copy of jQuery on the page + expando: "jQuery" + ( version + Math.random() ).replace( /\D/g, "" ), + + // Assume jQuery is ready without the ready module + isReady: true, + + error: function( msg ) { + throw new Error( msg ); + }, + + noop: function() {}, + + isFunction: function( obj ) { + return jQuery.type( obj ) === "function"; + }, + + isArray: Array.isArray, + + isWindow: function( obj ) { + return obj != null && obj === obj.window; + }, + + isNumeric: function( obj ) { + + // parseFloat NaNs numeric-cast false positives (null|true|false|"") + // ...but misinterprets leading-number strings, particularly hex literals ("0x...") + // subtraction forces infinities to NaN + // adding 1 corrects loss of precision from parseFloat (#15100) + var realStringObj = obj && obj.toString(); + return !jQuery.isArray( obj ) && ( realStringObj - parseFloat( realStringObj ) + 1 ) >= 0; + }, + + isPlainObject: function( obj ) { + var key; + + // Not plain objects: + // - Any object or value whose internal [[Class]] property is not "[object Object]" + // - DOM nodes + // - window + if ( jQuery.type( obj ) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call( obj, "constructor" ) && + !hasOwn.call( obj.constructor.prototype || {}, "isPrototypeOf" ) ) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + var name; + for ( name in obj ) { + return false; + } + return true; + }, + + type: function( obj ) { + if ( obj == null ) { + return obj + ""; + } + + // Support: Android<4.0, iOS<6 (functionish RegExp) + return typeof obj === "object" || typeof obj === "function" ? + class2type[ toString.call( obj ) ] || "object" : + typeof obj; + }, + + // Evaluates a script in a global context + globalEval: function( code ) { + var script, + indirect = eval; + + code = jQuery.trim( code ); + + if ( code ) { + + // If the code includes a valid, prologue position + // strict mode pragma, execute code by injecting a + // script tag into the document. + if ( code.indexOf( "use strict" ) === 1 ) { + script = document.createElement( "script" ); + script.text = code; + document.head.appendChild( script ).parentNode.removeChild( script ); + } else { + + // Otherwise, avoid the DOM node creation, insertion + // and removal by using an indirect global eval + + indirect( code ); + } + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Support: IE9-11+ + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + }, + + each: function( obj, callback ) { + var length, i = 0; + + if ( isArrayLike( obj ) ) { + length = obj.length; + for ( ; i < length; i++ ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } else { + for ( i in obj ) { + if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) { + break; + } + } + } + + return obj; + }, + + // Support: Android<4.1 + trim: function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArrayLike( Object( arr ) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + return arr == null ? -1 : indexOf.call( arr, elem, i ); + }, + + merge: function( first, second ) { + var len = +second.length, + j = 0, + i = first.length; + + for ( ; j < len; j++ ) { + first[ i++ ] = second[ j ]; + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, invert ) { + var callbackInverse, + matches = [], + i = 0, + length = elems.length, + callbackExpect = !invert; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + callbackInverse = !callback( elems[ i ], i ); + if ( callbackInverse !== callbackExpect ) { + matches.push( elems[ i ] ); + } + } + + return matches; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var length, value, + i = 0, + ret = []; + + // Go through the array, translating each of the items to their new values + if ( isArrayLike( elems ) ) { + length = elems.length; + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret.push( value ); + } + } + } + + // Flatten any nested arrays + return concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var tmp, args, proxy; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + now: Date.now, + + // jQuery.support is not used in Core but other projects attach their + // properties to it so it needs to exist. + support: support +} ); + +// JSHint would error on this code due to the Symbol not being defined in ES5. +// Defining this global in .jshintrc would create a danger of using the global +// unguarded in another place, it seems safer to just disable JSHint for these +// three lines. +/* jshint ignore: start */ +if ( typeof Symbol === "function" ) { + jQuery.fn[ Symbol.iterator ] = arr[ Symbol.iterator ]; +} +/* jshint ignore: end */ + +// Populate the class2type map +jQuery.each( "Boolean Number String Function Array Date RegExp Object Error Symbol".split( " " ), +function( i, name ) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +} ); + +function isArrayLike( obj ) { + + // Support: iOS 8.2 (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = !!obj && "length" in obj && obj.length, + type = jQuery.type( obj ); + + if ( type === "function" || jQuery.isWindow( obj ) ) { + return false; + } + + return type === "array" || length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj; +} +var Sizzle = +/*! + * Sizzle CSS Selector Engine v2.2.1 + * http://sizzlejs.com/ + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2015-10-17 + */ +(function( window ) { + +var i, + support, + Expr, + getText, + isXML, + tokenize, + compile, + select, + outermostContext, + sortInput, + hasDuplicate, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + 1 * new Date(), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + } + return 0; + }, + + // General-purpose constants + MAX_NEGATIVE = 1 << 31, + + // Instance methods + hasOwn = ({}).hasOwnProperty, + arr = [], + pop = arr.pop, + push_native = arr.push, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf as it's faster than native + // http://jsperf.com/thor-indexof-vs-for/5 + indexOf = function( list, elem ) { + var i = 0, + len = list.length; + for ( ; i < len; i++ ) { + if ( list[i] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + + // http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", + + // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace + + // Operator (capture 2) + "*([*^$|!~]?=)" + whitespace + + // "Attribute values must be CSS identifiers [capture 5] or strings [capture 3 or capture 4]" + "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" + whitespace + + "*\\]", + + pseudos = ":(" + identifier + ")(?:\\((" + + // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments: + // 1. quoted (capture 3; capture 4 or capture 5) + "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" + + // 2. simple (capture 6) + "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" + + // 3. anything else (capture 2) + ".*" + + ")\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rwhitespace = new RegExp( whitespace + "+", "g" ), + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), + + rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*?)" + whitespace + "*\\]", "g" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + identifier + ")" ), + "CLASS": new RegExp( "^\\.(" + identifier + ")" ), + "TAG": new RegExp( "^(" + identifier + "|[*])" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rsibling = /[+~]/, + rescape = /'|\\/g, + + // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), + funescape = function( _, escaped, escapedWhitespace ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + // Support: Firefox<24 + // Workaround erroneous numeric interpretation of +"0x" + return high !== high || escapedWhitespace ? + escaped : + high < 0 ? + // BMP codepoint + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }, + + // Used for iframes + // See setDocument() + // Removing the function wrapper causes a "Permission Denied" + // error in IE + unloadHandler = function() { + setDocument(); + }; + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + (arr = slice.call( preferredDoc.childNodes )), + preferredDoc.childNodes + ); + // Support: Android<4.0 + // Detect silently failing push.apply + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + push_native.apply( target, slice.call(els) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + // Can't trust NodeList.length + while ( (target[j++] = els[i++]) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var m, i, elem, nid, nidselect, match, groups, newSelector, + newContext = context && context.ownerDocument, + + // nodeType defaults to 9, since context defaults to document + nodeType = context ? context.nodeType : 9; + + results = results || []; + + // Return early from calls with invalid selector or context + if ( typeof selector !== "string" || !selector || + nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) { + + return results; + } + + // Try to shortcut find operations (as opposed to filters) in HTML documents + if ( !seed ) { + + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + context = context || document; + + if ( documentIsHTML ) { + + // If the selector is sufficiently simple, try using a "get*By*" DOM method + // (excepting DocumentFragment context, where the methods don't exist) + if ( nodeType !== 11 && (match = rquickExpr.exec( selector )) ) { + + // ID selector + if ( (m = match[1]) ) { + + // Document context + if ( nodeType === 9 ) { + if ( (elem = context.getElementById( m )) ) { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + + // Element context + } else { + + // Support: IE, Opera, Webkit + // TODO: identify versions + // getElementById can match elements by name instead of ID + if ( newContext && (elem = newContext.getElementById( m )) && + contains( context, elem ) && + elem.id === m ) { + + results.push( elem ); + return results; + } + } + + // Type selector + } else if ( match[2] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Class selector + } else if ( (m = match[3]) && support.getElementsByClassName && + context.getElementsByClassName ) { + + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // Take advantage of querySelectorAll + if ( support.qsa && + !compilerCache[ selector + " " ] && + (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + + if ( nodeType !== 1 ) { + newContext = context; + newSelector = selector; + + // qSA looks outside Element context, which is not what we want + // Thanks to Andrew Dupont for this workaround technique + // Support: IE <=8 + // Exclude object elements + } else if ( context.nodeName.toLowerCase() !== "object" ) { + + // Capture the context ID, setting it first if necessary + if ( (nid = context.getAttribute( "id" )) ) { + nid = nid.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", (nid = expando) ); + } + + // Prefix every selector in the list + groups = tokenize( selector ); + i = groups.length; + nidselect = ridentifier.test( nid ) ? "#" + nid : "[id='" + nid + "']"; + while ( i-- ) { + groups[i] = nidselect + " " + toSelector( groups[i] ); + } + newSelector = groups.join( "," ); + + // Expand context for sibling selectors + newContext = rsibling.test( selector ) && testContext( context.parentNode ) || + context; + } + + if ( newSelector ) { + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch ( qsaError ) { + } finally { + if ( nid === expando ) { + context.removeAttribute( "id" ); + } + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {function(string, object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key + " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key + " " ] = value); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created div and expects a boolean result + */ +function assert( fn ) { + var div = document.createElement("div"); + + try { + return !!fn( div ); + } catch (e) { + return false; + } finally { + // Remove from its parent by default + if ( div.parentNode ) { + div.parentNode.removeChild( div ); + } + // release memory in IE + div = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split("|"), + i = arr.length; + + while ( i-- ) { + Expr.attrHandle[ arr[i] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + ( ~b.sourceIndex || MAX_NEGATIVE ) - + ( ~a.sourceIndex || MAX_NEGATIVE ); + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( (cur = cur.nextSibling) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Checks a node for validity as a Sizzle context + * @param {Element|Object=} context + * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value + */ +function testContext( context ) { + return context && typeof context.getElementsByTagName !== "undefined" && context; +} + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Detects XML nodes + * @param {Element|Object} elem An element or a document + * @returns {Boolean} True iff elem is a non-HTML XML node + */ +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var hasCompare, parent, + doc = node ? node.ownerDocument || node : preferredDoc; + + // Return early if doc is invalid or already selected + if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Update global variables + document = doc; + docElem = document.documentElement; + documentIsHTML = !isXML( document ); + + // Support: IE 9-11, Edge + // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936) + if ( (parent = document.defaultView) && parent.top !== parent ) { + // Support: IE 11 + if ( parent.addEventListener ) { + parent.addEventListener( "unload", unloadHandler, false ); + + // Support: IE 9 - 10 only + } else if ( parent.attachEvent ) { + parent.attachEvent( "onunload", unloadHandler ); + } + } + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties + // (excepting IE8 booleans) + support.attributes = assert(function( div ) { + div.className = "i"; + return !div.getAttribute("className"); + }); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert(function( div ) { + div.appendChild( document.createComment("") ); + return !div.getElementsByTagName("*").length; + }); + + // Support: IE<9 + support.getElementsByClassName = rnative.test( document.getElementsByClassName ); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert(function( div ) { + docElem.appendChild( div ).id = expando; + return !document.getElementsByName || !document.getElementsByName( expando ).length; + }); + + // ID find and filter + if ( support.getById ) { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== "undefined" && documentIsHTML ) { + var m = context.getElementById( id ); + return m ? [ m ] : []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute("id") === attrId; + }; + }; + } else { + // Support: IE6/7 + // getElementById is not reliable as a find shortcut + delete Expr.find["ID"]; + + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== "undefined" && + elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + } + + // Tag + Expr.find["TAG"] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( tag ); + + // DocumentFragment nodes don't have gEBTN + } else if ( support.qsa ) { + return context.querySelectorAll( tag ); + } + } : + + function( tag, context ) { + var elem, + tmp = [], + i = 0, + // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( (elem = results[i++]) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See http://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( (support.qsa = rnative.test( document.querySelectorAll )) ) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( div ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + docElem.appendChild( div ).innerHTML = "" + + ""; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( div.querySelectorAll("[msallowcapture^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !div.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+ + if ( !div.querySelectorAll( "[id~=" + expando + "-]" ).length ) { + rbuggyQSA.push("~="); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + + // Support: Safari 8+, iOS 8+ + // https://bugs.webkit.org/show_bug.cgi?id=136851 + // In-page `selector#id sibing-combinator selector` fails + if ( !div.querySelectorAll( "a#" + expando + "+*" ).length ) { + rbuggyQSA.push(".#.+[+~]"); + } + }); + + assert(function( div ) { + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = document.createElement("input"); + input.setAttribute( "type", "hidden" ); + div.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( div.querySelectorAll("[name=d]").length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":enabled").length ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Opera 10-11 does not throw on post-comma invalid pseudos + div.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } + + if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector) )) ) { + + assert(function( div ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( div, "div" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( div, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + }); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully self-exclusive + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + + // Choose the first element that is related to our preferred document + if ( a === document || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { + return -1; + } + if ( b === document || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + return a === document ? -1 : + b === document ? 1 : + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( (cur = cur.parentNode) ) { + ap.unshift( cur ); + } + cur = b; + while ( (cur = cur.parentNode) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[i] === bp[i] ) { + i++; + } + + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[i], bp[i] ) : + + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; + + return document; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + if ( support.matchesSelector && documentIsHTML && + !compilerCache[ expr + " " ] && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch (e) {} + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + // Set document vars if needed + if ( ( context.ownerDocument || context ) !== document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + (val = elem.getAttributeNode(name)) && val.specified ? + val.value : + null; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( (elem = results[i++]) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + // If no nodeType, this is expected to be an array + while ( (node = elem[i++]) ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1].slice( 0, 3 ) === "nth" ) { + // nth-* requires argument + if ( !match[3] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + + // other types prohibit arguments + } else if ( match[3] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[6] && match[2]; + + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[3] ) { + match[2] = match[4] || match[5] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + match[0] = match[0].slice( 0, excess ); + match[2] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { return true; } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== "undefined" && elem.getAttribute("class") || "" ); + }); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, what, argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, context, xml ) { + var cache, uniqueCache, outerCache, node, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType, + diff = false; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( (node = node[ dir ]) ) { + if ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) { + + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + + // Seek `elem` from a previously-cached index + + // ...in a gzip-friendly way + node = parent; + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex && cache[ 2 ]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( (node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop()) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + uniqueCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + } else { + // Use previously-cached element index if available + if ( useCache ) { + // ...in a gzip-friendly way + node = elem; + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + cache = uniqueCache[ type ] || []; + nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ]; + diff = nodeIndex; + } + + // xml :nth-child(...) + // or :nth-last-child(...) or :nth(-last)?-of-type(...) + if ( diff === false ) { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? + node.nodeName.toLowerCase() === name : + node.nodeType === 1 ) && + ++diff ) { + + // Cache the index of each encountered element + if ( useCache ) { + outerCache = node[ expando ] || (node[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ node.uniqueID ] || + (outerCache[ node.uniqueID ] = {}); + + uniqueCache[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + // Potentially complex pseudos + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + // Don't keep the element (issue #299) + input[0] = null; + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + text = text.replace( runescape, funescape ); + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + // lang value must be a valid identifier + if ( !ridentifier.test(lang || "") ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( (elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + return false; + }; + }), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + // Boolean properties + "enabled": function( elem ) { + return elem.disabled === false; + }, + + "disabled": function( elem ) { + return elem.disabled === true; + }, + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; + } + groups.push( (tokens = []) ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + matched = match.shift(); + tokens.push({ + value: matched, + // Cast descendant combinators to space + type: match[0].replace( rtrim, " " ) + }); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + matched = match.shift(); + tokens.push({ + value: matched, + type: type, + matches: match + }); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[i].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + checkNonElements = base && dir === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, uniqueCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching + if ( xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + + // Support: IE <9 only + // Defend against cloned attroperties (jQuery gh-1709) + uniqueCache = outerCache[ elem.uniqueID ] || (outerCache[ elem.uniqueID ] = {}); + + if ( (oldCache = uniqueCache[ dir ]) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return (newCache[ 2 ] = oldCache[ 2 ]); + } else { + // Reuse newcache so results back-propagate to previous elements + uniqueCache[ dir ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { + return true; + } + } + } + } + } + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[0]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + }); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + // Avoid hanging onto element (issue #299) + checkContext = null; + return ret; + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), + len = elems.length; + + if ( outermost ) { + outermostContext = context === document || context || outermost; + } + + // Add elements passing elementMatchers directly to results + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + if ( !context && elem.ownerDocument !== document ) { + setDocument( elem ); + xml = !documentIsHTML; + } + while ( (matcher = elementMatchers[j++]) ) { + if ( matcher( elem, context || document, xml) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // `i` is now the count of elements visited above, and adding it to `matchedCount` + // makes the latter nonnegative. + matchedCount += i; + + // Apply set filters to unmatched elements + // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount` + // equals `i`), unless we didn't visit _any_ elements in the above loop because we have + // no element matchers and no seed. + // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that + // case, which will result in a "00" `matchedCount` that differs from `i` but is also + // numerically zero. + if ( bySet && i !== matchedCount ) { + j = 0; + while ( (matcher = setMatchers[j++]) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +}; + +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( (selector = compiled.selector || selector) ); + + results = results || []; + + // Try to minimize operations if there is only one selector in the list and no seed + // (the latter of which guarantees us context) + if ( match.length === 1 ) { + + // Reduce context if the leading compound selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + support.getById && context.nodeType === 9 && documentIsHTML && + Expr.relative[ tokens[1].type ] ) { + + context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + !context || rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +}; + +// One-time assignments + +// Sort stability +support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; + +// Support: Chrome 14-35+ +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert(function( div1 ) { + // Should return 1, but returns 4 (following) + return div1.compareDocumentPosition( document.createElement("div") ) & 1; +}); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert(function( div ) { + div.innerHTML = ""; + return div.firstChild.getAttribute("href") === "#" ; +}) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + }); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert(function( div ) { + div.innerHTML = ""; + div.firstChild.setAttribute( "value", "" ); + return div.firstChild.getAttribute( "value" ) === ""; +}) ) { + addHandle( "value", function( elem, name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + }); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert(function( div ) { + return div.getAttribute("disabled") == null; +}) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return elem[ name ] === true ? name.toLowerCase() : + (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + null; + } + }); +} + +return Sizzle; + +})( window ); + + + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[ ":" ] = jQuery.expr.pseudos; +jQuery.uniqueSort = jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + + +var dir = function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; +}; + + +var siblings = function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; +}; + + +var rneedsContext = jQuery.expr.match.needsContext; + +var rsingleTag = ( /^<([\w-]+)\s*\/?>(?:<\/\1>|)$/ ); + + + +var risSimple = /^.[^:#\[\.,]*$/; + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + /* jshint -W018 */ + return !!qualifier.call( elem, i, elem ) !== not; + } ); + + } + + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + } ); + + } + + if ( typeof qualifier === "string" ) { + if ( risSimple.test( qualifier ) ) { + return jQuery.filter( qualifier, elements, not ); + } + + qualifier = jQuery.filter( qualifier, elements ); + } + + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) > -1 ) !== not; + } ); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : + jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + } ) ); +}; + +jQuery.fn.extend( { + find: function( selector ) { + var i, + len = this.length, + ret = [], + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter( function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + } ) ); + } + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = this.selector ? this.selector + " " + selector : selector; + return ret; + }, + filter: function( selector ) { + return this.pushStack( winnow( this, selector || [], false ) ); + }, + not: function( selector ) { + return this.pushStack( winnow( this, selector || [], true ) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +} ); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + init = jQuery.fn.init = function( selector, context, root ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Method init() accepts an alternate rootjQuery + // so migrate can support jQuery.sub (gh-2101) + root = root || rootjQuery; + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[ 0 ] === "<" && + selector[ selector.length - 1 ] === ">" && + selector.length >= 3 ) { + + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && ( match[ 1 ] || !context ) ) { + + // HANDLE: $(html) -> $(array) + if ( match[ 1 ] ) { + context = context instanceof jQuery ? context[ 0 ] : context; + + // Option to run scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[ 1 ], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[ 2 ] ); + + // Support: Blackberry 4.6 + // gEBID returns nodes no longer in the document (#6963) + if ( elem && elem.parentNode ) { + + // Inject the element directly into the jQuery object + this.length = 1; + this[ 0 ] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || root ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this.context = this[ 0 ] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return root.ready !== undefined ? + root.ready( selector ) : + + // Execute immediately if ready is not present + selector( jQuery ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + + // Methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend( { + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter( function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[ i ] ) ) { + return true; + } + } + } ); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + for ( cur = this[ i ]; cur && cur !== context; cur = cur.parentNode ) { + + // Always skip document fragments + if ( cur.nodeType < 11 && ( pos ? + pos.index( cur ) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector( cur, selectors ) ) ) { + + matched.push( cur ); + break; + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.uniqueSort( matched ) : matched ); + }, + + // Determine the position of an element within the set + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // Index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.uniqueSort( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter( selector ) + ); + } +} ); + +function sibling( cur, dir ) { + while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each( { + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return siblings( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return siblings( elem.firstChild ); + }, + contents: function( elem ) { + return elem.contentDocument || jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.uniqueSort( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +} ); +var rnotwhite = ( /\S+/g ); + + + +// Convert String-formatted options into Object-formatted ones +function createOptions( options ) { + var object = {}; + jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + } ); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + createOptions( options ) : + jQuery.extend( {}, options ); + + var // Flag to know if list is currently firing + firing, + + // Last fire value for non-forgettable lists + memory, + + // Flag to know if list was already fired + fired, + + // Flag to prevent firing + locked, + + // Actual callback list + list = [], + + // Queue of execution data for repeatable lists + queue = [], + + // Index of currently firing callback (modified by add/remove as needed) + firingIndex = -1, + + // Fire callbacks + fire = function() { + + // Enforce single-firing + locked = options.once; + + // Execute callbacks for all pending executions, + // respecting firingIndex overrides and runtime changes + fired = firing = true; + for ( ; queue.length; firingIndex = -1 ) { + memory = queue.shift(); + while ( ++firingIndex < list.length ) { + + // Run callback and check for early termination + if ( list[ firingIndex ].apply( memory[ 0 ], memory[ 1 ] ) === false && + options.stopOnFalse ) { + + // Jump to end and forget the data so .add doesn't re-fire + firingIndex = list.length; + memory = false; + } + } + } + + // Forget the data if we're done with it + if ( !options.memory ) { + memory = false; + } + + firing = false; + + // Clean up if we're done firing for good + if ( locked ) { + + // Keep an empty list if we have data for future add calls + if ( memory ) { + list = []; + + // Otherwise, this object is spent + } else { + list = ""; + } + } + }, + + // Actual Callbacks object + self = { + + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + + // If we have memory from a past run, we should fire after adding + if ( memory && !firing ) { + firingIndex = list.length - 1; + queue.push( memory ); + } + + ( function add( args ) { + jQuery.each( args, function( _, arg ) { + if ( jQuery.isFunction( arg ) ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && jQuery.type( arg ) !== "string" ) { + + // Inspect recursively + add( arg ); + } + } ); + } )( arguments ); + + if ( memory && !firing ) { + fire(); + } + } + return this; + }, + + // Remove a callback from the list + remove: function() { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + + // Handle firing indexes + if ( index <= firingIndex ) { + firingIndex--; + } + } + } ); + return this; + }, + + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? + jQuery.inArray( fn, list ) > -1 : + list.length > 0; + }, + + // Remove all callbacks from the list + empty: function() { + if ( list ) { + list = []; + } + return this; + }, + + // Disable .fire and .add + // Abort any current/pending executions + // Clear all callbacks and values + disable: function() { + locked = queue = []; + list = memory = ""; + return this; + }, + disabled: function() { + return !list; + }, + + // Disable .fire + // Also disable .add unless we have memory (since it would have no effect) + // Abort any pending executions + lock: function() { + locked = queue = []; + if ( !memory ) { + list = memory = ""; + } + return this; + }, + locked: function() { + return !!locked; + }, + + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( !locked ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + queue.push( args ); + if ( !firing ) { + fire(); + } + } + return this; + }, + + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +jQuery.extend( { + + Deferred: function( func ) { + var tuples = [ + + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks( "once memory" ), "resolved" ], + [ "reject", "fail", jQuery.Callbacks( "once memory" ), "rejected" ], + [ "notify", "progress", jQuery.Callbacks( "memory" ) ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred( function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[ 1 ] ]( function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .progress( newDefer.notify ) + .done( newDefer.resolve ) + .fail( newDefer.reject ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( + this === promise ? newDefer.promise() : this, + fn ? [ returned ] : arguments + ); + } + } ); + } ); + fns = null; + } ).promise(); + }, + + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[ 1 ] ] = list.add; + + // Handle state + if ( stateString ) { + list.add( function() { + + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[ 0 ] ] = function() { + deferred[ tuple[ 0 ] + "With" ]( this === deferred ? promise : this, arguments ); + return this; + }; + deferred[ tuple[ 0 ] + "With" ] = list.fireWith; + } ); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || + ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. + // If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !( --remaining ) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // Add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .progress( updateFunc( i, progressContexts, progressValues ) ) + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ); + } else { + --remaining; + } + } + } + + // If we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } +} ); + + +// The deferred used on DOM ready +var readyList; + +jQuery.fn.ready = function( fn ) { + + // Add the callback + jQuery.ready.promise().done( fn ); + + return this; +}; + +jQuery.extend( { + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.triggerHandler ) { + jQuery( document ).triggerHandler( "ready" ); + jQuery( document ).off( "ready" ); + } + } +} ); + +/** + * The ready event handler and self cleanup method + */ +function completed() { + document.removeEventListener( "DOMContentLoaded", completed ); + window.removeEventListener( "load", completed ); + jQuery.ready(); +} + +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called + // after the browser event has already occurred. + // Support: IE9-10 only + // Older IE sometimes signals "interactive" too soon + if ( document.readyState === "complete" || + ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) { + + // Handle it asynchronously to allow scripts the opportunity to delay ready + window.setTimeout( jQuery.ready ); + + } else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed ); + } + } + return readyList.promise( obj ); +}; + +// Kick off the DOM ready check even if the user does not +jQuery.ready.promise(); + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + access( elems, fn, i, key[ i ], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !jQuery.isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( + elems[ i ], key, raw ? + value : + value.call( elems[ i ], i, fn( elems[ i ], key ) ) + ); + } + } + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + len ? fn( elems[ 0 ], key ) : emptyGet; +}; +var acceptData = function( owner ) { + + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + /* jshint -W018 */ + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + + + + +function Data() { + this.expando = jQuery.expando + Data.uid++; +} + +Data.uid = 1; + +Data.prototype = { + + register: function( owner, initial ) { + var value = initial || {}; + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable, non-writable property + // configurability must be true to allow the property to be + // deleted with the delete operator + } else { + Object.defineProperty( owner, this.expando, { + value: value, + writable: true, + configurable: true + } ); + } + return owner[ this.expando ]; + }, + cache: function( owner ) { + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( !acceptData( owner ) ) { + return {}; + } + + // Check if the owner object already has a cache + var value = owner[ this.expando ]; + + // If not, create one + if ( !value ) { + value = {}; + + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return an empty object. + if ( acceptData( owner ) ) { + + // If it is a node unlikely to be stringify-ed or looped over + // use plain assignment + if ( owner.nodeType ) { + owner[ this.expando ] = value; + + // Otherwise secure it in a non-enumerable property + // configurable must be true to allow the property to be + // deleted when data is removed + } else { + Object.defineProperty( owner, this.expando, { + value: value, + configurable: true + } ); + } + } + } + + return value; + }, + set: function( owner, data, value ) { + var prop, + cache = this.cache( owner ); + + // Handle: [ owner, key, value ] args + if ( typeof data === "string" ) { + cache[ data ] = value; + + // Handle: [ owner, { properties } ] args + } else { + + // Copy the properties one-by-one to the cache object + for ( prop in data ) { + cache[ prop ] = data[ prop ]; + } + } + return cache; + }, + get: function( owner, key ) { + return key === undefined ? + this.cache( owner ) : + owner[ this.expando ] && owner[ this.expando ][ key ]; + }, + access: function( owner, key, value ) { + var stored; + + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ( ( key && typeof key === "string" ) && value === undefined ) ) { + + stored = this.get( owner, key ); + + return stored !== undefined ? + stored : this.get( owner, jQuery.camelCase( key ) ); + } + + // When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, name, camel, + cache = owner[ this.expando ]; + + if ( cache === undefined ) { + return; + } + + if ( key === undefined ) { + this.register( owner ); + + } else { + + // Support array or space separated string of keys + if ( jQuery.isArray( key ) ) { + + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = key.concat( key.map( jQuery.camelCase ) ); + } else { + camel = jQuery.camelCase( key ); + + // Try the string as a key before any manipulation + if ( key in cache ) { + name = [ key, camel ]; + } else { + + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + name = camel; + name = name in cache ? + [ name ] : ( name.match( rnotwhite ) || [] ); + } + } + + i = name.length; + + while ( i-- ) { + delete cache[ name[ i ] ]; + } + } + + // Remove the expando if there's no more data + if ( key === undefined || jQuery.isEmptyObject( cache ) ) { + + // Support: Chrome <= 35-45+ + // Webkit & Blink performance suffers when deleting properties + // from DOM nodes, so set to undefined instead + // https://code.google.com/p/chromium/issues/detail?id=378607 + if ( owner.nodeType ) { + owner[ this.expando ] = undefined; + } else { + delete owner[ this.expando ]; + } + } + }, + hasData: function( owner ) { + var cache = owner[ this.expando ]; + return cache !== undefined && !jQuery.isEmptyObject( cache ); + } +}; +var dataPriv = new Data(); + +var dataUser = new Data(); + + + +// Implementation Summary +// +// 1. Enforce API surface and semantic compatibility with 1.9.x branch +// 2. Improve the module's maintainability by reducing the storage +// paths to a single mechanism. +// 3. Use the same single mechanism to support "private" and "user" data. +// 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) +// 5. Avoid exposing implementation details on user objects (eg. expando properties) +// 6. Provide a clear path for implementation upgrade to WeakMap in 2014 + +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /[A-Z]/g; + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$&" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch ( e ) {} + + // Make sure we set the data so it isn't changed later + dataUser.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend( { + hasData: function( elem ) { + return dataUser.hasData( elem ) || dataPriv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return dataUser.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + dataUser.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to dataPriv methods, these can be deprecated. + _data: function( elem, name, data ) { + return dataPriv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + dataPriv.remove( elem, name ); + } +} ); + +jQuery.fn.extend( { + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = dataUser.get( elem ); + + if ( elem.nodeType === 1 && !dataPriv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE11+ + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.slice( 5 ) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + dataPriv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each( function() { + dataUser.set( this, key ); + } ); + } + + return access( this, function( value ) { + var data, camelKey; + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + + // Attempt to get data from the cache + // with the key as-is + data = dataUser.get( elem, key ) || + + // Try to find dashed key if it exists (gh-2779) + // This is for 2.2.x only + dataUser.get( elem, key.replace( rmultiDash, "-$&" ).toLowerCase() ); + + if ( data !== undefined ) { + return data; + } + + camelKey = jQuery.camelCase( key ); + + // Attempt to get data from the cache + // with the key camelized + data = dataUser.get( elem, camelKey ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, camelKey, undefined ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + camelKey = jQuery.camelCase( key ); + this.each( function() { + + // First, attempt to store a copy or reference of any + // data that might've been store with a camelCased key. + var data = dataUser.get( this, camelKey ); + + // For HTML5 data-* attribute interop, we have to + // store property names with dashes in a camelCase form. + // This might not apply to all properties...* + dataUser.set( this, camelKey, value ); + + // *... In the case of properties that might _actually_ + // have dashes, we need to also store a copy of that + // unchanged property. + if ( key.indexOf( "-" ) > -1 && data !== undefined ) { + dataUser.set( this, key, value ); + } + } ); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each( function() { + dataUser.remove( this, key ); + } ); + } +} ); + + +jQuery.extend( { + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = dataPriv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray( data ) ) { + queue = dataPriv.access( elem, type, jQuery.makeArray( data ) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // Clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // Not public - generate a queueHooks object, or return the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return dataPriv.get( elem, key ) || dataPriv.access( elem, key, { + empty: jQuery.Callbacks( "once memory" ).add( function() { + dataPriv.remove( elem, [ type + "queue", key ] ); + } ) + } ); + } +} ); + +jQuery.fn.extend( { + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[ 0 ], type ); + } + + return data === undefined ? + this : + this.each( function() { + var queue = jQuery.queue( this, type, data ); + + // Ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[ 0 ] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + } ); + }, + dequeue: function( type ) { + return this.each( function() { + jQuery.dequeue( this, type ); + } ); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = dataPriv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +} ); +var pnum = ( /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/ ).source; + +var rcssNum = new RegExp( "^(?:([+-])=|)(" + pnum + ")([a-z%]*)$", "i" ); + + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var isHidden = function( elem, el ) { + + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || + !jQuery.contains( elem.ownerDocument, elem ); + }; + + + +function adjustCSS( elem, prop, valueParts, tween ) { + var adjusted, + scale = 1, + maxIterations = 20, + currentValue = tween ? + function() { return tween.cur(); } : + function() { return jQuery.css( elem, prop, "" ); }, + initial = currentValue(), + unit = valueParts && valueParts[ 3 ] || ( jQuery.cssNumber[ prop ] ? "" : "px" ), + + // Starting value computation is required for potential unit mismatches + initialInUnit = ( jQuery.cssNumber[ prop ] || unit !== "px" && +initial ) && + rcssNum.exec( jQuery.css( elem, prop ) ); + + if ( initialInUnit && initialInUnit[ 3 ] !== unit ) { + + // Trust units reported by jQuery.css + unit = unit || initialInUnit[ 3 ]; + + // Make sure we update the tween properties later on + valueParts = valueParts || []; + + // Iteratively approximate from a nonzero starting point + initialInUnit = +initial || 1; + + do { + + // If previous iteration zeroed out, double until we get *something*. + // Use string for doubling so we don't accidentally see scale as unchanged below + scale = scale || ".5"; + + // Adjust and apply + initialInUnit = initialInUnit / scale; + jQuery.style( elem, prop, initialInUnit + unit ); + + // Update scale, tolerating zero or NaN from tween.cur() + // Break the loop if scale is unchanged or perfect, or if we've just had enough. + } while ( + scale !== ( scale = currentValue() / initial ) && scale !== 1 && --maxIterations + ); + } + + if ( valueParts ) { + initialInUnit = +initialInUnit || +initial || 0; + + // Apply relative offset (+=/-=) if specified + adjusted = valueParts[ 1 ] ? + initialInUnit + ( valueParts[ 1 ] + 1 ) * valueParts[ 2 ] : + +valueParts[ 2 ]; + if ( tween ) { + tween.unit = unit; + tween.start = initialInUnit; + tween.end = adjusted; + } + } + return adjusted; +} +var rcheckableType = ( /^(?:checkbox|radio)$/i ); + +var rtagName = ( /<([\w:-]+)/ ); + +var rscriptType = ( /^$|\/(?:java|ecma)script/i ); + + + +// We have to close these tags to support XHTML (#13200) +var wrapMap = { + + // Support: IE9 + option: [ 1, "" ], + + // XHTML parsers do not magically insert elements in the + // same way that tag soup parsers do. So we cannot shorten + // this by omitting or other required elements. + thead: [ 1, "", "
" ], + col: [ 2, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + + _default: [ 0, "", "" ] +}; + +// Support: IE9 +wrapMap.optgroup = wrapMap.option; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + + +function getAll( context, tag ) { + + // Support: IE9-11+ + // Use typeof to avoid zero-argument method invocation on host objects (#15151) + var ret = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( tag || "*" ) : + typeof context.querySelectorAll !== "undefined" ? + context.querySelectorAll( tag || "*" ) : + []; + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], ret ) : + ret; +} + + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + dataPriv.set( + elems[ i ], + "globalEval", + !refElements || dataPriv.get( refElements[ i ], "globalEval" ) + ); + } +} + + +var rhtml = /<|&#?\w+;/; + +function buildFragment( elems, context, scripts, selection, ignored ) { + var elem, tmp, tag, wrap, contains, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement( "div" ) ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Ensure the created nodes are orphaned (#12392) + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( ( elem = nodes[ i++ ] ) ) { + + // Skip elements already in the context collection (trac-4087) + if ( selection && jQuery.inArray( elem, selection ) > -1 ) { + if ( ignored ) { + ignored.push( elem ); + } + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( ( elem = tmp[ j++ ] ) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; +} + + +( function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // Support: Android 4.0-4.3, Safari<=5.1 + // Check state lost if the name is set (#11217) + // Support: Windows Web Apps (WWA) + // `name` and `type` must use .setAttribute for WWA (#14901) + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Safari<=5.1, Android<4.2 + // Older WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE<=11+ + // Make sure textarea (and checkbox) defaultValue is properly cloned + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; +} )(); + + +var + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +// Support: IE9 +// See #13393 for more info +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +function on( elem, types, selector, data, fn, one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + on( elem, type, selector, data, types[ type ], one ); + } + return elem; + } + + if ( data == null && fn == null ) { + + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return elem; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return elem.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + } ); +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.get( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !( events = elemData.events ) ) { + events = elemData.events = {}; + } + if ( !( eventHandle = elemData.handle ) ) { + eventHandle = elemData.handle = function( e ) { + + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend( { + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join( "." ) + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !( handlers = events[ type ] ) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || + special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = dataPriv.hasData( elem ) && dataPriv.get( elem ); + + if ( !elemData || !( events = elemData.events ) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[ t ] ) || []; + type = origType = tmp[ 1 ]; + namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[ 2 ] && + new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || + selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || + special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove data and the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + dataPriv.remove( elem, "handle events" ); + } + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); + + var i, j, ret, matched, handleObj, + handlerQueue = [], + args = slice.call( arguments ), + handlers = ( dataPriv.get( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[ 0 ] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( ( handleObj = matched.handlers[ j++ ] ) && + !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or 2) have namespace(s) + // a subset or equal to those in the bound event (both can have no namespace). + if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || + handleObj.handler ).apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( ( event.result = ret ) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, matches, sel, handleObj, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Support (at least): Chrome, IE9 + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // + // Support: Firefox<=42+ + // Avoid non-left-click in FF but don't block IE radio events (#3861, gh-2343) + if ( delegateCount && cur.nodeType && + ( event.type !== "click" || isNaN( event.button ) || event.button < 1 ) ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && ( cur.disabled !== true || event.type !== "click" ) ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) > -1 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push( { elem: cur, handlers: matches } ); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push( { elem: this, handlers: handlers.slice( delegateCount ) } ); + } + + return handlerQueue; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: ( "altKey bubbles cancelable ctrlKey currentTarget detail eventPhase " + + "metaKey relatedTarget shiftKey target timeStamp view which" ).split( " " ), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split( " " ), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: ( "button buttons clientX clientY offsetX offsetY pageX pageY " + + "screenX screenY toElement" ).split( " " ), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - + ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - + ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; + + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: Cordova 2.5 (WebKit) (#13255) + // All events should have a target; Cordova deviceready doesn't + if ( !event.target ) { + event.target = document; + } + + // Support: Safari 6.0+, Chrome<28 + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + load: { + + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + focus: { + + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== safeActiveElement() && this.focus ) { + this.focus(); + return false; + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === safeActiveElement() && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + click: { + + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) { + this.click(); + return false; + } + }, + + // For cross-browser consistency, don't fire native .click() on links + _default: function( event ) { + return jQuery.nodeName( event.target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + } +}; + +jQuery.removeEvent = function( elem, type, handle ) { + + // This "if" is needed for plain objects + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle ); + } +}; + +jQuery.Event = function( src, props ) { + + // Allow instantiation without the 'new' keyword + if ( !( this instanceof jQuery.Event ) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + + // Support: Android<4.0 + src.returnValue === false ? + returnTrue : + returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + constructor: jQuery.Event, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + isSimulated: false, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e && !this.isSimulated ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && !this.isSimulated ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +// so that event delegation works in jQuery. +// Do the same for pointerenter/pointerleave and pointerover/pointerout +// +// Support: Safari 7 only +// Safari sends mouseenter too often; see: +// https://code.google.com/p/chromium/issues/detail?id=470258 +// for the description of the bug (it existed in older Chrome versions as well). +jQuery.each( { + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mouseenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +} ); + +jQuery.fn.extend( { + on: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn ); + }, + one: function( types, selector, data, fn ) { + return on( this, types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? + handleObj.origType + "." + handleObj.namespace : + handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each( function() { + jQuery.event.remove( this, types, fn, selector ); + } ); + } +} ); + + +var + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi, + + // Support: IE 10-11, Edge 10240+ + // In IE/Edge using regex groups here causes severe slowdowns. + // See https://connect.microsoft.com/IE/feedback/details/1736512/ + rnoInnerhtml = /\s*$/g; + +// Manipulating tables requires a tbody +function manipulationTarget( elem, content ) { + return jQuery.nodeName( elem, "table" ) && + jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? + + elem.getElementsByTagName( "tbody" )[ 0 ] || + elem.appendChild( elem.ownerDocument.createElement( "tbody" ) ) : + elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = ( elem.getAttribute( "type" ) !== null ) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); + + if ( match ) { + elem.type = match[ 1 ]; + } else { + elem.removeAttribute( "type" ); + } + + return elem; +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if ( dataPriv.hasData( src ) ) { + pdataOld = dataPriv.access( src ); + pdataCur = dataPriv.set( dest, pdataOld ); + events = pdataOld.events; + + if ( events ) { + delete pdataCur.handle; + pdataCur.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + // 2. Copy user data + if ( dataUser.hasData( src ) ) { + udataOld = dataUser.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + dataUser.set( dest, udataCur ); + } +} + +// Fix IE bugs, see support tests +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +function domManip( collection, args, callback, ignored ) { + + // Flatten any nested arrays + args = concat.apply( [], args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = collection.length, + iNoClone = l - 1, + value = args[ 0 ], + isFunction = jQuery.isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( isFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return collection.each( function( index ) { + var self = collection.eq( index ); + if ( isFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + domManip( self, args, callback, ignored ); + } ); + } + + if ( l ) { + fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + // Require either new content or an interest in ignored elements to invoke the callback + if ( first || ignored ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item + // instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + + // Support: Android<4.1, PhantomJS<2 + // push.apply(_, arraylike) throws on ancient WebKit + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( collection[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !dataPriv.access( node, "globalEval" ) && + jQuery.contains( doc, node ) ) { + + if ( node.src ) { + + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl ) { + jQuery._evalUrl( node.src ); + } + } else { + jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) ); + } + } + } + } + } + } + + return collection; +} + +function remove( elem, selector, keepData ) { + var node, + nodes = selector ? jQuery.filter( selector, elem ) : elem, + i = 0; + + for ( ; ( node = nodes[ i ] ) != null; i++ ) { + if ( !keepData && node.nodeType === 1 ) { + jQuery.cleanData( getAll( node ) ); + } + + if ( node.parentNode ) { + if ( keepData && jQuery.contains( node.ownerDocument, node ) ) { + setGlobalEval( getAll( node, "script" ) ); + } + node.parentNode.removeChild( node ); + } + } + + return elem; +} + +jQuery.extend( { + htmlPrefilter: function( html ) { + return html.replace( rxhtmlTag, "<$1>" ); + }, + + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = jQuery.contains( elem.ownerDocument, elem ); + + // Fix IE cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + cleanData: function( elems ) { + var data, elem, type, + special = jQuery.event.special, + i = 0; + + for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) { + if ( acceptData( elem ) ) { + if ( ( data = elem[ dataPriv.expando ] ) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + + // Support: Chrome <= 35-45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataPriv.expando ] = undefined; + } + if ( elem[ dataUser.expando ] ) { + + // Support: Chrome <= 35-45+ + // Assign undefined instead of using delete, see Data#remove + elem[ dataUser.expando ] = undefined; + } + } + } + } +} ); + +jQuery.fn.extend( { + + // Keep domManip exposed until 3.0 (gh-2225) + domManip: domManip, + + detach: function( selector ) { + return remove( this, selector, true ); + }, + + remove: function( selector ) { + return remove( this, selector ); + }, + + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each( function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + } ); + }, null, value, arguments.length ); + }, + + append: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + } ); + }, + + prepend: function() { + return domManip( this, arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + } ); + }, + + before: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + } ); + }, + + after: function() { + return domManip( this, arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + } ); + }, + + empty: function() { + var elem, + i = 0; + + for ( ; ( elem = this[ i ] ) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map( function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + } ); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = jQuery.htmlPrefilter( value ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch ( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var ignored = []; + + // Make the changes, replacing each non-ignored context element with the new content + return domManip( this, arguments, function( elem ) { + var parent = this.parentNode; + + if ( jQuery.inArray( this, ignored ) < 0 ) { + jQuery.cleanData( getAll( this ) ); + if ( parent ) { + parent.replaceChild( elem, this ); + } + } + + // Force callback invocation + }, ignored ); + } +} ); + +jQuery.each( { + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: QtWebKit + // .get() because push.apply(_, arraylike) throws + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +} ); + + +var iframe, + elemdisplay = { + + // Support: Firefox + // We have to pre-define these values for FF (#10227) + HTML: "block", + BODY: "block" + }; + +/** + * Retrieve the actual display of a element + * @param {String} name nodeName of the element + * @param {Object} doc Document object + */ + +// Called only from within defaultDisplay +function actualDisplay( name, doc ) { + var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), + + display = jQuery.css( elem[ 0 ], "display" ); + + // We don't have any data stored on the element, + // so use "detach" method as fast way to get rid of the element + elem.detach(); + + return display; +} + +/** + * Try to determine the default display value of an element + * @param {String} nodeName + */ +function defaultDisplay( nodeName ) { + var doc = document, + display = elemdisplay[ nodeName ]; + + if ( !display ) { + display = actualDisplay( nodeName, doc ); + + // If the simple way fails, read from inside an iframe + if ( display === "none" || !display ) { + + // Use the already-created iframe if possible + iframe = ( iframe || jQuery( "':""),e._keyEvent=!1,F},_generateMonthYearHeader:function(e,t,i,a,s,n,r,o){var h=this._get(e,"changeMonth"),l=this._get(e,"changeYear"),u=this._get(e,"showMonthAfterYear"),d='
',c="";if(n||!h)c+=''+r[t]+"";else{var p=a&&a.getFullYear()==i,m=s&&s.getFullYear()==i;c+='"}if(u||(d+=c+(!n&&h&&l?"":" ")),!e.yearshtml)if(e.yearshtml="",n||!l)d+=''+i+"";else{var g=this._get(e,"yearRange").split(":"),v=(new Date).getFullYear(),y=function(e){var t=e.match(/c[+-].*/)?i+parseInt(e.substring(1),10):e.match(/[+-].*/)?v+parseInt(e,10):parseInt(e,10);return isNaN(t)?v:t},b=y(g[0]),_=Math.max(b,y(g[1]||""));for(b=a?Math.max(b,a.getFullYear()):b,_=s?Math.min(_,s.getFullYear()):_,e.yearshtml+='",d+=e.yearshtml,e.yearshtml=null}return d+=this._get(e,"yearSuffix"),u&&(d+=(!n&&h&&l?"":" ")+c),d+="
"},_adjustInstDate:function(e,t,i){var a=e.drawYear+("Y"==i?t:0),s=e.drawMonth+("M"==i?t:0),n=Math.min(e.selectedDay,this._getDaysInMonth(a,s))+("D"==i?t:0),r=this._restrictMinMax(e,this._daylightSavingAdjust(new Date(a,s,n)));e.selectedDay=r.getDate(),e.drawMonth=e.selectedMonth=r.getMonth(),e.drawYear=e.selectedYear=r.getFullYear(),("M"==i||"Y"==i)&&this._notifyChange(e)},_restrictMinMax:function(e,t){var i=this._getMinMaxDate(e,"min"),a=this._getMinMaxDate(e,"max"),s=i&&i>t?i:t;return s=a&&s>a?a:s},_notifyChange:function(e){var t=this._get(e,"onChangeMonthYear");t&&t.apply(e.input?e.input[0]:null,[e.selectedYear,e.selectedMonth+1,e])},_getNumberOfMonths:function(e){var t=this._get(e,"numberOfMonths");return null==t?[1,1]:"number"==typeof t?[1,t]:t},_getMinMaxDate:function(e,t){return this._determineDate(e,this._get(e,t+"Date"),null)},_getDaysInMonth:function(e,t){return 32-this._daylightSavingAdjust(new Date(e,t,32)).getDate()},_getFirstDayOfMonth:function(e,t){return new Date(e,t,1).getDay()},_canAdjustMonth:function(e,t,i,a){var s=this._getNumberOfMonths(e),n=this._daylightSavingAdjust(new Date(i,a+(0>t?t:s[0]*s[1]),1));return 0>t&&n.setDate(this._getDaysInMonth(n.getFullYear(),n.getMonth())),this._isInRange(e,n)},_isInRange:function(e,t){var i=this._getMinMaxDate(e,"min"),a=this._getMinMaxDate(e,"max");return(!i||t.getTime()>=i.getTime())&&(!a||t.getTime()<=a.getTime())},_getFormatConfig:function(e){var t=this._get(e,"shortYearCutoff");return t="string"!=typeof t?t:(new Date).getFullYear()%100+parseInt(t,10),{shortYearCutoff:t,dayNamesShort:this._get(e,"dayNamesShort"),dayNames:this._get(e,"dayNames"),monthNamesShort:this._get(e,"monthNamesShort"),monthNames:this._get(e,"monthNames")}},_formatDate:function(e,t,i,a){t||(e.currentDay=e.selectedDay,e.currentMonth=e.selectedMonth,e.currentYear=e.selectedYear);var s=t?"object"==typeof t?t:this._daylightSavingAdjust(new Date(a,i,t)):this._daylightSavingAdjust(new Date(e.currentYear,e.currentMonth,e.currentDay));return this.formatDate(this._get(e,"dateFormat"),s,this._getFormatConfig(e))}}),$.fn.datepicker=function(e){if(!this.length)return this;$.datepicker.initialized||($(document).mousedown($.datepicker._checkExternalClick).find(document.body).append($.datepicker.dpDiv),$.datepicker.initialized=!0);var t=Array.prototype.slice.call(arguments,1);return"string"!=typeof e||"isDisabled"!=e&&"getDate"!=e&&"widget"!=e?"option"==e&&2==arguments.length&&"string"==typeof arguments[1]?$.datepicker["_"+e+"Datepicker"].apply($.datepicker,[this[0]].concat(t)):this.each(function(){"string"==typeof e?$.datepicker["_"+e+"Datepicker"].apply($.datepicker,[this].concat(t)):$.datepicker._attachDatepicker(this,e)}):$.datepicker["_"+e+"Datepicker"].apply($.datepicker,[this[0]].concat(t))},$.datepicker=new Datepicker,$.datepicker.initialized=!1,$.datepicker.uuid=(new Date).getTime(),$.datepicker.version="1.9.2",window["DP_jQuery_"+dpuuid]=$})(jQuery);(function(e,t){var i="ui-dialog ui-widget ui-widget-content ui-corner-all ",a={buttons:!0,height:!0,maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0,width:!0},s={maxHeight:!0,maxWidth:!0,minHeight:!0,minWidth:!0};e.widget("ui.dialog",{version:"1.9.2",options:{autoOpen:!0,buttons:{},closeOnEscape:!0,closeText:"close",dialogClass:"",draggable:!0,hide:null,height:"auto",maxHeight:!1,maxWidth:!1,minHeight:150,minWidth:150,modal:!1,position:{my:"center",at:"center",of:window,collision:"fit",using:function(t){var i=e(this).css(t).offset().top;0>i&&e(this).css("top",t.top-i)}},resizable:!0,show:null,stack:!0,title:"",width:300,zIndex:1e3},_create:function(){this.originalTitle=this.element.attr("title"),"string"!=typeof this.originalTitle&&(this.originalTitle=""),this.oldPosition={parent:this.element.parent(),index:this.element.parent().children().index(this.element)},this.options.title=this.options.title||this.originalTitle;var a,s,n,r,o,h=this,l=this.options,u=l.title||" ";a=(this.uiDialog=e("
")).addClass(i+l.dialogClass).css({display:"none",outline:0,zIndex:l.zIndex}).attr("tabIndex",-1).keydown(function(t){l.closeOnEscape&&!t.isDefaultPrevented()&&t.keyCode&&t.keyCode===e.ui.keyCode.ESCAPE&&(h.close(t),t.preventDefault())}).mousedown(function(e){h.moveToTop(!1,e)}).appendTo("body"),this.element.show().removeAttr("title").addClass("ui-dialog-content ui-widget-content").appendTo(a),s=(this.uiDialogTitlebar=e("
")).addClass("ui-dialog-titlebar ui-widget-header ui-corner-all ui-helper-clearfix").bind("mousedown",function(){a.focus()}).prependTo(a),n=e("").addClass("ui-dialog-titlebar-close ui-corner-all").attr("role","button").click(function(e){e.preventDefault(),h.close(e)}).appendTo(s),(this.uiDialogTitlebarCloseText=e("")).addClass("ui-icon ui-icon-closethick").text(l.closeText).appendTo(n),r=e("").uniqueId().addClass("ui-dialog-title").html(u).prependTo(s),o=(this.uiDialogButtonPane=e("
")).addClass("ui-dialog-buttonpane ui-widget-content ui-helper-clearfix"),(this.uiButtonSet=e("
")).addClass("ui-dialog-buttonset").appendTo(o),a.attr({role:"dialog","aria-labelledby":r.attr("id")}),s.find("*").add(s).disableSelection(),this._hoverable(n),this._focusable(n),l.draggable&&e.fn.draggable&&this._makeDraggable(),l.resizable&&e.fn.resizable&&this._makeResizable(),this._createButtons(l.buttons),this._isOpen=!1,e.fn.bgiframe&&a.bgiframe(),this._on(a,{keydown:function(i){if(l.modal&&i.keyCode===e.ui.keyCode.TAB){var s=e(":tabbable",a),n=s.filter(":first"),r=s.filter(":last");return i.target!==r[0]||i.shiftKey?i.target===n[0]&&i.shiftKey?(r.focus(1),!1):t:(n.focus(1),!1)}}})},_init:function(){this.options.autoOpen&&this.open()},_destroy:function(){var e,t=this.oldPosition;this.overlay&&this.overlay.destroy(),this.uiDialog.hide(),this.element.removeClass("ui-dialog-content ui-widget-content").hide().appendTo("body"),this.uiDialog.remove(),this.originalTitle&&this.element.attr("title",this.originalTitle),e=t.parent.children().eq(t.index),e.length&&e[0]!==this.element[0]?e.before(this.element):t.parent.append(this.element)},widget:function(){return this.uiDialog},close:function(t){var i,a,s=this;if(this._isOpen&&!1!==this._trigger("beforeClose",t))return this._isOpen=!1,this.overlay&&this.overlay.destroy(),this.options.hide?this._hide(this.uiDialog,this.options.hide,function(){s._trigger("close",t)}):(this.uiDialog.hide(),this._trigger("close",t)),e.ui.dialog.overlay.resize(),this.options.modal&&(i=0,e(".ui-dialog").each(function(){this!==s.uiDialog[0]&&(a=e(this).css("z-index"),isNaN(a)||(i=Math.max(i,a)))}),e.ui.dialog.maxZ=i),this},isOpen:function(){return this._isOpen},moveToTop:function(t,i){var a,s=this.options;return s.modal&&!t||!s.stack&&!s.modal?this._trigger("focus",i):(s.zIndex>e.ui.dialog.maxZ&&(e.ui.dialog.maxZ=s.zIndex),this.overlay&&(e.ui.dialog.maxZ+=1,e.ui.dialog.overlay.maxZ=e.ui.dialog.maxZ,this.overlay.$el.css("z-index",e.ui.dialog.overlay.maxZ)),a={scrollTop:this.element.scrollTop(),scrollLeft:this.element.scrollLeft()},e.ui.dialog.maxZ+=1,this.uiDialog.css("z-index",e.ui.dialog.maxZ),this.element.attr(a),this._trigger("focus",i),this)},open:function(){if(!this._isOpen){var t,i=this.options,a=this.uiDialog;return this._size(),this._position(i.position),a.show(i.show),this.overlay=i.modal?new e.ui.dialog.overlay(this):null,this.moveToTop(!0),t=this.element.find(":tabbable"),t.length||(t=this.uiDialogButtonPane.find(":tabbable"),t.length||(t=a)),t.eq(0).focus(),this._isOpen=!0,this._trigger("open"),this}},_createButtons:function(t){var i=this,a=!1;this.uiDialogButtonPane.remove(),this.uiButtonSet.empty(),"object"==typeof t&&null!==t&&e.each(t,function(){return!(a=!0)}),a?(e.each(t,function(t,a){var s,n;a=e.isFunction(a)?{click:a,text:t}:a,a=e.extend({type:"button"},a),n=a.click,a.click=function(){n.apply(i.element[0],arguments)},s=e("",a).appendTo(i.uiButtonSet),e.fn.button&&s.button()}),this.uiDialog.addClass("ui-dialog-buttons"),this.uiDialogButtonPane.appendTo(this.uiDialog)):this.uiDialog.removeClass("ui-dialog-buttons")},_makeDraggable:function(){function t(e){return{position:e.position,offset:e.offset}}var i=this,a=this.options;this.uiDialog.draggable({cancel:".ui-dialog-content, .ui-dialog-titlebar-close",handle:".ui-dialog-titlebar",containment:"document",start:function(a,s){e(this).addClass("ui-dialog-dragging"),i._trigger("dragStart",a,t(s))},drag:function(e,a){i._trigger("drag",e,t(a))},stop:function(s,n){a.position=[n.position.left-i.document.scrollLeft(),n.position.top-i.document.scrollTop()],e(this).removeClass("ui-dialog-dragging"),i._trigger("dragStop",s,t(n)),e.ui.dialog.overlay.resize()}})},_makeResizable:function(i){function a(e){return{originalPosition:e.originalPosition,originalSize:e.originalSize,position:e.position,size:e.size}}i=i===t?this.options.resizable:i;var s=this,n=this.options,r=this.uiDialog.css("position"),o="string"==typeof i?i:"n,e,s,w,se,sw,ne,nw";this.uiDialog.resizable({cancel:".ui-dialog-content",containment:"document",alsoResize:this.element,maxWidth:n.maxWidth,maxHeight:n.maxHeight,minWidth:n.minWidth,minHeight:this._minHeight(),handles:o,start:function(t,i){e(this).addClass("ui-dialog-resizing"),s._trigger("resizeStart",t,a(i))},resize:function(e,t){s._trigger("resize",e,a(t))},stop:function(t,i){e(this).removeClass("ui-dialog-resizing"),n.height=e(this).height(),n.width=e(this).width(),s._trigger("resizeStop",t,a(i)),e.ui.dialog.overlay.resize()}}).css("position",r).find(".ui-resizable-se").addClass("ui-icon ui-icon-grip-diagonal-se")},_minHeight:function(){var e=this.options;return"auto"===e.height?e.minHeight:Math.min(e.minHeight,e.height)},_position:function(t){var i,a=[],s=[0,0];t?(("string"==typeof t||"object"==typeof t&&"0"in t)&&(a=t.split?t.split(" "):[t[0],t[1]],1===a.length&&(a[1]=a[0]),e.each(["left","top"],function(e,t){+a[e]===a[e]&&(s[e]=a[e],a[e]=t)}),t={my:a[0]+(0>s[0]?s[0]:"+"+s[0])+" "+a[1]+(0>s[1]?s[1]:"+"+s[1]),at:a.join(" ")}),t=e.extend({},e.ui.dialog.prototype.options.position,t)):t=e.ui.dialog.prototype.options.position,i=this.uiDialog.is(":visible"),i||this.uiDialog.show(),this.uiDialog.position(t),i||this.uiDialog.hide()},_setOptions:function(t){var i=this,n={},r=!1;e.each(t,function(e,t){i._setOption(e,t),e in a&&(r=!0),e in s&&(n[e]=t)}),r&&this._size(),this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option",n)},_setOption:function(t,a){var s,n,r=this.uiDialog;switch(t){case"buttons":this._createButtons(a);break;case"closeText":this.uiDialogTitlebarCloseText.text(""+a);break;case"dialogClass":r.removeClass(this.options.dialogClass).addClass(i+a);break;case"disabled":a?r.addClass("ui-dialog-disabled"):r.removeClass("ui-dialog-disabled");break;case"draggable":s=r.is(":data(draggable)"),s&&!a&&r.draggable("destroy"),!s&&a&&this._makeDraggable();break;case"position":this._position(a);break;case"resizable":n=r.is(":data(resizable)"),n&&!a&&r.resizable("destroy"),n&&"string"==typeof a&&r.resizable("option","handles",a),n||a===!1||this._makeResizable(a);break;case"title":e(".ui-dialog-title",this.uiDialogTitlebar).html(""+(a||" "))}this._super(t,a)},_size:function(){var t,i,a,s=this.options,n=this.uiDialog.is(":visible");this.element.show().css({width:"auto",minHeight:0,height:0}),s.minWidth>s.width&&(s.width=s.minWidth),t=this.uiDialog.css({height:"auto",width:s.width}).outerHeight(),i=Math.max(0,s.minHeight-t),"auto"===s.height?e.support.minHeight?this.element.css({minHeight:i,height:"auto"}):(this.uiDialog.show(),a=this.element.css("height","auto").height(),n||this.uiDialog.hide(),this.element.height(Math.max(a,i))):this.element.height(Math.max(s.height-t,0)),this.uiDialog.is(":data(resizable)")&&this.uiDialog.resizable("option","minHeight",this._minHeight())}}),e.extend(e.ui.dialog,{uuid:0,maxZ:0,getTitleId:function(e){var t=e.attr("id");return t||(this.uuid+=1,t=this.uuid),"ui-dialog-title-"+t},overlay:function(t){this.$el=e.ui.dialog.overlay.create(t)}}),e.extend(e.ui.dialog.overlay,{instances:[],oldInstances:[],maxZ:0,events:e.map("focus,mousedown,mouseup,keydown,keypress,click".split(","),function(e){return e+".dialog-overlay"}).join(" "),create:function(i){0===this.instances.length&&(setTimeout(function(){e.ui.dialog.overlay.instances.length&&e(document).bind(e.ui.dialog.overlay.events,function(i){return e(i.target).zIndex()").addClass("ui-widget-overlay");return e(document).bind("keydown.dialog-overlay",function(t){var s=e.ui.dialog.overlay.instances;0!==s.length&&s[s.length-1]===a&&i.options.closeOnEscape&&!t.isDefaultPrevented()&&t.keyCode&&t.keyCode===e.ui.keyCode.ESCAPE&&(i.close(t),t.preventDefault())}),a.appendTo(document.body).css({width:this.width(),height:this.height()}),e.fn.bgiframe&&a.bgiframe(),this.instances.push(a),a},destroy:function(t){var i=e.inArray(t,this.instances),a=0;-1!==i&&this.oldInstances.push(this.instances.splice(i,1)[0]),0===this.instances.length&&e([document,window]).unbind(".dialog-overlay"),t.height(0).width(0).remove(),e.each(this.instances,function(){a=Math.max(a,this.css("z-index"))}),this.maxZ=a},height:function(){var t,i;return e.ui.ie?(t=Math.max(document.documentElement.scrollHeight,document.body.scrollHeight),i=Math.max(document.documentElement.offsetHeight,document.body.offsetHeight),i>t?e(window).height()+"px":t+"px"):e(document).height()+"px"},width:function(){var t,i;return e.ui.ie?(t=Math.max(document.documentElement.scrollWidth,document.body.scrollWidth),i=Math.max(document.documentElement.offsetWidth,document.body.offsetWidth),i>t?e(window).width()+"px":t+"px"):e(document).width()+"px"},resize:function(){var t=e([]);e.each(e.ui.dialog.overlay.instances,function(){t=t.add(this)}),t.css({width:0,height:0}).css({width:e.ui.dialog.overlay.width(),height:e.ui.dialog.overlay.height()})}}),e.extend(e.ui.dialog.overlay.prototype,{destroy:function(){e.ui.dialog.overlay.destroy(this.$el)}})})(jQuery);(function(e){var t=!1;e.widget("ui.menu",{version:"1.9.2",defaultElement:"
    ",delay:300,options:{icons:{submenu:"ui-icon-carat-1-e"},menus:"ul",position:{my:"left top",at:"right top"},role:"menu",blur:null,focus:null,select:null},_create:function(){this.activeMenu=this.element,this.element.uniqueId().addClass("ui-menu ui-widget ui-widget-content ui-corner-all").toggleClass("ui-menu-icons",!!this.element.find(".ui-icon").length).attr({role:this.options.role,tabIndex:0}).bind("click"+this.eventNamespace,e.proxy(function(e){this.options.disabled&&e.preventDefault()},this)),this.options.disabled&&this.element.addClass("ui-state-disabled").attr("aria-disabled","true"),this._on({"mousedown .ui-menu-item > a":function(e){e.preventDefault()},"click .ui-state-disabled > a":function(e){e.preventDefault()},"click .ui-menu-item:has(a)":function(i){var s=e(i.target).closest(".ui-menu-item");!t&&s.not(".ui-state-disabled").length&&(t=!0,this.select(i),s.has(".ui-menu").length?this.expand(i):this.element.is(":focus")||(this.element.trigger("focus",[!0]),this.active&&1===this.active.parents(".ui-menu").length&&clearTimeout(this.timer)))},"mouseenter .ui-menu-item":function(t){var i=e(t.currentTarget);i.siblings().children(".ui-state-active").removeClass("ui-state-active"),this.focus(t,i)},mouseleave:"collapseAll","mouseleave .ui-menu":"collapseAll",focus:function(e,t){var i=this.active||this.element.children(".ui-menu-item").eq(0);t||this.focus(e,i)},blur:function(t){this._delay(function(){e.contains(this.element[0],this.document[0].activeElement)||this.collapseAll(t)})},keydown:"_keydown"}),this.refresh(),this._on(this.document,{click:function(i){e(i.target).closest(".ui-menu").length||this.collapseAll(i),t=!1}})},_destroy:function(){this.element.removeAttr("aria-activedescendant").find(".ui-menu").andSelf().removeClass("ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons").removeAttr("role").removeAttr("tabIndex").removeAttr("aria-labelledby").removeAttr("aria-expanded").removeAttr("aria-hidden").removeAttr("aria-disabled").removeUniqueId().show(),this.element.find(".ui-menu-item").removeClass("ui-menu-item").removeAttr("role").removeAttr("aria-disabled").children("a").removeUniqueId().removeClass("ui-corner-all ui-state-hover").removeAttr("tabIndex").removeAttr("role").removeAttr("aria-haspopup").children().each(function(){var t=e(this);t.data("ui-menu-submenu-carat")&&t.remove()}),this.element.find(".ui-menu-divider").removeClass("ui-menu-divider ui-widget-content")},_keydown:function(t){function i(e){return e.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}var s,a,n,r,o,h=!0;switch(t.keyCode){case e.ui.keyCode.PAGE_UP:this.previousPage(t);break;case e.ui.keyCode.PAGE_DOWN:this.nextPage(t);break;case e.ui.keyCode.HOME:this._move("first","first",t);break;case e.ui.keyCode.END:this._move("last","last",t);break;case e.ui.keyCode.UP:this.previous(t);break;case e.ui.keyCode.DOWN:this.next(t);break;case e.ui.keyCode.LEFT:this.collapse(t);break;case e.ui.keyCode.RIGHT:this.active&&!this.active.is(".ui-state-disabled")&&this.expand(t);break;case e.ui.keyCode.ENTER:case e.ui.keyCode.SPACE:this._activate(t);break;case e.ui.keyCode.ESCAPE:this.collapse(t);break;default:h=!1,a=this.previousFilter||"",n=String.fromCharCode(t.keyCode),r=!1,clearTimeout(this.filterTimer),n===a?r=!0:n=a+n,o=RegExp("^"+i(n),"i"),s=this.activeMenu.children(".ui-menu-item").filter(function(){return o.test(e(this).children("a").text())}),s=r&&-1!==s.index(this.active.next())?this.active.nextAll(".ui-menu-item"):s,s.length||(n=String.fromCharCode(t.keyCode),o=RegExp("^"+i(n),"i"),s=this.activeMenu.children(".ui-menu-item").filter(function(){return o.test(e(this).children("a").text())})),s.length?(this.focus(t,s),s.length>1?(this.previousFilter=n,this.filterTimer=this._delay(function(){delete this.previousFilter},1e3)):delete this.previousFilter):delete this.previousFilter}h&&t.preventDefault()},_activate:function(e){this.active.is(".ui-state-disabled")||(this.active.children("a[aria-haspopup='true']").length?this.expand(e):this.select(e))},refresh:function(){var t,i=this.options.icons.submenu,s=this.element.find(this.options.menus);s.filter(":not(.ui-menu)").addClass("ui-menu ui-widget ui-widget-content ui-corner-all").hide().attr({role:this.options.role,"aria-hidden":"true","aria-expanded":"false"}).each(function(){var t=e(this),s=t.prev("a"),a=e("").addClass("ui-menu-icon ui-icon "+i).data("ui-menu-submenu-carat",!0);s.attr("aria-haspopup","true").prepend(a),t.attr("aria-labelledby",s.attr("id"))}),t=s.add(this.element),t.children(":not(.ui-menu-item):has(a)").addClass("ui-menu-item").attr("role","presentation").children("a").uniqueId().addClass("ui-corner-all").attr({tabIndex:-1,role:this._itemRole()}),t.children(":not(.ui-menu-item)").each(function(){var t=e(this);/[^\-—–\s]/.test(t.text())||t.addClass("ui-widget-content ui-menu-divider")}),t.children(".ui-state-disabled").attr("aria-disabled","true"),this.active&&!e.contains(this.element[0],this.active[0])&&this.blur()},_itemRole:function(){return{menu:"menuitem",listbox:"option"}[this.options.role]},focus:function(e,t){var i,s;this.blur(e,e&&"focus"===e.type),this._scrollIntoView(t),this.active=t.first(),s=this.active.children("a").addClass("ui-state-focus"),this.options.role&&this.element.attr("aria-activedescendant",s.attr("id")),this.active.parent().closest(".ui-menu-item").children("a:first").addClass("ui-state-active"),e&&"keydown"===e.type?this._close():this.timer=this._delay(function(){this._close()},this.delay),i=t.children(".ui-menu"),i.length&&/^mouse/.test(e.type)&&this._startOpening(i),this.activeMenu=t.parent(),this._trigger("focus",e,{item:t})},_scrollIntoView:function(t){var i,s,a,n,r,o;this._hasScroll()&&(i=parseFloat(e.css(this.activeMenu[0],"borderTopWidth"))||0,s=parseFloat(e.css(this.activeMenu[0],"paddingTop"))||0,a=t.offset().top-this.activeMenu.offset().top-i-s,n=this.activeMenu.scrollTop(),r=this.activeMenu.height(),o=t.height(),0>a?this.activeMenu.scrollTop(n+a):a+o>r&&this.activeMenu.scrollTop(n+a-r+o))},blur:function(e,t){t||clearTimeout(this.timer),this.active&&(this.active.children("a").removeClass("ui-state-focus"),this.active=null,this._trigger("blur",e,{item:this.active}))},_startOpening:function(e){clearTimeout(this.timer),"true"===e.attr("aria-hidden")&&(this.timer=this._delay(function(){this._close(),this._open(e)},this.delay))},_open:function(t){var i=e.extend({of:this.active},this.options.position);clearTimeout(this.timer),this.element.find(".ui-menu").not(t.parents(".ui-menu")).hide().attr("aria-hidden","true"),t.show().removeAttr("aria-hidden").attr("aria-expanded","true").position(i)},collapseAll:function(t,i){clearTimeout(this.timer),this.timer=this._delay(function(){var s=i?this.element:e(t&&t.target).closest(this.element.find(".ui-menu"));s.length||(s=this.element),this._close(s),this.blur(t),this.activeMenu=s},this.delay)},_close:function(e){e||(e=this.active?this.active.parent():this.element),e.find(".ui-menu").hide().attr("aria-hidden","true").attr("aria-expanded","false").end().find("a.ui-state-active").removeClass("ui-state-active")},collapse:function(e){var t=this.active&&this.active.parent().closest(".ui-menu-item",this.element);t&&t.length&&(this._close(),this.focus(e,t))},expand:function(e){var t=this.active&&this.active.children(".ui-menu ").children(".ui-menu-item").first();t&&t.length&&(this._open(t.parent()),this._delay(function(){this.focus(e,t)}))},next:function(e){this._move("next","first",e)},previous:function(e){this._move("prev","last",e)},isFirstItem:function(){return this.active&&!this.active.prevAll(".ui-menu-item").length},isLastItem:function(){return this.active&&!this.active.nextAll(".ui-menu-item").length},_move:function(e,t,i){var s;this.active&&(s="first"===e||"last"===e?this.active["first"===e?"prevAll":"nextAll"](".ui-menu-item").eq(-1):this.active[e+"All"](".ui-menu-item").eq(0)),s&&s.length&&this.active||(s=this.activeMenu.children(".ui-menu-item")[t]()),this.focus(i,s)},nextPage:function(t){var i,s,a;return this.active?(this.isLastItem()||(this._hasScroll()?(s=this.active.offset().top,a=this.element.height(),this.active.nextAll(".ui-menu-item").each(function(){return i=e(this),0>i.offset().top-s-a}),this.focus(t,i)):this.focus(t,this.activeMenu.children(".ui-menu-item")[this.active?"last":"first"]())),undefined):(this.next(t),undefined)},previousPage:function(t){var i,s,a;return this.active?(this.isFirstItem()||(this._hasScroll()?(s=this.active.offset().top,a=this.element.height(),this.active.prevAll(".ui-menu-item").each(function(){return i=e(this),i.offset().top-s+a>0}),this.focus(t,i)):this.focus(t,this.activeMenu.children(".ui-menu-item").first())),undefined):(this.next(t),undefined)},_hasScroll:function(){return this.element.outerHeight()
").appendTo(this.element),this.oldValue=this._value(),this._refreshValue()},_destroy:function(){this.element.removeClass("ui-progressbar ui-widget ui-widget-content ui-corner-all").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.valueDiv.remove()},value:function(e){return e===t?this._value():(this._setOption("value",e),this)},_setOption:function(e,t){"value"===e&&(this.options.value=t,this._refreshValue(),this._value()===this.options.max&&this._trigger("complete")),this._super(e,t)},_value:function(){var e=this.options.value;return"number"!=typeof e&&(e=0),Math.min(this.options.max,Math.max(this.min,e))},_percentage:function(){return 100*this._value()/this.options.max},_refreshValue:function(){var e=this.value(),t=this._percentage();this.oldValue!==e&&(this.oldValue=e,this._trigger("change")),this.valueDiv.toggle(e>this.min).toggleClass("ui-corner-right",e===this.options.max).width(t.toFixed(0)+"%"),this.element.attr("aria-valuenow",e)}})})(jQuery);(function(e){var t=5;e.widget("ui.slider",e.ui.mouse,{version:"1.9.2",widgetEventPrefix:"slide",options:{animate:!1,distance:0,max:100,min:0,orientation:"horizontal",range:!1,step:1,value:0,values:null},_create:function(){var i,s,a=this.options,n=this.element.find(".ui-slider-handle").addClass("ui-state-default ui-corner-all"),r="",o=[];for(this._keySliding=!1,this._mouseSliding=!1,this._animateOff=!0,this._handleIndex=null,this._detectOrientation(),this._mouseInit(),this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget"+" ui-widget-content"+" ui-corner-all"+(a.disabled?" ui-slider-disabled ui-disabled":"")),this.range=e([]),a.range&&(a.range===!0&&(a.values||(a.values=[this._valueMin(),this._valueMin()]),a.values.length&&2!==a.values.length&&(a.values=[a.values[0],a.values[0]])),this.range=e("
").appendTo(this.element).addClass("ui-slider-range ui-widget-header"+("min"===a.range||"max"===a.range?" ui-slider-range-"+a.range:""))),s=a.values&&a.values.length||1,i=n.length;s>i;i++)o.push(r);this.handles=n.add(e(o.join("")).appendTo(this.element)),this.handle=this.handles.eq(0),this.handles.add(this.range).filter("a").click(function(e){e.preventDefault()}).mouseenter(function(){a.disabled||e(this).addClass("ui-state-hover")}).mouseleave(function(){e(this).removeClass("ui-state-hover")}).focus(function(){a.disabled?e(this).blur():(e(".ui-slider .ui-state-focus").removeClass("ui-state-focus"),e(this).addClass("ui-state-focus"))}).blur(function(){e(this).removeClass("ui-state-focus")}),this.handles.each(function(t){e(this).data("ui-slider-handle-index",t)}),this._on(this.handles,{keydown:function(i){var s,a,n,r,o=e(i.target).data("ui-slider-handle-index");switch(i.keyCode){case e.ui.keyCode.HOME:case e.ui.keyCode.END:case e.ui.keyCode.PAGE_UP:case e.ui.keyCode.PAGE_DOWN:case e.ui.keyCode.UP:case e.ui.keyCode.RIGHT:case e.ui.keyCode.DOWN:case e.ui.keyCode.LEFT:if(i.preventDefault(),!this._keySliding&&(this._keySliding=!0,e(i.target).addClass("ui-state-active"),s=this._start(i,o),s===!1))return}switch(r=this.options.step,a=n=this.options.values&&this.options.values.length?this.values(o):this.value(),i.keyCode){case e.ui.keyCode.HOME:n=this._valueMin();break;case e.ui.keyCode.END:n=this._valueMax();break;case e.ui.keyCode.PAGE_UP:n=this._trimAlignValue(a+(this._valueMax()-this._valueMin())/t);break;case e.ui.keyCode.PAGE_DOWN:n=this._trimAlignValue(a-(this._valueMax()-this._valueMin())/t);break;case e.ui.keyCode.UP:case e.ui.keyCode.RIGHT:if(a===this._valueMax())return;n=this._trimAlignValue(a+r);break;case e.ui.keyCode.DOWN:case e.ui.keyCode.LEFT:if(a===this._valueMin())return;n=this._trimAlignValue(a-r)}this._slide(i,o,n)},keyup:function(t){var i=e(t.target).data("ui-slider-handle-index");this._keySliding&&(this._keySliding=!1,this._stop(t,i),this._change(t,i),e(t.target).removeClass("ui-state-active"))}}),this._refreshValue(),this._animateOff=!1},_destroy:function(){this.handles.remove(),this.range.remove(),this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all"),this._mouseDestroy()},_mouseCapture:function(t){var i,s,a,n,r,o,h,l,u=this,d=this.options;return d.disabled?!1:(this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()},this.elementOffset=this.element.offset(),i={x:t.pageX,y:t.pageY},s=this._normValueFromMouse(i),a=this._valueMax()-this._valueMin()+1,this.handles.each(function(t){var i=Math.abs(s-u.values(t));a>i&&(a=i,n=e(this),r=t)}),d.range===!0&&this.values(1)===d.min&&(r+=1,n=e(this.handles[r])),o=this._start(t,r),o===!1?!1:(this._mouseSliding=!0,this._handleIndex=r,n.addClass("ui-state-active").focus(),h=n.offset(),l=!e(t.target).parents().andSelf().is(".ui-slider-handle"),this._clickOffset=l?{left:0,top:0}:{left:t.pageX-h.left-n.width()/2,top:t.pageY-h.top-n.height()/2-(parseInt(n.css("borderTopWidth"),10)||0)-(parseInt(n.css("borderBottomWidth"),10)||0)+(parseInt(n.css("marginTop"),10)||0)},this.handles.hasClass("ui-state-hover")||this._slide(t,r,s),this._animateOff=!0,!0))},_mouseStart:function(){return!0},_mouseDrag:function(e){var t={x:e.pageX,y:e.pageY},i=this._normValueFromMouse(t);return this._slide(e,this._handleIndex,i),!1},_mouseStop:function(e){return this.handles.removeClass("ui-state-active"),this._mouseSliding=!1,this._stop(e,this._handleIndex),this._change(e,this._handleIndex),this._handleIndex=null,this._clickOffset=null,this._animateOff=!1,!1},_detectOrientation:function(){this.orientation="vertical"===this.options.orientation?"vertical":"horizontal"},_normValueFromMouse:function(e){var t,i,s,a,n;return"horizontal"===this.orientation?(t=this.elementSize.width,i=e.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)):(t=this.elementSize.height,i=e.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)),s=i/t,s>1&&(s=1),0>s&&(s=0),"vertical"===this.orientation&&(s=1-s),a=this._valueMax()-this._valueMin(),n=this._valueMin()+s*a,this._trimAlignValue(n)},_start:function(e,t){var i={handle:this.handles[t],value:this.value()};return this.options.values&&this.options.values.length&&(i.value=this.values(t),i.values=this.values()),this._trigger("start",e,i)},_slide:function(e,t,i){var s,a,n;this.options.values&&this.options.values.length?(s=this.values(t?0:1),2===this.options.values.length&&this.options.range===!0&&(0===t&&i>s||1===t&&s>i)&&(i=s),i!==this.values(t)&&(a=this.values(),a[t]=i,n=this._trigger("slide",e,{handle:this.handles[t],value:i,values:a}),s=this.values(t?0:1),n!==!1&&this.values(t,i,!0))):i!==this.value()&&(n=this._trigger("slide",e,{handle:this.handles[t],value:i}),n!==!1&&this.value(i))},_stop:function(e,t){var i={handle:this.handles[t],value:this.value()};this.options.values&&this.options.values.length&&(i.value=this.values(t),i.values=this.values()),this._trigger("stop",e,i)},_change:function(e,t){if(!this._keySliding&&!this._mouseSliding){var i={handle:this.handles[t],value:this.value()};this.options.values&&this.options.values.length&&(i.value=this.values(t),i.values=this.values()),this._trigger("change",e,i)}},value:function(e){return arguments.length?(this.options.value=this._trimAlignValue(e),this._refreshValue(),this._change(null,0),undefined):this._value()},values:function(t,i){var s,a,n;if(arguments.length>1)return this.options.values[t]=this._trimAlignValue(i),this._refreshValue(),this._change(null,t),undefined;if(!arguments.length)return this._values();if(!e.isArray(arguments[0]))return this.options.values&&this.options.values.length?this._values(t):this.value();for(s=this.options.values,a=arguments[0],n=0;s.length>n;n+=1)s[n]=this._trimAlignValue(a[n]),this._change(null,n);this._refreshValue()},_setOption:function(t,i){var s,a=0;switch(e.isArray(this.options.values)&&(a=this.options.values.length),e.Widget.prototype._setOption.apply(this,arguments),t){case"disabled":i?(this.handles.filter(".ui-state-focus").blur(),this.handles.removeClass("ui-state-hover"),this.handles.prop("disabled",!0),this.element.addClass("ui-disabled")):(this.handles.prop("disabled",!1),this.element.removeClass("ui-disabled"));break;case"orientation":this._detectOrientation(),this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-"+this.orientation),this._refreshValue();break;case"value":this._animateOff=!0,this._refreshValue(),this._change(null,0),this._animateOff=!1;break;case"values":for(this._animateOff=!0,this._refreshValue(),s=0;a>s;s+=1)this._change(null,s);this._animateOff=!1;break;case"min":case"max":this._animateOff=!0,this._refreshValue(),this._animateOff=!1}},_value:function(){var e=this.options.value;return e=this._trimAlignValue(e)},_values:function(e){var t,i,s;if(arguments.length)return t=this.options.values[e],t=this._trimAlignValue(t);for(i=this.options.values.slice(),s=0;i.length>s;s+=1)i[s]=this._trimAlignValue(i[s]);return i},_trimAlignValue:function(e){if(this._valueMin()>=e)return this._valueMin();if(e>=this._valueMax())return this._valueMax();var t=this.options.step>0?this.options.step:1,i=(e-this._valueMin())%t,s=e-i;return 2*Math.abs(i)>=t&&(s+=i>0?t:-t),parseFloat(s.toFixed(5))},_valueMin:function(){return this.options.min},_valueMax:function(){return this.options.max},_refreshValue:function(){var t,i,s,a,n,r=this.options.range,o=this.options,h=this,l=this._animateOff?!1:o.animate,u={};this.options.values&&this.options.values.length?this.handles.each(function(s){i=100*((h.values(s)-h._valueMin())/(h._valueMax()-h._valueMin())),u["horizontal"===h.orientation?"left":"bottom"]=i+"%",e(this).stop(1,1)[l?"animate":"css"](u,o.animate),h.options.range===!0&&("horizontal"===h.orientation?(0===s&&h.range.stop(1,1)[l?"animate":"css"]({left:i+"%"},o.animate),1===s&&h.range[l?"animate":"css"]({width:i-t+"%"},{queue:!1,duration:o.animate})):(0===s&&h.range.stop(1,1)[l?"animate":"css"]({bottom:i+"%"},o.animate),1===s&&h.range[l?"animate":"css"]({height:i-t+"%"},{queue:!1,duration:o.animate}))),t=i}):(s=this.value(),a=this._valueMin(),n=this._valueMax(),i=n!==a?100*((s-a)/(n-a)):0,u["horizontal"===this.orientation?"left":"bottom"]=i+"%",this.handle.stop(1,1)[l?"animate":"css"](u,o.animate),"min"===r&&"horizontal"===this.orientation&&this.range.stop(1,1)[l?"animate":"css"]({width:i+"%"},o.animate),"max"===r&&"horizontal"===this.orientation&&this.range[l?"animate":"css"]({width:100-i+"%"},{queue:!1,duration:o.animate}),"min"===r&&"vertical"===this.orientation&&this.range.stop(1,1)[l?"animate":"css"]({height:i+"%"},o.animate),"max"===r&&"vertical"===this.orientation&&this.range[l?"animate":"css"]({height:100-i+"%"},{queue:!1,duration:o.animate}))}})})(jQuery);(function(e){function t(e){return function(){var t=this.element.val();e.apply(this,arguments),this._refresh(),t!==this.element.val()&&this._trigger("change")}}e.widget("ui.spinner",{version:"1.9.2",defaultElement:"",widgetEventPrefix:"spin",options:{culture:null,icons:{down:"ui-icon-triangle-1-s",up:"ui-icon-triangle-1-n"},incremental:!0,max:null,min:null,numberFormat:null,page:10,step:1,change:null,spin:null,start:null,stop:null},_create:function(){this._setOption("max",this.options.max),this._setOption("min",this.options.min),this._setOption("step",this.options.step),this._value(this.element.val(),!0),this._draw(),this._on(this._events),this._refresh(),this._on(this.window,{beforeunload:function(){this.element.removeAttr("autocomplete")}})},_getCreateOptions:function(){var t={},i=this.element;return e.each(["min","max","step"],function(e,s){var a=i.attr(s);void 0!==a&&a.length&&(t[s]=a)}),t},_events:{keydown:function(e){this._start(e)&&this._keydown(e)&&e.preventDefault()},keyup:"_stop",focus:function(){this.previous=this.element.val()},blur:function(e){return this.cancelBlur?(delete this.cancelBlur,void 0):(this._refresh(),this.previous!==this.element.val()&&this._trigger("change",e),void 0)},mousewheel:function(e,t){if(t){if(!this.spinning&&!this._start(e))return!1;this._spin((t>0?1:-1)*this.options.step,e),clearTimeout(this.mousewheelTimer),this.mousewheelTimer=this._delay(function(){this.spinning&&this._stop(e)},100),e.preventDefault()}},"mousedown .ui-spinner-button":function(t){function i(){var e=this.element[0]===this.document[0].activeElement;e||(this.element.focus(),this.previous=s,this._delay(function(){this.previous=s}))}var s;s=this.element[0]===this.document[0].activeElement?this.previous:this.element.val(),t.preventDefault(),i.call(this),this.cancelBlur=!0,this._delay(function(){delete this.cancelBlur,i.call(this)}),this._start(t)!==!1&&this._repeat(null,e(t.currentTarget).hasClass("ui-spinner-up")?1:-1,t)},"mouseup .ui-spinner-button":"_stop","mouseenter .ui-spinner-button":function(t){return e(t.currentTarget).hasClass("ui-state-active")?this._start(t)===!1?!1:(this._repeat(null,e(t.currentTarget).hasClass("ui-spinner-up")?1:-1,t),void 0):void 0},"mouseleave .ui-spinner-button":"_stop"},_draw:function(){var e=this.uiSpinner=this.element.addClass("ui-spinner-input").attr("autocomplete","off").wrap(this._uiSpinnerHtml()).parent().append(this._buttonHtml());this.element.attr("role","spinbutton"),this.buttons=e.find(".ui-spinner-button").attr("tabIndex",-1).button().removeClass("ui-corner-all"),this.buttons.height()>Math.ceil(.5*e.height())&&e.height()>0&&e.height(e.height()),this.options.disabled&&this.disable()},_keydown:function(t){var i=this.options,s=e.ui.keyCode;switch(t.keyCode){case s.UP:return this._repeat(null,1,t),!0;case s.DOWN:return this._repeat(null,-1,t),!0;case s.PAGE_UP:return this._repeat(null,i.page,t),!0;case s.PAGE_DOWN:return this._repeat(null,-i.page,t),!0}return!1},_uiSpinnerHtml:function(){return""},_buttonHtml:function(){return""+""+""+""+""},_start:function(e){return this.spinning||this._trigger("start",e)!==!1?(this.counter||(this.counter=1),this.spinning=!0,!0):!1},_repeat:function(e,t,i){e=e||500,clearTimeout(this.timer),this.timer=this._delay(function(){this._repeat(40,t,i)},e),this._spin(t*this.options.step,i)},_spin:function(e,t){var i=this.value()||0;this.counter||(this.counter=1),i=this._adjustValue(i+e*this._increment(this.counter)),this.spinning&&this._trigger("spin",t,{value:i})===!1||(this._value(i),this.counter++)},_increment:function(t){var i=this.options.incremental;return i?e.isFunction(i)?i(t):Math.floor(t*t*t/5e4-t*t/500+17*t/200+1):1},_precision:function(){var e=this._precisionOf(this.options.step);return null!==this.options.min&&(e=Math.max(e,this._precisionOf(this.options.min))),e},_precisionOf:function(e){var t=""+e,i=t.indexOf(".");return-1===i?0:t.length-i-1},_adjustValue:function(e){var t,i,s=this.options;return t=null!==s.min?s.min:0,i=e-t,i=Math.round(i/s.step)*s.step,e=t+i,e=parseFloat(e.toFixed(this._precision())),null!==s.max&&e>s.max?s.max:null!==s.min&&s.min>e?s.min:e},_stop:function(e){this.spinning&&(clearTimeout(this.timer),clearTimeout(this.mousewheelTimer),this.counter=0,this.spinning=!1,this._trigger("stop",e))},_setOption:function(e,t){if("culture"===e||"numberFormat"===e){var i=this._parse(this.element.val());return this.options[e]=t,this.element.val(this._format(i)),void 0}("max"===e||"min"===e||"step"===e)&&"string"==typeof t&&(t=this._parse(t)),this._super(e,t),"disabled"===e&&(t?(this.element.prop("disabled",!0),this.buttons.button("disable")):(this.element.prop("disabled",!1),this.buttons.button("enable")))},_setOptions:t(function(e){this._super(e),this._value(this.element.val())}),_parse:function(e){return"string"==typeof e&&""!==e&&(e=window.Globalize&&this.options.numberFormat?Globalize.parseFloat(e,10,this.options.culture):+e),""===e||isNaN(e)?null:e},_format:function(e){return""===e?"":window.Globalize&&this.options.numberFormat?Globalize.format(e,this.options.numberFormat,this.options.culture):e},_refresh:function(){this.element.attr({"aria-valuemin":this.options.min,"aria-valuemax":this.options.max,"aria-valuenow":this._parse(this.element.val())})},_value:function(e,t){var i;""!==e&&(i=this._parse(e),null!==i&&(t||(i=this._adjustValue(i)),e=this._format(i))),this.element.val(e),this._refresh()},_destroy:function(){this.element.removeClass("ui-spinner-input").prop("disabled",!1).removeAttr("autocomplete").removeAttr("role").removeAttr("aria-valuemin").removeAttr("aria-valuemax").removeAttr("aria-valuenow"),this.uiSpinner.replaceWith(this.element)},stepUp:t(function(e){this._stepUp(e)}),_stepUp:function(e){this._spin((e||1)*this.options.step)},stepDown:t(function(e){this._stepDown(e)}),_stepDown:function(e){this._spin((e||1)*-this.options.step)},pageUp:t(function(e){this._stepUp((e||1)*this.options.page)}),pageDown:t(function(e){this._stepDown((e||1)*this.options.page)}),value:function(e){return arguments.length?(t(this._value).call(this,e),void 0):this._parse(this.element.val())},widget:function(){return this.uiSpinner}})})(jQuery);(function(e,t){function i(){return++a}function s(e){return e.hash.length>1&&e.href.replace(n,"")===location.href.replace(n,"").replace(/\s/g,"%20")}var a=0,n=/#.*$/;e.widget("ui.tabs",{version:"1.9.2",delay:300,options:{active:null,collapsible:!1,event:"click",heightStyle:"content",hide:null,show:null,activate:null,beforeActivate:null,beforeLoad:null,load:null},_create:function(){var i=this,s=this.options,a=s.active,n=location.hash.substring(1);this.running=!1,this.element.addClass("ui-tabs ui-widget ui-widget-content ui-corner-all").toggleClass("ui-tabs-collapsible",s.collapsible).delegate(".ui-tabs-nav > li","mousedown"+this.eventNamespace,function(t){e(this).is(".ui-state-disabled")&&t.preventDefault()}).delegate(".ui-tabs-anchor","focus"+this.eventNamespace,function(){e(this).closest("li").is(".ui-state-disabled")&&this.blur()}),this._processTabs(),null===a&&(n&&this.tabs.each(function(i,s){return e(s).attr("aria-controls")===n?(a=i,!1):t}),null===a&&(a=this.tabs.index(this.tabs.filter(".ui-tabs-active"))),(null===a||-1===a)&&(a=this.tabs.length?0:!1)),a!==!1&&(a=this.tabs.index(this.tabs.eq(a)),-1===a&&(a=s.collapsible?!1:0)),s.active=a,!s.collapsible&&s.active===!1&&this.anchors.length&&(s.active=0),e.isArray(s.disabled)&&(s.disabled=e.unique(s.disabled.concat(e.map(this.tabs.filter(".ui-state-disabled"),function(e){return i.tabs.index(e)}))).sort()),this.active=this.options.active!==!1&&this.anchors.length?this._findActive(this.options.active):e(),this._refresh(),this.active.length&&this.load(s.active)},_getCreateEventData:function(){return{tab:this.active,panel:this.active.length?this._getPanelForTab(this.active):e()}},_tabKeydown:function(i){var s=e(this.document[0].activeElement).closest("li"),a=this.tabs.index(s),n=!0;if(!this._handlePageNav(i)){switch(i.keyCode){case e.ui.keyCode.RIGHT:case e.ui.keyCode.DOWN:a++;break;case e.ui.keyCode.UP:case e.ui.keyCode.LEFT:n=!1,a--;break;case e.ui.keyCode.END:a=this.anchors.length-1;break;case e.ui.keyCode.HOME:a=0;break;case e.ui.keyCode.SPACE:return i.preventDefault(),clearTimeout(this.activating),this._activate(a),t;case e.ui.keyCode.ENTER:return i.preventDefault(),clearTimeout(this.activating),this._activate(a===this.options.active?!1:a),t;default:return}i.preventDefault(),clearTimeout(this.activating),a=this._focusNextTab(a,n),i.ctrlKey||(s.attr("aria-selected","false"),this.tabs.eq(a).attr("aria-selected","true"),this.activating=this._delay(function(){this.option("active",a)},this.delay))}},_panelKeydown:function(t){this._handlePageNav(t)||t.ctrlKey&&t.keyCode===e.ui.keyCode.UP&&(t.preventDefault(),this.active.focus())},_handlePageNav:function(i){return i.altKey&&i.keyCode===e.ui.keyCode.PAGE_UP?(this._activate(this._focusNextTab(this.options.active-1,!1)),!0):i.altKey&&i.keyCode===e.ui.keyCode.PAGE_DOWN?(this._activate(this._focusNextTab(this.options.active+1,!0)),!0):t},_findNextTab:function(t,i){function s(){return t>a&&(t=0),0>t&&(t=a),t}for(var a=this.tabs.length-1;-1!==e.inArray(s(),this.options.disabled);)t=i?t+1:t-1;return t},_focusNextTab:function(e,t){return e=this._findNextTab(e,t),this.tabs.eq(e).focus(),e},_setOption:function(e,i){return"active"===e?(this._activate(i),t):"disabled"===e?(this._setupDisabled(i),t):(this._super(e,i),"collapsible"===e&&(this.element.toggleClass("ui-tabs-collapsible",i),i||this.options.active!==!1||this._activate(0)),"event"===e&&this._setupEvents(i),"heightStyle"===e&&this._setupHeightStyle(i),t)},_tabId:function(e){return e.attr("aria-controls")||"ui-tabs-"+i()},_sanitizeSelector:function(e){return e?e.replace(/[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g,"\\$&"):""},refresh:function(){var t=this.options,i=this.tablist.children(":has(a[href])");t.disabled=e.map(i.filter(".ui-state-disabled"),function(e){return i.index(e)}),this._processTabs(),t.active!==!1&&this.anchors.length?this.active.length&&!e.contains(this.tablist[0],this.active[0])?this.tabs.length===t.disabled.length?(t.active=!1,this.active=e()):this._activate(this._findNextTab(Math.max(0,t.active-1),!1)):t.active=this.tabs.index(this.active):(t.active=!1,this.active=e()),this._refresh()},_refresh:function(){this._setupDisabled(this.options.disabled),this._setupEvents(this.options.event),this._setupHeightStyle(this.options.heightStyle),this.tabs.not(this.active).attr({"aria-selected":"false",tabIndex:-1}),this.panels.not(this._getPanelForTab(this.active)).hide().attr({"aria-expanded":"false","aria-hidden":"true"}),this.active.length?(this.active.addClass("ui-tabs-active ui-state-active").attr({"aria-selected":"true",tabIndex:0}),this._getPanelForTab(this.active).show().attr({"aria-expanded":"true","aria-hidden":"false"})):this.tabs.eq(0).attr("tabIndex",0)},_processTabs:function(){var t=this;this.tablist=this._getList().addClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all").attr("role","tablist"),this.tabs=this.tablist.find("> li:has(a[href])").addClass("ui-state-default ui-corner-top").attr({role:"tab",tabIndex:-1}),this.anchors=this.tabs.map(function(){return e("a",this)[0]}).addClass("ui-tabs-anchor").attr({role:"presentation",tabIndex:-1}),this.panels=e(),this.anchors.each(function(i,a){var n,r,o,h=e(a).uniqueId().attr("id"),l=e(a).closest("li"),u=l.attr("aria-controls");s(a)?(n=a.hash,r=t.element.find(t._sanitizeSelector(n))):(o=t._tabId(l),n="#"+o,r=t.element.find(n),r.length||(r=t._createPanel(o),r.insertAfter(t.panels[i-1]||t.tablist)),r.attr("aria-live","polite")),r.length&&(t.panels=t.panels.add(r)),u&&l.data("ui-tabs-aria-controls",u),l.attr({"aria-controls":n.substring(1),"aria-labelledby":h}),r.attr("aria-labelledby",h)}),this.panels.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").attr("role","tabpanel")},_getList:function(){return this.element.find("ol,ul").eq(0)},_createPanel:function(t){return e("
").attr("id",t).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").data("ui-tabs-destroy",!0)},_setupDisabled:function(t){e.isArray(t)&&(t.length?t.length===this.anchors.length&&(t=!0):t=!1);for(var i,s=0;i=this.tabs[s];s++)t===!0||-1!==e.inArray(s,t)?e(i).addClass("ui-state-disabled").attr("aria-disabled","true"):e(i).removeClass("ui-state-disabled").removeAttr("aria-disabled");this.options.disabled=t},_setupEvents:function(t){var i={click:function(e){e.preventDefault()}};t&&e.each(t.split(" "),function(e,t){i[t]="_eventHandler"}),this._off(this.anchors.add(this.tabs).add(this.panels)),this._on(this.anchors,i),this._on(this.tabs,{keydown:"_tabKeydown"}),this._on(this.panels,{keydown:"_panelKeydown"}),this._focusable(this.tabs),this._hoverable(this.tabs)},_setupHeightStyle:function(t){var i,s,a=this.element.parent();"fill"===t?(e.support.minHeight||(s=a.css("overflow"),a.css("overflow","hidden")),i=a.height(),this.element.siblings(":visible").each(function(){var t=e(this),s=t.css("position");"absolute"!==s&&"fixed"!==s&&(i-=t.outerHeight(!0))}),s&&a.css("overflow",s),this.element.children().not(this.panels).each(function(){i-=e(this).outerHeight(!0)}),this.panels.each(function(){e(this).height(Math.max(0,i-e(this).innerHeight()+e(this).height()))}).css("overflow","auto")):"auto"===t&&(i=0,this.panels.each(function(){i=Math.max(i,e(this).height("").height())}).height(i))},_eventHandler:function(t){var i=this.options,s=this.active,a=e(t.currentTarget),n=a.closest("li"),r=n[0]===s[0],o=r&&i.collapsible,h=o?e():this._getPanelForTab(n),l=s.length?this._getPanelForTab(s):e(),u={oldTab:s,oldPanel:l,newTab:o?e():n,newPanel:h};t.preventDefault(),n.hasClass("ui-state-disabled")||n.hasClass("ui-tabs-loading")||this.running||r&&!i.collapsible||this._trigger("beforeActivate",t,u)===!1||(i.active=o?!1:this.tabs.index(n),this.active=r?e():n,this.xhr&&this.xhr.abort(),l.length||h.length||e.error("jQuery UI Tabs: Mismatching fragment identifier."),h.length&&this.load(this.tabs.index(n),t),this._toggle(t,u))},_toggle:function(t,i){function s(){n.running=!1,n._trigger("activate",t,i)}function a(){i.newTab.closest("li").addClass("ui-tabs-active ui-state-active"),r.length&&n.options.show?n._show(r,n.options.show,s):(r.show(),s())}var n=this,r=i.newPanel,o=i.oldPanel;this.running=!0,o.length&&this.options.hide?this._hide(o,this.options.hide,function(){i.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),a()}):(i.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),o.hide(),a()),o.attr({"aria-expanded":"false","aria-hidden":"true"}),i.oldTab.attr("aria-selected","false"),r.length&&o.length?i.oldTab.attr("tabIndex",-1):r.length&&this.tabs.filter(function(){return 0===e(this).attr("tabIndex")}).attr("tabIndex",-1),r.attr({"aria-expanded":"true","aria-hidden":"false"}),i.newTab.attr({"aria-selected":"true",tabIndex:0})},_activate:function(t){var i,s=this._findActive(t);s[0]!==this.active[0]&&(s.length||(s=this.active),i=s.find(".ui-tabs-anchor")[0],this._eventHandler({target:i,currentTarget:i,preventDefault:e.noop}))},_findActive:function(t){return t===!1?e():this.tabs.eq(t)},_getIndex:function(e){return"string"==typeof e&&(e=this.anchors.index(this.anchors.filter("[href$='"+e+"']"))),e},_destroy:function(){this.xhr&&this.xhr.abort(),this.element.removeClass("ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible"),this.tablist.removeClass("ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all").removeAttr("role"),this.anchors.removeClass("ui-tabs-anchor").removeAttr("role").removeAttr("tabIndex").removeData("href.tabs").removeData("load.tabs").removeUniqueId(),this.tabs.add(this.panels).each(function(){e.data(this,"ui-tabs-destroy")?e(this).remove():e(this).removeClass("ui-state-default ui-state-active ui-state-disabled ui-corner-top ui-corner-bottom ui-widget-content ui-tabs-active ui-tabs-panel").removeAttr("tabIndex").removeAttr("aria-live").removeAttr("aria-busy").removeAttr("aria-selected").removeAttr("aria-labelledby").removeAttr("aria-hidden").removeAttr("aria-expanded").removeAttr("role")}),this.tabs.each(function(){var t=e(this),i=t.data("ui-tabs-aria-controls");i?t.attr("aria-controls",i):t.removeAttr("aria-controls")}),this.panels.show(),"content"!==this.options.heightStyle&&this.panels.css("height","")},enable:function(i){var s=this.options.disabled;s!==!1&&(i===t?s=!1:(i=this._getIndex(i),s=e.isArray(s)?e.map(s,function(e){return e!==i?e:null}):e.map(this.tabs,function(e,t){return t!==i?t:null})),this._setupDisabled(s))},disable:function(i){var s=this.options.disabled;if(s!==!0){if(i===t)s=!0;else{if(i=this._getIndex(i),-1!==e.inArray(i,s))return;s=e.isArray(s)?e.merge([i],s).sort():[i]}this._setupDisabled(s)}},load:function(t,i){t=this._getIndex(t);var a=this,n=this.tabs.eq(t),r=n.find(".ui-tabs-anchor"),o=this._getPanelForTab(n),h={tab:n,panel:o};s(r[0])||(this.xhr=e.ajax(this._ajaxSettings(r,i,h)),this.xhr&&"canceled"!==this.xhr.statusText&&(n.addClass("ui-tabs-loading"),o.attr("aria-busy","true"),this.xhr.success(function(e){setTimeout(function(){o.html(e),a._trigger("load",i,h)},1)}).complete(function(e,t){setTimeout(function(){"abort"===t&&a.panels.stop(!1,!0),n.removeClass("ui-tabs-loading"),o.removeAttr("aria-busy"),e===a.xhr&&delete a.xhr},1)})))},_ajaxSettings:function(t,i,s){var a=this;return{url:t.attr("href"),beforeSend:function(t,n){return a._trigger("beforeLoad",i,e.extend({jqXHR:t,ajaxSettings:n},s))}}},_getPanelForTab:function(t){var i=e(t).attr("aria-controls");return this.element.find(this._sanitizeSelector("#"+i))}}),e.uiBackCompat!==!1&&(e.ui.tabs.prototype._ui=function(e,t){return{tab:e,panel:t,index:this.anchors.index(e)}},e.widget("ui.tabs",e.ui.tabs,{url:function(e,t){this.anchors.eq(e).attr("href",t)}}),e.widget("ui.tabs",e.ui.tabs,{options:{ajaxOptions:null,cache:!1},_create:function(){this._super();var i=this;this._on({tabsbeforeload:function(s,a){return e.data(a.tab[0],"cache.tabs")?(s.preventDefault(),t):(a.jqXHR.success(function(){i.options.cache&&e.data(a.tab[0],"cache.tabs",!0)}),t)}})},_ajaxSettings:function(t,i,s){var a=this.options.ajaxOptions;return e.extend({},a,{error:function(e,t){try{a.error(e,t,s.tab.closest("li").index(),s.tab[0])}catch(i){}}},this._superApply(arguments))},_setOption:function(e,t){"cache"===e&&t===!1&&this.anchors.removeData("cache.tabs"),this._super(e,t)},_destroy:function(){this.anchors.removeData("cache.tabs"),this._super()},url:function(e){this.anchors.eq(e).removeData("cache.tabs"),this._superApply(arguments)}}),e.widget("ui.tabs",e.ui.tabs,{abort:function(){this.xhr&&this.xhr.abort()}}),e.widget("ui.tabs",e.ui.tabs,{options:{spinner:"Loading…"},_create:function(){this._super(),this._on({tabsbeforeload:function(e,t){if(e.target===this.element[0]&&this.options.spinner){var i=t.tab.find("span"),s=i.html();i.html(this.options.spinner),t.jqXHR.complete(function(){i.html(s)})}}})}}),e.widget("ui.tabs",e.ui.tabs,{options:{enable:null,disable:null},enable:function(t){var i,s=this.options;(t&&s.disabled===!0||e.isArray(s.disabled)&&-1!==e.inArray(t,s.disabled))&&(i=!0),this._superApply(arguments),i&&this._trigger("enable",null,this._ui(this.anchors[t],this.panels[t]))},disable:function(t){var i,s=this.options;(t&&s.disabled===!1||e.isArray(s.disabled)&&-1===e.inArray(t,s.disabled))&&(i=!0),this._superApply(arguments),i&&this._trigger("disable",null,this._ui(this.anchors[t],this.panels[t]))}}),e.widget("ui.tabs",e.ui.tabs,{options:{add:null,remove:null,tabTemplate:"
  • #{label}
  • "},add:function(i,s,a){a===t&&(a=this.anchors.length);var n,r,o=this.options,h=e(o.tabTemplate.replace(/#\{href\}/g,i).replace(/#\{label\}/g,s)),l=i.indexOf("#")?this._tabId(h):i.replace("#","");return h.addClass("ui-state-default ui-corner-top").data("ui-tabs-destroy",!0),h.attr("aria-controls",l),n=a>=this.tabs.length,r=this.element.find("#"+l),r.length||(r=this._createPanel(l),n?a>0?r.insertAfter(this.panels.eq(-1)):r.appendTo(this.element):r.insertBefore(this.panels[a])),r.addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").hide(),n?h.appendTo(this.tablist):h.insertBefore(this.tabs[a]),o.disabled=e.map(o.disabled,function(e){return e>=a?++e:e}),this.refresh(),1===this.tabs.length&&o.active===!1&&this.option("active",0),this._trigger("add",null,this._ui(this.anchors[a],this.panels[a])),this},remove:function(t){t=this._getIndex(t);var i=this.options,s=this.tabs.eq(t).remove(),a=this._getPanelForTab(s).remove();return s.hasClass("ui-tabs-active")&&this.anchors.length>2&&this._activate(t+(this.anchors.length>t+1?1:-1)),i.disabled=e.map(e.grep(i.disabled,function(e){return e!==t}),function(e){return e>=t?--e:e}),this.refresh(),this._trigger("remove",null,this._ui(s.find("a")[0],a[0])),this}}),e.widget("ui.tabs",e.ui.tabs,{length:function(){return this.anchors.length}}),e.widget("ui.tabs",e.ui.tabs,{options:{idPrefix:"ui-tabs-"},_tabId:function(t){var s=t.is("li")?t.find("a[href]"):t;return s=s[0],e(s).closest("li").attr("aria-controls")||s.title&&s.title.replace(/\s/g,"_").replace(/[^\w\u00c0-\uFFFF\-]/g,"")||this.options.idPrefix+i()}}),e.widget("ui.tabs",e.ui.tabs,{options:{panelTemplate:"
    "},_createPanel:function(t){return e(this.options.panelTemplate).attr("id",t).addClass("ui-tabs-panel ui-widget-content ui-corner-bottom").data("ui-tabs-destroy",!0)}}),e.widget("ui.tabs",e.ui.tabs,{_create:function(){var e=this.options;null===e.active&&e.selected!==t&&(e.active=-1===e.selected?!1:e.selected),this._super(),e.selected=e.active,e.selected===!1&&(e.selected=-1)},_setOption:function(e,t){if("selected"!==e)return this._super(e,t);var i=this.options;this._super("active",-1===t?!1:t),i.selected=i.active,i.selected===!1&&(i.selected=-1)},_eventHandler:function(){this._superApply(arguments),this.options.selected=this.options.active,this.options.selected===!1&&(this.options.selected=-1)}}),e.widget("ui.tabs",e.ui.tabs,{options:{show:null,select:null},_create:function(){this._super(),this.options.active!==!1&&this._trigger("show",null,this._ui(this.active.find(".ui-tabs-anchor")[0],this._getPanelForTab(this.active)[0]))},_trigger:function(e,t,i){var s,a,n=this._superApply(arguments);return n?("beforeActivate"===e?(s=i.newTab.length?i.newTab:i.oldTab,a=i.newPanel.length?i.newPanel:i.oldPanel,n=this._super("select",t,{tab:s.find(".ui-tabs-anchor")[0],panel:a[0],index:s.closest("li").index()})):"activate"===e&&i.newTab.length&&(n=this._super("show",t,{tab:i.newTab.find(".ui-tabs-anchor")[0],panel:i.newPanel[0],index:i.newTab.closest("li").index()})),n):!1}}),e.widget("ui.tabs",e.ui.tabs,{select:function(e){if(e=this._getIndex(e),-1===e){if(!this.options.collapsible||-1===this.options.selected)return;e=this.options.selected}this.anchors.eq(e).trigger(this.options.event+this.eventNamespace)}}),function(){var t=0;e.widget("ui.tabs",e.ui.tabs,{options:{cookie:null},_create:function(){var e,t=this.options;null==t.active&&t.cookie&&(e=parseInt(this._cookie(),10),-1===e&&(e=!1),t.active=e),this._super()},_cookie:function(i){var s=[this.cookie||(this.cookie=this.options.cookie.name||"ui-tabs-"+ ++t)];return arguments.length&&(s.push(i===!1?-1:i),s.push(this.options.cookie)),e.cookie.apply(null,s)},_refresh:function(){this._super(),this.options.cookie&&this._cookie(this.options.active,this.options.cookie)},_eventHandler:function(){this._superApply(arguments),this.options.cookie&&this._cookie(this.options.active,this.options.cookie)},_destroy:function(){this._super(),this.options.cookie&&this._cookie(null,this.options.cookie)}})}(),e.widget("ui.tabs",e.ui.tabs,{_trigger:function(t,i,s){var a=e.extend({},s);return"load"===t&&(a.panel=a.panel[0],a.tab=a.tab.find(".ui-tabs-anchor")[0]),this._super(t,i,a)}}),e.widget("ui.tabs",e.ui.tabs,{options:{fx:null},_getFx:function(){var t,i,s=this.options.fx;return s&&(e.isArray(s)?(t=s[0],i=s[1]):t=i=s),s?{show:i,hide:t}:null},_toggle:function(e,i){function s(){n.running=!1,n._trigger("activate",e,i)}function a(){i.newTab.closest("li").addClass("ui-tabs-active ui-state-active"),r.length&&h.show?r.animate(h.show,h.show.duration,function(){s()}):(r.show(),s())}var n=this,r=i.newPanel,o=i.oldPanel,h=this._getFx();return h?(n.running=!0,o.length&&h.hide?o.animate(h.hide,h.hide.duration,function(){i.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),a()}):(i.oldTab.closest("li").removeClass("ui-tabs-active ui-state-active"),o.hide(),a()),t):this._super(e,i)}}))})(jQuery);(function(e){function t(t,i){var s=(t.attr("aria-describedby")||"").split(/\s+/);s.push(i),t.data("ui-tooltip-id",i).attr("aria-describedby",e.trim(s.join(" ")))}function i(t){var i=t.data("ui-tooltip-id"),s=(t.attr("aria-describedby")||"").split(/\s+/),a=e.inArray(i,s);-1!==a&&s.splice(a,1),t.removeData("ui-tooltip-id"),s=e.trim(s.join(" ")),s?t.attr("aria-describedby",s):t.removeAttr("aria-describedby")}var s=0;e.widget("ui.tooltip",{version:"1.9.2",options:{content:function(){return e(this).attr("title")},hide:!0,items:"[title]:not([disabled])",position:{my:"left top+15",at:"left bottom",collision:"flipfit flip"},show:!0,tooltipClass:null,track:!1,close:null,open:null},_create:function(){this._on({mouseover:"open",focusin:"open"}),this.tooltips={},this.parents={},this.options.disabled&&this._disable()},_setOption:function(t,i){var s=this;return"disabled"===t?(this[i?"_disable":"_enable"](),this.options[t]=i,void 0):(this._super(t,i),"content"===t&&e.each(this.tooltips,function(e,t){s._updateContent(t)}),void 0)},_disable:function(){var t=this;e.each(this.tooltips,function(i,s){var a=e.Event("blur");a.target=a.currentTarget=s[0],t.close(a,!0)}),this.element.find(this.options.items).andSelf().each(function(){var t=e(this);t.is("[title]")&&t.data("ui-tooltip-title",t.attr("title")).attr("title","")})},_enable:function(){this.element.find(this.options.items).andSelf().each(function(){var t=e(this);t.data("ui-tooltip-title")&&t.attr("title",t.data("ui-tooltip-title"))})},open:function(t){var i=this,s=e(t?t.target:this.element).closest(this.options.items);s.length&&!s.data("ui-tooltip-id")&&(s.attr("title")&&s.data("ui-tooltip-title",s.attr("title")),s.data("ui-tooltip-open",!0),t&&"mouseover"===t.type&&s.parents().each(function(){var t,s=e(this);s.data("ui-tooltip-open")&&(t=e.Event("blur"),t.target=t.currentTarget=this,i.close(t,!0)),s.attr("title")&&(s.uniqueId(),i.parents[this.id]={element:this,title:s.attr("title")},s.attr("title",""))}),this._updateContent(s,t))},_updateContent:function(e,t){var i,s=this.options.content,a=this,n=t?t.type:null;return"string"==typeof s?this._open(t,e,s):(i=s.call(e[0],function(i){e.data("ui-tooltip-open")&&a._delay(function(){t&&(t.type=n),this._open(t,e,i)})}),i&&this._open(t,e,i),void 0)},_open:function(i,s,a){function n(e){l.of=e,r.is(":hidden")||r.position(l)}var r,o,h,l=e.extend({},this.options.position);if(a){if(r=this._find(s),r.length)return r.find(".ui-tooltip-content").html(a),void 0;s.is("[title]")&&(i&&"mouseover"===i.type?s.attr("title",""):s.removeAttr("title")),r=this._tooltip(s),t(s,r.attr("id")),r.find(".ui-tooltip-content").html(a),this.options.track&&i&&/^mouse/.test(i.type)?(this._on(this.document,{mousemove:n}),n(i)):r.position(e.extend({of:s},this.options.position)),r.hide(),this._show(r,this.options.show),this.options.show&&this.options.show.delay&&(h=setInterval(function(){r.is(":visible")&&(n(l.of),clearInterval(h))},e.fx.interval)),this._trigger("open",i,{tooltip:r}),o={keyup:function(t){if(t.keyCode===e.ui.keyCode.ESCAPE){var i=e.Event(t);i.currentTarget=s[0],this.close(i,!0)}},remove:function(){this._removeTooltip(r)}},i&&"mouseover"!==i.type||(o.mouseleave="close"),i&&"focusin"!==i.type||(o.focusout="close"),this._on(!0,s,o)}},close:function(t){var s=this,a=e(t?t.currentTarget:this.element),n=this._find(a);this.closing||(a.data("ui-tooltip-title")&&a.attr("title",a.data("ui-tooltip-title")),i(a),n.stop(!0),this._hide(n,this.options.hide,function(){s._removeTooltip(e(this))}),a.removeData("ui-tooltip-open"),this._off(a,"mouseleave focusout keyup"),a[0]!==this.element[0]&&this._off(a,"remove"),this._off(this.document,"mousemove"),t&&"mouseleave"===t.type&&e.each(this.parents,function(t,i){e(i.element).attr("title",i.title),delete s.parents[t]}),this.closing=!0,this._trigger("close",t,{tooltip:n}),this.closing=!1)},_tooltip:function(t){var i="ui-tooltip-"+s++,a=e("
    ").attr({id:i,role:"tooltip"}).addClass("ui-tooltip ui-widget ui-corner-all ui-widget-content "+(this.options.tooltipClass||""));return e("
    ").addClass("ui-tooltip-content").appendTo(a),a.appendTo(this.document[0].body),e.fn.bgiframe&&a.bgiframe(),this.tooltips[i]=t,a},_find:function(t){var i=t.data("ui-tooltip-id");return i?e("#"+i):e()},_removeTooltip:function(e){e.remove(),delete this.tooltips[e.attr("id")]},_destroy:function(){var t=this;e.each(this.tooltips,function(i,s){var a=e.Event("blur");a.target=a.currentTarget=s[0],t.close(a,!0),e("#"+i).remove(),s.data("ui-tooltip-title")&&(s.attr("title",s.data("ui-tooltip-title")),s.removeData("ui-tooltip-title"))})}})})(jQuery);jQuery.effects||function(e,t){var i=e.uiBackCompat!==!1,a="ui-effects-";e.effects={effect:{}},function(t,i){function a(e,t,i){var a=c[t.type]||{};return null==e?i||!t.def?null:t.def:(e=a.floor?~~e:parseFloat(e),isNaN(e)?t.def:a.mod?(e+a.mod)%a.mod:0>e?0:e>a.max?a.max:e)}function s(e){var a=u(),s=a._rgba=[];return e=e.toLowerCase(),m(l,function(t,n){var r,o=n.re.exec(e),h=o&&n.parse(o),l=n.space||"rgba";return h?(r=a[l](h),a[d[l].cache]=r[d[l].cache],s=a._rgba=r._rgba,!1):i}),s.length?("0,0,0,0"===s.join()&&t.extend(s,r.transparent),a):r[e]}function n(e,t,i){return i=(i+1)%1,1>6*i?e+6*(t-e)*i:1>2*i?t:2>3*i?e+6*(t-e)*(2/3-i):e}var r,o="backgroundColor borderBottomColor borderLeftColor borderRightColor borderTopColor color columnRuleColor outlineColor textDecorationColor textEmphasisColor".split(" "),h=/^([\-+])=\s*(\d+\.?\d*)/,l=[{re:/rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,parse:function(e){return[e[1],e[2],e[3],e[4]]}},{re:/rgba?\(\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,parse:function(e){return[2.55*e[1],2.55*e[2],2.55*e[3],e[4]]}},{re:/#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})/,parse:function(e){return[parseInt(e[1],16),parseInt(e[2],16),parseInt(e[3],16)]}},{re:/#([a-f0-9])([a-f0-9])([a-f0-9])/,parse:function(e){return[parseInt(e[1]+e[1],16),parseInt(e[2]+e[2],16),parseInt(e[3]+e[3],16)]}},{re:/hsla?\(\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)\%\s*,\s*(\d+(?:\.\d+)?)\%\s*(?:,\s*(\d+(?:\.\d+)?)\s*)?\)/,space:"hsla",parse:function(e){return[e[1],e[2]/100,e[3]/100,e[4]]}}],u=t.Color=function(e,i,a,s){return new t.Color.fn.parse(e,i,a,s)},d={rgba:{props:{red:{idx:0,type:"byte"},green:{idx:1,type:"byte"},blue:{idx:2,type:"byte"}}},hsla:{props:{hue:{idx:0,type:"degrees"},saturation:{idx:1,type:"percent"},lightness:{idx:2,type:"percent"}}}},c={"byte":{floor:!0,max:255},percent:{max:1},degrees:{mod:360,floor:!0}},p=u.support={},f=t("

    ")[0],m=t.each;f.style.cssText="background-color:rgba(1,1,1,.5)",p.rgba=f.style.backgroundColor.indexOf("rgba")>-1,m(d,function(e,t){t.cache="_"+e,t.props.alpha={idx:3,type:"percent",def:1}}),u.fn=t.extend(u.prototype,{parse:function(n,o,h,l){if(n===i)return this._rgba=[null,null,null,null],this;(n.jquery||n.nodeType)&&(n=t(n).css(o),o=i);var c=this,p=t.type(n),f=this._rgba=[];return o!==i&&(n=[n,o,h,l],p="array"),"string"===p?this.parse(s(n)||r._default):"array"===p?(m(d.rgba.props,function(e,t){f[t.idx]=a(n[t.idx],t)}),this):"object"===p?(n instanceof u?m(d,function(e,t){n[t.cache]&&(c[t.cache]=n[t.cache].slice())}):m(d,function(t,i){var s=i.cache;m(i.props,function(e,t){if(!c[s]&&i.to){if("alpha"===e||null==n[e])return;c[s]=i.to(c._rgba)}c[s][t.idx]=a(n[e],t,!0)}),c[s]&&0>e.inArray(null,c[s].slice(0,3))&&(c[s][3]=1,i.from&&(c._rgba=i.from(c[s])))}),this):i},is:function(e){var t=u(e),a=!0,s=this;return m(d,function(e,n){var r,o=t[n.cache];return o&&(r=s[n.cache]||n.to&&n.to(s._rgba)||[],m(n.props,function(e,t){return null!=o[t.idx]?a=o[t.idx]===r[t.idx]:i})),a}),a},_space:function(){var e=[],t=this;return m(d,function(i,a){t[a.cache]&&e.push(i)}),e.pop()},transition:function(e,t){var i=u(e),s=i._space(),n=d[s],r=0===this.alpha()?u("transparent"):this,o=r[n.cache]||n.to(r._rgba),h=o.slice();return i=i[n.cache],m(n.props,function(e,s){var n=s.idx,r=o[n],l=i[n],u=c[s.type]||{};null!==l&&(null===r?h[n]=l:(u.mod&&(l-r>u.mod/2?r+=u.mod:r-l>u.mod/2&&(r-=u.mod)),h[n]=a((l-r)*t+r,s)))}),this[s](h)},blend:function(e){if(1===this._rgba[3])return this;var i=this._rgba.slice(),a=i.pop(),s=u(e)._rgba;return u(t.map(i,function(e,t){return(1-a)*s[t]+a*e}))},toRgbaString:function(){var e="rgba(",i=t.map(this._rgba,function(e,t){return null==e?t>2?1:0:e});return 1===i[3]&&(i.pop(),e="rgb("),e+i.join()+")"},toHslaString:function(){var e="hsla(",i=t.map(this.hsla(),function(e,t){return null==e&&(e=t>2?1:0),t&&3>t&&(e=Math.round(100*e)+"%"),e});return 1===i[3]&&(i.pop(),e="hsl("),e+i.join()+")"},toHexString:function(e){var i=this._rgba.slice(),a=i.pop();return e&&i.push(~~(255*a)),"#"+t.map(i,function(e){return e=(e||0).toString(16),1===e.length?"0"+e:e}).join("")},toString:function(){return 0===this._rgba[3]?"transparent":this.toRgbaString()}}),u.fn.parse.prototype=u.fn,d.hsla.to=function(e){if(null==e[0]||null==e[1]||null==e[2])return[null,null,null,e[3]];var t,i,a=e[0]/255,s=e[1]/255,n=e[2]/255,r=e[3],o=Math.max(a,s,n),h=Math.min(a,s,n),l=o-h,u=o+h,d=.5*u;return t=h===o?0:a===o?60*(s-n)/l+360:s===o?60*(n-a)/l+120:60*(a-s)/l+240,i=0===d||1===d?d:.5>=d?l/u:l/(2-u),[Math.round(t)%360,i,d,null==r?1:r]},d.hsla.from=function(e){if(null==e[0]||null==e[1]||null==e[2])return[null,null,null,e[3]];var t=e[0]/360,i=e[1],a=e[2],s=e[3],r=.5>=a?a*(1+i):a+i-a*i,o=2*a-r;return[Math.round(255*n(o,r,t+1/3)),Math.round(255*n(o,r,t)),Math.round(255*n(o,r,t-1/3)),s]},m(d,function(e,s){var n=s.props,r=s.cache,o=s.to,l=s.from;u.fn[e]=function(e){if(o&&!this[r]&&(this[r]=o(this._rgba)),e===i)return this[r].slice();var s,h=t.type(e),d="array"===h||"object"===h?e:arguments,c=this[r].slice();return m(n,function(e,t){var i=d["object"===h?e:t.idx];null==i&&(i=c[t.idx]),c[t.idx]=a(i,t)}),l?(s=u(l(c)),s[r]=c,s):u(c)},m(n,function(i,a){u.fn[i]||(u.fn[i]=function(s){var n,r=t.type(s),o="alpha"===i?this._hsla?"hsla":"rgba":e,l=this[o](),u=l[a.idx];return"undefined"===r?u:("function"===r&&(s=s.call(this,u),r=t.type(s)),null==s&&a.empty?this:("string"===r&&(n=h.exec(s),n&&(s=u+parseFloat(n[2])*("+"===n[1]?1:-1))),l[a.idx]=s,this[o](l)))})})}),m(o,function(e,i){t.cssHooks[i]={set:function(e,a){var n,r,o="";if("string"!==t.type(a)||(n=s(a))){if(a=u(n||a),!p.rgba&&1!==a._rgba[3]){for(r="backgroundColor"===i?e.parentNode:e;(""===o||"transparent"===o)&&r&&r.style;)try{o=t.css(r,"backgroundColor"),r=r.parentNode}catch(h){}a=a.blend(o&&"transparent"!==o?o:"_default")}a=a.toRgbaString()}try{e.style[i]=a}catch(l){}}},t.fx.step[i]=function(e){e.colorInit||(e.start=u(e.elem,i),e.end=u(e.end),e.colorInit=!0),t.cssHooks[i].set(e.elem,e.start.transition(e.end,e.pos))}}),t.cssHooks.borderColor={expand:function(e){var t={};return m(["Top","Right","Bottom","Left"],function(i,a){t["border"+a+"Color"]=e}),t}},r=t.Color.names={aqua:"#00ffff",black:"#000000",blue:"#0000ff",fuchsia:"#ff00ff",gray:"#808080",green:"#008000",lime:"#00ff00",maroon:"#800000",navy:"#000080",olive:"#808000",purple:"#800080",red:"#ff0000",silver:"#c0c0c0",teal:"#008080",white:"#ffffff",yellow:"#ffff00",transparent:[null,null,null,0],_default:"#ffffff"}}(jQuery),function(){function i(){var t,i,a=this.ownerDocument.defaultView?this.ownerDocument.defaultView.getComputedStyle(this,null):this.currentStyle,s={};if(a&&a.length&&a[0]&&a[a[0]])for(i=a.length;i--;)t=a[i],"string"==typeof a[t]&&(s[e.camelCase(t)]=a[t]);else for(t in a)"string"==typeof a[t]&&(s[t]=a[t]);return s}function a(t,i){var a,s,r={};for(a in i)s=i[a],t[a]!==s&&(n[a]||(e.fx.step[a]||!isNaN(parseFloat(s)))&&(r[a]=s));return r}var s=["add","remove","toggle"],n={border:1,borderBottom:1,borderColor:1,borderLeft:1,borderRight:1,borderTop:1,borderWidth:1,margin:1,padding:1};e.each(["borderLeftStyle","borderRightStyle","borderBottomStyle","borderTopStyle"],function(t,i){e.fx.step[i]=function(e){("none"!==e.end&&!e.setAttr||1===e.pos&&!e.setAttr)&&(jQuery.style(e.elem,i,e.end),e.setAttr=!0)}}),e.effects.animateClass=function(t,n,r,o){var h=e.speed(n,r,o);return this.queue(function(){var n,r=e(this),o=r.attr("class")||"",l=h.children?r.find("*").andSelf():r;l=l.map(function(){var t=e(this);return{el:t,start:i.call(this)}}),n=function(){e.each(s,function(e,i){t[i]&&r[i+"Class"](t[i])})},n(),l=l.map(function(){return this.end=i.call(this.el[0]),this.diff=a(this.start,this.end),this}),r.attr("class",o),l=l.map(function(){var t=this,i=e.Deferred(),a=jQuery.extend({},h,{queue:!1,complete:function(){i.resolve(t)}});return this.el.animate(this.diff,a),i.promise()}),e.when.apply(e,l.get()).done(function(){n(),e.each(arguments,function(){var t=this.el;e.each(this.diff,function(e){t.css(e,"")})}),h.complete.call(r[0])})})},e.fn.extend({_addClass:e.fn.addClass,addClass:function(t,i,a,s){return i?e.effects.animateClass.call(this,{add:t},i,a,s):this._addClass(t)},_removeClass:e.fn.removeClass,removeClass:function(t,i,a,s){return i?e.effects.animateClass.call(this,{remove:t},i,a,s):this._removeClass(t)},_toggleClass:e.fn.toggleClass,toggleClass:function(i,a,s,n,r){return"boolean"==typeof a||a===t?s?e.effects.animateClass.call(this,a?{add:i}:{remove:i},s,n,r):this._toggleClass(i,a):e.effects.animateClass.call(this,{toggle:i},a,s,n)},switchClass:function(t,i,a,s,n){return e.effects.animateClass.call(this,{add:i,remove:t},a,s,n)}})}(),function(){function s(t,i,a,s){return e.isPlainObject(t)&&(i=t,t=t.effect),t={effect:t},null==i&&(i={}),e.isFunction(i)&&(s=i,a=null,i={}),("number"==typeof i||e.fx.speeds[i])&&(s=a,a=i,i={}),e.isFunction(a)&&(s=a,a=null),i&&e.extend(t,i),a=a||i.duration,t.duration=e.fx.off?0:"number"==typeof a?a:a in e.fx.speeds?e.fx.speeds[a]:e.fx.speeds._default,t.complete=s||i.complete,t}function n(t){return!t||"number"==typeof t||e.fx.speeds[t]?!0:"string"!=typeof t||e.effects.effect[t]?!1:i&&e.effects[t]?!1:!0}e.extend(e.effects,{version:"1.9.2",save:function(e,t){for(var i=0;t.length>i;i++)null!==t[i]&&e.data(a+t[i],e[0].style[t[i]])},restore:function(e,i){var s,n;for(n=0;i.length>n;n++)null!==i[n]&&(s=e.data(a+i[n]),s===t&&(s=""),e.css(i[n],s))},setMode:function(e,t){return"toggle"===t&&(t=e.is(":hidden")?"show":"hide"),t},getBaseline:function(e,t){var i,a;switch(e[0]){case"top":i=0;break;case"middle":i=.5;break;case"bottom":i=1;break;default:i=e[0]/t.height}switch(e[1]){case"left":a=0;break;case"center":a=.5;break;case"right":a=1;break;default:a=e[1]/t.width}return{x:a,y:i}},createWrapper:function(t){if(t.parent().is(".ui-effects-wrapper"))return t.parent();var i={width:t.outerWidth(!0),height:t.outerHeight(!0),"float":t.css("float")},a=e("

    ").addClass("ui-effects-wrapper").css({fontSize:"100%",background:"transparent",border:"none",margin:0,padding:0}),s={width:t.width(),height:t.height()},n=document.activeElement;try{n.id}catch(r){n=document.body}return t.wrap(a),(t[0]===n||e.contains(t[0],n))&&e(n).focus(),a=t.parent(),"static"===t.css("position")?(a.css({position:"relative"}),t.css({position:"relative"})):(e.extend(i,{position:t.css("position"),zIndex:t.css("z-index")}),e.each(["top","left","bottom","right"],function(e,a){i[a]=t.css(a),isNaN(parseInt(i[a],10))&&(i[a]="auto")}),t.css({position:"relative",top:0,left:0,right:"auto",bottom:"auto"})),t.css(s),a.css(i).show()},removeWrapper:function(t){var i=document.activeElement;return t.parent().is(".ui-effects-wrapper")&&(t.parent().replaceWith(t),(t[0]===i||e.contains(t[0],i))&&e(i).focus()),t},setTransition:function(t,i,a,s){return s=s||{},e.each(i,function(e,i){var n=t.cssUnit(i);n[0]>0&&(s[i]=n[0]*a+n[1])}),s}}),e.fn.extend({effect:function(){function t(t){function i(){e.isFunction(n)&&n.call(s[0]),e.isFunction(t)&&t()}var s=e(this),n=a.complete,r=a.mode;(s.is(":hidden")?"hide"===r:"show"===r)?i():o.call(s[0],a,i)}var a=s.apply(this,arguments),n=a.mode,r=a.queue,o=e.effects.effect[a.effect],h=!o&&i&&e.effects[a.effect];return e.fx.off||!o&&!h?n?this[n](a.duration,a.complete):this.each(function(){a.complete&&a.complete.call(this)}):o?r===!1?this.each(t):this.queue(r||"fx",t):h.call(this,{options:a,duration:a.duration,callback:a.complete,mode:a.mode})},_show:e.fn.show,show:function(e){if(n(e))return this._show.apply(this,arguments);var t=s.apply(this,arguments);return t.mode="show",this.effect.call(this,t)},_hide:e.fn.hide,hide:function(e){if(n(e))return this._hide.apply(this,arguments);var t=s.apply(this,arguments);return t.mode="hide",this.effect.call(this,t)},__toggle:e.fn.toggle,toggle:function(t){if(n(t)||"boolean"==typeof t||e.isFunction(t))return this.__toggle.apply(this,arguments);var i=s.apply(this,arguments);return i.mode="toggle",this.effect.call(this,i)},cssUnit:function(t){var i=this.css(t),a=[];return e.each(["em","px","%","pt"],function(e,t){i.indexOf(t)>0&&(a=[parseFloat(i),t])}),a}})}(),function(){var t={};e.each(["Quad","Cubic","Quart","Quint","Expo"],function(e,i){t[i]=function(t){return Math.pow(t,e+2)}}),e.extend(t,{Sine:function(e){return 1-Math.cos(e*Math.PI/2)},Circ:function(e){return 1-Math.sqrt(1-e*e)},Elastic:function(e){return 0===e||1===e?e:-Math.pow(2,8*(e-1))*Math.sin((80*(e-1)-7.5)*Math.PI/15)},Back:function(e){return e*e*(3*e-2)},Bounce:function(e){for(var t,i=4;((t=Math.pow(2,--i))-1)/11>e;);return 1/Math.pow(4,3-i)-7.5625*Math.pow((3*t-2)/22-e,2)}}),e.each(t,function(t,i){e.easing["easeIn"+t]=i,e.easing["easeOut"+t]=function(e){return 1-i(1-e)},e.easing["easeInOut"+t]=function(e){return.5>e?i(2*e)/2:1-i(-2*e+2)/2}})}()}(jQuery);(function(e){var t=/up|down|vertical/,i=/up|left|vertical|horizontal/;e.effects.effect.blind=function(a,s){var n,r,o,l=e(this),h=["position","top","bottom","left","right","height","width"],u=e.effects.setMode(l,a.mode||"hide"),d=a.direction||"up",c=t.test(d),p=c?"height":"width",m=c?"top":"left",f=i.test(d),g={},v="show"===u;l.parent().is(".ui-effects-wrapper")?e.effects.save(l.parent(),h):e.effects.save(l,h),l.show(),n=e.effects.createWrapper(l).css({overflow:"hidden"}),r=n[p](),o=parseFloat(n.css(m))||0,g[p]=v?r:0,f||(l.css(c?"bottom":"right",0).css(c?"top":"left","auto").css({position:"absolute"}),g[m]=v?o:r+o),v&&(n.css(p,0),f||n.css(m,o+r)),n.animate(g,{duration:a.duration,easing:a.easing,queue:!1,complete:function(){"hide"===u&&l.hide(),e.effects.restore(l,h),e.effects.removeWrapper(l),s()}})}})(jQuery);(function(e){e.effects.effect.bounce=function(t,i){var a,s,n,r=e(this),o=["position","top","bottom","left","right","height","width"],l=e.effects.setMode(r,t.mode||"effect"),h="hide"===l,u="show"===l,d=t.direction||"up",c=t.distance,p=t.times||5,m=2*p+(u||h?1:0),f=t.duration/m,g=t.easing,v="up"===d||"down"===d?"top":"left",y="up"===d||"left"===d,b=r.queue(),_=b.length;for((u||h)&&o.push("opacity"),e.effects.save(r,o),r.show(),e.effects.createWrapper(r),c||(c=r["top"===v?"outerHeight":"outerWidth"]()/3),u&&(n={opacity:1},n[v]=0,r.css("opacity",0).css(v,y?2*-c:2*c).animate(n,f,g)),h&&(c/=Math.pow(2,p-1)),n={},n[v]=0,a=0;p>a;a++)s={},s[v]=(y?"-=":"+=")+c,r.animate(s,f,g).animate(n,f,g),c=h?2*c:c/2;h&&(s={opacity:0},s[v]=(y?"-=":"+=")+c,r.animate(s,f,g)),r.queue(function(){h&&r.hide(),e.effects.restore(r,o),e.effects.removeWrapper(r),i()}),_>1&&b.splice.apply(b,[1,0].concat(b.splice(_,m+1))),r.dequeue()}})(jQuery);(function(e){e.effects.effect.clip=function(t,i){var a,s,n,r=e(this),o=["position","top","bottom","left","right","height","width"],l=e.effects.setMode(r,t.mode||"hide"),h="show"===l,u=t.direction||"vertical",d="vertical"===u,c=d?"height":"width",p=d?"top":"left",m={};e.effects.save(r,o),r.show(),a=e.effects.createWrapper(r).css({overflow:"hidden"}),s="IMG"===r[0].tagName?a:r,n=s[c](),h&&(s.css(c,0),s.css(p,n/2)),m[c]=h?n:0,m[p]=h?0:n/2,s.animate(m,{queue:!1,duration:t.duration,easing:t.easing,complete:function(){h||r.hide(),e.effects.restore(r,o),e.effects.removeWrapper(r),i()}})}})(jQuery);(function(e){e.effects.effect.drop=function(t,i){var a,s=e(this),n=["position","top","bottom","left","right","opacity","height","width"],r=e.effects.setMode(s,t.mode||"hide"),o="show"===r,l=t.direction||"left",h="up"===l||"down"===l?"top":"left",u="up"===l||"left"===l?"pos":"neg",d={opacity:o?1:0};e.effects.save(s,n),s.show(),e.effects.createWrapper(s),a=t.distance||s["top"===h?"outerHeight":"outerWidth"](!0)/2,o&&s.css("opacity",0).css(h,"pos"===u?-a:a),d[h]=(o?"pos"===u?"+=":"-=":"pos"===u?"-=":"+=")+a,s.animate(d,{queue:!1,duration:t.duration,easing:t.easing,complete:function(){"hide"===r&&s.hide(),e.effects.restore(s,n),e.effects.removeWrapper(s),i()}})}})(jQuery);(function(e){e.effects.effect.explode=function(t,i){function a(){b.push(this),b.length===d*c&&s()}function s(){p.css({visibility:"visible"}),e(b).remove(),f||p.hide(),i()}var n,r,o,l,h,u,d=t.pieces?Math.round(Math.sqrt(t.pieces)):3,c=d,p=e(this),m=e.effects.setMode(p,t.mode||"hide"),f="show"===m,g=p.show().css("visibility","hidden").offset(),v=Math.ceil(p.outerWidth()/c),y=Math.ceil(p.outerHeight()/d),b=[];for(n=0;d>n;n++)for(l=g.top+n*y,u=n-(d-1)/2,r=0;c>r;r++)o=g.left+r*v,h=r-(c-1)/2,p.clone().appendTo("body").wrap("
    ").css({position:"absolute",visibility:"visible",left:-r*v,top:-n*y}).parent().addClass("ui-effects-explode").css({position:"absolute",overflow:"hidden",width:v,height:y,left:o+(f?h*v:0),top:l+(f?u*y:0),opacity:f?0:1}).animate({left:o+(f?0:h*v),top:l+(f?0:u*y),opacity:f?1:0},t.duration||500,t.easing,a)}})(jQuery);(function(e){e.effects.effect.fade=function(t,i){var a=e(this),s=e.effects.setMode(a,t.mode||"toggle");a.animate({opacity:s},{queue:!1,duration:t.duration,easing:t.easing,complete:i})}})(jQuery);(function(e){e.effects.effect.fold=function(t,i){var a,s,n=e(this),r=["position","top","bottom","left","right","height","width"],o=e.effects.setMode(n,t.mode||"hide"),l="show"===o,h="hide"===o,u=t.size||15,d=/([0-9]+)%/.exec(u),c=!!t.horizFirst,p=l!==c,m=p?["width","height"]:["height","width"],f=t.duration/2,g={},v={};e.effects.save(n,r),n.show(),a=e.effects.createWrapper(n).css({overflow:"hidden"}),s=p?[a.width(),a.height()]:[a.height(),a.width()],d&&(u=parseInt(d[1],10)/100*s[h?0:1]),l&&a.css(c?{height:0,width:u}:{height:u,width:0}),g[m[0]]=l?s[0]:u,v[m[1]]=l?s[1]:0,a.animate(g,f,t.easing).animate(v,f,t.easing,function(){h&&n.hide(),e.effects.restore(n,r),e.effects.removeWrapper(n),i()})}})(jQuery);(function(e){e.effects.effect.highlight=function(t,i){var a=e(this),s=["backgroundImage","backgroundColor","opacity"],n=e.effects.setMode(a,t.mode||"show"),r={backgroundColor:a.css("backgroundColor")};"hide"===n&&(r.opacity=0),e.effects.save(a,s),a.show().css({backgroundImage:"none",backgroundColor:t.color||"#ffff99"}).animate(r,{queue:!1,duration:t.duration,easing:t.easing,complete:function(){"hide"===n&&a.hide(),e.effects.restore(a,s),i()}})}})(jQuery);(function(e){e.effects.effect.pulsate=function(t,i){var a,s=e(this),n=e.effects.setMode(s,t.mode||"show"),r="show"===n,o="hide"===n,l=r||"hide"===n,h=2*(t.times||5)+(l?1:0),u=t.duration/h,d=0,c=s.queue(),p=c.length;for((r||!s.is(":visible"))&&(s.css("opacity",0).show(),d=1),a=1;h>a;a++)s.animate({opacity:d},u,t.easing),d=1-d;s.animate({opacity:d},u,t.easing),s.queue(function(){o&&s.hide(),i()}),p>1&&c.splice.apply(c,[1,0].concat(c.splice(p,h+1))),s.dequeue()}})(jQuery);(function(e){e.effects.effect.puff=function(t,i){var a=e(this),s=e.effects.setMode(a,t.mode||"hide"),n="hide"===s,r=parseInt(t.percent,10)||150,o=r/100,h={height:a.height(),width:a.width(),outerHeight:a.outerHeight(),outerWidth:a.outerWidth()};e.extend(t,{effect:"scale",queue:!1,fade:!0,mode:s,complete:i,percent:n?r:100,from:n?h:{height:h.height*o,width:h.width*o,outerHeight:h.outerHeight*o,outerWidth:h.outerWidth*o}}),a.effect(t)},e.effects.effect.scale=function(t,i){var a=e(this),s=e.extend(!0,{},t),n=e.effects.setMode(a,t.mode||"effect"),r=parseInt(t.percent,10)||(0===parseInt(t.percent,10)?0:"hide"===n?0:100),o=t.direction||"both",h=t.origin,l={height:a.height(),width:a.width(),outerHeight:a.outerHeight(),outerWidth:a.outerWidth()},u={y:"horizontal"!==o?r/100:1,x:"vertical"!==o?r/100:1};s.effect="size",s.queue=!1,s.complete=i,"effect"!==n&&(s.origin=h||["middle","center"],s.restore=!0),s.from=t.from||("show"===n?{height:0,width:0,outerHeight:0,outerWidth:0}:l),s.to={height:l.height*u.y,width:l.width*u.x,outerHeight:l.outerHeight*u.y,outerWidth:l.outerWidth*u.x},s.fade&&("show"===n&&(s.from.opacity=0,s.to.opacity=1),"hide"===n&&(s.from.opacity=1,s.to.opacity=0)),a.effect(s)},e.effects.effect.size=function(t,i){var a,s,n,r=e(this),o=["position","top","bottom","left","right","width","height","overflow","opacity"],h=["position","top","bottom","left","right","overflow","opacity"],l=["width","height","overflow"],u=["fontSize"],d=["borderTopWidth","borderBottomWidth","paddingTop","paddingBottom"],c=["borderLeftWidth","borderRightWidth","paddingLeft","paddingRight"],p=e.effects.setMode(r,t.mode||"effect"),f=t.restore||"effect"!==p,m=t.scale||"both",g=t.origin||["middle","center"],v=r.css("position"),y=f?o:h,b={height:0,width:0,outerHeight:0,outerWidth:0};"show"===p&&r.show(),a={height:r.height(),width:r.width(),outerHeight:r.outerHeight(),outerWidth:r.outerWidth()},"toggle"===t.mode&&"show"===p?(r.from=t.to||b,r.to=t.from||a):(r.from=t.from||("show"===p?b:a),r.to=t.to||("hide"===p?b:a)),n={from:{y:r.from.height/a.height,x:r.from.width/a.width},to:{y:r.to.height/a.height,x:r.to.width/a.width}},("box"===m||"both"===m)&&(n.from.y!==n.to.y&&(y=y.concat(d),r.from=e.effects.setTransition(r,d,n.from.y,r.from),r.to=e.effects.setTransition(r,d,n.to.y,r.to)),n.from.x!==n.to.x&&(y=y.concat(c),r.from=e.effects.setTransition(r,c,n.from.x,r.from),r.to=e.effects.setTransition(r,c,n.to.x,r.to))),("content"===m||"both"===m)&&n.from.y!==n.to.y&&(y=y.concat(u).concat(l),r.from=e.effects.setTransition(r,u,n.from.y,r.from),r.to=e.effects.setTransition(r,u,n.to.y,r.to)),e.effects.save(r,y),r.show(),e.effects.createWrapper(r),r.css("overflow","hidden").css(r.from),g&&(s=e.effects.getBaseline(g,a),r.from.top=(a.outerHeight-r.outerHeight())*s.y,r.from.left=(a.outerWidth-r.outerWidth())*s.x,r.to.top=(a.outerHeight-r.to.outerHeight)*s.y,r.to.left=(a.outerWidth-r.to.outerWidth)*s.x),r.css(r.from),("content"===m||"both"===m)&&(d=d.concat(["marginTop","marginBottom"]).concat(u),c=c.concat(["marginLeft","marginRight"]),l=o.concat(d).concat(c),r.find("*[width]").each(function(){var i=e(this),a={height:i.height(),width:i.width(),outerHeight:i.outerHeight(),outerWidth:i.outerWidth()};f&&e.effects.save(i,l),i.from={height:a.height*n.from.y,width:a.width*n.from.x,outerHeight:a.outerHeight*n.from.y,outerWidth:a.outerWidth*n.from.x},i.to={height:a.height*n.to.y,width:a.width*n.to.x,outerHeight:a.height*n.to.y,outerWidth:a.width*n.to.x},n.from.y!==n.to.y&&(i.from=e.effects.setTransition(i,d,n.from.y,i.from),i.to=e.effects.setTransition(i,d,n.to.y,i.to)),n.from.x!==n.to.x&&(i.from=e.effects.setTransition(i,c,n.from.x,i.from),i.to=e.effects.setTransition(i,c,n.to.x,i.to)),i.css(i.from),i.animate(i.to,t.duration,t.easing,function(){f&&e.effects.restore(i,l)})})),r.animate(r.to,{queue:!1,duration:t.duration,easing:t.easing,complete:function(){0===r.to.opacity&&r.css("opacity",r.from.opacity),"hide"===p&&r.hide(),e.effects.restore(r,y),f||("static"===v?r.css({position:"relative",top:r.to.top,left:r.to.left}):e.each(["top","left"],function(e,t){r.css(t,function(t,i){var a=parseInt(i,10),s=e?r.to.left:r.to.top;return"auto"===i?s+"px":a+s+"px"})})),e.effects.removeWrapper(r),i()}})}})(jQuery);(function(e){e.effects.effect.shake=function(t,i){var a,s=e(this),n=["position","top","bottom","left","right","height","width"],r=e.effects.setMode(s,t.mode||"effect"),o=t.direction||"left",h=t.distance||20,l=t.times||3,u=2*l+1,d=Math.round(t.duration/u),c="up"===o||"down"===o?"top":"left",p="up"===o||"left"===o,f={},m={},g={},v=s.queue(),y=v.length;for(e.effects.save(s,n),s.show(),e.effects.createWrapper(s),f[c]=(p?"-=":"+=")+h,m[c]=(p?"+=":"-=")+2*h,g[c]=(p?"-=":"+=")+2*h,s.animate(f,d,t.easing),a=1;l>a;a++)s.animate(m,d,t.easing).animate(g,d,t.easing);s.animate(m,d,t.easing).animate(f,d/2,t.easing).queue(function(){"hide"===r&&s.hide(),e.effects.restore(s,n),e.effects.removeWrapper(s),i()}),y>1&&v.splice.apply(v,[1,0].concat(v.splice(y,u+1))),s.dequeue()}})(jQuery);(function(e){e.effects.effect.slide=function(t,i){var a,s=e(this),n=["position","top","bottom","left","right","width","height"],r=e.effects.setMode(s,t.mode||"show"),o="show"===r,h=t.direction||"left",l="up"===h||"down"===h?"top":"left",u="up"===h||"left"===h,d={};e.effects.save(s,n),s.show(),a=t.distance||s["top"===l?"outerHeight":"outerWidth"](!0),e.effects.createWrapper(s).css({overflow:"hidden"}),o&&s.css(l,u?isNaN(a)?"-"+a:-a:a),d[l]=(o?u?"+=":"-=":u?"-=":"+=")+a,s.animate(d,{queue:!1,duration:t.duration,easing:t.easing,complete:function(){"hide"===r&&s.hide(),e.effects.restore(s,n),e.effects.removeWrapper(s),i()}})}})(jQuery);(function(e){e.effects.effect.transfer=function(t,i){var a=e(this),s=e(t.to),n="fixed"===s.css("position"),r=e("body"),o=n?r.scrollTop():0,h=n?r.scrollLeft():0,l=s.offset(),u={top:l.top-o,left:l.left-h,height:s.innerHeight(),width:s.innerWidth()},d=a.offset(),c=e('
    ').appendTo(document.body).addClass(t.className).css({top:d.top-o,left:d.left-h,height:a.innerHeight(),width:a.innerWidth(),position:n?"fixed":"absolute"}).animate(u,t.duration,t.easing,function(){c.remove(),i()})}})(jQuery); \ No newline at end of file diff --git a/nengo_gui/static/lib/js/jqueryFileTree/images/directory.png b/nengo_gui/static/lib/js/jqueryFileTree/images/directory.png deleted file mode 100644 index 784e8fa4..00000000 Binary files a/nengo_gui/static/lib/js/jqueryFileTree/images/directory.png and /dev/null differ diff --git a/nengo_gui/static/lib/js/jqueryFileTree/images/file.png b/nengo_gui/static/lib/js/jqueryFileTree/images/file.png deleted file mode 100644 index 0406f4df..00000000 Binary files a/nengo_gui/static/lib/js/jqueryFileTree/images/file.png and /dev/null differ diff --git a/nengo_gui/static/lib/js/jqueryFileTree/images/folder_open.png b/nengo_gui/static/lib/js/jqueryFileTree/images/folder_open.png deleted file mode 100644 index 4e354835..00000000 Binary files a/nengo_gui/static/lib/js/jqueryFileTree/images/folder_open.png and /dev/null differ diff --git a/nengo_gui/static/lib/js/jqueryFileTree/images/scriptgen.png b/nengo_gui/static/lib/js/jqueryFileTree/images/scriptgen.png deleted file mode 100644 index 672ffcf6..00000000 Binary files a/nengo_gui/static/lib/js/jqueryFileTree/images/scriptgen.png and /dev/null differ diff --git a/nengo_gui/static/lib/js/jqueryFileTree/images/spinner.gif b/nengo_gui/static/lib/js/jqueryFileTree/images/spinner.gif deleted file mode 100644 index 85b99d46..00000000 Binary files a/nengo_gui/static/lib/js/jqueryFileTree/images/spinner.gif and /dev/null differ diff --git a/nengo_gui/static/lib/js/jqueryFileTree/jqueryFileTree.css b/nengo_gui/static/lib/js/jqueryFileTree/jqueryFileTree.css deleted file mode 100644 index c7f18d9e..00000000 --- a/nengo_gui/static/lib/js/jqueryFileTree/jqueryFileTree.css +++ /dev/null @@ -1,32 +0,0 @@ -UL.jqueryFileTree { - font-family: Verdana, sans-serif; - font-size: 14px; - line-height: 20px; - padding: 0px; - margin: 0px; -} - -UL.jqueryFileTree LI { - list-style: none; - padding: 0px; - padding-left: 20px; - margin: 0px; - white-space: nowrap; -} - -UL.jqueryFileTree A { - color: #333; - text-decoration: none; - display: block; - padding: 0px 2px; -} - -UL.jqueryFileTree A:hover { - background: #BBB; -} - -/* Core Styles */ -.jqueryFileTree LI.directory { background: url(images/directory.png) left top no-repeat; } -.jqueryFileTree LI.expanded { background: url(images/folder_open.png) left top no-repeat; } -.jqueryFileTree LI.file { background: url(images/file.png) left top no-repeat; } -.jqueryFileTree LI.wait { background: url(images/spinner.gif) left top no-repeat; } diff --git a/nengo_gui/static/lib/js/jqueryFileTree/jqueryFileTree.js b/nengo_gui/static/lib/js/jqueryFileTree/jqueryFileTree.js deleted file mode 100644 index 48704678..00000000 --- a/nengo_gui/static/lib/js/jqueryFileTree/jqueryFileTree.js +++ /dev/null @@ -1,95 +0,0 @@ -// jQuery File Tree Plugin -// -// Version 1.01 -// -// Cory S.N. LaViska -// A Beautiful Site (http://abeautifulsite.net/) -// 24 March 2008 -// -// Visit http://abeautifulsite.net/notebook.php?article=58 for more information -// -// Usage: $('.fileTreeDemo').fileTree( options, callback ) -// -// Options: root - root folder to display; default = / -// script - location of the serverside AJAX file to use; default = jqueryFileTree.php -// folderEvent - event to trigger expand/collapse; default = click -// expandSpeed - default = 500 (ms); use -1 for no animation -// collapseSpeed - default = 500 (ms); use -1 for no animation -// expandEasing - easing function to use on expand (optional) -// collapseEasing - easing function to use on collapse (optional) -// multiFolder - whether or not to limit the browser to one subfolder at a time -// loadMessage - Message to display while initial tree loads (can be HTML) -// -// History: -// -// 1.01 - updated to work with foreign characters in directory/file names (12 April 2008) -// 1.00 - released (24 March 2008) -// -// TERMS OF USE -// -// This plugin is dual-licensed under the GNU General Public License and the MIT License and -// is copyright 2008 A Beautiful Site, LLC. -// -if(jQuery) (function($){ - - $.extend($.fn, { - fileTree: function(o, h) { - // Defaults - if( !o ) var o = {}; - if( o.root == undefined ) o.root = '/'; - if( o.script == undefined ) o.script = 'jqueryFileTree.php'; - if( o.folderEvent == undefined ) o.folderEvent = 'click'; - if( o.expandSpeed == undefined ) o.expandSpeed= 500; - if( o.collapseSpeed == undefined ) o.collapseSpeed= 500; - if( o.expandEasing == undefined ) o.expandEasing = null; - if( o.collapseEasing == undefined ) o.collapseEasing = null; - if( o.multiFolder == undefined ) o.multiFolder = true; - if( o.loadMessage == undefined ) o.loadMessage = 'Loading...'; - - $(this).each( function() { - - function showTree(c, t) { - $(c).addClass('wait'); - $(".jqueryFileTree.start").remove(); - $.post(o.script, { dir: t }, function(data) { - $(c).find('.start').html(''); - $(c).removeClass('wait').append(data); - if( o.root == t ) $(c).find('UL:hidden').show(); else $(c).find('UL:hidden').slideDown({ duration: o.expandSpeed, easing: o.expandEasing }); - bindTree(c); - }); - } - - function bindTree(t) { - $(t).find('LI A').bind(o.folderEvent, function() { - if( $(this).parent().hasClass('directory') ) { - if( $(this).parent().hasClass('collapsed') ) { - // Expand - if( !o.multiFolder ) { - $(this).parent().parent().find('UL').slideUp({ duration: o.collapseSpeed, easing: o.collapseEasing }); - $(this).parent().parent().find('LI.directory').removeClass('expanded').addClass('collapsed'); - } - $(this).parent().find('UL').remove(); // cleanup - showTree( $(this).parent(), escape($(this).attr('rel').match( /.*\// )) ); - $(this).parent().removeClass('collapsed').addClass('expanded'); - } else { - // Collapse - $(this).parent().find('UL').slideUp({ duration: o.collapseSpeed, easing: o.collapseEasing }); - $(this).parent().removeClass('expanded').addClass('collapsed'); - } - } else { - h($(this).attr('rel')); - } - return false; - }); - // Prevent A from triggering the # on non-click events - if( o.folderEvent.toLowerCase != 'click' ) $(t).find('LI A').bind('click', function() { return false; }); - } - // Loading message - $(this).html('
    • ' + o.loadMessage + '
    '); - // Get the initial file list - showTree( $(this), escape(o.root) ); - }); - } - }); - -})(jQuery); \ No newline at end of file diff --git a/nengo_gui/static/lib/js/validator.js b/nengo_gui/static/lib/js/validator.js deleted file mode 100644 index 58d414ce..00000000 --- a/nengo_gui/static/lib/js/validator.js +++ /dev/null @@ -1,303 +0,0 @@ -/*! - * Validator v0.8.1 for Bootstrap 3, by @1000hz - * Copyright 2015 Cina Saffary - * Licensed under http://opensource.org/licenses/MIT - * - * https://github.com/1000hz/bootstrap-validator - */ - -+function ($) { - 'use strict'; - - var inputSelector = ':input:not([type="submit"], button):enabled:visible' - // VALIDATOR CLASS DEFINITION - // ========================== - - var Validator = function (element, options) { - this.$element = $(element) - this.options = options - - options.errors = $.extend({}, Validator.DEFAULTS.errors, options.errors) - - for (var custom in options.custom) { - if (!options.errors[custom]) throw new Error('Missing default error message for custom validator: ' + custom) - } - - $.extend(Validator.VALIDATORS, options.custom) - - this.$element.attr('novalidate', true) // disable automatic native validation - this.toggleSubmit() - - this.$element.on('input.bs.validator change.bs.validator focusout.bs.validator', $.proxy(this.validateInput, this)) - this.$element.on('submit.bs.validator', $.proxy(this.onSubmit, this)) - - this.$element.find('[data-match]').each(function () { - var $this = $(this) - var target = $this.data('match') - - $(target).on('input.bs.validator', function (e) { - $this.val() && $this.trigger('input.bs.validator') - }) - }) - } - - Validator.DEFAULTS = { - delay: 500, - html: false, - disable: true, - custom: {}, - errors: { - match: 'Does not match', - minlength: 'Not long enough' - }, - feedback: { - success: 'glyphicon-ok', - error: 'glyphicon-warning-sign' - } - } - - Validator.VALIDATORS = { - native: function ($el) { - var el = $el[0] - return el.checkValidity ? el.checkValidity() : true - }, - match: function ($el) { - var target = $el.data('match') - return !$el.val() || $el.val() === $(target).val() - }, - minlength: function ($el) { - var minlength = $el.data('minlength') - return !$el.val() || $el.val().length >= minlength - } - } - - Validator.prototype.validateInput = function (e) { - var $el = $(e.target) - var prevErrors = $el.data('bs.validator.errors') - var errors - - if ($el.is('[type="radio"]')) $el = this.$element.find('input[name="' + $el.attr('name') + '"]') - - this.$element.trigger(e = $.Event('validate.bs.validator', {relatedTarget: $el[0]})) - - if (e.isDefaultPrevented()) return - - var self = this - - this.runValidators($el).done(function (errors) { - $el.data('bs.validator.errors', errors) - - errors.length ? self.showErrors($el) : self.clearErrors($el) - - if (!prevErrors || errors.toString() !== prevErrors.toString()) { - e = errors.length - ? $.Event('invalid.bs.validator', {relatedTarget: $el[0], detail: errors}) - : $.Event('valid.bs.validator', {relatedTarget: $el[0], detail: prevErrors}) - - self.$element.trigger(e) - } - - self.toggleSubmit() - - self.$element.trigger($.Event('validated.bs.validator', {relatedTarget: $el[0]})) - }) - } - - - Validator.prototype.runValidators = function ($el) { - var errors = [] - var deferred = $.Deferred() - var options = this.options - - $el.data('bs.validator.deferred') && $el.data('bs.validator.deferred').reject() - $el.data('bs.validator.deferred', deferred) - - function getErrorMessage(key) { - return $el.data(key + '-error') - || $el.data('error') - || key == 'native' && $el[0].validationMessage - || options.errors[key] - } - - $.each(Validator.VALIDATORS, $.proxy(function (key, validator) { - if (($el.data(key) || key == 'native') && !validator.call(this, $el)) { - var error = getErrorMessage(key) - !~errors.indexOf(error) && errors.push(error) - } - }, this)) - - if (!errors.length && $el.val() && $el.data('remote')) { - this.defer($el, function () { - var data = {} - data[$el.attr('name')] = $el.val() - $.get($el.data('remote'), data) - .fail(function (jqXHR, textStatus, error) { errors.push(getErrorMessage('remote') || error) }) - .always(function () { deferred.resolve(errors)}) - }) - } else deferred.resolve(errors) - - return deferred.promise() - } - - Validator.prototype.validate = function () { - var delay = this.options.delay - - this.options.delay = 0 - this.$element.find(inputSelector).trigger('input.bs.validator') - this.options.delay = delay - - return this - } - - Validator.prototype.showErrors = function ($el) { - var method = this.options.html ? 'html' : 'text' - - this.defer($el, function () { - var $group = $el.closest('.form-group') - var $block = $group.find('.help-block.with-errors') - var $feedback = $group.find('.form-control-feedback') - var errors = $el.data('bs.validator.errors') - - if (!errors.length) return - - errors = $('
      ') - .addClass('list-unstyled') - .append($.map(errors, function (error) { return $('
    • ')[method](error) })) - - $block.data('bs.validator.originalContent') === undefined && $block.data('bs.validator.originalContent', $block.html()) - $block.empty().append(errors) - $group.addClass('has-error') - - $feedback.length - && $feedback.removeClass(this.options.feedback.success) - && $feedback.addClass(this.options.feedback.error) - && $group.removeClass('has-success') - }) - } - - Validator.prototype.clearErrors = function ($el) { - var $group = $el.closest('.form-group') - var $block = $group.find('.help-block.with-errors') - var $feedback = $group.find('.form-control-feedback') - - $block.html($block.data('bs.validator.originalContent')) - $group.removeClass('has-error') - - $feedback.length - && $feedback.removeClass(this.options.feedback.error) - && $feedback.addClass(this.options.feedback.success) - && $group.addClass('has-success') - } - - Validator.prototype.hasErrors = function () { - function fieldErrors() { - return !!($(this).data('bs.validator.errors') || []).length - } - - return !!this.$element.find(inputSelector).filter(fieldErrors).length - } - - Validator.prototype.isIncomplete = function () { - function fieldIncomplete() { - return this.type === 'checkbox' ? !this.checked : - this.type === 'radio' ? !$('[name="' + this.name + '"]:checked').length : - $.trim(this.value) === '' - } - - return !!this.$element.find(inputSelector).filter('[required]').filter(fieldIncomplete).length - } - - Validator.prototype.onSubmit = function (e) { - this.validate() - if (this.isIncomplete() || this.hasErrors()) e.preventDefault() - } - - Validator.prototype.toggleSubmit = function () { - if(!this.options.disable) return - var $btn = $('button[type="submit"], input[type="submit"]') - .filter('[form="' + this.$element.attr('id') + '"]') - .add(this.$element.find('input[type="submit"], button[type="submit"]')) - $btn.toggleClass('disabled', this.isIncomplete() || this.hasErrors()) - .css({'pointer-events': 'all', 'cursor': 'pointer'}) - } - - Validator.prototype.defer = function ($el, callback) { - callback = $.proxy(callback, this) - if (!this.options.delay) return callback() - window.clearTimeout($el.data('bs.validator.timeout')) - $el.data('bs.validator.timeout', window.setTimeout(callback, this.options.delay)) - } - - Validator.prototype.destroy = function () { - this.$element - .removeAttr('novalidate') - .removeData('bs.validator') - .off('.bs.validator') - - this.$element.find(inputSelector) - .off('.bs.validator') - .removeData(['bs.validator.errors', 'bs.validator.deferred']) - .each(function () { - var $this = $(this) - var timeout = $this.data('bs.validator.timeout') - window.clearTimeout(timeout) && $this.removeData('bs.validator.timeout') - }) - - this.$element.find('.help-block.with-errors').each(function () { - var $this = $(this) - var originalContent = $this.data('bs.validator.originalContent') - - $this - .removeData('bs.validator.originalContent') - .html(originalContent) - }) - - this.$element.find('input[type="submit"], button[type="submit"]').removeClass('disabled') - - this.$element.find('.has-error').removeClass('has-error') - - return this - } - - // VALIDATOR PLUGIN DEFINITION - // =========================== - - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var options = $.extend({}, Validator.DEFAULTS, $this.data(), typeof option == 'object' && option) - var data = $this.data('bs.validator') - - if (!data && option == 'destroy') return - if (!data) $this.data('bs.validator', (data = new Validator(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - var old = $.fn.validator - - $.fn.validator = Plugin - $.fn.validator.Constructor = Validator - - - // VALIDATOR NO CONFLICT - // ===================== - - $.fn.validator.noConflict = function () { - $.fn.validator = old - return this - } - - - // VALIDATOR DATA-API - // ================== - - $(window).on('load', function () { - $('form[data-toggle="validator"]').each(function () { - var $form = $(this) - Plugin.call($form, $form.data()) - }) - }) - -}(jQuery); diff --git a/nengo_gui/static/main.css b/nengo_gui/static/main.css index b078eecb..c7e8ec8a 100644 --- a/nengo_gui/static/main.css +++ b/nengo_gui/static/main.css @@ -1,60 +1,45 @@ html { height: 100%; + overflow-x: hidden; } body { - margin: 0px; - height: 100%; - width: 100%; cursor: inherit; - display: -webkit-flex; display: flex; - -webkit-flex-direction: column; flex-direction: column; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; + margin: 0; + height: 100%; user-select: none; + width: 100%; } +/* #main { + background: white; + flex: 1 1 auto; + overflow: hidden; + position: relative; + } */ -#main { - background: white; - -webkit-flex: 0 0 auto; +#vmiddle { + display: flex; flex: 1 1 auto; + flex-direction: row; position: relative; - overflow: hidden; } -#vmiddle { - position: relative; - -webkit-flex: 1 1 auto; - flex: 1 1 auto; - display: -webkit-flex; +div.main { display: flex; - -webkit-flex-direction: row; - flex-direction: row; + flex: 1 1 auto; + order: 2; } div.minimap { background: white; border: 2px solid #787878; height: 15%; - width: 15%; left: 84.5%; - top: 82.5%; position: absolute; + top: 82.5%; + width: 15%; z-index: 99999999; } - -.graph { - background: #eeeeee; -} - -.line { - fill: none; - stroke: black; - stroke-width: 1.5px; -} diff --git a/nengo_gui/static/main.ts b/nengo_gui/static/main.ts new file mode 100644 index 00000000..e140fb61 --- /dev/null +++ b/nengo_gui/static/main.ts @@ -0,0 +1,237 @@ +/** + * Entry point into the Nengo application. + */ + +import "bootstrap/dist/css/bootstrap.min.css"; +import "imports-loader?$=jquery,jQuery=jquery!bootstrap"; +import "imports-loader?$=jquery,jQuery=jquery!bootstrap-validator"; +import "imports-loader?$=jquery,jQuery=jquery!jquery-ui"; +import "imports-loader?$=jquery,jQuery=jquery!jqueryfiletree/src/jQueryFileTree"; +import "jqueryfiletree/dist/jQueryFileTree.min.css"; + +import "./favicon.ico"; + +import { VNode, dom, h } from "maquette"; + +import "./main.css"; + +import * as items from "./debug/items"; +import { Editor } from "./editor"; +import { HotkeyManager } from "./hotkeys"; +import { NetGraph } from "./netgraph/main"; +import { MockConnection, ServerConnection } from "./server"; +import { Sidebar } from "./sidebar"; +import { SimControl } from "./sim-control"; +import { Toolbar } from "./toolbar"; +import { Network } from "./components/network"; + +export interface NengoWindow extends Window { + nengo: Nengo; + nengoDebug: NengoDebug; +} + +export class DebugItem { + category: string; + name: string; + obj: any; + root: HTMLDivElement; + + constructor(category: string, name: string) { + this.category = category; + this.name = name; + this.obj = items[category][name](); + + if ("view" in this.obj) { + this.root = this.obj.view.root; + } else if ("root" in this.obj) { + this.root = this.obj.root; + } else { + console.error("Cannot find root."); + } + } + + eval(command: string) { + const obj = this.obj; + const retval = eval(command); + if (retval) { + console.log(retval); + } + return retval; + } +} + +export class NengoDebug { + items: Array = []; + netgraph: NetGraph = new NetGraph(new MockConnection()); + + constructor() { + this.netgraph.view.root.style.width = "600px"; + this.netgraph.view.root.style.height = "600px"; + this.netgraph.view.root.style.outline = "red solid 1px"; + document.body.appendChild(this.netgraph.view.root); + } + + add(category: string, name: string) { + const item = new DebugItem(category, name); + if (item.category === "componentview") { + this.netgraph.view.root.appendChild(item.root); + if ("ondomadd" in item.obj) { + item.obj.ondomadd(); + } + } else if (item.category === "component") { + // Add the item to the last added network. + let network = null; + this.netgraph.components.components.forEach(component => { + if (component instanceof Network) { + network = component; + } + }); + this.netgraph.add(item.obj, network); + } else { + document.body.appendChild(item.root); + } + this.items.push(item); + return item; + } + + remove(item: DebugItem) { + if (item.category === "componentview") { + this.netgraph.view.root.removeChild(item.root); + } else if (item.category === "component") { + this.netgraph.remove(item.obj); + } else { + document.body.removeChild(item.root); + } + // Bootstrap modals can leave behind a backdrop. Annoying! + const backdrop = document.body.querySelector(".modal-backdrop"); + if (backdrop !== null) { + document.body.classList.remove("modal-open"); + document.body.removeChild(backdrop); + } + this.items.splice(this.items.indexOf(item), 1); + } + + toggleLog() { + MockConnection.verbose = !MockConnection.verbose; + } + + toggleOutline() { + const stylesheet = document.styleSheets[0] as CSSStyleSheet; + const rule = stylesheet.cssRules[0]; + const ruleText = "* { outline: red solid 1px; }"; + const active = rule.cssText === ruleText; + + if (active) { + stylesheet.deleteRule(0); + } else { + stylesheet.insertRule(ruleText, 0); + } + } +} + +export class Nengo { + control; + editor; + filename: string; + hotkeys; + main; + modal; + netgraph; + sidebar; + sim; + toolbar; + + private server: ServerConnection; + private view: MainView = new MainView(); + + constructor(server: ServerConnection) { + this.editor = new Editor(server); + this.sim = new SimControl(server); + this.toolbar = new Toolbar(server); + this.sidebar = new Sidebar(server); + this.netgraph = new NetGraph(server); + + // Add hotkeys + this.hotkeys = new HotkeyManager(server); + this.sim.hotkeys(this.hotkeys); + this.netgraph.hotkeys(this.hotkeys); + this.editor.hotkeys(this.hotkeys); + + document.body.appendChild(this.toolbar.view.root); + document.body.appendChild(this.view.root); + document.body.appendChild(this.sim.view.root); + this.view.root.appendChild(this.editor.view.root); + this.view.root.appendChild(this.netgraph.view.root); + this.view.root.appendChild(this.sidebar.view.root); + window.dispatchEvent(new Event("resize")); + + server.bind("netgraph.update", cfg => { + // TODO + // this.filename = filename; + // this.toolbar.filename = filename + // document.title = this.filename; + // document.body.appendChild(this.sim.view.root); + + // In case anything needs to be adjusted + window.dispatchEvent(new Event("resize")); + }); + + // Request config and update accordingly + server.send("netgraph.request_update", { initialize: true }); + this.server = server; + } +} + +export class MainView { + root: HTMLElement; + + constructor() { + const node = h("div.main"); + + this.root = dom.create(node).domNode as HTMLElement; + } +} + +// TODO: splash screen, if we want it +// class VMiddle { +// node: VNode; +// root: HTMLElement; + +// constructor() { +// this.node = +// h("div#loading", {styles: { +// "z-index": "100000002", +// "position": "absolute", +// "top": "0", +// "right": "0", +// "bottom": "0", +// "left": "0", +// "background": "#ffffff", +// }}); + +// this.root = dom.create(this.node).domNode as HTMLElement; +// } +// } + +if (typeof localStorage === "undefined" || localStorage === null) { + console.error("localStorage not available. Please update your browser!"); +} + +if (typeof document !== "undefined") { + document.addEventListener("DOMContentLoaded", () => { + // Attempt to make a connection with the server + const server = new ServerConnection(); + server.bind("open", () => { + console.log("Server connection opened successfully"); + (window).nengo = new Nengo(server); + server.send("page.ready"); + }); + setTimeout(() => { + if (!server.isReady()) { + server.close(); + console.log("Server connection timeout, entering debug mode"); + (window).nengoDebug = new NengoDebug(); + } + }, 1000); // Time out after 1 second + }); +} diff --git a/nengo_gui/static/menu.css b/nengo_gui/static/menu.css index fb5f0f57..35ed4cc9 100644 --- a/nengo_gui/static/menu.css +++ b/nengo_gui/static/menu.css @@ -1,5 +1,14 @@ -.menu-item{ - font-size: .8em; - text-align: left; - cursor: pointer !important; +div.menu { + position: fixed; + + .dropdown-menu { + display: block; + position: absolute; + + .menu-item { + cursor: pointer !important; + font-size: 90%; + text-align: left; + } + } } diff --git a/nengo_gui/static/menu.js b/nengo_gui/static/menu.js deleted file mode 100644 index 978662e8..00000000 --- a/nengo_gui/static/menu.js +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Create a menu that will appear inside the given div - * - * Each element that has a menu makes a call to Nengo.Menu constructor - */ -Nengo.Menu = function(div) { - this.visible = false; // whether it's currently visible - this.div = div; // the parent div - this.menu_div = null; // the div for the menu itself - this.actions = null; // the current action list for the menu -}; - - -/** - * Dictionary of currently shown menus - * The key is the div the menu is in - */ -Nengo.Menu.visible_menus = {}; - - -/** - * Show this menu at the given (x,y) location - * Automatically hides any menu that's in the same div - * Called by a listener from netgraph.js - */ -Nengo.Menu.prototype.show = function (x, y, items) { - Nengo.Menu.hide_menu_in(this.div); - - if (items.length == 0) { - return; - } - - // TODO: move this to the constructor - this.menu_div = document.createElement('div'); - this.menu_div.style.position = 'fixed'; - this.menu_div.style.left = x; - this.menu_div.style.top = y; - this.menu_div.style.zIndex = Nengo.next_zindex(); - - this.menu = document.createElement('ul'); - this.menu.className = 'dropdown-menu'; - this.menu.style.position = 'absolute'; - this.menu.style.display = 'block'; - this.menu.role = 'menu'; - - this.menu_div.appendChild(this.menu); - this.div.appendChild(this.menu_div); - - this.actions = {} - - var self = this; - for (var i in items) { - var item = items[i]; - var b = document.createElement('li'); - var a = document.createElement('a'); - a.setAttribute('href','#'); - a.className = 'menu-item'; - a.innerHTML = item[0]; - a.func = item[1]; - this.actions[a] = item[1]; - $(a).click(function(e) { - e.target.func(); - self.hide(); - }) - .on('contextmenu', function(e) { - e.preventDefault(); - e.target.func(); - self.hide(); - }); - b.appendChild(a); - this.menu.appendChild(b); - } - this.visible = true; - this.check_overflow(x, y); - Nengo.Menu.visible_menus[this.div] = this; -}; - - -/** - * Hide this menu - */ -Nengo.Menu.prototype.hide = function () { - this.div.removeChild(this.menu_div); - this.visible = false; - this.menu_div = null; - delete Nengo.Menu.visible_menus[this.div]; -}; - - -/** - * Hide any menu that is displayed in the given div - */ -Nengo.Menu.hide_menu_in = function (div) { - var menu = Nengo.Menu.visible_menus[div]; - if (menu !== undefined) { - menu.hide(); - } -} - -Nengo.Menu.prototype.visible_any = function () { - return Nengo.Menu.visible_menus[this.div] !== undefined; -} - -Nengo.Menu.prototype.hide_any = function () { - for(var k in Nengo.Menu.visible_menus) { - Nengo.Menu.hide_menu_in(k); - } -} - -Nengo.Menu.prototype.check_overflow = function (x, y) { - var corrected_y = y - $(toolbar.toolbar).height(); - var h = $(this.menu).outerHeight(); - var w = $(this.menu).outerWidth(); - - var main_h = $('#main').height() - var main_w = $('#main').width() - - if (corrected_y + h > main_h) { - this.menu_div.style.top = y - h; - } - - if(x + w > main_w) { - this.menu_div.style.left = main_w - w; - } -} diff --git a/nengo_gui/static/menu.ts b/nengo_gui/static/menu.ts new file mode 100644 index 00000000..a3580322 --- /dev/null +++ b/nengo_gui/static/menu.ts @@ -0,0 +1,162 @@ +/** + * Create a menu that will appear inside the given div + * + * Each element that has a menu makes a call to Menu constructor + */ + +import { VNode, dom, h } from "maquette"; + +import "./menu.css"; + +import * as utils from "./utils"; + +export class MenuAction { + active: () => boolean | null; + element: HTMLLIElement; + + constructor(element: HTMLLIElement, active: () => boolean = null) { + this.element = element; + this.active = active; + } + + isActive(): boolean { + if (this.active === null) { + return true; + } else { + return this.active(); + } + } +} + +export class Menu { + static shown: Menu = null; + + static hideShown = utils.debounce( + () => { + if (Menu.shown !== null) { + document.body.removeChild(Menu.shown.view.root); + Menu.shown = null; + } + }, + 50, + { immediate: true } + ); + + actions: MenuAction[] = []; + view: MenuView = new MenuView(); + + addAction( + label: string, + callback: (event: Event) => void, + active: () => boolean = null + ) { + const element = this.view.addAction(label); + element.addEventListener("click", (event: Event) => { + callback(event); + Menu.hideShown(); + }); + element.addEventListener("contextmenu", (event: Event) => { + event.preventDefault(); + callback(event); + Menu.hideShown(); + }); + this.actions.push(new MenuAction(element, active)); + } + + addHeader(label: string) { + this.view.addHeader(label); + } + + addSeparator() { + this.view.addSeparator(); + } + + /** + * Show this menu at the given (x,y) location. + * + * Automatically hides any menu with the same parent. + * + * Called by a listener from netgraph.js + */ + show(x: number, y: number) { + // TODO: have to get toolbar height somehow... + // For now, we know it's always 35 px + const toolbarHeight = 35; + const correctedY = y - toolbarHeight; + const h = this.view.height; + const w = this.view.width; + + // TODO: mainH and mainW: get from viewport...? + const mainH = 600; + const mainW = 600; + + if (correctedY + h > mainH) { + y = mainH - h; + } + if (x + w > mainW) { + x = mainW - w; + } + + Menu.hideShown(); + this.actions.forEach((action, i) => { + if (action.isActive()) { + this.view.showAction(action.element); + } else { + this.view.hideAction(action.element); + } + }); + this.view.show(x, y); + document.body.appendChild(this.view.root); + Menu.shown = this; + } +} + +export class MenuView { + menu: HTMLUListElement; + root: HTMLDivElement; + + constructor() { + const node = h("div.menu", [h("ul.dropdown-menu", { role: "menu" })]); + this.root = dom.create(node).domNode as HTMLDivElement; + this.menu = this.root.firstChild as HTMLUListElement; + } + + get height(): number { + return this.root.offsetHeight; + } + + get width(): number { + return this.root.offsetWidth; + } + + addAction(label: string): HTMLLIElement { + const node = h("li", [h("a.menu-item", { href: "#" }, [label])]); + const li = dom.create(node).domNode as HTMLLIElement; + this.menu.appendChild(li); + return li; + } + + addHeader(label: string) { + const node = h("li.dropdown-header", [label]); + this.menu.appendChild(dom.create(node).domNode); + } + + addSeparator() { + const node = h("li.divider", { role: "separator" }); + this.menu.appendChild(dom.create(node).domNode); + } + + hideAction(element: HTMLLIElement) { + element.style.display = "none"; + } + + showAction(element: HTMLLIElement) { + element.style.display = null; + } + + show(x: number, y: number) { + this.root.style.left = x + "px"; + this.root.style.top = y + "px"; + this.root.style.zIndex = String(utils.nextZindex()); + } +} diff --git a/nengo_gui/static/minimap.ts b/nengo_gui/static/minimap.ts new file mode 100644 index 00000000..fe82c70c --- /dev/null +++ b/nengo_gui/static/minimap.ts @@ -0,0 +1,193 @@ +// import { NetGraphConnection } from "./connection"; +// import { NetGraphItem } from "./item"; + +export class Minimap { + // conns: {[uid: string]: NetGraphConnection} = {}; + div; + display; + height; + maxX: number = 0; + minX: number = 0; + maxY: number = 0; + minY: number = 0; + // objects: {[uid: string]: NetGraphItem} = {}; + // scale: number = 0.1; + width; + + constructor() { + // this.minimapDiv = document.createElement("div"); + // this.minimapDiv.className = "minimap"; + // this.parent.appendChild(this.minimapDiv); + + // this.minimap = h("svg"); + // this.minimap.classList.add("minimap"); + // this.minimap.id = "minimap"; + // this.minimapDiv.appendChild(this.minimap); + + // Box to show current view + // this.view = h("rect"); + // this.view.classList.add("view"); + // this.minimap.appendChild(this.view); + + // this.gNetworksMini = h("g"); + // this.gConnsMini = h("g"); + // this.gItemsMini = h("g"); + // Order these are appended is important for layering + // this.minimap.appendChild(this.gNetworksMini); + // this.minimap.appendChild(this.gConnsMini); + // this.minimap.appendChild(this.gItemsMini); + + // this.mmWidth = $(this.minimap).width(); + // this.mmHeight = $(this.minimap).height(); + + // Default display minimap + // this.mmDisplay = true; + this.toggle(); + } + + scale() { + // if (!this.mmDisplay) { + // return; + // } + // const keys = Object.keys(this.svgObjects); + // if (keys.length === 0) { + // return; + // } + // // TODO: Could also store the items at the four min max values + // // and only compare against those, or check against all items + // // in the lists when they move. Might be important for larger + // // networks. + // let firstItem = true; + // Object.keys(this.svgObjects).forEach(key => { + // const item = this.svgObjects[key]; + // // Ignore anything inside a subnetwork + // if (item.depth > 1) { + // return; + // } + // const minmaxXy = item.getMinMaxXY(); + // if (firstItem === true) { + // this.mmMinX = minmaxXy[0]; + // this.mmMaxX = minmaxXy[1]; + // this.mmMinY = minmaxXy[2]; + // this.mmMaxY = minmaxXy[3]; + // firstItem = false; + // return; + // } + // if (this.mmMinX > minmaxXy[0]) { + // this.mmMinX = minmaxXy[0]; + // } + // if (this.mmMaxX < minmaxXy[1]) { + // this.mmMaxX = minmaxXy[1]; + // } + // if (this.mmMinY > minmaxXy[2]) { + // this.mmMinY = minmaxXy[2]; + // } + // if (this.mmMaxY < minmaxXy[3]) { + // this.mmMaxY = minmaxXy[3]; + // } + // }); + // this.mmScale = 1 / Math.max(this.mmMaxX - this.mmMinX, + // this.mmMaxY - this.mmMinY); + // // Give a bit of a border + // this.mmMinX -= this.mmScale * .05; + // this.mmMaxX += this.mmScale * .05; + // this.mmMinY -= this.mmScale * .05; + // this.mmMaxY += this.mmScale * .05; + // // TODO: there is a better way to do this than recalculate + // this.mmScale = 1 / Math.max(this.mmMaxX - this.mmMinX, + // this.mmMaxY - this.mmMinY); + // this.redraw(); + // this.scaleMiniMapViewBox(); + } + + scaleViewBox() { + // if (!this.mmDisplay) { + // return; + // } + // const mmW = this.mmWidth; + // const mmH = this.mmHeight; + // const w = mmW * this.mmScale; + // const h = mmH * this.mmScale; + // const dispW = (this.mmMaxX - this.mmMinX) * w; + // const dispH = (this.mmMaxY - this.mmMinY) * h; + // const viewOffsetX = -(this.mmMinX + this.offsetX) * + // w + (mmW - dispW) / 2.; + // const viewOffsetY = -(this.mmMinY + this.offsetY) * + // h + (mmH - dispH) / 2.; + // this.view.setAttributeNS(null, "x", viewOffsetX); + // this.view.setAttributeNS(null, "y", viewOffsetY); + // this.view.setAttribute("width", w / this.scale); + // this.view.setAttribute("height", h / this.scale); + } + + toggle() { + // if (this.mmDisplay === true) { + // $(".minimap")[0].style.visibility = "hidden"; + // this.gConnsMini.style.opacity = 0; + // this.mmDisplay = false; + // } else { + // $(".minimap")[0].style.visibility = "visible"; + // this.gConnsMini.style.opacity = 1; + // this.mmDisplay = true ; + // this.scaleMiniMap(); + // } + } +} + +// TODO: This is probably still going to need a type parameter so that +// it draws the correct icon using the correct view +// export class MinimapItem extends NetGraphItem { +// constructor(ngiArg) { +// super(ngiArg); +// this.gNetworks = this.ng.gNetworksMini; +// this.gItems = this.ng.gItemsMini; +// } + +// _renderShape() { +// console.log("render"); +// } + +// _getPos() { +// const mmW = this.ng.mmWidth; +// const mmH = this.ng.mmHeight; + +// const w = mmW * this.ng.mmScale; +// const h = mmH * this.ng.mmScale; + +// const dispW = (this.ng.mmMaxX - this.ng.mmMinX) * w; +// const dispH = (this.ng.mmMaxY - this.ng.mmMinY) * h; + +// const offsetX = -this.ng.mmMinX * w + (mmW - dispW) / 2.; +// const offsetY = -this.ng.mmMinY * h + (mmH - dispH) / 2.; + +// return {w, h, offsetX, offsetY}; +// } + +// _getScreenW() { +// return this.getNestedWidth() * this.ng.mmWidth * this.ng.mmScale; +// } + +// getScreenWidth() { +// if (!this.ng.mmDisplay) { +// return 1; +// } +// super.getScreenWidth(); +// } + +// _getScreenH() { +// return this.getNestedHeight() * this.ng.mmHeight * this.ng.mmScale; +// } + +// getScreenHeight() { +// if (!this.ng.mmDisplay) { +// return 1; +// } +// super.getScreenHeight(); +// } + +// getScreenLocation() { +// if (!this.ng.mmDisplay) { +// return [1, 1]; +// } +// } +// } diff --git a/nengo_gui/static/modal.css b/nengo_gui/static/modal.css index 97ac0a9e..420322c2 100644 --- a/nengo_gui/static/modal.css +++ b/nengo_gui/static/modal.css @@ -1,138 +1,139 @@ -.modal-body .tab-pane { - margin: 20px; -} - -.modal-body input { - cursor: text !important; -} - -.modal-body .tab-pane .dt-connections { - margin-top: 20px; -} - -.modal-body .axis path, -.modal-body .axis line { - fill: none; - stroke: #000; - shape-rendering: crispEdges; -} - -.modal-body .form-group { - text-indent: 20px; -} - -.modal-body .with-errors { - font-size: .8em; -} - -div.modal-backdrop { - z-index: 100000000; -} - -.alert .glyphicon { - padding-right: 5px; -} - -.modal-body .tab-pane .list-group-item { - padding: 5px 10px; -} - -.modal-body .tab-pane .list-group-item.shaded { - color: #aaaaaa; -} - -.modal-body .tab-pane > h3 { - font-size: 1.3em; - font-weight: bold; -} - -.modal-body .tab-pane th { - font-size: 1em; - font-weight: bold; - border: 0px; - background-color: #dddddd; -} - -.modal-body .tab-pane td { - font-size: 0.9em; - border: 0px; -} - -.modal-body table .conn-objs { - width: 30%; -} - -.modal-body table .conn-funcs { - width: 50%; -} - -.modal-body table .conn-fan { - width: 20%; -} - -.modal-body table ul > li > .glyphicon, -.modal-body table ul > li > .glyphicon { - padding-right: 8px; -} - -.modal-body th .glyphicon { - padding-left: 8px; -} - -.modal-body .tooltip-inner { - white-space: pre-wrap; - text-align: left; - max-width: 350px; -} - -.modal-body .popover { - max-width: 350px; -} - -.modal-body .popover-title, -.modal-body .popover-content { - white-space: pre-wrap; - text-align: left; - font-size: 0.9em; - padding: 5px 10px; -} - -.modal-body .popover-title { - line-height: 1.2; -} - div.modal { + cursor: default; z-index: 100000001; - cursor: default !important; -} - -#config-fontsize.form-control, #config-scriptdir.form-control { - padding: 6px 6px; - height: 35px; -} - -.input-group .input-group-addon { - text-indent:0px; -} -#myModalForm .form-group { - margin-bottom: 0px; + .modal-body { + margin-left: 20px; + margin-right: 20px; + + .tab-pane { + margin: 20px; + } + + input[type="text"], + input[type="number"] { + cursor: text; + width: 100%; + } + + .tab-pane .dt-connections { + margin-top: 20px; + } + + .axis path, + .axis line { + fill: none; + stroke: #000; + shape-rendering: crispEdges; + } + + .form-group { + margin-bottom: 0; + /* text-indent: 20px; */ + + label { + text-align: left; + width: 100%; + } + } + + .with-errors { + font-size: .8em; + } + + .tab-pane { + + .list-group-item { + padding: 5px 10px; + } + + .list-group-item.shaded { + color: #aaaaaa; + } + + > h3 { + font: { + size: 1.3em; + weight: bold; + } + } + + th { + font-size: 1em; + font-weight: bold; + border: 0px; + background-color: #dddddd; + } + + td { + font-size: 0.9em; + border: 0px; + } + } + + table { + .conn-objs { + width: 30%; + } + + .conn-funcs { + width: 50%; + } + + .conn-fan { + width: 20%; + } + + ul > li > .glyphicon { + padding-right: 8px; + } + + th .glyphicon { + padding-left: 8px; + } + } + + .tooltip-inner { + max-width: 350px; + text-align: left; + white-space: pre-wrap; + } + + .popover { + max-width: 350px; + } + + .popover-title, + .popover-content { + font-size: 0.9em; + padding: 5px 10px; + text-align: left; + white-space: pre-wrap; + } + + .popover-title { + line-height: 1.2; + } + + .alert .glyphicon { + padding-right: 5px; + } + } +} + +.modal-dialog.modal-sm { + + .modal-body { + padding-left: 0; + padding-right: 0; + + td { + padding: 4px; + } + } } -#myModalForm .checkbox { - padding-top: 0px; - text-indent: 10px; -} - -#myModalForm .form-group select, #myModalForm .form-group input[type=text] { - width: calc(100% - 40px); - margin-left: 20px; -} -.input-group.col-xs-2 { - left:20px; -} - -.modal-dialog.modal-sm td { - padding: 4px; +div.modal-backdrop { + z-index: 100000000; } diff --git a/nengo_gui/static/modal.js b/nengo_gui/static/modal.js deleted file mode 100644 index 2a532701..00000000 --- a/nengo_gui/static/modal.js +++ /dev/null @@ -1,808 +0,0 @@ -Nengo.Modal = function($div) { - var self = this; - this.$div = $div; - this.$title = this.$div.find('.modal-title').first(); - this.$footer = this.$div.find('.modal-footer').first(); - this.$body = this.$div.find('.modal-body').first(); - - this.sim_was_running = false; - - //This listener is triggered when the modal is closed - this.$div.on('hidden.bs.modal', function () { - if (self.sim_was_running) { - sim.play(); - } - Nengo.hotkeys.set_active(true); - }) -} - -Nengo.Modal.prototype.show = function() { - Nengo.hotkeys.set_active(false); - this.sim_was_running = !sim.paused; - this.$div.modal('show'); - sim.pause() -} - -Nengo.Modal.prototype.title = function(title) { - this.$title.text(title); -} - -Nengo.Modal.prototype.footer = function(type, ok_function, cancel_function){ - this.$footer.empty(); - - if (type === "close") { - this.$footer.append(''); - } else if (type === "ok_cancel") { - var $footerBtn = $('
      ').appendTo(this.$footer); - $footerBtn.append(''); - $footerBtn.append(''); - $('#OK').on('click', ok_function); - if (typeof cancel_function !== 'undefined') { - $('#cancel-button').on('click', cancel_function); - } else { - $('#cancel-button').on('click', function () { - $('#cancel-button').attr('data-dismiss', 'modal'); - }); - } - } else if (type === 'confirm_reset') { - this.$footer.append(''); - this.$footer.append(''); - $('#confirm_reset_button').on('click', function() { - toolbar.reset_model_layout(); - }); - } else if (type === 'confirm_savepdf') { - this.$footer.append(''); - this.$footer.append(''); - $('#confirm_savepdf_button').on('click', function() { - var svg = $("#main svg")[0]; - - // Serialize SVG as XML - var svg_xml = (new XMLSerializer).serializeToString(svg); - source = '' + svg_xml; - source = source.replace("<", "<"); - source = source.replace(">", ">"); - - var svg_uri = 'data:image/svg+xml;base64,' + btoa(source); - - // Extract filename from the path - var path = $("#filename")[0].textContent; - filename = path.split('/').pop() - filename = filename.split('.')[0] - - // Initiate download - var link = document.createElement("a"); - link.download = filename + ".svg"; - link.href = svg_uri; - - // Adding element to the DOM (needed for Firefox) - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }); - } else if (type === 'confirm_savecsv') { - this.$footer.append(''); - this.$footer.append(''); - $('#confirm_savecsv_button').on('click', function() { - - var data_items = Nengo.Component.components; - var CSV = data_to_csv(data_items); - // Extract filename from the path - var path = $("#filename")[0].textContent; - var filename = path.split('/').pop(); - filename = filename.split('.')[0]; - - var uri = 'data:text/csv;charset=utf-8,' + escape(CSV); - - - var link = document.createElement("a"); - link.href = uri; - link.style = "visibility:hidden"; - // Adding element to the DOM (needed for Firefox) - link.download = filename + ".csv"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }); - - } else if (type === 'refresh') { - this.$footer.append(''); - $('#refresh_button').on('click', function() {location.reload()}) - } else { - console.warn('Modal footer type ' + type + ' not recognized.') - } -} - -Nengo.Modal.prototype.clear_body = function() { - this.$body.empty(); - this.$div.find('.modal-dialog').removeClass('modal-sm'); - this.$div.off('shown.bs.modal'); -} - -Nengo.Modal.prototype.text_body = function(text, type) { - if (typeof type === 'undefined') { type = "info"; } - - this.clear_body(); - var $alert = $('