commit 0eee92660ad8c8e6f3367949374b69e5b8233d7e Author: Senad Uka Date: Tue Nov 7 09:23:57 2017 +0100 first commit diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..15927d5 --- /dev/null +++ b/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["stage-2", "es2015" ], + "plugins": ["transform-object-assign"] +} diff --git a/.cfignore b/.cfignore new file mode 100644 index 0000000..c2d6b36 --- /dev/null +++ b/.cfignore @@ -0,0 +1,10 @@ +env +__pycache__ +Gemfile* +node_modules +test +spec +lib +package.json +npm-shrinkwrap.json +Rakefile diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ba62d77 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Matches multiple files with brace expansion notation +# Set default charset +[*.{js,py}] +charset = utf-8 + +# Tab indentation (no size specified) +[Makefile] +indent_style = tab + +# Matches the exact files either package.json or .travis.yml +[{package.json,*.js}] +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef2af3a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules +env +.idea +__pycache__ +.DS_Store +*.log +*cache +.js.map diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1e7fbd8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,80 @@ +from python:3.5 + +RUN apt-get update + +RUN apt-get install -y \ + git \ + imagemagick \ + libmagickcore-dev \ + libxml2-dev \ + libxslt1-dev \ + ruby \ + ruby-dev + +RUN gem install bundler + +RUN apt-get install -y \ + build-essential \ + chrpath \ + libssl-dev \ + libxft-dev \ + libfreetype6 \ + libfreetype6-dev \ + libfontconfig1 \ + libfontconfig1-dev \ + wget \ + curl + +# postgres client +RUN apt-get install -y \ + postgresql-client \ + postgresql-client-common \ + postgresql-contrib \ + libpq-dev + +# redis client +RUN apt-get install -y redis-tools + +# phantom.js +ENV PHANTOM_JS=phantomjs-2.1.1-linux-x86_64 +RUN wget https://bitbucket.org/ariya/phantomjs/downloads/$PHANTOM_JS.tar.bz2 +RUN tar xvjf $PHANTOM_JS.tar.bz2 && \ + mv $PHANTOM_JS /usr/local/share && \ + ln -sf /usr/local/share/$PHANTOM_JS/bin/phantomjs /usr/local/bin + +# nodejs +RUN curl -sL https://deb.nodesource.com/setup_6.x | bash - +RUN apt-get install -y nodejs + +# configure ssh +RUN apt-get install -y openssh-server +RUN mkdir /var/run/sshd +RUN echo 'root:screencast' | chpasswd +RUN sed -i 's/PermitRootLogin without-password/PermitRootLogin yes/' /etc/ssh/sshd_config + +# SSH login fix. Otherwise user is kicked off after login +RUN sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd + +ENV NOTVISIBLE "in users profile" +RUN echo "export VISIBLE=now" >> /etc/profile +RUN sed -ie 's/Port 22/Port 2222/g' /etc/ssh/sshd_config + +# install python libs +ADD ./requirements.txt /requirements/ +ADD ./requirements.test.txt /requirements/ + +WORKDIR /requirements + +RUN pip install -r requirements.txt +RUN pip install -r requirements.test.txt + +RUN pip3 install invoke + + +# install nodejs libs +ADD ./package.json /code/ +WORKDIR /code +RUN npm install + +CMD ["/usr/sbin/sshd", "-D"] +#CMD ["invoke", "serve_debug"] diff --git a/Dockerfile.db b/Dockerfile.db new file mode 100644 index 0000000..cc0356c --- /dev/null +++ b/Dockerfile.db @@ -0,0 +1,34 @@ +from ubuntu:16.04 + +RUN apt-get update && apt-get install -y sudo vim + +RUN apt-get install -y \ + apt-utils \ + postgresql \ + postgresql-client \ + postgresql-client-common \ + postgresql-contrib \ + libpq-dev + +RUN sudo cat /etc/postgresql/9.5/main/pg_hba.conf + +RUN sudo echo "local all postgres trust" > /etc/postgresql/9.5/main/pg_hba.conf && \ + sudo echo "local all all trust" >> /etc/postgresql/9.5/main/pg_hba.conf && \ + sudo echo "host all all 127.0.0.1/32 trust" >> /etc/postgresql/9.5/main/pg_hba.conf && \ + sudo echo "host all all ::1/128 trust" >> /etc/postgresql/9.5/main/pg_hba.conf + +USER postgres + +RUN /etc/init.d/postgresql start && \ + createdb test && \ + createdb pivotal && \ + echo "CREATE ROLE pivotal WITH UNENCRYPTED PASSWORD 'password';" | psql -U postgres && \ + echo "ALTER ROLE pivotal WITH LOGIN;" | psql -U postgres && \ + echo "GRANT SELECT, UPDATE, INSERT ON ALL TABLES IN SCHEMA public TO pivotal;" | psql -U postgres && \ + echo "GRANT CREATE, CONNECT ON DATABASE test TO pivotal;" | psql -U postgres && \ + echo "GRANT SELECT, UPDATE, INSERT ON ALL TABLES IN SCHEMA public TO pivotal;" | psql -U postgres && \ + echo "GRANT CREATE, CONNECT ON DATABASE pivotal TO pivotal;" | psql -U postgres + +EXPOSE 5432 + +CMD ["/usr/lib/postgresql/9.5/bin/postgres", "-D", "/var/lib/postgresql/9.5/main", "-c", "config_file=/etc/postgresql/9.5/main/postgresql.conf"] diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..ffbf249 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +gem 'sass' +gem 'pivotal-tracker' diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..d9b00a6 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,46 @@ +GEM + remote: https://rubygems.org/ + specs: + builder (3.2.2) + crack (0.4.3) + safe_yaml (~> 1.0.0) + domain_name (0.5.20160615) + unf (>= 0.0.5, < 1.0.0) + http-cookie (1.0.2) + domain_name (~> 0.5) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + mini_portile2 (2.1.0) + netrc (0.11.0) + nokogiri (1.6.8) + mini_portile2 (~> 2.1.0) + pkg-config (~> 1.1.7) + nokogiri-happymapper (0.5.9) + nokogiri (~> 1.5) + pivotal-tracker (0.5.13) + builder + crack + nokogiri (>= 1.5.5) + nokogiri-happymapper (>= 0.5.4) + rest-client (>= 1.8.0) + pkg-config (1.1.7) + rest-client (2.0.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) + safe_yaml (1.0.4) + sass (3.4.22) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.2) + +PLATFORMS + ruby + +DEPENDENCIES + pivotal-tracker + sass + +BUNDLED WITH + 1.12.5 diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..1a74262 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: python helix/main.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ace2ee --- /dev/null +++ b/README.md @@ -0,0 +1,286 @@ +## Helix Calculator + +- [Staging](https://sp-helix-staging.herokuapp.com) +- [PreProd](https://sp-helix-preprod.herokuapp.com) +- [Production](https://helix-calculator.sunpower.com/) + + +### Setup + +#### macOS systems + +This assumes you already have [homebrew](https://brew.sh) installed as well as an [ssh key added to your github account](https://help.github.com/articles/generating-an-ssh-key/). +This also assumes your username is pivotal, see the [circleci configuration](circleci.yml) for examples of how to add a pivotal user to postgres. + +```sh +xcode-select --install +brew install python3 node nodenv redis phantomjs imagemagick@6 postgres brew-services +brew services start postgres +createdb test +createdb pivotal +git clone git@github.com:SunPower/Helix_Roof_Calculator.git +cd Helix_Roof_Calculator +pyvenv env +source env/bin/activate +pip install invoke +invoke install +npm install +invoke db_migrate +``` + +This installs python3, the [invoke utility](https://github.com/pyinvoke/invoke), xcode, node, imagemagick, redis, and postgres. It also sets up a virtual environment for the calculator, installs all the python dependencies into the virtual environment, and installs all the node dependencies. +Note that invoke should be installed after the virtual env is installed to ensure that the python 3.x version of invoke is used (instead of the system version, which is typically 2.7.x). + +#### Linux/Debian-based systems + +While most of this README assumes you're running macOS, for linux/debian based systems (e.g. ubuntu), follow these instructions. See the mac section for an explanation. + +##### PostgreSQL + +Postgres needs to be installed and configured before we do anything else. + +``` +sudo apt-get install postgresql postgresql-client postgresql-client-common postgresql-contrib libpq-dev +# Modify /etc/postgresql/common/*/pg_hba.conf and set local connections to trust +sudo -u postgres createdb test +sudo -u postgres createdb pivotal +# create pivotal role in postgres, grant it access to the test/pivotal databases. +echo "CREATE ROLE pivotal WITH UNENCRYPTED PASSWORD 'password';" | psql -U postgres +echo "ALTER ROLE pivotal WITH LOGIN;" | psql -U postgres +echo "GRANT SELECT, UPDATE, INSERT ON ALL TABLES IN SCHEMA test.public TO pivotal;" | psql -U postgres +echo "GRANT CREATE, CONNECT ON DATABASE test TO pivotal;" | psql -U postgres +echo "GRANT SELECT, UPDATE, INSERT ON ALL TABLES IN SCHEMA pivotal.public TO pivotal;" | psql -U postgres +echo "GRANT CREATE, CONNECT ON DATABASE pivotal TO pivotal;" | psql -U postgres +``` + +This installs postgres and creates the database and roles that helix looks for in test/local usage. Because ubuntu is systemd, you can start/stop/restart the server by running something along the lines of `sudo systemctl [start|stop|restart] postgresql.service`. Please see [this ubuntu postgres documentation page](https://help.ubuntu.com/community/PostgreSQL) for more information. + +The comment for the second line tells you how to enable connections to postgres without requiring a password. See http://dba.stackexchange.com/questions/83164/remove-password-requirement-for-user-postgres for more information + +##### Other dependencies/the project + +```sh +sudo apt-get install git python3 python3-dev python3-venv nodejs npm redis-server imagemagick libmagickcore-dev libxml2-dev libxslt1-dev ruby ruby-dev +sudo gem install bundler +python3 --version # make sure you have >3.5 installed! +git clone git@github.com:SunPower/Helix_Roof_Calculator.git +cd Helix_Roof_Calculator +pyvenv env +source env/bin/activate +pip install invoke +invoke install +npm install +invoke db_migrate +``` + +##### PhantomJS + +```sh +sudo apt-get install build-essential chrpath libssl-dev libxft-dev libfreetype6 libfreetype6-dev libfontconfig1 libfontconfig1-dev +export PHANTOM_JS=phantomjs-2.1.1-linux-x86_64 +wget https://bitbucket.org/ariya/phantomjs/downloads/$PHANTOM_JS.tar.bz2 +sudo targ xvjf $PHANTOM_JS.tar.bz2 + +sudo mv $PHANTOM_JS /usr/local/share +sudo ln -sf /usr/local/share/$PHANTOM_JS/bin/phantomjs /usr/local/bin +``` + +Note that it doesn't work to use the nodejs-provided phantom, nor does apt-get install phantomjs work. Yes, it's annoying. + +### Adding Dependencies + +Because of how we're separating production dependencies and test dependencies, it's recommended that each new dependency be manually added to either the [requirements.txt](requirements.txt) or the [requirements.test.txt](requirements.test.txt), depending on whether it's a production or test dependency, respectively. + +### Adding database migrations + +To manage the database, we use [SQLAlchemy](http://www.sqlalchemy.org/). +To manage database migrations, we use [Alembic](http://alembic.zzzcomputing.com/en/latest/). + +First, make your changes to the sql models (in the models/sql) package to reflect your new vision of the sql schema. Next, we need to take those changes and generate an alembic migration from them. Run `PYTHONPATH=. alembic revision -m "(migration description)"` to generate that migration. Now, add the changes (and revert instructions!) to the generated migration file. Finally, run `invoke db_migrate` to run that migration. + +##### Downgrading a migration + +There currently is not an invoke task to revert migrations. Instead, just directly use the alembic cli to do this, something along the lines of `PYTHONPATH=.:$PYTHONPATH alembic downgrade -1`. Setting PYTHONPATH to include the current directory will ensure that the correct sql models get picked up when alembic does the migration. To redo a migration using alembic, run `PYTHONPATH=.:$PYTHONPATH alembic upgrade +1`. + +Please refer to the [Alembic Documentation](http://alembic.zzzcomputing.com/en/latest/) for more information on using Alembic. + +### Running Tests + +First, install PhantomJS (`brew install phantomjs`) (this should already be done as part of the setup). + +Run `invoke test` from the project directory will run every test. + +Note that we use [Python's built-in unittest library](https://docs.python.org/3/library/unittest.html) for our python tests. Additionally, there are a few other helper libraries we use in tests. See the [requirements.test.txt](requirements.test.txt) file for those. + +##### Running Javascript Tests + +`invoke test` will, by default, run both the python tests and the javascript tests. Because the python tests can take a while, it's advantageous to be able to run the javascript tests (used for the array visualization) separately, especially when working entirely on the visualization frontend. + +To do this, run `invoke test_js` from your command. This spins up an instance of [karma](https://karma-runner.github.io/0.13/index.html) attached to (by default) Chrome which watches the [spec](spec) and the [helix/javascript](helix/javascript) directories for changes, and (usually) re-runs the javascript tests if anything changes. + +Note that we use [Jasmine](http://jasmine.github.io/2.4/introduction.html) for our javascript specs. + +### Building JS + +Part of the application (as of now only files in `helix/javascript/array_summary`) are built using Webpack and leveraging ES6. + +To run the build use this command from the root of the repository +```bash +./node_modules/.bin/webpack +``` + +### Running Locally + +First, install redis (`brew install redis`), and then (in a separate terminal) run `redis-server /usr/local/etc/redis.conf` to run redis locally. + +Next, install postgres (`brew install postgres`), and then start it (`brew services start postgres`), follow the instructions at the top for additional configuration information. + +Then, tell alembic to migrate your postgres db to the latest (`invoke db_migrate`). + +Run `invoke serve` from the project directory to run locally. + +This will run the project on [port 5000](http://localhost:5000/). + +##### Running in debug mode + +Run `invoke serve_debug` to start the project in debug mode. + +##### The image tests + +Note that you may see image comparison tests failing for erroneous reasons. These are flaky and machine-dependent. If you've done anything to the image generation code, please verify visually that you're getting the output you want. + +### Circle CI + +[CI (Continuous Integration)](https://en.wikipedia.org/wiki/Continuous_integration) is a service we use that automatically runs all the tests whenever a new commit is pushed. This is important because it runs the code in a context-free (the container the code runs in is set up for each run, then destroyed afterwards) environment. We're using [Circle CI](http://circleci.com/) here because Rachel recommended it. In addition to automatically running tests, circle also automatically deploys successful builds to cloud foundry for us. + +### Deploying to Heroku (locally) + +Install the Heroku CLI by running `brew install heroku` from your terminal. + +Add heroku git endpoints for the different environments you want to push to. Typically, you'll just want to push to staging (especially seeing that preprod/production are automatically pushed to heroku for you). The heroku git endpoints are: + +- staging: `https://git.heroku.com/sp-helix-staging.git` +- preprod: `https://git.heroku.com/sp-helix-preprod.git` +- production: `https://git.heroku.com/sp-helix-production.git` + +login to heroku with `heroku login` + +Finally, push to heroku using git! (Something like `git push heroku master`) + +Note that circle ci does all that for us! + +#### Deploying to preprod + +To deploy to preprod, rebase master (up to the commit you want) onto preprod, then push preprod to github. CI will automatically deploy to preprod assuming that tests pass. + +To push the current state of master to preprod: + +```bash +git pull -r # Pull from origin (assuming origin is github) +git checkout preprod # switch to preprod branch +git rebase master # rebase head of master onto preprod +git push origin preprod # push preprod to github +``` + +To push a past commit to preprod: + +```bash +git pull -r # Pull from origin (assuming origin is github) +git log +# [grab the sha of the commit you want] +git checkout preprod # switch to preprod branch +git rebase f00d # replace f00d with the sha of the commit you want +git push origin preprod # push preprod to github +``` + +#### Deploying to production + +To deploy to production, tag a commit as `release-` (for example, `release-2`) and push the commit (`git push origin release-2`). CI will automatically deploy to production assuming that tests pass. + +Please increment the release count each time. + +### Setting up PyCharm + +[PyCharm](https://www.jetbrains.com/pycharm/) is an IDE from Jetbrains for python. It can be installed by running `brew cask install pycharm`. + +Open pycharm, input a license, and select to install the command line tool. Then, open the project in pycharm by running `charm .` from the project directory. + +Right now, you can start to use pycharm as an editor, but it's not very useful as an IDE, there's a bit more configuration stuff before it's useful as an IDE. + +#### Use the correct Python + +By default, PyCharm assumes you're using Python 2.7.x, we need to tell it to not only use 3.5.x, but use the 3.5.x for our local virtual environment. + +To do this, open the preferences window, and select `Project: Helix_Roof_Calculator` (our project directory is named helix instead of Helix_Roof_Calculator, so the screenshots will be different than what you see), then select Project Intepreter and change that to `3.5.2 virtualenv at [projectdirectory]/env`. + +![Screenshot describing this](documentation/setup_pycharm_correct_python.png) + +#### Configuring targets + +For this, we're going to reference the target configuration menu on pycharm (this is the top-right triangle-esque menu) + +![See here](documentation/setup_pycharm_target_configuration.png) + +##### Running Tests + +Make sure to change the python tests configuration for nosetests and unittests (target selection menu (top-right) -> Edit Configurations -> Python Tests -> Nosetests) so that the working directory is the project directory (not the test directory!). + +![See this screenshot for unittests, nosestest looks similar](documentation/setup_pycharm_unittests_configuration.png) + +Additionally, you can set up an 'All Tests' target, just click the + button, name the target something like 'All Tests', and tell it to run all in folder, and point it at the test subdirectory of the project. Your configuration should look something like the attached screenshot + +![Configuring an All Tests target](documentation/setup_pycharm_alltests_target.png) + +##### Running local server in PyCharm + +Again, we need to edit the target configurations. This time add (or change, if it already exists) a python target named `helix`. It should be configured to run the `helix/main.py` script (on pivotal's machine, that field is filled with `/Users/pivotal/workspace/helix/helix/main.py`), with a working directory of the project directory (on pivotal's machine, that would be `/Users/pivotal/workspace/helix`). Additionally, the `FLASK_DEBUG` environment variable should be set to `1`. ![See this screenshot](documentation/setup_pycharm_server_target.png) + +#### PhantomJS w/ PyCharm + +You may need to ensure that phantomjs is in your PyCharm PATH. One way to do this is to symlink it into the project's env/bin folder +(for example `ln -s /usr/local/bin/phantomjs env/bin/`). + +### Using Git + +See [Github's documentation](https://help.github.com/). We prefer to commit directly to master, with small-ish commits that always have passing tests. + +### Environment set-up using Docker +In order to set up complete environment using `docker-compose` you need to execute following line from project root directory: + +``` +docker-compose up -d +``` + +On Windows remember that repository should be inside Users dir. Additionally you need to set envvar: +export COMPOSE_CONVERT_WINDOWS_PATHS=1 + +Dependencies (including `node.js` packages) are resolved when building an image. +Environment will be exposed via ssh and accessible via following command: + +``` +ssh -p 2222 root@localhost +``` +ssh password is: `screencast` + +It can be seamlessly used via PyCharm by setting proper python interpreter: +`File -> Settings... -> Project: Helix_Roof_Calculator -> Project Interpreter` + +![See this screenshot](documentation/pycharm_docker.png) + +It's necessary to set path mappings when configuring targets: + +![See this screenshot](documentation/pycharm_docker_config.png) + +To initialize database, use following command: + +``` +ssh -p 2222 root@localhost 'cd /code && invoke db_migrate' +``` + +or: + +``` +docker exec -it helixroofcalculator_helix_1 invoke db_migrate +``` + +Debugging is possible directly from PyCharm, all code changes are transparent between host and container. diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..c2ed9dd --- /dev/null +++ b/Rakefile @@ -0,0 +1,23 @@ +namespace :ci do + desc 'Delivers stories to tracker' + task :deliver do + require 'pivotal-tracker' + TRACKER_TOKEN = 'a092a63a88a7e3f1cdd08ffca82ba53c' + TRACKER_PROJECT_ID = '1544689' + + PivotalTracker::Client.token = TRACKER_TOKEN + PivotalTracker::Client.use_ssl = true + + unpakt_project = PivotalTracker::Project.find(TRACKER_PROJECT_ID) + stories = unpakt_project.stories.all(:state => "finished", :story_type => ['bug', 'feature']) + + stories.each do | story | + puts "Searching for #{story.id} in local git repo." + search_result = `git log -i --grep "[Finish(es)?|Fix(es)?] ##{story.id}"` + if search_result.length > 0 + story.notes.create(:text => "Delivered by staging deploy script.") + story.update({"current_state" => "delivered"}) + end + end + end +end diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..28faa6b --- /dev/null +++ b/alembic.ini @@ -0,0 +1,76 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = db + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# max length of characters to apply to the +# "slug" field +#truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to db/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat db/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +;sqlalchemy.url = postgres://pivotal:@localhost/pivotal + +databases = local, test + +[local] +sqlalchemy.url = postgres://pivotal:@localhost/pivotal + +[test] +sqlalchemy.url = postgres://pivotal:@localhost/test + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..75441f5 --- /dev/null +++ b/circle.yml @@ -0,0 +1,58 @@ +machine: + python: + version: 3.5.1 + node: + version: 6.1.0 + +dependencies: + pre: + - pip install invoke + override: + - invoke install + - invoke update_version + +database: + override: + - createdb test + - createdb pivotal + - echo "CREATE ROLE pivotal WITH UNENCRYPTED PASSWORD 'password';" | psql -U postgres + - echo "ALTER ROLE pivotal WITH LOGIN;" | psql -U postgres + - echo "GRANT SELECT, UPDATE, INSERT ON ALL TABLES IN SCHEMA test.public TO pivotal;" | psql -U postgres + - echo "GRANT CREATE, CONNECT ON DATABASE test TO pivotal;" | psql -U postgres + - echo "GRANT SELECT, UPDATE, INSERT ON ALL TABLES IN SCHEMA pivotal.public TO pivotal;" | psql -U postgres + - echo "GRANT CREATE, CONNECT ON DATABASE pivotal TO pivotal;" | psql -U postgres + - PYTHONPATH=.:$PYTHONPATH alembic upgrade head + +test: + override: + - invoke test_ci + +deployment: + staging: + branch: master + commands: + - invoke update_heroku_version staging + - "[[ ! -s \"$(git rev-parse --git-dir)/shallow\" ]] || git fetch --unshallow" + - git push -f git@heroku.com:sp-helix-staging.git $CIRCLE_SHA1:refs/heads/master + - heroku run --app sp-helix-staging invoke db_migrate + - rake ci:deliver + preprod: + branch: preprod + commands: + - invoke update_heroku_version preprod + - "[[ ! -s \"$(git rev-parse --git-dir)/shallow\" ]] || git fetch --unshallow" + - git push -f git@heroku.com:sp-helix-preprod.git $CIRCLE_SHA1:refs/heads/master + - heroku run --app sp-helix-preprod invoke db_migrate + - rake ci:deliver + production: + tag: /release-.*/ + commands: + - invoke update_heroku_version production + - "[[ ! -s \"$(git rev-parse --git-dir)/shallow\" ]] || git fetch --unshallow" + - git push -f git@heroku.com:sp-helix-production.git $CIRCLE_SHA1:refs/heads/master + - heroku run --app sp-helix-production invoke db_migrate + - rake ci:deliver + +notify: + webhooks: + - url: http://pulse.pivotallabs.com/projects/03ba990f-b8f5-4508-b4c1-19038b2cb791/status diff --git a/db/README b/db/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/db/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/db/env.py b/db/env.py new file mode 100644 index 0000000..db6a424 --- /dev/null +++ b/db/env.py @@ -0,0 +1,142 @@ +from __future__ import with_statement +import os +from alembic import context +from sqlalchemy import engine_from_config, pool +from logging.config import fileConfig +import logging +import re + +USE_TWOPHASE = False + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + +# gather section names referring to different +# databases. These are named "engine1", "engine2" +# in the sample .ini file. +db_names = config.get_main_option('databases') + +# add your model's MetaData objects here +# for 'autogenerate' support. These must be set +# up to hold just those tables targeting a +# particular database. table.tometadata() may be +# helpful here in case a "copy" of +# a MetaData is needed. +# from myapp import mymodel +# target_metadata = { +# 'engine1':mymodel.metadata1, +# 'engine2':mymodel.metadata2 +#} +target_metadata = {} + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + # for the --sql use case, run migrations for each URL into + # individual files. + + engines = {} + db_url = os.getenv('DATABASE_URL') + if db_url: + engines['heroku'] = {'url': db_url} + else: + for name in re.split(r',\s*', db_names): + engines[name] = rec = {} + rec['url'] = context.config.get_section_option(name, + "sqlalchemy.url") + + for name, rec in engines.items(): + logger.info("Migrating database %s" % name) + file_ = "%s.sql" % name + logger.info("Writing output to %s" % file_) + with open(file_, 'w') as buffer: + context.configure(url=rec['url'], output_buffer=buffer) + with context.begin_transaction(): + context.run_migrations(engine_name=name) + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # for the direct-to-DB use case, start a transaction on all + # engines, then run all migrations, then commit all transactions. + + engines = {} + db_url = os.getenv('DATABASE_URL') + if db_url: + engines['heroku'] = rec = {} + rec['engine'] = engine_from_config( + {'sqlalchemy.url': db_url}, + poolclass=pool.NullPool) + else: + for name in re.split(r',\s*', db_names): + engines[name] = rec = {} + rec['engine'] = engine_from_config( + context.config.get_section(name), + prefix='sqlalchemy.', + poolclass=pool.NullPool) + + for name, rec in engines.items(): + engine = rec['engine'] + rec['connection'] = conn = engine.connect() + + if USE_TWOPHASE: + rec['transaction'] = conn.begin_twophase() + else: + rec['transaction'] = conn.begin() + + try: + for name, rec in engines.items(): + logger.info("Migrating database %s" % name) + context.configure( + connection=rec['connection'], + upgrade_token="%s_upgrades" % name, + downgrade_token="%s_downgrades" % name, + target_metadata=target_metadata.get(name) + ) + context.run_migrations() + + if USE_TWOPHASE: + for rec in engines.values(): + rec['transaction'].prepare() + + for rec in engines.values(): + rec['transaction'].commit() + except: + for rec in engines.values(): + rec['transaction'].rollback() + raise + finally: + for rec in engines.values(): + rec['connection'].close() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/db/script.py.mako b/db/script.py.mako new file mode 100644 index 0000000..43c0940 --- /dev/null +++ b/db/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/db/versions/00817cda9d17_add_ebom_tables.py b/db/versions/00817cda9d17_add_ebom_tables.py new file mode 100644 index 0000000..6b383a7 --- /dev/null +++ b/db/versions/00817cda9d17_add_ebom_tables.py @@ -0,0 +1,64 @@ +"""Add ebom tables + +Revision ID: 00817cda9d17 +Revises: a904d0d1e1a7 +Create Date: 2016-09-01 14:05:49.858683 + +""" + +# revision identifiers, used by Alembic. +from sqlalchemy.orm import backref +from helix.constants.inverter_type import InverterType + +revision = '00817cda9d17' +down_revision = 'a904d0d1e1a7' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table( + 'power_stations', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('site_id', sa.Integer, sa.ForeignKey('sites.id')), + sa.Column('quantity', sa.Integer, nullable=False), + sa.Column('ac_run_length', sa.Integer, nullable=False), + sa.Column('description', sa.Unicode, nullable=False), + ) + + op.create_table( + 'standalone_inverters', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('site_id', sa.Integer, sa.ForeignKey('sites.id')), + sa.Column('attachment_point_id', sa.Integer, sa.ForeignKey('power_stations.id')), + sa.Column('ac_run_length', sa.Integer, nullable=False), + ) + + op.create_table( + 'inverters', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('model', sa.Enum(str(InverterType.SMA.MODEL_12KW.value), str(InverterType.SMA.MODEL_15KW.value), str(InverterType.SMA.MODEL_20KW.value), str(InverterType.SMA.MODEL_24KW.value), name='InverterType'), nullable=False), + sa.Column('strings_per_inverter', sa.Integer, nullable=False), + sa.Column('sunshade', sa.Boolean), + sa.Column('dc_switch', sa.Boolean), + sa.Column('power_station_id', sa.Integer, sa.ForeignKey('power_stations.id')), + sa.Column('standalone_inverter_id', sa.Integer, sa.ForeignKey('standalone_inverters.id')), + ) + + op.create_table( + 'power_monitors', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('site_id', sa.Integer, sa.ForeignKey('sites.id')), + sa.Column('power_station_id', sa.Integer, sa.ForeignKey('power_stations.id')) + ) + + +def downgrade(): + op.drop_table('inverters') + op.drop_table('power_monitors') + op.drop_table('power_stations') + op.drop_table('standalone_inverters') + sa.Enum(name='InverterType').drop(op.get_bind(), checkfirst=False) diff --git a/db/versions/1e472a2f3cbb_add_splice_box_check_on_inverters.py b/db/versions/1e472a2f3cbb_add_splice_box_check_on_inverters.py new file mode 100644 index 0000000..68f86f6 --- /dev/null +++ b/db/versions/1e472a2f3cbb_add_splice_box_check_on_inverters.py @@ -0,0 +1,24 @@ +"""Add splice box check on inverters + +Revision ID: 1e472a2f3cbb +Revises: ed4c4bd22d6a +Create Date: 2017-07-10 12:20:44.492597 + +""" + +# revision identifiers, used by Alembic. +revision = '1e472a2f3cbb' +down_revision = 'ed4c4bd22d6a' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('inverters', sa.Column('splice_box', sa.Boolean)) + + +def downgrade(): + op.drop_column('inverters', 'splice_box') diff --git a/db/versions/3cb6ab91fdc2_update_inverter_models_enum.py b/db/versions/3cb6ab91fdc2_update_inverter_models_enum.py new file mode 100644 index 0000000..7d42ef7 --- /dev/null +++ b/db/versions/3cb6ab91fdc2_update_inverter_models_enum.py @@ -0,0 +1,37 @@ +"""Update Inverter models enum + +Revision ID: 3cb6ab91fdc2 +Revises: f90d04c490dc +Create Date: 2017-06-26 12:41:17.882779 + +""" + +# revision identifiers, used by Alembic. +revision = '3cb6ab91fdc2' +down_revision = 'f90d04c490dc' +branch_labels = None +depends_on = None + +from alembic import op +from helix.constants.inverter_type import InverterType + + +def upgrade(): + op.execute('COMMIT') + + connection = None + if not op.get_context().as_sql: + connection = op.get_bind() + connection.execution_options(isolation_level='AUTOCOMMIT') + + op.execute('''ALTER TYPE "InverterType" ADD VALUE IF NOT EXISTS \'%s\'''' % (str(InverterType.DELTA.MODEL_36KW.value), )) + op.execute('''ALTER TYPE "InverterType" ADD VALUE IF NOT EXISTS \'%s\'''' % (str(InverterType.DELTA.MODEL_42KW.value), )) + op.execute('''ALTER TYPE "InverterType" ADD VALUE IF NOT EXISTS \'%s\'''' % (str(InverterType.DELTA.MODEL_60KW.value), )) + op.execute('''ALTER TYPE "InverterType" ADD VALUE IF NOT EXISTS \'%s\'''' % (str(InverterType.DELTA.MODEL_80KW.value), )) + + if connection is not None: + connection.execution_options(isolation_level='READ_COMMITTED') + + +def downgrade(): + pass diff --git a/db/versions/72342d883290_add_file_names_to_site.py b/db/versions/72342d883290_add_file_names_to_site.py new file mode 100644 index 0000000..1ca55e0 --- /dev/null +++ b/db/versions/72342d883290_add_file_names_to_site.py @@ -0,0 +1,28 @@ +"""add_file_names_to_site + +Revision ID: 72342d883290 +Revises: 00817cda9d17 +Create Date: 2016-10-06 16:56:41.943103 + +""" + +# revision identifiers, used by Alembic. +revision = '72342d883290' +down_revision = '00817cda9d17' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('sites', sa.Column('cad_file_name', sa.Unicode)) + op.add_column('sites', sa.Column('dxf_file', sa.Unicode)) + op.add_column('sites', sa.Column('dxf_file_name', sa.Unicode)) + + +def downgrade(): + op.drop_column('sites', 'cad_file_name') + op.drop_column('sites', 'dxf_file') + op.drop_column('sites', 'dxf_file_name') diff --git a/db/versions/a904d0d1e1a7_create_initial_tables.py b/db/versions/a904d0d1e1a7_create_initial_tables.py new file mode 100644 index 0000000..5e582b6 --- /dev/null +++ b/db/versions/a904d0d1e1a7_create_initial_tables.py @@ -0,0 +1,56 @@ +"""create initial tables + +Revision ID: a904d0d1e1a7 +Revises: +Create Date: 2016-08-31 11:16:58.286054 + +""" + +# revision identifiers, used by Alembic. +revision = 'a904d0d1e1a7' +down_revision = None +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table( + 'users', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('username', sa.Unicode, nullable=False), + sa.Column('password_hash', sa.Unicode, nullable=False) + ) + + op.create_table( + 'sites', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('user_id', sa.Integer, sa.ForeignKey('users.id')), + sa.Column('project_name', sa.Unicode, nullable=False), + sa.Column('building_height', sa.Float, nullable=False), + sa.Column('building_width', sa.Float, nullable=False), + sa.Column('building_length', sa.Float, nullable=False), + sa.Column('parapet_height', sa.Float, nullable=False), + sa.Column('wind_speed', sa.Integer, nullable=False), + sa.Column('exposure_category', sa.Unicode, nullable=False), + sa.Column('exposure_transition_distance', sa.Integer), + sa.Column('ballast_block_weight', sa.Float, nullable=False), + sa.Column('max_psf', sa.Float, nullable=False), + sa.Column('system_type', sa.Enum('0', '1', name='SystemType'), nullable=False), + sa.Column('module_type', sa.Enum('96 Cell', '128 Cell', 'P-Series', name='ModuleType'), nullable=False), + sa.Column('anchor_type', sa.Enum('OMG PowerGrip', 'OMG PowerGrip Plus', 'EcoFasten Eco 65', name='AnchorType'), nullable=False), + sa.Column('spectral_response', sa.Float, nullable=False), + sa.Column('seismic_importance_factor', sa.Float, nullable=False), + sa.Column('cad_file', sa.Unicode), + ) + + +def downgrade(): + op.drop_table('sites') + op.drop_table('users') + sa.Enum(name='SystemType').drop(op.get_bind(), checkfirst=False) + sa.Enum(name='ModuleType').drop(op.get_bind(), checkfirst=False) + sa.Enum(name='AnchorType').drop(op.get_bind(), checkfirst=False) + diff --git a/db/versions/ed4c4bd22d6a_add_cascade_delete_for_inverter.py b/db/versions/ed4c4bd22d6a_add_cascade_delete_for_inverter.py new file mode 100644 index 0000000..e7daf2f --- /dev/null +++ b/db/versions/ed4c4bd22d6a_add_cascade_delete_for_inverter.py @@ -0,0 +1,39 @@ +"""add cascade delete for inverter + +Revision ID: ed4c4bd22d6a +Revises: 3cb6ab91fdc2 +Create Date: 2017-06-30 11:10:24.762958 + +""" + +# revision identifiers, used by Alembic. +revision = 'ed4c4bd22d6a' +down_revision = '3cb6ab91fdc2' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.drop_constraint('inverters_standalone_inverter_id_fkey', 'inverters', type_='foreignkey') + op.create_foreign_key( + 'inverters_standalone_inverter_id_fkey', + 'inverters', + 'standalone_inverters', + ['standalone_inverter_id'], + ['id'], + ondelete='CASCADE' + ) + + +def downgrade(): + op.drop_constraint('inverters_standalone_inverter_id_fkey', 'inverters', type_='foreignkey') + op.create_foreign_key( + 'inverters_standalone_inverter_id_fkey', + 'inverters', + 'standalone_inverters', + ['standalone_inverter_id'], + ['id'] + ) diff --git a/db/versions/f90d04c490dc_add_inverter_brand_table.py b/db/versions/f90d04c490dc_add_inverter_brand_table.py new file mode 100644 index 0000000..a61b50f --- /dev/null +++ b/db/versions/f90d04c490dc_add_inverter_brand_table.py @@ -0,0 +1,30 @@ +"""Add Inverter Brand table + +Revision ID: f90d04c490dc +Revises: 72342d883290 +Create Date: 2017-06-22 15:58:21.358210 + +""" + +# revision identifiers, used by Alembic. +revision = 'f90d04c490dc' +down_revision = '72342d883290' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table( + 'inverter_brands', + sa.Column('id', sa.Integer, primary_key=True), + sa.Column('site_id', sa.Integer, sa.ForeignKey('sites.id'), primary_key=True), + ) + pass + + +def downgrade(): + op.drop_table('inverter_brands') + pass diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bc7d4b0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,23 @@ +version: '2' +services: + helix: + build: . +# ports: +# - "5000:5000" # flask server +# - "2222:2222" # ssh + volumes: + - .:/code + - /code/node_modules # http://jdlm.info/articles/2016/03/06/lessons-building-node-app-docker.html + network_mode: host + db: + build: + context: . + dockerfile: Dockerfile.db + volumes: + - /docker_data_volume/helix_roof_calculator_db:/var/lib/postgresql/data + network_mode: host + cache: + image: redis:3.2.4 + volumes: + - /docker_data_volume/helix_roof_calculator_cache:/data + network_mode: host diff --git a/documentation/pycharm_docker.png b/documentation/pycharm_docker.png new file mode 100644 index 0000000..3220c7b Binary files /dev/null and b/documentation/pycharm_docker.png differ diff --git a/documentation/pycharm_docker_config.png b/documentation/pycharm_docker_config.png new file mode 100644 index 0000000..8cd177f Binary files /dev/null and b/documentation/pycharm_docker_config.png differ diff --git a/documentation/setup_pycharm_alltests_target.png b/documentation/setup_pycharm_alltests_target.png new file mode 100644 index 0000000..4e87490 Binary files /dev/null and b/documentation/setup_pycharm_alltests_target.png differ diff --git a/documentation/setup_pycharm_correct_python.png b/documentation/setup_pycharm_correct_python.png new file mode 100644 index 0000000..ed6b0c9 Binary files /dev/null and b/documentation/setup_pycharm_correct_python.png differ diff --git a/documentation/setup_pycharm_server_target.png b/documentation/setup_pycharm_server_target.png new file mode 100644 index 0000000..b80fd54 Binary files /dev/null and b/documentation/setup_pycharm_server_target.png differ diff --git a/documentation/setup_pycharm_target_configuration.png b/documentation/setup_pycharm_target_configuration.png new file mode 100644 index 0000000..65c8725 Binary files /dev/null and b/documentation/setup_pycharm_target_configuration.png differ diff --git a/documentation/setup_pycharm_unittests_configuration.png b/documentation/setup_pycharm_unittests_configuration.png new file mode 100644 index 0000000..32ac04f Binary files /dev/null and b/documentation/setup_pycharm_unittests_configuration.png differ diff --git a/helix/Repositories/__init__.py b/helix/Repositories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/helix/Repositories/graph_repository.py b/helix/Repositories/graph_repository.py new file mode 100644 index 0000000..cc7a8ac --- /dev/null +++ b/helix/Repositories/graph_repository.py @@ -0,0 +1,42 @@ +import copy +from helix.calculators.subarray_graph import SubarrayGraph, Direction +from helix.calculators.subarray_helper import extract_subarray +from helix.constants.panel_type import PanelType + + +class GraphRepository(object): + def __init__(self, panels, subarrays, system_type): + self.graphs = {} + for subarray in subarrays: + subarray_panels = extract_subarray(panels, subarray.subarray_number) + self.graphs[subarray.subarray_number] = SubarrayGraph(panels=subarray_panels, system_type=system_type) + + def walk_callback(_, next_direction, direction_to_get_here): + if next_direction == Direction.North: + self.rows_perimeter += 1 + elif next_direction == Direction.East: + self.columns_perimeter += 1 + + for subarray in subarrays: + graph = self.graphs[subarray.subarray_number] + + subarray_panels = extract_subarray(panels, subarray.subarray_number) + rows_fallback = sum(1 for panel in subarray_panels if panel.panel_type == PanelType.Corner or panel.panel_type == PanelType.EastWest) / 2 + columns_fallback = sum(1 for panel in subarray_panels if panel.panel_type == PanelType.Corner or panel.panel_type == PanelType.NorthSouth) / 2 + + if len(graph.nodes) == 0: + subarray.row_count = rows_fallback + subarray.column_count = columns_fallback + subarray.row_counted_geometrically = False + subarray.column_counted_geometrically = False + continue + self.rows_perimeter = 1 + self.columns_perimeter = 1 + graph.walk_graph_perimeter(graph.lower_left_node(graph.nodes), walk_callback, repeat_steps=False) + subarray.row_count = max(self.rows_perimeter, rows_fallback) + subarray.row_counted_geometrically = self.rows_perimeter >= rows_fallback + subarray.column_count = max(self.columns_perimeter, columns_fallback) + subarray.column_counted_geometrically = self.columns_perimeter >= columns_fallback + + def subarray_graph(self, subarray_number): + return copy.deepcopy(self.graphs[subarray_number]) diff --git a/helix/Services/__init__.py b/helix/Services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/helix/Services/doc_gen_service.py b/helix/Services/doc_gen_service.py new file mode 100644 index 0000000..0178e76 --- /dev/null +++ b/helix/Services/doc_gen_service.py @@ -0,0 +1,22 @@ +import json + + +class DocGenServiceError(Exception): + pass + + +class DocGenService(object): + def __init__(self, request_maker, request_builder): + self.request_maker = request_maker + self.request_builder = request_builder + + def generate(self): + url = 'https://dcs.us.sunpower.com/ws/docgen/docx/generatePdf' + headers = {'content-type': 'application/json'} + params = json.dumps(self.request_builder.build()) + + result = self.request_maker.post(url, params, headers=headers) + + if result.status_code != 200: + raise DocGenServiceError(result.content) + return result.content diff --git a/helix/Services/dxf_helper.py b/helix/Services/dxf_helper.py new file mode 100644 index 0000000..4b5e021 --- /dev/null +++ b/helix/Services/dxf_helper.py @@ -0,0 +1,1002 @@ +import math +from statistics import mean + +from helix.calculators.subarray_helper import get_subarray_sizes_and_rows +from helix.constants.dxf_validation import INVALID_DUAL_TILT_DESIGN +from helix.constants.panel_type import PanelType +from helix.constants.subarray import SUBARRAY_SIZE_BIG +from helix.constants.system_type import SystemType +from helix.helpers.polygon_helper import * +from helix.models.coordinate import Coordinate +from helix.models.dxf.dxf_error import DXFError +from helix.models.dxf.graph_direction import GraphDirection +from helix.models.dxf.graph_node import GraphNode +from helix.models.dxf.graph_node_store import GraphNodeStore +from helix.models.dxf.polygon import Polygon +from helix.models.panel import Panel +from helix.validators.dxf_layer_validator import DXFLayerValidator + + +class DXFHelper(object): + def __init__(self): + self.aurora_detector = DXFLayerValidator() + + def is_new_aurora_format(self): + """ Determine if it's the new dxf format + + return + boolean + + """ + + return self.aurora_detector.is_new_aurora_format() + + @staticmethod + def generate_panels(modules, translated_modules): + """Joins the modules and creates + + Parameters: + modules (list): List of obtained modules + translated_modules (list): A list containing the shifted + positions of the modules + Returns: + list + + """ + + panels = [] + for idx, module in enumerate(modules): + x_center = mean([x for x, _ in module.points]) + y_center = mean([y for _, y in module.points]) + + rotation = module.determine_orientation() + + x_translated_center = mean([x for x, _ in translated_modules[idx].points]) + y_translated_center = mean([y for _, y in translated_modules[idx].points]) + + panels.append(Panel(id=idx + 1, coordinate=Coordinate( + x_translated_center, y_translated_center, rotation), original_coordinate=Coordinate(x_center, y_center, rotation))) + return panels + + @staticmethod + def should_consolidate_modules(modules, system_type, module_constants): + """Determine if the modules should be consolidated into panel pairs. + Some dual tilt files came with separate modules, this method determines + if we should consolidate them. + + Arguments: + modules (list) List of helix.models.dxf.Polygon + system_type (object) Type of system selected single tilt, dual tilt + module_constants (object) + helix.constants.module_type_constants.dual_tilt_96_cell_constants.DualTilt96CellConstants + + Returns: + boolean + + """ + + if system_type != SystemType.dualTilt: + return False + for module in modules: + points = module.sorted_points() + p1 = points[0] + other_points = sorted(points[1:], key=lambda x: (x[0] - p1[0]) ** 2 + (x[1] - p1[1]) ** 2) + p2 = other_points[1] + if (math.hypot(p2[0] - p1[0], p2[1] - p1[1]) - min(module_constants.panel_spacing)) < 1e-3: + return True + return False + + @staticmethod + def consolidate_dual_tilt_modules(original_modules, system_type, + pair_spacing): + """Consolidate the dual tilt modules. This method is called + with a certain type of dual tilt files that had the modules + separated + + Parameters: + original_modules (list) Contains the list of modules from the dxf + file + system_type (object) Type of system selected single tilt, dual tilt + pair_spacing (object) Contains the possible pair spacing that a + dual tilt system will have + + """ + + if system_type == SystemType.dualTilt: + modules = [] + while len(original_modules) > 0: + current_module = original_modules.pop(0) + modules.append(current_module) + found_pair = False + for idx, potential_pair in enumerate(original_modules): + if current_module.shares_module_on_long_edge(potential_pair, + pair_spacing): + current_module.consolidate_with(potential_pair, + pair_spacing) + original_modules.pop(idx) + found_pair = True + break + if not found_pair: + raise DXFError(INVALID_DUAL_TILT_DESIGN) + else: + return original_modules + return modules + + def build_polygons(self, dxf_entities): + """Obtain two types of polygon objects, + that represent modules and buildings. + It's important to understand that the + polygons are scaled using inches. + + Arguments: + dxf_entities (list) This list contains + dxfgrabber.entities + + """ + + building_lines = [] + module_lines = [] + + for entity in dxf_entities: + self.aurora_detector.add_layer(entity.layer) + if entity.layer == 'Modules': # is a module + module_lines.append(entity) + elif entity.layer == 'Roofs' or entity.layer == 'Buildings': # is a building/roof line + building_lines.append(entity) + + self.aurora_detector.determine_file() + inches_per_feet = 12 + + buildings = [p.scale(inches_per_feet, inches_per_feet) for p in DXFHelper.generate_polygons(building_lines)] + modules = [p.scale(inches_per_feet, inches_per_feet) for p in DXFHelper.generate_polygons(module_lines)] + return buildings, modules + + @staticmethod + def generate_polygons(lines): + polygons = [] + for line in lines: + if len(polygons) == 0 or not polygons[-1].continues_with_line(line): + polygons.append(Polygon(line)) + elif line.end in polygons[-1].points: + continue + else: + polygons[-1].points.append(line.end) + return polygons + + @staticmethod + def translate_towards_origin(buildings, modules): + """Obtains the minimum value of x and y points and shifts + all the modules and buildings. + + Arguments: + buildings (list): List of buildings + modules (list): List of modules + Returns: + tuple + + """ + + min_x = float('inf') # positive infinity + min_y = float('inf') + for polygon in buildings + modules: + min_x = min(min_x, min(x for x, _ in polygon.points)) + min_y = min(min_y, min(y for _, y in polygon.points)) + + translated_buildings = [Polygon(points=[(x - min_x, y - min_y) for x, y in polygon.points]) for polygon in buildings] + translated_modules = [Polygon(points=[(x - min_x, y - min_y) for x, y in polygon.points]) for polygon in modules] + + return translated_buildings, translated_modules + + @staticmethod + def get_polygons_counterclockwise(polygons): + """Checks if polygon's points are in counterclockwise order, if not reverse the order. + + Arguments: + polygons (list): List of polygons + Returns: + list of polygons with points in counterclockwise order + """ + + output_polygons = [] + + for polygon in polygons: + points = polygon.points + + # checking if building's points are in clockwise order + cumulative = 0 + + for i in range(len(points)): + current = points[i - 1] + next = points[i] + cumulative += (next[0] - current[0]) * (next[1] + current[1]) + + clockwise = cumulative > 0 + + # change to counter-clockwise if necessary + # further code assumes we have counter-clockwise points for the building + if clockwise: + points = list(reversed(points)) + + output_polygons.append(Polygon(points=[(x, y) for x, y in points])) + + return output_polygons + + + @staticmethod + def build_node_graph(panels, spacing): + nodes = [] + node_store = GraphNodeStore() + for panel in panels: + x_spacing = spacing[0] + y_spacing = spacing[1] + node = GraphNode(panel, x_spacing, y_spacing) + nodes.append(node) + + node_store.add_node(node) + + for node in nodes: + if len(node.neighboring_nodes()) == 8: + continue + for x in (0, 1, -1): + for y in (0, 1, -1): + if x == y == 0: + continue + rotation = math.radians(node.coordinate.rotation) + x_spacing = (x * node.x_spacing * math.cos(rotation)) - (y * node.y_spacing * math.sin(rotation)) + y_spacing = (x * node.x_spacing * math.sin(rotation)) + (y * node.y_spacing * math.cos(rotation)) + + coordinate = Coordinate(node.coordinate.x + x_spacing, node.coordinate.y + y_spacing, node.coordinate.rotation) + if coordinate.x < 0 or coordinate.y < 0: + continue + direction = GraphDirection((x, y)) + if node.has_existing_neighbor(direction): + continue + + neighbor = node_store.find_coordinate(coordinate) + if neighbor: + node.add_neighbor(neighbor, direction) + if len(node.neighboring_nodes()) == 0: + raise DXFError("Error - invalid module spacing. Please check to make sure the correct system type and panel spacing are present") + + return nodes + + @staticmethod + def detect_subarrays(nodes, panels): + subarray_number = 0 + + def walk_graph_and_assign_subarray(node, subarray_number): + if node.panel.subarray is not None: + return + node.panel.subarray = subarray_number + for neighbor in node.neighboring_nodes(): + walk_graph_and_assign_subarray(neighbor, subarray_number) + + for node in nodes: + if node.panel.subarray is None: + subarray_number += 1 + try: + walk_graph_and_assign_subarray(node, subarray_number) + except RecursionError: + raise DXFError(SUBARRAY_SIZE_BIG) + + panels.sort(key=lambda p: p.subarray) + subarray_list = get_subarray_sizes_and_rows(panels) + subarrays = {} + for subarray in subarray_list: + subarray_number = subarray.subarray_number + if not subarrays.get(subarray.subarray_number): + subarrays[subarray_number] = subarray + else: + subarrays[subarray_number].size += subarray.size + return list(subarrays.values()) + + @staticmethod + def detect_panel_types(nodes): + for node in nodes: + ordinal_neighbors = node.ordinal_neighbors() + ordinal_neighbors_count = len(ordinal_neighbors) + if ordinal_neighbors_count == 4: + node.panel.panel_type = PanelType.Middle + elif ordinal_neighbors_count <= 2: + node.panel.panel_type = PanelType.Corner + elif ordinal_neighbors.get(GraphDirection.North) is None or ordinal_neighbors.get(GraphDirection.South) is None: + node.panel.panel_type = PanelType.NorthSouth + elif ordinal_neighbors.get(GraphDirection.East) is None or ordinal_neighbors.get(GraphDirection.West) is None: + node.panel.panel_type = PanelType.EastWest + else: + raise DXFError("Invalid Array") + pass + + @staticmethod + def detect_wind_zones(panels, buildings, modules, l_b, system_type): + if system_type == SystemType.dualTilt: + DXFHelper.__detect_dual_tilt_wind_zones__(panels, buildings, modules, l_b) + else: + DXFHelper.__detect_single_tilt_wind_zones__(panels, buildings, modules, l_b) + + @staticmethod + def __detect_dual_tilt_wind_zones__(panels, buildings, modules, l_b): + sorted_panels = sorted(panels, key=lambda p: p.id) + wind_zone_a = DXFHelper.__generate_wind_zone_a_dual_tilt__(buildings, l_b) + fuzzy_wind_zone_a = DXFHelper.__generate_wind_zone_a_fuzzy_dual_tilt__(buildings, l_b) + wind_zone_b = DXFHelper.__generate_wind_zone_b_dual_tilt__(buildings, l_b) + fuzzy_wind_zone_b = DXFHelper.__generate_wind_zone_b_fuzzy_dual_tilt__(buildings, l_b) + wind_zone_c = DXFHelper.__generate_wind_zone_c_dual_tilt__(buildings, l_b) + fuzzy_wind_zone_c = DXFHelper.__generate_wind_zone_c_fuzzy_dual_tilt__(buildings, l_b) + wind_zone_d = DXFHelper.__generate_wind_zone_d_dual_tilt__(buildings, l_b) + fuzzy_wind_zone_d = DXFHelper.__generate_wind_zone_d_fuzzy_dual_tilt__(buildings, l_b) + for idx, panel in enumerate(sorted_panels): + module = modules[idx] + if DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_a): + panel.wind_zone = 0 + elif DXFHelper.__is_panel_in_wind_zone__(module, fuzzy_wind_zone_a): + panel.wind_zone = 0 + panel.fuzzy_wind_zone = True + elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_b): + panel.wind_zone = 1 + elif DXFHelper.__is_panel_in_wind_zone__(module, fuzzy_wind_zone_b): + panel.wind_zone = 1 + panel.fuzzy_wind_zone = True + elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_c): + panel.wind_zone = 2 + elif DXFHelper.__is_panel_in_wind_zone__(module, fuzzy_wind_zone_c): + panel.wind_zone = 2 + panel.fuzzy_wind_zone = True + elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_d): + panel.wind_zone = 3 + elif DXFHelper.__is_panel_in_wind_zone__(module, fuzzy_wind_zone_d): + panel.wind_zone = 3 + panel.fuzzy_wind_zone = True + else: # wind zone E + panel.wind_zone = 4 + + @staticmethod + def __detect_single_tilt_wind_zones__(panels, buildings, modules, l_b): + sorted_panels = sorted(panels, key=lambda p: p.id) + panel_orientation = panels[0].coordinate.rotation + wind_zone_a = DXFHelper.__generate_wind_zone_a_single_tilt__(buildings, l_b, panel_orientation) + fuzzy_wind_zone_a = DXFHelper.__generate_wind_zone_a_fuzzy_single_tilt__(buildings, l_b) + wind_zone_b = DXFHelper.__generate_wind_zone_b_single_tilt__(buildings, l_b, panel_orientation) + fuzzy_wind_zone_b = DXFHelper.__generate_wind_zone_b_fuzzy_single_tilt__(buildings, l_b) + wind_zone_c = DXFHelper.__generate_wind_zone_c_single_tilt__(buildings, l_b, panel_orientation) + fuzzy_wind_zone_c = DXFHelper.__generate_wind_zone_c_fuzzy_single_tilt__(buildings, l_b) + wind_zone_d = DXFHelper.__generate_wind_zone_d_single_tilt__(buildings, l_b, panel_orientation) + fuzzy_wind_zone_d = DXFHelper.__generate_wind_zone_d_fuzzy_single_tilt__(buildings, l_b) + wind_zone_i = DXFHelper.__generate_wind_zone_i_single_tilt__(buildings, l_b, panel_orientation) + wind_zone_h = DXFHelper.__generate_wind_zone_h_single_tilt__(buildings, l_b, panel_orientation) + wind_zone_j = DXFHelper.__generate_wind_zone_j_single_tilt__(buildings, l_b, panel_orientation) + wind_zone_e = DXFHelper.__generate_wind_zone_e_single_tilt__(buildings, l_b, panel_orientation) + wind_zone_f = DXFHelper.__generate_wind_zone_f_single_tilt__(buildings, l_b, panel_orientation) + wind_zone_g = DXFHelper.__generate_wind_zone_g_single_tilt__(buildings, l_b, panel_orientation) + for idx, panel in enumerate(sorted_panels): + module = modules[idx] + if DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_a): + panel.wind_zone = 0 + elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_i): + panel.wind_zone = 8 + elif DXFHelper.__is_panel_in_wind_zone__(module, fuzzy_wind_zone_a): + panel.wind_zone = 0 + panel.fuzzy_wind_zone = True + elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_j): + panel.wind_zone = 9 + elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_b): + panel.wind_zone = 1 + elif DXFHelper.__is_panel_in_wind_zone__(module, fuzzy_wind_zone_b): + panel.wind_zone = 1 + panel.fuzzy_wind_zone = True + elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_h): + panel.wind_zone = 7 + elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_c): + panel.wind_zone = 2 + elif DXFHelper.__is_panel_in_wind_zone__(module, fuzzy_wind_zone_c): + panel.wind_zone = 2 + panel.fuzzy_wind_zone = True + elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_e): + panel.wind_zone = 4 + elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_f): + panel.wind_zone = 5 + elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_d): + panel.wind_zone = 3 + elif DXFHelper.__is_panel_in_wind_zone__(module, fuzzy_wind_zone_d): + panel.wind_zone = 3 + panel.fuzzy_wind_zone = True + elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_g): + panel.wind_zone = 6 + else: + panel.wind_zone = 10 + + @staticmethod + def __is_panel_in_wind_zone__(module, wind_zone): + for x, y in module.points: + for wind_subzone in wind_zone: + if point_inside_polygon(x, y, wind_subzone): + return True + return False + + @staticmethod + def __compute_segment_direction(p1, p2): + """ + Computes direction of a segment. Points taken from building outline are assumed to be in counterclockwise order. + + :param p1: first point + :param p2: second point + :return: tuple representing orientation ('north', 'south', 'east', 'west') and variation in degrees from ideally + directed segment + """ + segment_angle = (math.degrees(math.atan2(p1[1] - p2[1], p1[0] - p2[0])) + 360) % 360 + + if segment_angle >= 315 or segment_angle < 45: + return 'north', segment_angle if segment_angle < 45 else 360 - segment_angle + elif 45 <= segment_angle < 135: + return 'west', abs(90 - segment_angle) + elif 135 <= segment_angle < 225: + return 'south', abs(180 - segment_angle) + else: + return 'east', abs(270 - segment_angle) + + @staticmethod + def compute_corner_directions(vertex, prev, next, angle_correction): + """ + Determines if point is located in north/east corner + + :param vertex: point located in the corner + :param prev: point previous to vertex, assuming counterclockwise order + :param next: point next to vertex, assuming counterclockwise order + :param angle_correction: points are rotated by this angle first, in degrees + :return: (is_north, is_east) tuple + """ + + rotated = DXFHelper.__rotate_points([vertex, prev, next], math.radians(angle_correction)) + + vertex_rotated, prev_rotated, next_rotated = rotated[0], rotated[1], rotated[2] + + prev_segment_orientation, prev_dev = DXFHelper.__compute_segment_direction(prev_rotated, vertex_rotated) + next_segment_orientation, next_dev = DXFHelper.__compute_segment_direction(vertex_rotated, next_rotated) + + is_north = prev_segment_orientation == 'north' or next_segment_orientation == 'north' + + dirs = (prev_segment_orientation, next_segment_orientation) + + if 'north' in dirs and 'south' not in dirs: + is_north = True + elif 'south' in dirs and 'north' not in dirs: + is_north = False + elif dirs == ('north', 'south'): + is_north = prev_dev < next_dev + elif dirs == ('south', 'north'): + is_north = next_dev < prev_dev + elif dirs == ('east', 'west'): + is_north = True + elif dirs == ('west', 'east'): + is_north = False + elif dirs == ('west', 'west') or dirs == ('east', 'east'): + is_north = prev_rotated[0] > next_rotated[0] + + if 'east' in dirs and 'west' not in dirs: + is_east = True + elif 'west' in dirs and 'east' not in dirs: + is_east = False + elif dirs == ('east', 'west'): + is_east = prev_dev < next_dev + elif dirs == ('west', 'east'): + is_east = next_dev < prev_dev + elif dirs == ('north', 'south'): + is_east = False + elif dirs == ('south', 'north'): + is_east = True + elif dirs == ('north', 'north') or dirs == ('south', 'south'): + is_east = prev_rotated[1] < next_rotated[1] + + return is_north, is_east + + @staticmethod + def __rotate_points(points, angle): + """ + :param points: points to be rotated as list of tuples + :param angle: angle in radians + :return: list of rotated points + """ + sin = math.sin(angle) + cos = math.cos(angle) + + return [(x * cos - y * sin, x * sin + y * cos) for x, y in points] + + + @staticmethod + def __rotate_point(point, angle): + return DXFHelper.__rotate_points([point], angle)[0] + + + @staticmethod + def __generate_wind_zone__(buildings, scaling_factor, points_callback, panel_orientation): + """ + Important: polygons representing buildings are expected to have points in counterclockwise order + """ + + wind_zones = [] + for building in buildings: + for idx, vertex in enumerate(building.points): + prev = building.points[(idx - 1) % len(building.points)] + next = building.points[(idx + 1) % len(building.points)] + + a_sq = (vertex[0] - prev[0]) ** 2 + (vertex[1] - prev[1]) ** 2 + b_sq = (vertex[0] - next[0]) ** 2 + (vertex[1] - next[1]) ** 2 + c_sq = (next[0] - prev[0]) ** 2 + (next[1] - prev[1]) ** 2 + + distance_to_prev = math.sqrt(a_sq) + distance_to_next = math.sqrt(b_sq) + angle = math.acos((c_sq - a_sq - b_sq) / (-2 * distance_to_prev * distance_to_next)) + + # angles between x-axis and line created by current and next/previous vertex + angle_to_next = math.atan2(next[1] - vertex[1], next[0] - vertex[0]) + angle_to_prev = math.atan2(prev[1] - vertex[1], prev[0] - vertex[0]) + + is_north, is_east = DXFHelper.compute_corner_directions(vertex, prev, next, -panel_orientation) + + relative_angle = ((angle_to_next - angle_to_prev + 2 * math.pi) % (2 * math.pi)) - math.pi + + if abs(angle_to_next - math.radians(panel_orientation)) > abs(angle_to_prev - math.radians(panel_orientation)): + max_distance = distance_to_next + else: + max_distance = distance_to_prev + + orientation = angle_to_next + + def generate_point(x, y): + d_x, d_y = DXFHelper.__rotate_point((x, y), orientation) + return vertex[0] + d_x, vertex[1] + d_y + + if angle <= math.radians(135) and relative_angle >= 0: + points = points_callback(angle, is_north, is_east, max_distance) + if points: + wind_zones.append([generate_point(scaling_factor * x, scaling_factor * y) for x, y in points]) + return wind_zones + + @staticmethod + def __generate_dual_tilt_wind_zone_with_transforms__(buildings, scaling_factor, points): + def callback(angle, *_): + if angle < math.radians(80): + return [] + rotation = angle - math.radians(90) + + def inner_transform(x, y, perform_rotation, rotation_value=rotation): + if perform_rotation: + return DXFHelper.__rotate_point((x, y), rotation_value) + else: + return x, y + + transformed_points = [] + for x, y, apply_rotation, rotation_scale in points: + if rotation_scale is not None: + rotation_value = rotation_scale * rotation + else: + rotation_value = rotation + transformed_points.append(inner_transform(x, y, apply_rotation, rotation_value)) + + return transformed_points + + return DXFHelper.__generate_wind_zone__(buildings, scaling_factor, callback, 0) + + @staticmethod + def __generate_wind_zone_a_dual_tilt__(buildings, scaling_factor): + wind_zone_points = [ + (0, 0, False, None), + (2, 0, False, None), + (2, 1, False, None), + (1, 1, True, 0.5), + (1, 2, True, None), + (0, 2, True, None) + ] + + return DXFHelper.__generate_dual_tilt_wind_zone_with_transforms__(buildings, scaling_factor, wind_zone_points) + + @staticmethod + def __generate_wind_zone_b_dual_tilt__(buildings, scaling_factor): + wind_zone_points = [ + (1, 1, True, 0.5), + (2, 1, False, None), + (2, 0, False, None), + (2, 0, False, None), + (4, 0, False, None), + (4, 2, False, None), + (2, 2, True, 0.5), + (2, 4, True, None), + (0, 4, True, None), + (0, 2, True, None), + (1, 2, True, None), + ] + return DXFHelper.__generate_dual_tilt_wind_zone_with_transforms__(buildings, scaling_factor, wind_zone_points) + + @staticmethod + def __generate_wind_zone_c_dual_tilt__(buildings, scaling_factor): + wind_zone_points = [ + (2, 2, True, 0.5), + (4, 2, False, None), + (4, 0, False, None), + (6, 0, False, None), + (6, 3, False, None), + (3, 3, True, 0.5), + (3, 6, True, None), + (0, 6, True, None), + (0, 4, True, None), + (2, 4, True, None), + ] + return DXFHelper.__generate_dual_tilt_wind_zone_with_transforms__(buildings, scaling_factor, wind_zone_points) + + @staticmethod + def __generate_wind_zone_d_dual_tilt__(buildings, scaling_factor): + wind_zone_points = [ + (3, 3, True, 0.5), + (6, 3, False, None), + (6, 0, False, None), + (8, 0, False, None), + (8, 4, False, None), + (4, 8, True, None), + (0, 8, True, None), + (0, 6, True, None), + (3, 6, True, None), + ] + return DXFHelper.__generate_dual_tilt_wind_zone_with_transforms__(buildings, scaling_factor, wind_zone_points) + + @staticmethod + def __generate_fuzzy_wind_zone_dual_tilt__(buildings, scaling_factor, inner_l_b, outer_l_b): + def callback(angle, *_): + if angle >= math.radians(80): + return [] + number_points_on_inner_arc = int(math.ceil(inner_l_b * scaling_factor / 10)) + number_points_on_outer_arc = int(math.ceil(outer_l_b * scaling_factor / 10)) + + inner_points_length = (inner_l_b, number_points_on_inner_arc) + outer_points_length = (outer_l_b, number_points_on_outer_arc) + + points = [] + if inner_l_b == 0: + points.append((0, 0)) + for idx, (l_b_length, number_points) in enumerate((inner_points_length, outer_points_length)): + if number_points == 0: + continue + for step in range(number_points + 1): + sub_angle = (angle / number_points) * step + x = l_b_length * math.cos(sub_angle) + y = l_b_length * math.sin(sub_angle) + if idx == 0: + points.append((x, y)) + else: + points.insert(0, (x, y)) + return points + + return DXFHelper.__generate_wind_zone__(buildings, scaling_factor, callback, 0) + + @staticmethod + def __generate_wind_zone_a_fuzzy_dual_tilt__(buildings, scaling_factor): + return DXFHelper.__generate_fuzzy_wind_zone_dual_tilt__(buildings, scaling_factor, 0, math.sqrt(5)) + + @staticmethod + def __generate_wind_zone_b_fuzzy_dual_tilt__(buildings, scaling_factor): + return DXFHelper.__generate_fuzzy_wind_zone_dual_tilt__(buildings, scaling_factor, math.sqrt(5), math.sqrt(20)) + + @staticmethod + def __generate_wind_zone_c_fuzzy_dual_tilt__(buildings, scaling_factor): + return DXFHelper.__generate_fuzzy_wind_zone_dual_tilt__(buildings, scaling_factor, math.sqrt(20), math.sqrt(45)) + + @staticmethod + def __generate_wind_zone_d_fuzzy_dual_tilt__(buildings, scaling_factor): + return DXFHelper.__generate_fuzzy_wind_zone_dual_tilt__(buildings, scaling_factor, math.sqrt(45), math.sqrt(80)) + + # Single Tilt + + @staticmethod + def __generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, northern_zone, points, panel_orientation): + def callback(angle, is_north, is_east, max_distance): + if is_north != northern_zone: + return [] + if angle < math.radians(80): + return [] + rotation = angle - math.radians(90) + + def inner_transform(x, y, perform_rotation, rotation_value=rotation): + if y == -1: + y = max_distance / scaling_factor + if not is_east: + perform_rotation = not perform_rotation + + # swap x,y coordinates - reflection about line x = y + new_x = y + new_y = x + x = new_x + y = new_y + if perform_rotation: + return DXFHelper.__rotate_point((x, y), rotation_value) + else: + return x, y + + transformed_points = [] + for x, y, apply_rotation, rotation_scale in points: + if rotation_scale is not None: + rotation_value = rotation_scale * rotation + else: + rotation_value = rotation + transformed_points.append(inner_transform(x, y, apply_rotation, rotation_value)) + + return transformed_points + + return DXFHelper.__generate_wind_zone__(buildings, scaling_factor, callback, panel_orientation) + + @staticmethod + def __generate_wind_zone_a_single_tilt__(buildings, scaling_factor, panel_orientation): + wind_zone_points = [ + (0, 0, False, None), + (0, 2, True, None), + (2, 2, True, 0.5), + ] + return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, True, + wind_zone_points, panel_orientation) + + @staticmethod + def __generate_wind_zone_b_single_tilt__(buildings, scaling_factor, panel_orientation): + first_wind_zone_points = [ + (0, 0, False, None), + (2, 0, False, None), + (2, 2, True, 0.5), + ] + first_wind_zone_polygons = DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, + scaling_factor, True, + first_wind_zone_points, + panel_orientation) + + second_wind_zone_points = [ + (0, 2, True, None), + (0, 4, True, None), + (1, 4, True, None), + (2, 3, True, None), + (2, 2, True, 0.5), + ] + second_wind_zone_polygons = DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, + scaling_factor, True, + second_wind_zone_points, + panel_orientation) + return first_wind_zone_polygons + second_wind_zone_polygons + + @staticmethod + def __generate_wind_zone_c_single_tilt__(buildings, scaling_factor, panel_orientation): + wind_zone_points = [ + (2, 0, False, None), + (4, 0, False, None), + (4, 4, True, 0.5), + (2, 6, True, None), + (0, 6, True, None), + (0, 4, True, None), + (1, 4, True, None), + (2, 3, True, None), + ] + return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, True, + wind_zone_points, panel_orientation) + + @staticmethod + def __generate_wind_zone_d_single_tilt__(buildings, scaling_factor, panel_orientation): + wind_zone_points = [ + (4, 0, False, None), + (6, 0, False, None), + (6, 6, True, 0.5), + (4, 8, True, None), + (0, 8, True, None), + (0, 6, True, None), + (2, 6, True, None), + (2, 6, True, None), + (4, 4, True, 0.5), + ] + return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, True, + wind_zone_points, panel_orientation) + + @staticmethod + def __generate_wind_zone_i_single_tilt__(buildings, scaling_factor, panel_orientation): + wind_zone_points = [ + (0, 0, False, None), + (0, 2, True, None), + (1.5, 2, True, 2.0 / 3.0), + (1, 0, False, None), + ] + return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, False, + wind_zone_points, panel_orientation) + + @staticmethod + def __generate_wind_zone_h_single_tilt__(buildings, scaling_factor, panel_orientation): + wind_zone_points = [ + (0, 2, True, None), + (0, 4, True, None), + (2, 4, True, 2.0 / 3.0), + (1.5, 2, True, 2.0 / 3.0), + ] + return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, False, + wind_zone_points, panel_orientation) + + @staticmethod + def __generate_wind_zone_j_single_tilt__(buildings, scaling_factor, panel_orientation): + wind_zone_points = [ + (1, 0, False, None), + (3, 0, False, None), + (3, 2, True, 2.0 / 3.0), + (1.5, 2, True, 2.0 / 3.0), + ] + return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, False, + wind_zone_points, panel_orientation) + + @staticmethod + def __generate_wind_zone_e_single_tilt__(buildings, scaling_factor, panel_orientation): + wind_zone_points = [ + (0, 4, True, None), + (0, 7, True, None), + (3, 7, True, 2.0 / 3.0), + (2, 4, True, 2.0 / 3.0), + ] + return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, False, + wind_zone_points, panel_orientation) + + @staticmethod + def __generate_wind_zone_f_single_tilt__(buildings, scaling_factor, panel_orientation): + wind_zone_points = [ + (3, 0, False, None), + (8, 0, False, None), + (8, 2, False, None), + (3, 7, True, 2.0 / 3.0), + (2, 4, True, 2.0 / 3.0), + (1.5, 2, True, 2.0 / 3.0), + (3, 2, True, 2.0 / 3.0), + ] + return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, False, + wind_zone_points, panel_orientation) + + @staticmethod + def __generate_wind_zone_g_single_tilt__(buildings, scaling_factor, panel_orientation): + wind_zone_points = [ + (0, 7, True, None), + (0, -1, True, None), + (4, -1, True, None), + (4, 6, True, 2.0 / 3.0), + (3, 7, True, 2.0 / 3.0), + ] + return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, False, + wind_zone_points, panel_orientation) + + @staticmethod + def __generate_fuzzy_wind_zone_single_tilt__(buildings, scaling_factor, inner_l_b, outer_l_b): + def callback(angle, *_): + if angle >= math.radians(80): + return [] + number_points_on_inner_arc = int(math.ceil(inner_l_b * scaling_factor / 10)) + number_points_on_outer_arc = int(math.ceil(outer_l_b * scaling_factor / 10)) + + inner_points_length = (inner_l_b, number_points_on_inner_arc) + outer_points_length = (outer_l_b, number_points_on_outer_arc) + + points = [] + if inner_l_b == 0: + points.append((0, 0)) + for idx, (l_b_length, number_points) in enumerate((inner_points_length, outer_points_length)): + if number_points == 0: + continue + start = 0 + end = number_points + for step in range(start, end + 1): + sub_angle = (angle / number_points) * step + x = l_b_length * math.cos(sub_angle) + y = l_b_length * math.sin(sub_angle) + if idx == 0: + points.append((x, y)) + else: + points.insert(0, (x, y)) + return points + + return DXFHelper.__generate_wind_zone__(buildings, scaling_factor, callback, 0) + + @staticmethod + def __generate_wind_zone_a_fuzzy_single_tilt__(buildings, scaling_factor): + return DXFHelper.__generate_fuzzy_wind_zone_single_tilt__(buildings, scaling_factor, 0, math.sqrt(8)) + + @staticmethod + def __generate_wind_zone_b_fuzzy_single_tilt__(buildings, scaling_factor): + return DXFHelper.__generate_fuzzy_wind_zone_single_tilt__(buildings, scaling_factor, math.sqrt(8), math.sqrt(20)) + + @staticmethod + def __generate_wind_zone_c_fuzzy_single_tilt__(buildings, scaling_factor): + return DXFHelper.__generate_fuzzy_wind_zone_single_tilt__(buildings, scaling_factor, math.sqrt(20), math.sqrt(44)) + + @staticmethod + def __generate_wind_zone_d_fuzzy_single_tilt__(buildings, scaling_factor): + return DXFHelper.__generate_fuzzy_wind_zone_single_tilt__(buildings, scaling_factor, math.sqrt(44), + math.sqrt(80)) + + @staticmethod + def l_b_polygons(buildings, scaling_factor, system_type, panel_orientation): + if system_type == SystemType.dualTilt: + return DXFHelper.__dual_tilt_wind_zone_polygons__(buildings, scaling_factor) + else: + return DXFHelper.__single_tilt_wind_zone_polygons__(buildings, scaling_factor, panel_orientation) + + @staticmethod + def __dual_tilt_wind_zone_polygons__(buildings, scaling_factor): + wind_zone_a = DXFHelper.__generate_wind_zone_a_dual_tilt__(buildings, scaling_factor) + wind_zone_a += DXFHelper.__generate_wind_zone_a_fuzzy_dual_tilt__(buildings, scaling_factor) + polygons = [] + for wind_zone in wind_zone_a: + polygon = Polygon(points=wind_zone) + polygon.color = "red" + polygons.append(polygon) + wind_zone_b = DXFHelper.__generate_wind_zone_b_dual_tilt__(buildings, scaling_factor) + wind_zone_b += DXFHelper.__generate_wind_zone_b_fuzzy_dual_tilt__(buildings, scaling_factor) + for wind_zone in wind_zone_b: + polygon = Polygon(points=wind_zone) + polygon.color = "green" + polygons.append(polygon) + wind_zone_c = DXFHelper.__generate_wind_zone_c_dual_tilt__(buildings, scaling_factor) + wind_zone_c += DXFHelper.__generate_wind_zone_c_fuzzy_dual_tilt__(buildings, scaling_factor) + for wind_zone in wind_zone_c: + polygon = Polygon(points=wind_zone) + polygon.color = "blue" + polygons.append(polygon) + wind_zone_d = DXFHelper.__generate_wind_zone_d_dual_tilt__(buildings, scaling_factor) + wind_zone_d += DXFHelper.__generate_wind_zone_d_fuzzy_dual_tilt__(buildings, scaling_factor) + for wind_zone in wind_zone_d: + polygon = Polygon(points=wind_zone) + polygon.color = "black" + polygons.append(polygon) + return reversed(polygons) + + @staticmethod + def __single_tilt_wind_zone_polygons__(buildings, scaling_factor, panel_orientation): + wind_zone_a = DXFHelper.__generate_wind_zone_a_single_tilt__(buildings, scaling_factor, panel_orientation) + wind_zone_a += DXFHelper.__generate_wind_zone_a_fuzzy_single_tilt__(buildings, scaling_factor) + polygons = [] + for wind_zone in wind_zone_a: + polygon = Polygon(points=wind_zone) + polygon.color = "red" + polygons.append(polygon) + wind_zone_i = DXFHelper.__generate_wind_zone_i_single_tilt__(buildings, scaling_factor, panel_orientation) + for wind_zone in wind_zone_i: + polygon = Polygon(points=wind_zone) + polygon.color = "orange" + polygons.append(polygon) + wind_zone_j = DXFHelper.__generate_wind_zone_j_single_tilt__(buildings, scaling_factor, panel_orientation) + for wind_zone in wind_zone_j: + polygon = Polygon(points=wind_zone) + polygon.color = "forestgreen" + polygons.append(polygon) + wind_zone_b = DXFHelper.__generate_wind_zone_b_single_tilt__(buildings, scaling_factor, panel_orientation) + wind_zone_b += DXFHelper.__generate_wind_zone_b_fuzzy_single_tilt__(buildings, scaling_factor) + for wind_zone in wind_zone_b: + polygon = Polygon(points=wind_zone) + polygon.color = "green" + polygons.append(polygon) + wind_zone_h = DXFHelper.__generate_wind_zone_h_single_tilt__(buildings, scaling_factor, panel_orientation) + for wind_zone in wind_zone_h: + polygon = Polygon(points=wind_zone) + polygon.color = "yellow" + polygons.append(polygon) + wind_zone_c = DXFHelper.__generate_wind_zone_c_single_tilt__(buildings, scaling_factor, panel_orientation) + wind_zone_c += DXFHelper.__generate_wind_zone_c_fuzzy_single_tilt__(buildings, scaling_factor) + for wind_zone in wind_zone_c: + polygon = Polygon(points=wind_zone) + polygon.color = "blue" + polygons.append(polygon) + wind_zone_e = DXFHelper.__generate_wind_zone_e_single_tilt__(buildings, scaling_factor, panel_orientation) + for wind_zone in wind_zone_e: + polygon = Polygon(points=wind_zone) + polygon.color = "purple" + polygons.append(polygon) + wind_zone_f = DXFHelper.__generate_wind_zone_f_single_tilt__(buildings, scaling_factor, panel_orientation) + for wind_zone in wind_zone_f: + polygon = Polygon(points=wind_zone) + polygon.color = "grey" + polygons.append(polygon) + wind_zone_d = DXFHelper.__generate_wind_zone_d_single_tilt__(buildings, scaling_factor, panel_orientation) + wind_zone_d += DXFHelper.__generate_wind_zone_d_fuzzy_single_tilt__(buildings, scaling_factor) + for wind_zone in wind_zone_d: + polygon = Polygon(points=wind_zone) + polygon.color = "black" + polygons.append(polygon) + wind_zone_g = DXFHelper.__generate_wind_zone_g_single_tilt__(buildings, scaling_factor, panel_orientation) + for wind_zone in wind_zone_g: + polygon = Polygon(points=wind_zone) + polygon.color = "hotpink" + polygons.append(polygon) + return reversed(polygons) diff --git a/helix/Services/dxf_service.py b/helix/Services/dxf_service.py new file mode 100644 index 0000000..6c3c1a0 --- /dev/null +++ b/helix/Services/dxf_service.py @@ -0,0 +1,94 @@ +import io + +import dxfgrabber + +from helix.constants.file_validation_error import FileValidationMessage +from helix.models.dxf.dxf_error import OldDxfFormatException + + +class DXFService(object): + """ + Takes the contents of a DXF file and creates modules, buildings, panels, + subarrays, and polygons based on the data + """ + + # l_b is expected to be in inches, not feet! + def parse(self, dxf_file_contents, module_constants, system_type, l_b, dxf_helper, subarray_validator): + """Parse will generate the panels, subarrays, buildings + and modules that are present in the dxf file + + Arguments: + dxf_file_contents (str) Content of the uploaded file + """ + + dxf = dxfgrabber.read(io.StringIO(dxf_file_contents, newline=None)) + + buildings, modules = dxf_helper.build_polygons(dxf.entities) + + """ + A new type of aurora format file was added in the project + The visibile difference if you read the file, is that the + new format contains the string buildings. + The old version doesn't + + """ + + pair_spacing = None + dxf_helper.is_new_aurora_format() + if hasattr(module_constants, "spacing_size_inches"): + pair_spacing = module_constants.spacing_size_inches + + if dxf_helper.should_consolidate_modules(modules, system_type, + module_constants): + modules = dxf_helper.consolidate_dual_tilt_modules(modules, + system_type, + pair_spacing) + + translated_buildings, translated_modules = dxf_helper.translate_towards_origin(buildings, modules) + translated_buildings_ccw = dxf_helper.get_polygons_counterclockwise(translated_buildings) + buildings_ccw = dxf_helper.get_polygons_counterclockwise(buildings) + + + + panels = dxf_helper.generate_panels(modules, translated_modules) + node_graph = dxf_helper.build_node_graph(panels, module_constants.panel_spacing) + subarrays = dxf_helper.detect_subarrays(node_graph, panels) + for subarray in subarrays: + subarray_validator.validate_subarray(node_graph, subarray.subarray_number, system_type) + dxf_helper.detect_panel_types(node_graph) + dxf_helper.detect_wind_zones(panels, translated_buildings_ccw, translated_modules, l_b, system_type) + + all_points = [p.points for p in translated_buildings_ccw + translated_modules] + + points = [point for points_list in all_points for point in points_list] + + max_x = max(p[0] for p in points) + max_y = max(p[1] for p in points) + + panel_orientation = panels[0].coordinate.rotation + + return { + 'size': (max_x, max_y), # Used for debugging + 'buildings': buildings_ccw, + 'modules': translated_modules, + 'panels': panels, + 'subarrays': subarrays, + 'lb_polygons': dxf_helper.l_b_polygons(translated_buildings_ccw, l_b, system_type, panel_orientation), + 'is_panel_drawing_inaccurate': self.is_panel_drawing_inaccurate(panels) + } + + def is_panel_drawing_inaccurate(self,panels): + '''True if subarrays are not rotated more than allowed tolerance, false otherwise''' + ROTATION_ACCURACY_DELTA_DEGREES = 0.1 + + if panels is None or panels == []: + return True + + first_panel_rotation = panels[0].coordinate.rotation # rotation is in degrees + + for panel in panels: + difference = abs(first_panel_rotation - panel.coordinate.rotation) + if difference >= ROTATION_ACCURACY_DELTA_DEGREES: + return True + + return False diff --git a/helix/__init__.py b/helix/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/helix/api/__init__.py b/helix/api/__init__.py new file mode 100644 index 0000000..8908269 --- /dev/null +++ b/helix/api/__init__.py @@ -0,0 +1 @@ +__author__ = 'pivotal' diff --git a/helix/api/api.py b/helix/api/api.py new file mode 100644 index 0000000..cbe4b2e --- /dev/null +++ b/helix/api/api.py @@ -0,0 +1,65 @@ +from flask import Blueprint, request, session, jsonify + +from helix.calculators.calculator import Calculator +from helix.presenters.panel_presenter import ProjectPresenter +from helix.session_manager import SessionManager +from helix.constants import redis_constant, sql_constant +from helix.seismic_validator_user_values import SeismicValidatorUserValues +from helix.validators.file_validator import FileValidator +from helix.validators.seismic_anchor_validator import SeismicAnchorValidator + +api = Blueprint('api', __name__, template_folder='templates') + + +@api.route("/panel_data") +def panel_data(): + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + user_values = session_manager.user_values() + calculator = Calculator(user_values) + system_type = user_values.system_type() + module_type = user_values.module_type() + data = ProjectPresenter(system_type, module_type).get_panel_data(calculator.get_computed_csv_columns(), calculator.subarrays) + db_session.close() + return jsonify({'panel_data': data}) + + +@api.route("/update_panel_data", methods=['POST']) +def update_panel_data(): + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + user_seismic_data = request.get_json() + user_values = SeismicValidatorUserValues(session_manager.user_values(), user_seismic_data) + calculator = Calculator(user_values) + validator = SeismicAnchorValidator(calculator) + validation_result = validator.validate(calculator.panels) + + subarrays = calculator.subarray_summary() + subarray_data = [] + for subarray in subarrays: + data = { + "subarray": subarray.subarray_number, + "required_seismic_anchors": subarray.required_seismic_anchors, + "weight": round(subarray.weight), + } + subarray_data.append(data) + + if not validation_result: + session_manager.save_user_provided_seismic_anchors(user_seismic_data) + panel_data = ProjectPresenter(user_values.system_type(), user_values.module_type()).get_panel_data(calculator.get_computed_csv_columns(), + calculator.subarrays) + db_session.close() + return jsonify({ + "status": "success", + "error": None, + "panel_data": panel_data, + "subarray_data": subarray_data + }) + else: + db_session.close() + return jsonify({ + "status": "error", + "error": validation_result.value, + "panel_data": None, + "subarray_data": subarray_data + }) diff --git a/helix/calculated_data_repository.py b/helix/calculated_data_repository.py new file mode 100644 index 0000000..ef0d225 --- /dev/null +++ b/helix/calculated_data_repository.py @@ -0,0 +1,78 @@ +import json +from numpy import array + + +class CalculatedDataRepository(object): + summary_values_key = 'summary_values' + computed_csv_columns_key = 'computed_csv_columns' + compute_bom_key = 'compute_bom' + subarray_summary_key = 'subarray_summary' + + def __init__(self, store, calculator): + self.store = store + self.calculator = calculator + + def reset_data(self): + pass + + def reset_site_data(self): + pass + + def reset_panel_data(self): + pass + + def reset_subarray_data(self): + self.store.delete(self.summary_values_key) + self.store.delete(self.subarray_summary_key) + + def reset_ebom_data(self): + self.store.delete(self.compute_bom_key) + + def k_z(self): + return self.calculator.k_z() + + def L_B(self): + return self.calculator.L_B() + + def summary_table(self): + return self.calculator.summary_table() + + def minimum_array_sizes(self): + return self.calculator.minimum_array_sizes() + + def summary_values(self): + key = self.summary_values_key + if self.store.exists(key): + summary_values_json = self.store.get(key).decode('utf-8)') + return array(json.loads(summary_values_json)) + summary_values = self.calculator.summary_values() + self.store.set(key, json.dumps(list(summary_values))) + return summary_values + + def get_computed_csv_columns(self): + return self.calculator.get_computed_csv_columns() # TODO: revisit after getting rid of DataMatrix + # key = self.computed_csv_columns_key + # if self.store.exists(key): + # computed_csv_columns_json = self.store.get(key).decode('utf-8') + # return jsonpickle.decode(computed_csv_columns_json) + # computed_csv_columns = self.calculator.get_computed_csv_columns() + # self.store.set(key, jsonpickle.encode(computed_csv_columns)) + # return computed_csv_columns + + def compute_bom(self): + key = self.compute_bom_key + if self.store.exists(key): + compute_bom_json = self.store.get(key).decode('utf-8') + return array(json.loads(compute_bom_json)) + computed_bom = self.calculator.compute_bom() + self.store.set(key, json.dumps(computed_bom.tolist())) + return computed_bom + + def subarray_summary(self): + key = self.subarray_summary_key + if self.store.exists(key): + received_json = self.store.get(key).decode('utf-8') + return array(json.loads(received_json)) + data = self.calculator.subarray_summary() + self.store.set(key, json.dumps(data.tolist())) + return data diff --git a/helix/calculators/__init__.py b/helix/calculators/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/helix/calculators/ballast_calculator.py b/helix/calculators/ballast_calculator.py new file mode 100644 index 0000000..83c6348 --- /dev/null +++ b/helix/calculators/ballast_calculator.py @@ -0,0 +1,248 @@ +from collections import namedtuple, OrderedDict +from math import ceil, floor + +from helix.constants.panel_type import PanelType +from helix.constants.system_type import SystemType +from helix.models.panel import Panel, PanelWarnings + +Result = namedtuple('Result', ['ballast_count', 'link_tray_count', 'cross_tray_count', 'system_weight', 'needs_anchor']) + + +class BallastCalculator(object): + def __init__(self, user_values): + self.values = user_values + self.system_type = user_values.system_type() + self.anchor_type = user_values.anchor_type() + self.system_constants = self.system_type.system_constants() + self.module_constants = user_values.module_system_constants() + + def ballast_and_trays_matrix(self, c_p_matrix, q_z, panels, ballast_block_weight=None): + if not ballast_block_weight: + ballast_block_weight = self.values.ballast_block_weight() + ballast_store = self.calculate_ballast_store(c_p_matrix, q_z, ballast_block_weight) + + for idx, panel in enumerate(panels): + stored_panel = ballast_store[panel.panel_type][panel.wind_zone][panel.fuzzy_wind_zone] + panels[idx] = stored_panel.merge(panel) + return panels + + def update_ballast(self, c_p_matrix, q_z, panels): + ballast_block_weight = self.values.ballast_block_weight() + ballast_store = self.calculate_ballast_store(c_p_matrix, q_z, ballast_block_weight) + + seismic_ballast_store = {} + + for panel in panels: + seismic_anchors = panel.seismic_anchors if panel.seismic_anchors else 0 + if seismic_anchors != 0: + key = hash(panel.wind_zone) + hash(panel.panel_type) + seismic_anchors + hash(panel.fuzzy_wind_zone) # hack + stored_panel = seismic_ballast_store.get(key) + if stored_panel: + panel.ballast = stored_panel.ballast + panel.link_tray = stored_panel.link_tray + panel.cross_tray = stored_panel.cross_tray + panel.pressure = stored_panel.pressure + else: + anchors = panel.wind_anchors + seismic_anchors + c_p = c_p_matrix[panel.wind_zone, panel.panel_type.index()] * (1.15 if panel.fuzzy_wind_zone else 1) + force = self.uplift(c_p, q_z) - anchors * self.anchor_type.uplift_capacity() + ballast_and_tray_count = self.ballast_and_tray_count(force, panel.panel_type, ballast_block_weight, anchors) + pressure = self.calculate_pressure_on_roof(ballast_and_tray_count.ballast_count, ballast_block_weight, ballast_and_tray_count.system_weight) + + panel.ballast = ballast_and_tray_count.ballast_count + panel.link_tray = ballast_and_tray_count.link_tray_count + panel.cross_tray = ballast_and_tray_count.cross_tray_count + panel.pressure = pressure + + seismic_ballast_store[key] = panel + else: + stored_panel = ballast_store[panel.panel_type][panel.wind_zone][panel.fuzzy_wind_zone] + + panel.ballast = stored_panel.ballast + panel.link_tray = stored_panel.link_tray + panel.cross_tray = stored_panel.cross_tray + panel.pressure = stored_panel.pressure + return panels + + def calculate_ballast_store(self, cp_matrix, qz, ballast_block_weight): + max_psf = self.values.max_system_pressure() + store = {} + for panel_type in PanelType.all(): + sub_store = {} + for wind_zone, _ in enumerate(self.values.system_type().system_constants().wind_zones): + sub_store[wind_zone] = {} + for use_fuzzy in (True, False): + sub_store[wind_zone][use_fuzzy] = self.ballast_tray_and_anchor_count(wind_zone=wind_zone, + panel_type=panel_type, + ballast_block_weight=ballast_block_weight, + max_system_pressure=max_psf, + c_p_matrix=cp_matrix, + q_z=qz, + use_fuzzy=use_fuzzy) + store[panel_type] = sub_store + + return store + + def summary_table(self, c_p_matrix, q_z): + wind_zones = self.system_constants.wind_zones + ballast_block_weight = self.values.ballast_block_weight() + max_system_pressure = self.values.max_system_pressure() + + table = OrderedDict() + for panel_type in PanelType.all(): + ballast_counts = [] + anchor_counts = [] + pressures = [] + warnings = [] + for wind_zone_index, _ in enumerate(wind_zones): + ballast_tray_anchor_panels = self.ballast_tray_and_anchor_count(wind_zone_index, panel_type, + ballast_block_weight, max_system_pressure, + c_p_matrix, q_z) + anchor_count = ballast_tray_anchor_panels.wind_anchors + ballast_count = ballast_tray_anchor_panels.ballast + pressure = ballast_tray_anchor_panels.pressure + warning = ballast_tray_anchor_panels.warnings + + pressure_as_string = "{0:.2f}".format(pressure) + # Because pressure is stored as a floating point number, it is possible, because floats + # for pressure to be something like 5.02999999999999. Which is clearly meant to be 5.03. + # This represents that as a string, which doesn't have that issue. + + anchor_counts.append(anchor_count) + ballast_counts.append(ballast_count) + pressures.append(pressure_as_string) + warnings.append(warning) + + table[panel_type] = { + 'ballast blocks': ballast_counts, + 'anchors': anchor_counts, + 'pressure': pressures, + 'warnings': warnings + } + return table + + def ballast_tray_and_anchor_count(self, wind_zone, panel_type, ballast_block_weight, max_system_pressure, + c_p_matrix, q_z, use_fuzzy=False): + fuzzy_factor = 1.15 if use_fuzzy else 1 + c_p = c_p_matrix[wind_zone, panel_type.index()] * fuzzy_factor + uplift_force = self.uplift(c_p, q_z) + + warnings = [] + keep_trying = True + anchor_count = 0 + pressure = 0. + tries = 0 + ballast_and_tray_count = None + while keep_trying: + remainder_force = uplift_force - anchor_count * self.anchor_type.uplift_capacity() + ballast_and_tray_count = self.ballast_and_tray_count(remainder_force, panel_type, ballast_block_weight, anchor_count) + pressure = self.calculate_pressure_on_roof(ballast_and_tray_count.ballast_count, ballast_block_weight, ballast_and_tray_count.system_weight) + keep_trying = (ballast_and_tray_count.needs_anchor or pressure > max_system_pressure) and ballast_and_tray_count.ballast_count > 0 + if keep_trying: + anchor_count = self.calculate_anchors(panel_type, uplift_force) + tries + tries += 1 + keep_trying &= tries < 100 + + if uplift_force / self.module_constants.surface_area >= self.module_constants.max_psf: + warnings.append(PanelWarnings.MaxPsf) + + return Panel(wind_zone=wind_zone, + panel_type=panel_type, + ballast=ballast_and_tray_count.ballast_count, + link_tray=self.interpret_tray_count(ballast_and_tray_count.link_tray_count, panel_type), + cross_tray=ballast_and_tray_count.cross_tray_count, + wind_anchors=anchor_count, + pressure=pressure, + fuzzy_wind_zone=use_fuzzy, + warnings=warnings) + + def ballast_and_tray_count(self, force_to_resist, panel_type, ballast_block_weight, anchor_count): + system_weight = self.module_constants.base_weight(panel_type, 0) + ballast_count = self.calculate_ballast(force_to_resist, system_weight, ballast_block_weight) + + link_tray_count = 0 + cross_tray_count = 0 + needs_anchor = False + + keep_trying = True + tries = 0 + + while keep_trying and tries < 3: + tries += 1 + if ballast_count: + new_link_tray_count, _ = self.calculate_trays(ballast_count + 2 * anchor_count, + self.module_constants.link_tray_thresholds(panel_type)) + + # Recalculate weight given new link trays; recalculate ballast given new weight + system_weight = self.module_constants.base_weight(panel_type, new_link_tray_count + cross_tray_count) + ballast_count = self.calculate_ballast(force_to_resist, system_weight, ballast_block_weight) + + new_cross_tray_count, needs_anchor = self.calculate_trays(ballast_count + 2 * anchor_count, + self.module_constants.cross_tray_thresholds( + panel_type)) + system_weight = self.module_constants.base_weight(panel_type, new_cross_tray_count + new_link_tray_count) + ballast_count = self.calculate_ballast(force_to_resist, system_weight, ballast_block_weight) + + if link_tray_count == new_link_tray_count and cross_tray_count == new_cross_tray_count: + keep_trying = False + link_tray_count = new_link_tray_count + cross_tray_count = new_cross_tray_count + else: + keep_trying = False + + return Result(ballast_count, link_tray_count=link_tray_count, cross_tray_count=cross_tray_count, + system_weight=system_weight, needs_anchor=needs_anchor) + + def uplift(self, c_p, q_z): + return q_z * self.module_constants.surface_area * c_p + + def calculate_ballast(self, uplift, non_ballast_weight, ballast_block_weight): + if non_ballast_weight > uplift: + return 0 + + return ceil((uplift - non_ballast_weight) / ballast_block_weight) + + def calculate_trays(self, ballast_count, thresholds): + for idx, threshold in enumerate(thresholds): + if ballast_count <= threshold: + return idx, False + return len(thresholds) - 1, True + + def calculate_pressure_on_roof(self, ballast_count, ballast_block_weight, non_ballast_weight): + effective_area = self.module_constants.surface_area / self.module_constants.ground_coverage_ratio + return (ballast_count * ballast_block_weight + non_ballast_weight) / effective_area + + def interpret_tray_count(self, link_tray_count, panel_type): + if self.system_type == SystemType.singleTilt: + if panel_type == PanelType.EastWest: + return 2 + elif panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth: + return 0 + return link_tray_count or 0 + + def calculate_anchors(self, panel_type, uplift): + base_system_weight = self.module_constants.base_weight(panel_type, 0) + anchor_capacity = self.anchor_type.uplift_capacity() + return max(floor((uplift - base_system_weight) / anchor_capacity), 1) + + def show_presented_link_trays(self, panels): + for panel in panels: + panel.presented_link_tray = self.present_link_tray(panel.link_tray, panel.panel_type) + return panels + + def present_link_tray(self, link_tray_count, panel_type): + if self.system_type == SystemType.singleTilt: + link_tray_representation = { + PanelType.Corner: 0, + PanelType.NorthSouth: 0, + PanelType.EastWest: 2, + PanelType.Middle: min(1, int(link_tray_count)), + }[panel_type] + else: + link_tray_representation = { + PanelType.Corner: 2, + PanelType.NorthSouth: 2, + PanelType.EastWest: 1, + PanelType.Middle: min(1, int(link_tray_count)), + }[panel_type] + return link_tray_representation diff --git a/helix/calculators/bom_calculator.py b/helix/calculators/bom_calculator.py new file mode 100644 index 0000000..7102180 --- /dev/null +++ b/helix/calculators/bom_calculator.py @@ -0,0 +1,58 @@ +from numpy import array, ceil + +from helix.calculators.bom_helper import add_parts_to_list, apply_package_size_rounding +from helix.calculators.ebom_calculator import EbomCalculator +from helix.calculators.mechanical_bom_calculator import MechanicalBomCalculator +from helix.constants.parts import * + + +class BomCalculator(object): + def __init__(self, values, panels, subarrays, graph_repository): + self.values = values + self.panels = panels + self.subarrays = subarrays + self.graph_repository = graph_repository + + def compute_bom(self): + output_array = [] + for part, quantity in self.parts_list().items(): + if ceil(quantity) <= 0: + continue + row = list(part) + row.append(int(ceil(quantity))) + output_array.append(row) + + output_array.sort(key=lambda x: x[0] + x[1]) + return array(output_array) + + def documentation_bom(self): + parts_list = self.parts_list() + + for part in all_parts: + if part not in parts_list.keys(): + parts_list[part] = 0 + + output_array = [] + for part, quantity in parts_list.items(): + if part == ballast: + row = 'ballast' + elif part == anchor: + row = 'anchors' + elif part == module: + row = 'modules' + else: + row = part[0] + quantity = max(0, quantity) + output_array.append((row, int(ceil(quantity)))) + return output_array + + def parts_list(self): + row_count = sum(subarray.row_count for subarray in self.subarrays) + column_count = sum(subarray.column_count for subarray in self.subarrays) + parts_list = MechanicalBomCalculator(self.values, self.panels, self.subarrays).mechanical_bom() + ebom_parts_list = EbomCalculator(self.values, ceil(row_count), ceil(column_count), parts_list.get(module)).compute_ebom() + + add_parts_to_list(parts_list, ebom_parts_list) + + apply_package_size_rounding(parts_list, package_sizes) + return parts_list diff --git a/helix/calculators/bom_helper.py b/helix/calculators/bom_helper.py new file mode 100644 index 0000000..115838a --- /dev/null +++ b/helix/calculators/bom_helper.py @@ -0,0 +1,36 @@ +from math import ceil + +from helix.constants.panel_type import PanelType + + +def add_parts_to_list(parts_list, parts_to_add, multiplier=1): + for part, quantity in parts_to_add.items(): + previous_value = parts_list.get(part) or 0 + if quantity != 0: + parts_list[part] = previous_value + quantity * multiplier + + +def apply_fudge_factors(parts_list, fudge_factors): + for part, quantity in parts_list.items(): + fudge_factor = fudge_factors.get(part) or 1.0 + parts_list[part] = quantity * fudge_factor + + +def apply_package_size_rounding(parts_list, package_sizes): + for part, quantity in parts_list.items(): + package_size = package_sizes.get(part) or 1 + parts_list[part] = package_size * ceil(quantity / package_size) + + +def get_panel_type_counts(panels): + panel_type_counts = { + PanelType.Corner: 0, + PanelType.NorthSouth: 0, + PanelType.EastWest: 0, + PanelType.Middle: 0, + } + + for panel in panels: + panel_type_counts[panel.panel_type] += 1 + + return panel_type_counts diff --git a/helix/calculators/calculator.py b/helix/calculators/calculator.py new file mode 100644 index 0000000..dc722fe --- /dev/null +++ b/helix/calculators/calculator.py @@ -0,0 +1,169 @@ +from math import ceil, floor +import copy +from helix.Repositories.graph_repository import GraphRepository +from helix.calculators.ballast_calculator import BallastCalculator +from helix.calculators.bom_calculator import BomCalculator +from helix.calculators.coordinates_calculator import CoordinatesCalculator +from helix.calculators.pressure_coefficient_calculator import PressureCoefficientCalculator +from helix.calculators.seismic_calculator import SeismicCalculator +from helix.calculators.subarray_helper import get_subarray_sizes_and_rows, extract_subarray +from helix.calculators.summary_values_calculator import SummaryValuesCalculator +from helix.calculators.wind_pressure_calculator import WindPressureCalculator + + +class Calculator(object): + def __init__(self, user_values, calculate_panel_data=True): + self.values = user_values + self._q_z = None + self._c_p_matrix = None + self._L_B = None + self._K_z = None + self.subarrays = None + self.buildings = self.values.buildings_polygons() + self.buildings_for_drawing = [] + + self.panels = self.values.csv() + if calculate_panel_data and self.panels is not None: + for idx, panel in enumerate(self.panels): + panel.id = idx + 1 + self.panels.sort(key=lambda x: x.subarray) + self.subarrays = get_subarray_sizes_and_rows(self.panels) + + self.__compute_ballast() + _,_,self.buildings_for_drawing = self.__transform_coordinates() + self.graph_repository = GraphRepository(self.panels, self.subarrays, self.values.system_type()) + if self.values.user_override_seismic_anchors(): + user_provided_panels = self.values.get_user_provided_seismic_anchors() + for user_panel in user_provided_panels: + panel = [panel for panel in self.panels if panel.id == user_panel.id][0] + panel.seismic_anchors = user_panel.seismic_anchors + self.__compute_seismic_anchors(self.panels) # Update subarrays to include required seismic anchors + self.__update_ballast(self.panels) + else: + # Update subarrays *and panels* to include required seismic anchors + self.panels = self.__compute_seismic_anchors(self.panels) + + def k_z(self): + if self._K_z is None: + self._K_z = WindPressureCalculator(self.values).K_z() + return self._K_z + + def L_B(self): + if self._L_B is None: + self._L_B = PressureCoefficientCalculator(self.values).L_B() + return self._L_B + + def summary_table(self): + return BallastCalculator(self.values).summary_table(self.__c_p_matrix(), self.q_z()) + + def minimum_array_sizes(self): + return PressureCoefficientCalculator(self.values).minimum_array_size(self.L_B()) + + # Used in the array summary page - is the table of weight, psf, anchors, ballast, etc. for the entire system + def summary_values(self): + seismic_anchors = self.subarray_summary() + ballast_calculator = BallastCalculator(self.values) + seismic_interval = SeismicCalculator(self.values, self.graph_repository).seismic_anchor_interval() + + return SummaryValuesCalculator(self.values).summary_values(self.panels, seismic_anchors, self.__c_p_matrix(), + self.q_z(), seismic_interval, ballast_calculator) + + def documentation_summary_values(self): + seismic_anchors = self.subarray_summary() + ballast_calculator = BallastCalculator(self.values) + seismic_interval = SeismicCalculator(self.values, self.graph_repository).seismic_anchor_interval() + + return SummaryValuesCalculator(self.values).documentation_summary_values(self.panels, seismic_anchors, self.__c_p_matrix(), + self.q_z(), seismic_interval, ballast_calculator) + + # used in the array visualization - is parsed into json and displayed using the fancy canvas + def get_computed_csv_columns(self): + return BallastCalculator(self.values).show_presented_link_trays(self.panels) + + def compute_bom(self): + required_seismic_anchors = self.subarray_summary() + return BomCalculator(self.values, self.panels, required_seismic_anchors, self.graph_repository).compute_bom() + + def documentation_bom(self): + required_seismic_anchors = self.subarray_summary() + return BomCalculator(self.values, self.panels, required_seismic_anchors, self.graph_repository).documentation_bom() + + # used in the array summary page - is part of the fancy scrolling table of summing up each subarray + def subarray_summary(self): + summary_values_calculator = SummaryValuesCalculator(self.values) + for subarray in self.subarrays: + panels_for_subarray = extract_subarray(self.panels, subarray.subarray_number) + weight, _ = summary_values_calculator.system_weight_and_pressure(panels_for_subarray) + subarray.weight = weight + + return self.subarrays + + def q_z(self): + if self._q_z is None: + self._q_z = WindPressureCalculator(self.values).q_z(self.k_z()) + return self._q_z + + def __c_p_matrix(self): + if self._c_p_matrix is None: + self._c_p_matrix = PressureCoefficientCalculator(self.values).c_p_matrix(self.L_B()) + return self._c_p_matrix + + def __compute_seismic_anchors(self, panels): + panels = copy.deepcopy(panels) + seismic_calculator = SeismicCalculator(self.values, self.graph_repository) + for subarray in self.subarrays: + if subarray.required_seismic_anchors is None: + subarray.required_seismic_anchors = 0 + panels = self.__seismic_anchors_for_subarray(panels, subarray, seismic_calculator) + return panels + + def __seismic_anchors_for_subarray(self, panels, subarray, seismic_calculator): + # do first estimation to obtain upper bound + required_seismic = seismic_calculator.required_force_seismic_anchors(subarray.subarray_number, panels) + + test_value = required_seismic + tried_acceptable_values = [] + + def assign_seismic_anchors(count): + subarray.required_seismic_anchors = count + seismic_calculator.assign_seismic_anchors(subarray, panels) + self.__update_ballast(panels) + + assigned = sum([panel.seismic_anchors for panel in panels if panel.seismic_anchors is not None]) + + return assigned + + step = max(1, test_value // 2) + + while True: + seismic_anchors_assigned = assign_seismic_anchors(test_value) + + provided_force = seismic_calculator.compute_provided_lateral_capacity(subarray.subarray_number, panels) + required_seismic_force = seismic_calculator.required_force_seismic_demand(subarray.subarray_number, panels) + + if seismic_anchors_assigned == 0: + # anchors were not assigned propably because self.graph_repository.subarray_graph(subarray.subarray_number) is empty + # which may be because of test construction + return panels + + if provided_force < required_seismic_force: + test_value += step + else: + if (test_value in tried_acceptable_values) or (required_seismic_force == 0): + return panels + + tried_acceptable_values.append(test_value) + test_value = max(0, test_value - step) + + step = max(1, step // 2) + + def __transform_coordinates(self): + return CoordinatesCalculator(self.values).transform_coordinates(self.panels, self.subarrays, self.buildings) + + def __compute_ballast(self): + ballast_calculator = BallastCalculator(self.values) + changed_panels = ballast_calculator.ballast_and_trays_matrix(self.__c_p_matrix(), self.q_z(), self.panels) + self.panels = [panel.merge(changed_panels[idx]) for idx, panel in enumerate(self.panels)] + + def __update_ballast(self, panels): + BallastCalculator(self.values).update_ballast(self.__c_p_matrix(), self.q_z(), panels) diff --git a/helix/calculators/coordinates_calculator.py b/helix/calculators/coordinates_calculator.py new file mode 100644 index 0000000..4d4352d --- /dev/null +++ b/helix/calculators/coordinates_calculator.py @@ -0,0 +1,104 @@ +from numpy import array, math, dot, vectorize + +from helix.calculators.subarray_helper import extract_subarray +from helix.models.coordinate import Coordinate + + +class CoordinatesCalculator(object): + def __init__(self, values): + self.values = values + + + def transform_coordinates(self, panels, subarrays, buildings): + """ + Scales, rotates, and translates the coordinates so that they're all + in inches, and in positive unit space. + Coordinates are rounded to whole values (used in drawing on the + array_summary page + + Parameters: + panels (obj): List of panels + subarrays (obj): List of subarrays + buildings (obj): List of lists of building polygons + + Returns: + tupple + + """ + + rotate_all = vectorize(self.rotate) + scale_all = vectorize(self.scale) + round_all = vectorize(round) + neg_translate_all = vectorize(self.neg_translate) + origins = [] + + + first_subarray_rotation = None + + for subarray in subarrays: + begin, size = (subarray.start_row, subarray.size) + extracted_panels = extract_subarray(panels, subarray.subarray_number) + + raw_coordinates = [panel.coordinate for panel in extracted_panels] + + if first_subarray_rotation is None: + first_subarray_rotation = raw_coordinates[0].rotation + + rotated_coordinates = rotate_all(raw_coordinates) + scaled_coordinates = scale_all(rotated_coordinates) + + origin = self.find_origin(scaled_coordinates) + rounded_coordinates = round_all(scaled_coordinates - origin) + + for idx, val in enumerate(rounded_coordinates): + panels[begin + idx].coordinate = val + origins.append(origin) + + + prepared_buildings = self.prepare_buildings(buildings, first_subarray_rotation) + rotated_buildings = list(map(lambda building: rotate_all(building), prepared_buildings )) + scaled_buildings = list(map(lambda building: scale_all(building), rotated_buildings)) + + all_building_coordinates = [point for sublist in scaled_buildings for point in sublist] + global_origin = self.find_origin(all_building_coordinates + origins) + origins = array(origins) - global_origin + + for idx, origin in enumerate(origins): + subarrays[idx].origin = origin + + translated_buildings = list(map(lambda building: neg_translate_all(building, global_origin), scaled_buildings)) + # rounded_buildings = list(map(lambda building: round_all(building), translated_buildings)) + + return panels, subarrays, translated_buildings + + def rotate(self, coordinate): + rotation = math.radians(coordinate.rotation) + rotation_matrix = array([[math.cos(rotation), -math.sin(rotation)], + [math.sin(rotation), math.cos(rotation)]]) + vector = (coordinate.x, coordinate.y) + rotated_vector = dot(vector, rotation_matrix) + return Coordinate(rotated_vector[0], rotated_vector[1]) + + def scale(self, coordinate): + constants = self.values.module_system_constants() + panel_x, panel_y = constants.panel_spacing + return coordinate.scale(1. / panel_x, 1. / panel_y) + + def neg_translate(self, coordinate, other): + return coordinate.neg_translate(other) + + def find_origin(self, coordinates): + if coordinates == []: + return Coordinate(0,0) + min_x = min(list(map(lambda x: x.x, coordinates))) + min_y = min(list(map(lambda x: x.y, coordinates))) + return Coordinate(min_x, min_y) + + def prepare_buildings(self, buildings, rotation): + return list(map(lambda building: self.prepare_single_building(building, rotation), buildings)) + + def prepare_single_building(self, building_array, rotation): + return list(map(lambda point: Coordinate(point[0],point[1],rotation), building_array)) + + + diff --git a/helix/calculators/ebom_calculator.py b/helix/calculators/ebom_calculator.py new file mode 100644 index 0000000..316a55d --- /dev/null +++ b/helix/calculators/ebom_calculator.py @@ -0,0 +1,133 @@ +from math import ceil + +from helix.calculators.bom_helper import add_parts_to_list +from helix.constants import ebom_parts +from helix.constants.ebom_parts import * +from helix.constants.parts import wire_clip_large, cable_support, cable_support_lid, channel_nut, sunshade +from helix.constants.system_type import SystemType + + +class EbomCalculator(object): + def __init__(self, user_values, row_count, column_count, modules_count = None): + self.values = user_values + self.row_count = row_count + self.column_count = column_count + self.modules_count = modules_count + + def resolve_power_monitor_type(self): + module_type = self.values.module_type() + + thresholds = { + ModuleType.Cell96: 306, + ModuleType.Cell128: 230, + ModuleType.PSeries: 286 + } + + if (not self.modules_count) or self.modules_count >= thresholds[module_type]: + return monitor_controller_480_v + else: + return monitor_controller_240_v + + def compute_ebom(self): + part_list = {} + + power_stations = self.values.power_stations() + standalone_inverters = self.values.standalone_inverters() + monitors = self.values.power_monitors() + module_type = self.values.module_type() + system_type = self.values.system_type() + inverter_count = 0 + total_ac_run_length = 0 + panel_board_counts = [0, 0] + proper_monitor_controller = self.resolve_power_monitor_type() + + for power_station in power_stations: + power_station_count = power_station['power_station_quantity'] + total_ac_run_length += power_station['ac_run_length'] + inverter_quantity = power_station['inverter_quantity'] + self.get_standalone_inverters(power_station) + if inverter_quantity <= 2: + panel_board_counts[0] += power_station_count + else: + panel_board_counts[1] += power_station_count + + if self.power_station_has_monitor(power_station, monitors): + panel_board_parts_to_use = panel_board_parts_with_monitor(inverter_quantity, proper_monitor_controller) + else: + panel_board_parts_to_use = panel_board_parts(inverter_quantity, with_aux=False) + add_parts_to_list(part_list, panel_board_parts_to_use, power_station_count) + add_parts_to_list(part_list, shared_panel_board_parts(module_type, system_type), power_station_count) + add_parts_to_list(part_list, {channel_nut: 4}, power_station_count) + + for inverter in power_station['inverters']: + inverter_count += power_station_count + self.add_parts_for_inverter(part_list, inverter, power_station_count) + add_parts_to_list(part_list, inverter_parts(inverter, module_type), power_station_count) + + for inverter in standalone_inverters: + inverter_count += 1 + total_ac_run_length += inverter['ac_run_length'] + self.add_parts_for_inverter(part_list, inverter) + add_parts_to_list(part_list, standalone_inverter_parts(inverter, system_type, module_type), 1) + add_parts_to_list(part_list, inverter_parts(inverter, module_type), 1) + if inverter['attachment_point'][1]: + add_parts_to_list(part_list, standalone_inverter_attached_to_panel_board_parts, 1) + + for monitor in monitors: + if monitor['power_source'][0] == 'Switch Gear/External': + add_parts_to_list(part_list, {proper_monitor_controller: 1}, 1) + + add_parts_to_list(part_list, {wire_clip_large: inverter_count}, self.row_count) + + add_parts_to_list(part_list, {stump: 1}, ceil(total_ac_run_length / 4.0)) + cable_supports = self.calculate_cable_supports(panel_board_counts, len(standalone_inverters)) + add_parts_to_list(part_list, {cable_support: 1, cable_support_lid: 1}, cable_supports) + add_parts_to_list(part_list, {rear_skirt: -1}, ceil(cable_supports*.38)) + + dependent_part_list = {} + for part, quantity in part_list.items(): + dependent_parts = ebom_parts.dependent_parts(module_type, system_type).get(part) + if dependent_parts: + add_parts_to_list(dependent_part_list, dependent_parts, quantity) + + add_parts_to_list(part_list, dependent_part_list) + + return part_list + + def add_parts_for_inverter(self, part_list, inverter, multiplier=1): + strings_per_inverter = inverter_strings_parts.get(inverter['strings_per_inverter'], {}) + add_parts_to_list(part_list, inverter_model_parts[inverter['model']], multiplier) + add_parts_to_list(part_list, strings_per_inverter, multiplier) + if inverter['sunshade']: + add_parts_to_list(part_list, {sunshade: 1, sunshade_bolt: 2, sunshade_washer: 2}, multiplier) + if inverter['dc_switch']: + add_parts_to_list(part_list, dc_switch_parts, multiplier) + + def calculate_cable_supports(self, panel_board_counts, standalone_inverter_count): + if sum(panel_board_counts) == 0: + return 0 + + if self.values.system_type() == SystemType.dualTilt: + dimension1 = self.column_count + dimension2 = self.row_count + else: + dimension1 = self.row_count + dimension2 = self.column_count + + result = (standalone_inverter_count + panel_board_counts[0] + (2 * panel_board_counts[1])) / sum(panel_board_counts) + result *= dimension1 * max(dimension1 / dimension2, 1) + result += dimension1 + return ceil(result) + + def get_standalone_inverters(self, power_station): + standalone_inverters = self.values.standalone_inverters() + count = 0 + for inverter in standalone_inverters: + if inverter['attachment_point'][1] == power_station['power_station_id']: + count += 1 + return count + + def power_station_has_monitor(self, power_station, monitors): + for monitor in monitors: + if monitor['power_source'][1] == power_station['power_station_id']: + return True + return False diff --git a/helix/calculators/mechanical_bom_calculator.py b/helix/calculators/mechanical_bom_calculator.py new file mode 100644 index 0000000..c4315d8 --- /dev/null +++ b/helix/calculators/mechanical_bom_calculator.py @@ -0,0 +1,148 @@ +from math import ceil, floor + +from helix.calculators.bom_helper import add_parts_to_list, apply_fudge_factors, \ + get_panel_type_counts +from helix.calculators.subarray_helper import extract_subarray +from helix.constants.module_type import ModuleType +from helix.constants.panel_type import PanelType +from helix.constants.parts import link_tray, cross_tray, ballast, cross_tray_1_1, leading_tray +from helix.constants.system_type import SystemType + + +class MechanicalBomCalculator(object): + def __init__(self, values, panels, subarrays): + self.values = values + self.panels = panels + self.subarrays = subarrays + + def mechanical_bom(self): + module_type = self.values.module_type() + system_type = self.values.system_type() + system_parts = system_type.parts(module_type) + combined_parts_list = {} + + for subarray in self.subarrays: + panels = extract_subarray(self.panels, subarray.subarray_number) + + ballast_count = sum(panel.ballast for panel in panels) + cross_count = sum(panel.cross_tray for panel in panels) + assigned_seismic_anchors_count = sum(panel.seismic_anchors for panel in panels) + required_seismic_anchors_count = subarray.required_seismic_anchors + seismic_anchors_count = max(assigned_seismic_anchors_count, required_seismic_anchors_count) + required_wind_anchors_count = sum(panel.wind_anchors for panel in panels) + anchor_count = required_wind_anchors_count + seismic_anchors_count + panel_type_counts = get_panel_type_counts(panels) + + subarray_parts_list = {} + + for index, panel_type_parts in enumerate(system_parts.parts_per_panel_type()): + add_parts_to_list(subarray_parts_list, panel_type_parts, panel_type_counts[PanelType.from_index(index)]) + + add_parts_to_list(subarray_parts_list, self.values.anchor_type().parts().parts, anchor_count) + + link_count = self.link_count(panel_type_counts, panels, subarray) + + add_parts_to_list(subarray_parts_list, {link_tray: 1}, link_count) + cross_tray_parts = cross_tray if self.values.module_type() == ModuleType.Cell96 else cross_tray_1_1 + add_parts_to_list(subarray_parts_list, {cross_tray_parts: 1}, cross_count) + add_parts_to_list(subarray_parts_list, {ballast: 1}, ballast_count) + + add_parts_to_list(subarray_parts_list, system_parts.row_parts(module_type), subarray.row_count) + add_parts_to_list(subarray_parts_list, system_parts.column_parts(module_type), subarray.column_count) + + add_parts_to_list(subarray_parts_list, system_parts.sub_array_parts, 1) + + apply_fudge_factors(subarray_parts_list, system_parts.fudge_factors(not subarray.row_counted_geometrically)) + + dependent_parts_list = {} + for part, quantity in subarray_parts_list.items(): + dependent_parts = system_parts.dependent_parts(module_type).get(part) + if dependent_parts: + add_parts_to_list(dependent_parts_list, dependent_parts, quantity) + + subarray_parts_list.update(dependent_parts_list) + add_parts_to_list(combined_parts_list, subarray_parts_list) + + return combined_parts_list + + def link_count(self, panel_type_counts, panels, subarray): + if self.values.system_type() == SystemType.dualTilt: + + # check if info about position of panels is available + coordinates_available = all(p.coordinate for p in panels) \ + and not all(p.coordinate.x == 0 and p.coordinate.y == 0 for p in panels) + + if coordinates_available: + # initially every C, NS, EW panels has 2 link trays attached + panel_types = [PanelType.Corner, PanelType.NorthSouth, PanelType.EastWest] + layout = dict([((p.coordinate.x, p.coordinate.y), 2) for p in panels if p.panel_type in panel_types]) + + row_count = ceil(subarray.row_count) + column_count = ceil(subarray.column_count) + + # reduce number of link trays between every two vertically adjoining panels + for y in range(row_count): + for x in range(column_count): + + if (x, y) not in layout: + continue + + if (x, y + 1) in layout: + layout[(x, y + 1)] = 1 + + # count link trays located on perimeter + link_count = sum([layout[p] for p in layout]) + + # subtract places reserved for leading trays + link_count -= subarray.row_count + 1 + + # add link trays for panels of type Middle + link_count += sum([1 for panel in panels if panel.link_tray != 0 and panel.panel_type == PanelType.Middle]) + + return max(link_count, 0) + else: + total_possible_link_trays = len(panels) + subarray.column_count + link_count = total_possible_link_trays + + for panel in panels: + if panel.link_tray == 0 and panel.panel_type == PanelType.Middle: + link_count -= 1 + + link_count -= floor(subarray.row_count) + + return link_count + else: + return sum([self.compute_link_count_single_tilt(panel_type, panel_type_counts, panels) for panel_type in PanelType.all()]) + + def compute_link_count_single_tilt(self, panel_type, panel_type_counts, panels): + if panel_type == PanelType.Corner: + return 0 + elif panel_type == PanelType.NorthSouth: + return 0 + elif panel_type == PanelType.EastWest: + return panel_type_counts[panel_type] * 2 + elif panel_type == PanelType.Middle: + return self.get_panel_type_middle_link_trays_single_tilt(panels) + else: + return 0 + + def get_panel_type_middle_link_trays_single_tilt(self, panels): + wind_zones = self.values.system_type().system_constants().wind_zones + + middle_panels_per_wind_zone = [0 for _ in wind_zones] + middle_link_trays_per_wind_zone = [0 for _ in wind_zones] + + for panel in panels: + if panel.panel_type != PanelType.Middle: + continue + + wind_zone = panel.wind_zone + middle_panels_per_wind_zone[wind_zone] += 1 + middle_link_trays_per_wind_zone[wind_zone] += panel.link_tray # use calculated number of link trays to see if it is non-zero + + total_link_trays_required = 0 + for wind_zone, panel_count in enumerate(middle_panels_per_wind_zone): + if middle_link_trays_per_wind_zone[wind_zone] > 0: # if any middle panels in this wind zone need link trays + total_link_trays_required += ceil(panel_count * 1.05) + return total_link_trays_required + diff --git a/helix/calculators/pressure_coefficient_calculator.py b/helix/calculators/pressure_coefficient_calculator.py new file mode 100644 index 0000000..5269d88 --- /dev/null +++ b/helix/calculators/pressure_coefficient_calculator.py @@ -0,0 +1,93 @@ +from math import sqrt, log +import math + +from helix.constants.global_constants import parapet_coefficients, parapet_factor_max +from numpy import array +from numpy.ma import maximum + +from helix.constants.panel_type import PanelType + + +class PressureCoefficientCalculator(object): + def __init__(self, user_values): + self.values = user_values + self.system_constants = self.values.system_type().system_constants() + self.module_constants = self.values.module_system_constants() + + def c_p_matrix(self, L_B): + parapet = self.parapet_factor() + return self.compute_c_p_matrix(L_B, parapet) + + def L_B(self): + """ Building scaling factor """ + height = max(15, self.values.building_height()) + + length = self.values.building_length() + width = self.values.building_width() + + longest_side = max(width, length) + + return min(height, 0.4 * sqrt(height * max(1, longest_side))) + + def minimum_array_size(self, L_B): + panel_area = self.module_constants.panel_area + module_count = self.system_constants.module_count + minimum_array_size = [] + for minimum_A_n in self.minimum_A_n(L_B): + if minimum_A_n is None: + value = 6 + else: + value = int(math.ceil((minimum_A_n * L_B ** 2) / (panel_area * 1000) / module_count)) + minimum_array_size.append(value) + return minimum_array_size + + # Normalized area, scales the tributary area by the building scaling factor and panel area + def A_n(self, L_B): + return self.module_constants.tributary_area * (self.module_constants.panel_area * 1000. / L_B ** 2) + + def compute_c_p_matrix(self, L_B, parapet): + A_n = self.A_n(L_B) + wind_zones = self.system_constants.wind_zones + return array([self.c_p_row(A_n, wind_zone, parapet) for wind_zone in wind_zones]) + + def c_p_row(self, A_n_row, wind_zone, parapet_factor): + c_p_lower_bound = self.module_constants.c_p_lower_bound() + + if wind_zone == self.system_constants.wind_zones[-1]: + return c_p_lower_bound + computed_row = [] + for index, A_n in enumerate(A_n_row): + edge_factor = self.module_constants.edge_factor(wind_zone, PanelType.from_index(index)) + computed_row.append(self.c_p(A_n, wind_zone, parapet_factor, edge_factor)) + + return maximum(array(computed_row), c_p_lower_bound) + + def c_p(self, A_n, wind_zone, parapet_factor, edge_factor): + c0, c1 = self.module_constants.c_p_constants(A_n, wind_zone) + return max(0., c0 * log(A_n) + c1) * parapet_factor * edge_factor + + def parapet_factor(self): + height = max(15, self.values.building_height()) + parapet_height = max(0, self.values.building_parapet_height()) + + factor = parapet_height / height + c0, c1 = parapet_coefficients + return min(parapet_factor_max, c0 + c1 * factor) + + def ideal_subarray_average_uplift_c_p(self, L_B): + c_p_matrix = self.compute_c_p_matrix(L_B, 1) + return [self.module_constants.weighted_average_c_p(c, n, e, m) for c, n, e, m in c_p_matrix] + + def minimum_A_n(self, L_B): + uplift_c_p = self.ideal_subarray_average_uplift_c_p(L_B) + minimum_A_n = [] + for idx, wind_zone in enumerate(self.system_constants.wind_zones): + wind_zone_uplift = uplift_c_p[idx] + coefficients = self.module_constants.minimum_a_n_coefficients(wind_zone_uplift, wind_zone) + if not coefficients: + minimum_A_n.append(None) + continue + + value = math.exp((coefficients[0] - wind_zone_uplift) / coefficients[1]) + minimum_A_n.append(value) + return minimum_A_n diff --git a/helix/calculators/seismic_calculator.py b/helix/calculators/seismic_calculator.py new file mode 100644 index 0000000..c2f4157 --- /dev/null +++ b/helix/calculators/seismic_calculator.py @@ -0,0 +1,184 @@ +from math import ceil, floor + +from helix.calculators.subarray_graph import SubarrayGraph +from helix.calculators.subarray_helper import extract_subarray + +from helix.constants.global_constants import minimum_racking_capacity +from helix.constants.panel_type import PanelType +from helix.models.subarray import Subarray + + +class SeismicCalculator(object): + def __init__(self, values, graph_repository): + self.values = values + self.system_type = values.system_type() + self.system_constants = values.module_system_constants() + self.anchor_type = values.anchor_type() + self.graph_repository = graph_repository + + def assign_seismic_anchors(self, subarray, panels): + panel_data_for_subarray = extract_subarray(panels, subarray.subarray_number) + self.assign_anchors_to_subarray(panel_data_for_subarray, subarray) + return panels + + def assign_anchors_to_subarray(self, panels, subarray): + sds = self.values.spectral_response() + wind_anchors = sum([panel.wind_anchors for panel in panels]) + for panel in panels: + if panel.seismic_anchors is None: + panel.seismic_anchors = 0 + required_anchors = subarray.required_seismic_anchors + if required_anchors == 0 and (wind_anchors == 0 or sds < 1): + return panels + + graph = self.graph_repository.subarray_graph(subarray.subarray_number) + if len(graph.nodes) == 0: + return panels + + more_anchors_needed = True + perimeter_covered = sds < 1.0 + anchor_threshold = 0 + while more_anchors_needed: + rung = graph.pop_rung() + interval = int(self.seismic_anchor_interval()) + nodes_since_last_anchor = interval + if len(rung) == 0: + graph.reset() + anchor_threshold += 1 + continue + while more_anchors_needed and interval >= 0: + for node in rung: + nodes_since_last_anchor += 1 + if node.wind_anchor + node.seismic_anchor > anchor_threshold: + nodes_since_last_anchor = 0 + + if nodes_since_last_anchor > interval: + node.assign_seismic_anchor() + required_anchors -= 1 + nodes_since_last_anchor = 0 + + more_anchors_needed = (not perimeter_covered) or required_anchors > 0 + if not more_anchors_needed: + break + + perimeter_covered = True + if interval <= 1: + interval -= 1 + else: + interval /= 2 + + for idx, node in enumerate(graph.nodes): + panels[idx].seismic_anchors = node.seismic_anchor + + return panels + + def compute_provided_lateral_capacity(self, subarray_number, panels): + subarray_panels = extract_subarray(panels, subarray_number) + + anchors = {PanelType.Corner: 0, PanelType.NorthSouth: 0, PanelType.EastWest: 0, PanelType.Middle: 0} + + for panel in subarray_panels: + if panel.seismic_anchors is not None: + anchors[panel.panel_type] += panel.seismic_anchors + + return self.anchors_shear_capacity(anchors[PanelType.Corner], anchors[PanelType.NorthSouth], + anchors[PanelType.EastWest], anchors[PanelType.Middle]) + + def seismic_anchors_for_subarray(self, F_p, subarray_weight, spectral_response, friction_coefficient, + seismic_anchors, corner_anchors, + north_south_anchors, east_west_anchors, middle_anchors, shear_capacity): + demand = self.seismic_demand_for_subarray(F_p, subarray_weight, spectral_response, friction_coefficient, + seismic_anchors, corner_anchors, north_south_anchors, + east_west_anchors, middle_anchors) + return ceil(demand / shear_capacity) + + def anchors_shear_capacity(self, corner_anchors, north_south_anchors, east_west_anchors, middle_anchors): + + anchor_shear_capacity = self.anchor_type.shear_capacity() + panel_racking_capacity = self.system_constants.racking_capacity + + corner_capacity = corner_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.Corner)) + north_south_capacity = north_south_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.NorthSouth)) + east_west_capacity = east_west_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.EastWest)) + middle_capacity = middle_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.Middle)) + + return corner_capacity + north_south_capacity + east_west_capacity + middle_capacity + + def seismic_demand_for_subarray(self, F_p, subarray_weight, spectral_response, friction_coefficient, + seismic_anchors, corner_anchors, + north_south_anchors, east_west_anchors, middle_anchors): + if (corner_anchors + north_south_anchors + east_west_anchors + middle_anchors == 0) and seismic_anchors == 0: + return 0 + + existing_shear_resistance = self.anchors_shear_capacity(corner_anchors, north_south_anchors, east_west_anchors, + middle_anchors) + + shear_force = 0.7 * F_p * subarray_weight - ( + (0.6 - 0.14 * spectral_response) * 0.7 * friction_coefficient * subarray_weight) - existing_shear_resistance + + return max(shear_force, 0) + + def required_force_seismic_anchors(self, subarray_number, panels): + + demand = self.required_force_seismic_demand(subarray_number, panels) + + system_shear_capacity = min(self.anchor_type.shear_capacity(), + minimum_racking_capacity) + + return ceil(demand / system_shear_capacity) + + def required_force_seismic_demand(self, subarray_number, panels): + subarray_panels = extract_subarray(panels, subarray_number) + + importance_factor = self.values.importance_factor() + spectral_response = self.values.spectral_response() + F_p = 1.2 * spectral_response / (1.5 / importance_factor) + + # number of wind anchors by panel type + anchors = {PanelType.Corner: 0, + PanelType.NorthSouth: 0, + PanelType.EastWest: 0, + PanelType.Middle: 0} + + # total weight + subarray_weight = 0 + + # total number of seismic anchors + seismic_anchors = 0 + + for panel in subarray_panels: + if panel.seismic_anchors is not None: + seismic_anchors += panel.seismic_anchors + + # it could be calculated before the loop to avoid redundant calculations + effective_area = self.system_constants.surface_area / self.system_constants.ground_coverage_ratio + weight = panel.pressure * effective_area + + anchors[panel.panel_type] += panel.wind_anchors + subarray_weight += weight + + force_required_demand = self.seismic_demand_for_subarray(F_p, subarray_weight, spectral_response, + self.system_constants.friction_coefficient, + seismic_anchors, + anchors[PanelType.Corner], + anchors[PanelType.NorthSouth], + anchors[PanelType.EastWest], + anchors[PanelType.Middle]) + return force_required_demand + + def required_geometric_seismic_anchors(self, subarray_number, panels): + if panels[0].coordinate is None or self.values.spectral_response() < 1: + return 0 + panel_data_for_subarray = extract_subarray(panels, subarray_number) + subarray = Subarray(required_seismic_anchors=0, subarray_number=subarray_number) + anchors_for_subarray = self.assign_anchors_to_subarray(panel_data_for_subarray, subarray) + return sum(panel.seismic_anchors for panel in anchors_for_subarray if panel.seismic_anchors) + + def seismic_anchor_interval(self): + sds = self.values.spectral_response() + importance_factor = self.values.importance_factor() + interval_constant, interval_multiplier = self.system_constants.seismic_anchor_interval_constants + denom = (22.96 * importance_factor * sds - interval_constant + interval_multiplier * sds) + if denom <= 0: + return 15 + return floor(minimum_racking_capacity / denom) diff --git a/helix/calculators/subarray_graph.py b/helix/calculators/subarray_graph.py new file mode 100644 index 0000000..c97cf43 --- /dev/null +++ b/helix/calculators/subarray_graph.py @@ -0,0 +1,246 @@ +import copy +from enum import Enum + +from helix.constants.system_type import SystemType +from helix.models.coordinate import Coordinate +from helix.models.dxf.graph_node_store import GraphNodeStore + + +class Direction(Enum): + North = Coordinate(0, -1) + South = Coordinate(0, 1) + East = Coordinate(-1, 0) + West = Coordinate(1, 0) + + def opposite_direction(self): + if self == Direction.North: + return Direction.South + elif self == Direction.South: + return Direction.North + elif self == Direction.East: + return Direction.West + else: + return Direction.East + + @classmethod + def all(cls): + return [ + cls.North, + cls.West, + cls.South, + cls.East, + ] + + @classmethod + def all_coordinates(cls): + return [d.value for d in cls.all()] + + def directions_to_try(self): + return { + Direction.North: [Direction.East, Direction.North, Direction.West, Direction.South], + Direction.East: [Direction.South, Direction.East, Direction.North, Direction.West], + Direction.South: [Direction.West, Direction.South, Direction.East, Direction.North], + Direction.West: [Direction.North, Direction.West, Direction.South, Direction.East], + }[self] + + +class SubarrayGraphNode(object): + """ + A node that contains the panel, it's neighbors to the four cardinal + directions (N, S, E, W), and a + count of the seismic anchors attached. + + """ + + def __init__(self, panel): + self.neighbors = {} + self.panel = panel + self.seismic_anchor = 0 + + @property + def location(self): + return self.panel.coordinate + + @property + def coordinate(self): # GraphNodeStore expects a coordinate property + return self.panel.coordinate + + @property + def wind_anchor(self): + return self.panel.wind_anchors + + def add_neighbor(self, neighbor, direction): + self.neighbors[direction] = neighbor + neighbor.neighbors[direction.opposite_direction()] = self + + def remove_neighbor_references(self): + for direction, neighbor in self.neighbors.items(): + neighbor.neighbors.pop(direction.opposite_direction(), None) + + def assign_seismic_anchor(self): + self.seismic_anchor += 1 + + def step(self, direction): + return self.neighbors.get(direction) + + def __repr__(self): + val = "(" + str(self.location) + ":\n" + for direction, neighbor in self.neighbors.items(): + val += "\t%s: %s\n" % (str(direction), str(neighbor.location)) + return val + ")" + + def __eq__(self, other): + if self is other: + return True + us = self.panel.coordinate + them = other.panel.coordinate + # Quick and dirty, inline equality makes for a slightly faster equality check + # Also don't bother with floating point equality - it only slows us down. :( + return us.x == them.x and us.y == them.y and us.rotation == them.rotation + + def __hash__(self): + return self.panel.coordinate.__hash__() + + +class SubarrayGraph(object): + def __init__(self, panels, system_type): + self.nodes = [] + self.system_type = system_type + self.node_store = GraphNodeStore() + + self.nodes = [] + if len(panels) == 0 or panels[0].coordinate == panels[-1].coordinate: + self.nodes = [] + else: + for panel in panels: + node = SubarrayGraphNode(panel) + self.nodes.append(node) + self.node_store.add_node(node) + + self.graph = list(self.nodes) + self.rungs = [] + self.current_rung = 0 + self.assemble_graph() + + def __deepcopy__(self, _): + panels = [node.panel for node in self.nodes] + graph = SubarrayGraph(panels, self.system_type) + return graph + + def assemble_graph(self): + for node in self.nodes: + if len(node.neighbors) == 4: + continue + for direction in Direction.all(): + coordinate = node.location + direction.value + neighbor = self.node_store.find_coordinate(coordinate) + if neighbor: + node.add_neighbor(neighbor, direction.opposite_direction()) + if len(node.neighbors) == 4: + break + + def reset(self): + self.graph = list(self.nodes) + self.current_rung = 0 + + def find_disconnected_subgraphs(self): + graph = list(self.graph) + subgraphs = [] + while len(graph) > 0: + node = self.lower_left_node(graph) + subgraph = self.add_all_neighbors(node) + for subgraph_node in subgraph: + try: + graph.remove(subgraph_node) + except: + continue + subgraphs.append(subgraph) + return subgraphs + + def find_node(self, coordinate): + if coordinate.x < 0 or coordinate.y < 0: + return None + for node in self.graph: + if node.location == coordinate: + return node + + @staticmethod + def add_all_neighbors(node): + seen = {node} + visited = set() + visited_list = [] # apparently, order matters! + while len(seen) > 0: + node = seen.pop() + if node in visited: + continue + visited.add(node) + visited_list.append(node) + for neighbor in node.neighbors.values(): + if neighbor not in visited and neighbor not in seen: + seen.add(neighbor) + return visited_list + + @staticmethod + def lower_left_node(graph): + lower_left_node = None + lower_left_location = Coordinate(float('inf'), float('inf')) + for node in graph: + node_location = node.location + if node_location.x <= lower_left_location.x and node_location.y <= lower_left_location.y: + lower_left_node = node + lower_left_location = lower_left_node.location + return lower_left_node + + def pop_rung(self): + if self.current_rung < len(self.rungs): + rung = self.rungs[self.current_rung] + self.current_rung += 1 + return rung + rung = [] + + def assemble_rung_callback(node, next_direction, previous_direction): + rung.append(node) + if self.system_type == SystemType.dualTilt: + east_west = [Direction.East, Direction.West] + if next_direction in east_west or previous_direction in east_west: + rung.append(node) + + subgraphs = self.find_disconnected_subgraphs() + for subgraph in subgraphs: + try: + start_node = self.lower_left_node(subgraph) + except: + break + self.walk_graph_perimeter(start_node, assemble_rung_callback) + + for node in rung: + node.remove_neighbor_references() + try: + self.graph.remove(node) + except: + pass + + self.rungs.append(rung) + self.current_rung += 1 + return rung + + def walk_graph_perimeter(self, start_node, fn, repeat_steps=True): + total_nodes = len(self.nodes) + steps = 0 + node = start_node + direction = Direction.East + while True: + next_node = None + directions_to_try = list(direction.directions_to_try()) + last_direction = direction + while next_node is None and len(directions_to_try) > 0: + direction = directions_to_try.pop(0) + next_node = node.step(direction) + if next_node is None: + break + + fn(node, direction, last_direction) + node = next_node + steps += 1 + if node == start_node or (steps > total_nodes and repeat_steps): + break diff --git a/helix/calculators/subarray_helper.py b/helix/calculators/subarray_helper.py new file mode 100644 index 0000000..99b7676 --- /dev/null +++ b/helix/calculators/subarray_helper.py @@ -0,0 +1,19 @@ +from helix.models.subarray import Subarray + + +def get_subarray_sizes_and_rows(panels): + subarrays = [] + last_subarray = None + for index, panel in enumerate(panels): + if last_subarray != panel.subarray: + subarray = Subarray(subarray_number=panel.subarray, start_row=index, size=0) + + subarrays.append(subarray) + last_subarray = panel.subarray + subarrays[-1].size += 1 + + return subarrays + + +def extract_subarray(panels, subarray_number): + return [panel for panel in panels if panel.subarray == subarray_number] diff --git a/helix/calculators/summary_values_calculator.py b/helix/calculators/summary_values_calculator.py new file mode 100644 index 0000000..287d0f6 --- /dev/null +++ b/helix/calculators/summary_values_calculator.py @@ -0,0 +1,85 @@ +from collections import namedtuple + +SummaryValues = namedtuple('SummaryValues', ['total_weight', 'max_psf', 'avg_psf', 'anchors', 'ballast', 'max_weight', 'ballast_weight']) + + +class SummaryValuesCalculator(object): + def __init__(self, user_values): + self.values = user_values + self.constants = self.values.module_system_constants() + + def summary_values(self, panels, subarrays, c_p_matrix, q_z, seismic_interval, ballast_calculator): + summary_values = self.compute_summary(panels, subarrays, c_p_matrix, q_z, ballast_calculator) + + return [ + {'label': 'Total System Weight (lbs)', 'value': summary_values.total_weight}, + {'label': 'Max PSF', 'value': summary_values.max_psf}, + {'label': 'Avg PSF', 'value': summary_values.avg_psf}, + {'label': 'Total Anchors', 'value': summary_values.anchors}, + {'label': 'Total Ballast', 'value': summary_values.ballast}, + {'label': 'Max Possible System Weight', 'value': summary_values.max_weight}, + {'label': 'Max System Weight Ballast Block', 'value': summary_values.ballast_weight}, + {'label': 'Seismic Anchor Max. Spacing', 'value': seismic_interval} + ] + + def documentation_summary_values(self, panels, subarrays, c_p_matrix, q_z, seismic_interval, ballast_calculator): + summary_values = self.compute_summary(panels, subarrays, c_p_matrix, q_z, ballast_calculator) + + return { + 'total_system_weight': summary_values.total_weight, + 'max_psf': summary_values.max_psf, + 'ave_psf': summary_values.avg_psf, + 'total_anchors': summary_values.anchors, + 'total_ballast': summary_values.ballast, + 'max_possible_system_weight': summary_values.max_weight, + 'max_system_weight_ballast_block': summary_values.ballast_weight, + 'seismic_anchor_max_spacing': seismic_interval + } + + def compute_summary(self, panels, subarrays, c_p_matrix, q_z, ballast_calculator): + total_weight, avg_psf = self.system_weight_and_pressure(panels) + + max_psf = 0 + wind_anchors = 0 + total_ballast = 0 + for panel in panels: + max_psf = panel.pressure if panel.pressure > max_psf else max_psf + wind_anchors += panel.wind_anchors + total_ballast += panel.ballast + + required_seismic_anchors = sum(subarray.required_seismic_anchors for subarray in subarrays) + + total_anchors = int(wind_anchors + required_seismic_anchors) + + max_weight, ballast_weight = self.find_max_system_weight(panels, c_p_matrix, q_z, ballast_calculator) + + return SummaryValues( + total_weight=round(total_weight), + max_psf=round(max_psf, 2), + avg_psf=round(avg_psf, 2), + anchors=total_anchors, + ballast=int(total_ballast), + max_weight=round(max_weight, 0), + ballast_weight=ballast_weight + ) + + def system_weight_and_pressure(self, panels): + constants = self.values.module_system_constants() + effective_area = constants.surface_area / constants.ground_coverage_ratio + + psf_sum = sum(panel.pressure for panel in panels) + return psf_sum * effective_area, psf_sum / len(panels) + + def find_max_system_weight(self, panels, c_p_matrix, q_z, ballast_calculator): + copied_panels = list(panels) + max_weight = 0 + ballast_block_weight_for_max_weight = 0 + for weight in range(12, 19): + ballast_matrix = ballast_calculator.ballast_and_trays_matrix(c_p_matrix, q_z, copied_panels, + ballast_block_weight=weight) + + total_weight, _ = self.system_weight_and_pressure(ballast_matrix) + if total_weight > max_weight: + max_weight = total_weight + ballast_block_weight_for_max_weight = weight + return max_weight, ballast_block_weight_for_max_weight diff --git a/helix/calculators/wind_pressure_calculator.py b/helix/calculators/wind_pressure_calculator.py new file mode 100644 index 0000000..08f3f43 --- /dev/null +++ b/helix/calculators/wind_pressure_calculator.py @@ -0,0 +1,56 @@ +from math import log + +from helix.constants.exposure_category import ExposureCategory +from helix.constants.global_constants import k_zt, k_d + + +class WindPressureCalculator(object): + def __init__(self, user_values): + self.values = user_values + + def q_z(self, k_z): + v = self.values.wind_speed() + return 0.00256 * k_z * k_zt * k_d * v ** 2 + + def K_z(self): + height = self.values.building_height() + exposure = self.values.exposure_category() + transition_distance = self.values.exposure_category_transition_distance() + return self.calculate_k_z(height, exposure, transition_distance) + + def calculate_k_z(self, h, exp, transition_distance): + if exp == ExposureCategory.B and h < 30: + return 0.7 + elif exp == ExposureCategory.B_C or exp == ExposureCategory.C_B: + k_zd = self.k_zd(exp, h) + return k_zd + self.delta_k(exp, transition_distance, k_zd) + else: + return 2.01 * (self.h_eff(h, exp) / exp.z_g()) ** (2 / exp.alpha()) + + def h_eff(self, h, exposure): + if (exposure == ExposureCategory.C or exposure == ExposureCategory.D) and h < 15: + return 15. + else: + return h + + def k_zd(self, exp, h): + if exp == ExposureCategory.B_C: + h_eff = max(15, h) + return self.calculate_expression(h_eff, exp.z_g()[1], exp.alpha()[1]) + elif exp == ExposureCategory.C_B and h < 30: + return 0.7 + else: + return self.calculate_expression(h, exp.z_g()[1], exp.alpha()[1]) + + def delta_k(self, exp, transition_distance, k_zd): + k_upwind = self.calculate_expression(33, exp.z_g()[0], exp.alpha()[0]) + k_downwind = self.calculate_expression(33, exp.z_g()[1], exp.alpha()[1]) + return (k_upwind - k_downwind) * (k_zd / k_downwind) * self.dk_x(k_upwind, k_downwind, transition_distance / 5280.) + + def dk_x(self, k_upwind, k_downwind, transition_distance_in_miles): + x0 = 0.621 * 10 ** (-1 * ((k_upwind - k_downwind) ** 2) - 2.3) + x1 = 6.21 if k_upwind > k_downwind else 62.1 + return log(x1 / transition_distance_in_miles, x1 / x0) + + def calculate_expression(self, height, z_g, alpha): + return 2.01 * (height / z_g) ** (2 / alpha) diff --git a/helix/constants/__init__.py b/helix/constants/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/helix/constants/anchor_parts.py b/helix/constants/anchor_parts.py new file mode 100644 index 0000000..01559ca --- /dev/null +++ b/helix/constants/anchor_parts.py @@ -0,0 +1,23 @@ +from helix.constants.parts import anchor_plate, anchor, anchor_washer + + +class OmgPowerGripParts(object): + parts = { + anchor_plate: 1, + anchor: 1, + anchor_washer: 1 + } + +class OmgPowerGripPlusParts(object): + parts = { + anchor_plate: 1, + anchor: 1, + anchor_washer: 1 + } + +class EcoFastenParts(object): + parts = { + anchor_plate: 1, + anchor: 1, + anchor_washer: 1 + } diff --git a/helix/constants/anchor_type.py b/helix/constants/anchor_type.py new file mode 100644 index 0000000..ba02ea6 --- /dev/null +++ b/helix/constants/anchor_type.py @@ -0,0 +1,29 @@ +from enum import Enum + +from helix.constants.anchor_parts import OmgPowerGripParts, OmgPowerGripPlusParts, EcoFastenParts +from helix.constants.global_constants import system_force_capacity + + +class AnchorType(Enum): + OMG_PowerGrip = 'OMG PowerGrip' + OMG_PowerGrip_Plus = 'OMG PowerGrip Plus' + EcoFasten = 'EcoFasten Eco 65' + + def uplift_capacity(self): + return min(system_force_capacity, {AnchorType.OMG_PowerGrip: 305., + AnchorType.OMG_PowerGrip_Plus: 2000., + AnchorType.EcoFasten: 1343.}[self]) + + def shear_capacity(self): + return {AnchorType.OMG_PowerGrip: 142., + AnchorType.OMG_PowerGrip_Plus: 431., + AnchorType.EcoFasten: 1008.}[self] + + def parts(self): + return {AnchorType.OMG_PowerGrip: OmgPowerGripParts(), + AnchorType.OMG_PowerGrip_Plus: OmgPowerGripPlusParts(), + AnchorType.EcoFasten: EcoFastenParts()}[self] + + @classmethod + def default_value(cls): + return cls.OMG_PowerGrip_Plus.value diff --git a/helix/constants/bom_constants.py b/helix/constants/bom_constants.py new file mode 100644 index 0000000..cc52878 --- /dev/null +++ b/helix/constants/bom_constants.py @@ -0,0 +1,4 @@ +dt_chassis_fudge_factor = 1.04 +leading_tray_fudge_factor = 1.05 + +bolts_per_package = 50 \ No newline at end of file diff --git a/helix/constants/color.py b/helix/constants/color.py new file mode 100644 index 0000000..91c7383 --- /dev/null +++ b/helix/constants/color.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class Color(Enum): + array_background = "white" + seismic_background = "#F1E8A2" + wind_background = "#B8F3E5" + default_panel_background = "#133256" + light_text = "white" + dark_text = "#6490BA" + border = "#537DAA" diff --git a/helix/constants/dual_tilt_parts.py b/helix/constants/dual_tilt_parts.py new file mode 100644 index 0000000..0585786 --- /dev/null +++ b/helix/constants/dual_tilt_parts.py @@ -0,0 +1,89 @@ +from helix.constants.module_type import ModuleType +from helix.constants.parts import * + + +class DualTiltParts(object): + east_west_panel_parts = { + dual_tilt_chassis: 1, + module: 2 + } + + center_panel_parts = { + dual_tilt_chassis: 1, + module: 2 + } + + sub_array_parts = { + leading_tray: 1 + } + + def __init__(self, module_type): + if module_type == ModuleType.PSeries: + self.corner_panel_parts = { + left_deflector_1_1: 1, + right_deflector_1_1: 1, + dual_tilt_chassis: 1.5, + module: 2 + } + self.north_south_panel_parts = { + left_deflector_1_1: 1, + right_deflector_1_1: 1, + dual_tilt_chassis: 1.5, + module: 2 + } + else: + self.corner_panel_parts = { + left_deflector: 1, + right_deflector: 1, + dual_tilt_chassis: 1.5, + module: 2 + } + self.north_south_panel_parts = { + left_deflector: 1, + right_deflector: 1, + dual_tilt_chassis: 1.5, + module: 2 + } + + def row_parts(self, module_type): + if module_type == ModuleType.Cell96: + front_skirt_parts = front_skirt + else: + front_skirt_parts = front_skirt_1_1 + return { + front_skirt_parts: 2, + leading_tray: 1 + } + + def column_parts(self, _): + return {} + + def parts_per_panel_type(self): + return [ + self.corner_panel_parts, + self.north_south_panel_parts, + self.east_west_panel_parts, + self.center_panel_parts + ] + + def dependent_parts(self, _): + return { + module: { + wire_clip: 2, + rubber_foot: 0.1 + }, + dual_tilt_chassis: { + dual_tilt_platform: 1, + platform_bolt: 4 + } + } + + def fudge_factors(self, used_fallback): + if used_fallback: + return { + dual_tilt_chassis: 1.04, + leading_tray: 1.05 + } + return { + dual_tilt_chassis: 1.04, + } diff --git a/helix/constants/dxf_validation.py b/helix/constants/dxf_validation.py new file mode 100644 index 0000000..f29f74d --- /dev/null +++ b/helix/constants/dxf_validation.py @@ -0,0 +1,7 @@ +''' +Created on May 22, 2017 + +@author: jvazquez +''' +INVALID_DUAL_TILT_DESIGN = "Error - not a dual tilt file, or invalid "\ + "dual tilt design." diff --git a/helix/constants/ebom_parts.py b/helix/constants/ebom_parts.py new file mode 100644 index 0000000..2c7e55f --- /dev/null +++ b/helix/constants/ebom_parts.py @@ -0,0 +1,281 @@ +from helix.constants.inverter_type import InverterType +from helix.constants.module_type import ModuleType +from helix.constants.parts import * +from helix.constants.system_type import SystemType + +inverter_model_parts = { + InverterType.SMA.MODEL_12KW: {sma_12kw_inverter: 1}, + InverterType.SMA.MODEL_15KW: {sma_15kw_inverter: 1}, + InverterType.SMA.MODEL_20KW: {sma_20kw_inverter: 1}, + InverterType.SMA.MODEL_24KW: {sma_24kw_inverter: 1}, + InverterType.DELTA.MODEL_36KW: {delta_36kw_inverter: 1}, + InverterType.DELTA.MODEL_42KW: {delta_42kw_inverter: 1}, + InverterType.DELTA.MODEL_60KW: {delta_60kw_inverter: 1}, + InverterType.DELTA.MODEL_80KW: {delta_80kw_inverter: 1}, +} + +inverter_strings_parts = { + 0: {}, + 2: { + harness_2_string_mf: 1, + harness_2_string_fm: 1 + }, + 3: { + harness_3_string_mf: 1, + harness_3_string_fm: 1 + }, + 4: { + harness_2_string_mf: 2, + harness_2_string_fm: 2 + }, + 5: { + harness_2_string_mf: 1, + harness_2_string_fm: 1, + harness_3_string_mf: 1, + harness_3_string_fm: 1 + }, + 6: { + harness_3_string_mf: 2, + harness_3_string_fm: 2 + }, + 7: { + harness_3_string_mf: 1, + harness_3_string_fm: 1, + harness_4_string_mf: 1, + harness_4_string_fm: 1 + }, + 8: { + harness_4_string_mf: 2, + harness_4_string_fm: 2 + }, +} + + +def shared_panel_board_parts(module_type, system_type): + v1_1_inverter_links = 0 + if system_type == SystemType.singleTilt and (module_type == ModuleType.PSeries or module_type == ModuleType.Cell128): + v1_1_inverter_links = 1 + return { + front_legs: 1, + back_legs: 1, + inverter_link: 2, + inverter_rail: 1, + rubber_foot: 3, + mounting_back_plate: 1, + ethernet_plug: 1.8, + flat_washer: 4, + hex_nut_three_eighths_16: 2, + hex_bolt_1_2: 9, + inverter_link_long: v1_1_inverter_links, + inverter_link_short: v1_1_inverter_links, + } + +dc_switch_parts = { + dc_switch_bracket: 1, + dc_switch_box: 1, + hex_bolt_3_4: 4, + hex_bolt_quarter_20: 4, + hex_nut_three_eighths_16: 4, + hex_nut_quarter_20: 4, + flat_washer_quarter_inch: 4, +} + + +def panel_board_parts(inverter_quantity, with_aux): + if with_aux: + parts = { + 1: { + panel_board_2_aux: 1, + }, + 2: { + panel_board_2_aux: 1, + }, + 3: { + panel_board_3_aux: 1, + }, + 4: { + panel_board_4_aux: 1, + } + } + else: + parts = { + 1: { + panel_board_2: 1, + }, + 2: { + panel_board_2: 1, + }, + 3: { + panel_board_3: 1, + }, + 4: { + panel_board_4: 1, + } + } + return parts[inverter_quantity] + + +def panel_board_parts_with_monitor(inverter_quantity, monitor_controller_type): + parts = panel_board_parts(inverter_quantity, monitor_controller_type != monitor_controller_240_v) + parts[monitor_controller_type] = 1 + return parts + + +def standalone_inverter_parts(inverter, system_type, module_type): + multiplier = 1 + v1_1_inverter_links = 0 + + if inverter['model'] in InverterType.DELTA.all(): + parts = {} + if system_type == SystemType.singleTilt: + parts = {**parts, delta_kit_inverter_mount: 1} + else: + parts = {**parts, delta_kit_inverter_mount_dt: 1} + if inverter['splice_box']: + parts = {**parts, delta_splice_box: 1} + return parts + + if system_type == SystemType.singleTilt: + multiplier = 2 + v1_1_inverter_links = 1 if module_type == ModuleType.PSeries or module_type == ModuleType.Cell128 else 0 + return { + ac_switch: 1, + star_washer: 4, + phillips_screw: 4 * multiplier, + ac_inverter_bracket: 1 * multiplier, + hex_bolt_1_2: 4, + flat_washer_6: 4, + inverter_link_short: v1_1_inverter_links, + inverter_link_long: v1_1_inverter_links, + whip_tray: 1 + } + + +standalone_inverter_attached_to_panel_board_parts = { + ac_splice_box: 1, +} + + +def inverter_parts(inverter, module_type): + if inverter['model'] in InverterType.DELTA.all(): + return { + delta_inverter_leg: 3, + rubber_foot: 3, + } + else: + if module_type == ModuleType.PSeries: + return { + flat_washer: 8, + hex_nut_three_eighths_16: 6, + hex_bolt_3_4: 5, + hex_bolt_1_2: 18, + front_legs: 1, + back_legs: 1, + mounting_back_plate: 1, + rubber_foot: 3, + inverter_link: 2, + inverter_rail: 1, + stump: 6, + fuseshade: 1, + screw_12_24x1_25: 2, + fuseshade_brace: 1, + } + else: + return { + flat_washer: 4, + hex_nut_three_eighths_16: 4, + hex_bolt_3_4: 2, + hex_bolt_1_2: 18, + front_legs: 1, + back_legs: 1, + mounting_back_plate: 1, + rubber_foot: 3, + inverter_link: 2, + inverter_rail: 1, + stump: 6, + } + +def dependent_parts(module_type, system_type): + v1_1_inverter_links = 0 + if system_type == SystemType.singleTilt and (module_type == ModuleType.Cell128 or module_type == ModuleType.PSeries): + v1_1_inverter_links = 1 + return { + panel_board_2: { + harness_ac_inner: 2, + whip_tray: 2, + comm_cable: 1 + }, + panel_board_2_aux: { + harness_ac_inner: 2, + whip_tray: 2, + comm_cable: 2 + }, + panel_board_3: { + harness_ac_inner: 2, + harness_ac_outer: 1, + whip_tray: 3, + comm_cable: 2, + inverter_link_short: v1_1_inverter_links, + inverter_link_long: v1_1_inverter_links, + inverter_link: -2 * v1_1_inverter_links, + }, + panel_board_3_aux: { + harness_ac_inner: 2, + harness_ac_outer: 1, + whip_tray: 3, + comm_cable: 3, + inverter_link_short: v1_1_inverter_links, + inverter_link_long: v1_1_inverter_links, + inverter_link: -2 * v1_1_inverter_links, + }, + panel_board_4: { + harness_ac_inner: 2, + harness_ac_outer: 2, + whip_tray: 4, + comm_cable: 3, + inverter_link_short: v1_1_inverter_links, + inverter_link_long: v1_1_inverter_links, + inverter_link: -2 * v1_1_inverter_links, + }, + panel_board_4_aux: { + harness_ac_inner: 2, + harness_ac_outer: 2, + whip_tray: 4, + comm_cable: 4, + inverter_link_short: v1_1_inverter_links, + inverter_link_long: v1_1_inverter_links, + inverter_link: -2 * v1_1_inverter_links, + }, + monitor_controller_480_v: { + monitor_power_plug: 1, + flat_washer: 4, + channel_nut: 4, + hex_nut_three_eighths_16: 2, + front_legs: 1, + back_legs: 1, + inverter_link: 2, + inverter_rail: 1, + rubber_foot: 3, + hex_bolt_1_2: 9, + mounting_back_plate: 1, + }, + monitor_controller_240_v: {}, + ac_splice_box: { + hex_bolt_1_2: 4, + hex_bolt_3_4: 4, + hex_nut_three_eighths_16: 8, + flat_washer: 8, + back_legs: 1, + front_legs: 1, + rubber_foot: 3, + inverter_rail: 1, + inverter_link: 2, + mounting_back_plate: 1, + }, + inverter_link_long: { + inverter_link: -1, + }, + inverter_link_short: { + inverter_link: -1, + } + } diff --git a/helix/constants/exposure_category.py b/helix/constants/exposure_category.py new file mode 100644 index 0000000..f8b4dc9 --- /dev/null +++ b/helix/constants/exposure_category.py @@ -0,0 +1,34 @@ +from enum import Enum + + +class ExposureCategory(Enum): + B = 'B' + C = 'C' + D = 'D' + B_C = 'B to C' + C_B = 'C to B' + + def z_g(self): + return { + ExposureCategory.B: 1200., + ExposureCategory.C: 900., + ExposureCategory.D: 700., + ExposureCategory.B_C: (1273., 906.), + ExposureCategory.C_B: (906., 1273.) + }[self] + + def alpha(self): + return { + ExposureCategory.B: 7., + ExposureCategory.C: 9.5, + ExposureCategory.D: 11.5, + ExposureCategory.B_C: (6.62, 9.5), + ExposureCategory.C_B: (9.5, 6.62) + }[self] + + def is_interpolated(self): + return self in [ExposureCategory.B_C, ExposureCategory.C_B] + + @classmethod + def default_value(cls): + return cls.C.value diff --git a/helix/constants/file_validation_error.py b/helix/constants/file_validation_error.py new file mode 100644 index 0000000..3c042fb --- /dev/null +++ b/helix/constants/file_validation_error.py @@ -0,0 +1,50 @@ +from enum import Enum +from helix.constants.system_type import SystemType + + +class FileValidationMessage(Enum): + Generic = 'Input file has invalid data' + InvalidHeaders = 'Input file has less than 5 headers' + InvalidRowCount = 'Input file has no data' + DualTiltWindZone = 'Invalid wind zone for Dual Tilt System' + SingleTiltWindZone = 'Invalid wind zone for Single Tilt System' + PanelTypeOutOfBounds = 'Invalid Panel Type (Not in 1-4)' + PanelTypeTooFewCornersSingleTilt = 'Input file must have at least 4 corner modules per subarray' + PanelTypeTooFewCornersDualTilt = 'Input file must have at least 2 corner modules per subarray' + UnknownFileUploaded = 'Please upload a valid .txt or .dxf file' + ExpectedTxtFile = 'Invalid file uploaded. Please upload a valid .txt file.' + ExpectedDxfFile = 'Invalid file uploaded. Please upload a valid .dxf file.' + OldDxfFormat = 'Invalid Aurora format uploaded. Please upload a new format.' + + @classmethod + def invalid_wind_zones(cls, system_type): + if system_type == SystemType.singleTilt: + return cls.SingleTiltWindZone + else: + return cls.DualTiltWindZone + + @classmethod + def panel_type_too_few_corners(cls, system_type): + return { + SystemType.singleTilt: cls.PanelTypeTooFewCornersSingleTilt, + SystemType.dualTilt: cls.PanelTypeTooFewCornersDualTilt, + }[system_type] + + +class FileValidationError(object): + def __init__(self, validation_message, row_number): + self.row_number = row_number + self.validation_message = validation_message + + def format_error_message(self): + if self.row_number: + return self.validation_message.value + ' on line ' + str(self.row_number) + return self.validation_message.value + + def __repr__(self): + return "" + + def __eq__(self, other): + if self.__class__ != other.__class__: + return False + return self.row_number == other.row_number and self.validation_message == other.validation_message diff --git a/helix/constants/global_constants.py b/helix/constants/global_constants.py new file mode 100644 index 0000000..9d457e1 --- /dev/null +++ b/helix/constants/global_constants.py @@ -0,0 +1,9 @@ +k_d = 0.85 +k_zt = 1. + +parapet_factor_max = 1.12 +parapet_coefficients = 0.88, 1.2 + +system_force_capacity = 418. + +minimum_racking_capacity = 226 diff --git a/helix/constants/inverter_brand.py b/helix/constants/inverter_brand.py new file mode 100644 index 0000000..80f7a7a --- /dev/null +++ b/helix/constants/inverter_brand.py @@ -0,0 +1,25 @@ +from enum import IntEnum + + +class InverterBrand(IntEnum): + SMA = 1 + DELTA = 2 + + @property + def label(self): + return { + self.SMA: 'SMA', + self.DELTA: 'Delta', + }[self] + + @classmethod + def default_value(cls): + return cls.SMA.value + + @classmethod + def all(cls): + return [cls.SMA, cls.DELTA] + + @classmethod + def dict(cls): + return {cls.SMA.label: cls.SMA.value, cls.DELTA.label: cls.DELTA.value} diff --git a/helix/constants/inverter_type.py b/helix/constants/inverter_type.py new file mode 100644 index 0000000..eadfcb6 --- /dev/null +++ b/helix/constants/inverter_type.py @@ -0,0 +1,94 @@ +from enum import IntEnum + + +class InverterTypeSMA(IntEnum): + MODEL_12KW = 4 + MODEL_15KW = 5 + MODEL_20KW = 6 + MODEL_24KW = 8 + + @property + def default_string(self): + return { + self.MODEL_12KW: 4, + self.MODEL_15KW: 5, + self.MODEL_20KW: 6, + self.MODEL_24KW: 8, + }[self] + + @property + def label(self): + return { + self.MODEL_12KW: '12kW SMA Tripower - 514686', + self.MODEL_15KW: '15kW SMA Tripower - 514687', + self.MODEL_20KW: '20kW SMA Tripower - 512676', + self.MODEL_24KW: '24kW SMA Tripower - 514685', + }[self] + + @property + def valid_string_ranges(self): + return { + self.MODEL_12KW: range(2, 9), + self.MODEL_15KW: range(2, 9), + self.MODEL_20KW: range(2, 9), + self.MODEL_24KW: range(2, 9), + }[self] + + @classmethod + def default_value(cls): + return cls.MODEL_24KW.value + + @classmethod + def all(cls): + return [cls.MODEL_12KW, cls.MODEL_15KW, cls.MODEL_20KW, cls.MODEL_24KW] + + +class InverterTypeDelta(IntEnum): + MODEL_36KW = 9 + MODEL_42KW = 10 + MODEL_60KW = 11 + MODEL_80KW = 12 + + @property + def label(self): + return { + self.MODEL_36KW: '36kW Delta - 524952', + self.MODEL_42KW: '42kW Delta - 524969', + self.MODEL_60KW: '60kW Delta - 524954', + self.MODEL_80KW: '80kW Delta - 524955', + }[self] + + @property + def default_string(self): + return { + self.MODEL_36KW: None, + self.MODEL_42KW: None, + self.MODEL_60KW: None, + self.MODEL_80KW: 14, + }[self] + + @property + def valid_string_ranges(self): + return { + self.MODEL_36KW: None, + self.MODEL_42KW: None, + self.MODEL_60KW: None, + self.MODEL_80KW: range(14, 25), + }[self] + + @classmethod + def default_value(cls): + return cls.MODEL_60KW.value + + @classmethod + def all(cls): + return [cls.MODEL_36KW, cls.MODEL_42KW, cls.MODEL_60KW, cls.MODEL_80KW] + + +class InverterType: + SMA = InverterTypeSMA + DELTA = InverterTypeDelta + + @classmethod + def all(cls): + return cls.SMA.all() + cls.DELTA.all() diff --git a/helix/constants/module_type.py b/helix/constants/module_type.py new file mode 100644 index 0000000..fa61097 --- /dev/null +++ b/helix/constants/module_type.py @@ -0,0 +1,11 @@ +from enum import Enum + + +class ModuleType(Enum): + Cell96 = '96 Cell' + Cell128 = '128 Cell' + PSeries = 'P-Series' + + @classmethod + def default_value(cls): + return cls.Cell96.value diff --git a/helix/constants/module_type_constants/__init__.py b/helix/constants/module_type_constants/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/helix/constants/module_type_constants/dual_tilt_128_cell_constants.py b/helix/constants/module_type_constants/dual_tilt_128_cell_constants.py new file mode 100644 index 0000000..1fd0931 --- /dev/null +++ b/helix/constants/module_type_constants/dual_tilt_128_cell_constants.py @@ -0,0 +1,113 @@ +from numpy import array + +from helix.constants.global_constants import minimum_racking_capacity +from helix.constants.panel_type import PanelType + + +class DualTilt128CellConstants(object): + panel_spacing = (88.24, 82.0) # inches + spacing_size_inches = 5.12 # This is inches + presenter_spacing = (1.5, 1.5) + panel_area = 23.29 + surface_area = 46.58 # aka tent area + tributary_area = array([3.23, 6.84, 6.54, 13.57]) + ground_coverage_ratio = 0.91 + friction_coefficient = 0.59 + seismic_anchor_interval_constants = (13.8768, 3.23792) + max_psf = 32 + + def c_p_lower_bound(self): + return array([0.06, 0.05, 0.052, 0.039]) + + def edge_factor(self, wind_zone, panel_type): + return 1 + + def racking_capacity(self, panel_type): + if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth: + return minimum_racking_capacity + else: + return 308 + + def c_p_constants(self, A_n, wind_zone): + if wind_zone == 'A': + if A_n < 825: + return -0.144, 1.1451 + else: + return -0.058, 0.5651 + elif wind_zone == 'B': + if A_n < 465: + return -0.117, 0.8824 + elif A_n < 825: + return -0.087, 0.6981 + else: + return -0.033, 0.3336 + elif wind_zone == 'C': + if A_n < 465: + return -0.073, 0.5479 + elif A_n < 825: + return -0.035, 0.3143 + else: + return -0.015, 0.184 + elif wind_zone == 'D': + if A_n < 465: + return -0.039, 0.303 + elif A_n < 825: + return -0.023, 0.2023 + else: + return -0.008, 0.102 + else: + return 1, 1 + + def base_weight(self, panel_type, tray_count): + if panel_type == PanelType.Corner: + return [108.66, 110.96, 112.11, 116.44, 119.62, 122.80, 125.98][tray_count] + elif panel_type == PanelType.NorthSouth: + return [107.58, 109.88, 111.03, 114.21, 117.39, 120.57, 123.75][tray_count] + elif panel_type == PanelType.EastWest: + return [103.19, 105.49, 105.49, 108.67, 111.85, 115.03, 118.21][tray_count] + else: + return [102.11, 104.41, 104.41, 107.59, 110.77, 113.95, 117.13][tray_count] + + def link_tray_thresholds(self, panel_type): + if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth: + return [7.5, 10, 15] + else: + return [5, 7.5, 10] + + def cross_tray_thresholds(self, panel_type): + if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth: + return [15, 23, 31, 39, 47] + else: + return [10, 18, 26, 34, 42] + + def weighted_average_c_p(self, corner, north_south, east_west, middle): + return (corner * 2. + east_west * 26. + middle * 104. + north_south * 8.) / 140. + + def minimum_a_n_coefficients(self, uplift_c_p, wind_zone): + if wind_zone == 'A': + if uplift_c_p > 0.275: + return 1.5305, 0.187 + else: + return 0.9252, 0.097 + elif wind_zone == 'B': + if uplift_c_p > 0.2292: + return 1.2058, 0.159 + elif uplift_c_p > 0.1534: + return 1.0334, 0.131 + else: + return 0.4411, 0.043 + elif wind_zone == 'C': + if uplift_c_p > 0.151: + return 0.7899, 0.104 + elif uplift_c_p > 0.108: + return 0.5785, 0.07 + else: + return 0.2921, 0.027 + elif wind_zone == 'D': + if uplift_c_p > 0.0929: + return 0.3939, 0.049 + elif uplift_c_p > 0.069: + return 0.3043, 0.035 + else: + return 0.122, 0.008 + return None diff --git a/helix/constants/module_type_constants/dual_tilt_96_cell_constants.py b/helix/constants/module_type_constants/dual_tilt_96_cell_constants.py new file mode 100644 index 0000000..3a3d15a --- /dev/null +++ b/helix/constants/module_type_constants/dual_tilt_96_cell_constants.py @@ -0,0 +1,121 @@ +from numpy import array + +from helix.constants.global_constants import minimum_racking_capacity +from helix.constants.panel_type import PanelType + + +class DualTilt96CellConstants(object): + panel_spacing = (88.24, 62.0) # inches + spacing_size_inches = 5.12 # This is inches + presenter_spacing = (1.5, 1) + panel_area = 17.58 + surface_area = 35.14 # aka tent area + tributary_area = array([3.32, 6.58, 7.29, 21.74]) # for each panel type + ground_coverage_ratio = 0.91 + friction_coefficient = 0.59 + seismic_anchor_interval_constants = (10.1598, 2.3706) + max_psf = 48 + + def c_p_lower_bound(self): + return array([0.06, 0.05, 0.052, 0.039]) + + def edge_factor(self, wind_zone, panel_type): + return 1 + + def racking_capacity(self, panel_type): + if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth: + return minimum_racking_capacity + else: + return 308 + + def c_p_constants(self, A_n, wind_zone): + if wind_zone == 'A': + if A_n < 825: + return -0.144, 1.1451 + else: + return -0.058, 0.5651 + elif wind_zone == 'B': + if A_n < 465: + return -0.117, 0.8824 + elif A_n < 825: + return -0.087, 0.6981 + else: + return -0.033, 0.3336 + elif wind_zone == 'C': + if A_n < 465: + return -0.073, 0.5479 + elif A_n < 825: + return -0.035, 0.3143 + else: + return -0.015, 0.184 + elif wind_zone == 'D': + if A_n < 465: + return -0.039, 0.303 + elif A_n < 825: + return -0.023, 0.2023 + else: + return -0.008, 0.102 + else: + return 1, 1 + + def base_weight(self, panel_type, tray_count): + if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth: + return [92.58, + 94.31, + 96.03, + 98.33, + 100.63, + 102.93, + 105.23][tray_count] + else: + return [87.11, + 88.84, + 89.41, + 91.71, + 94.01, + 96.31, + 98.61][tray_count] + + def link_tray_thresholds(self, panel_type): + if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth: + return [7.5, 10, 15] + else: + return [5, 7.5, 10] + + def cross_tray_thresholds(self, panel_type): + if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth: + return [15, 24, 33, 42, 51] + else: + return [10, 19, 28, 37, 46] + + def weighted_average_c_p(self, corner, north_south, east_west, middle): + return (corner * 2. + east_west * 26. + middle * 104. + north_south * 8.) / 140. + + def minimum_a_n_coefficients(self, uplift_c_p, wind_zone): + if wind_zone == 'A': + if uplift_c_p > 0.275: + return 1.5305, 0.187 + else: + return 0.9252, 0.097 + elif wind_zone == 'B': + if uplift_c_p > 0.2292: + return 1.2058, 0.159 + elif uplift_c_p > 0.1537: + return 1.0334, 0.131 + else: + return 0.4411, 0.043 + elif wind_zone == 'C': + if uplift_c_p > 0.151: + return 0.7899, 0.104 + elif uplift_c_p > 0.108: + return 0.5785, 0.07 + else: + return 0.2921, 0.027 + elif wind_zone == 'D': + if uplift_c_p > 0.093: + return 0.3939, 0.049 + elif uplift_c_p > 0.069: + return 0.3043, 0.035 + else: + return 0.122, 0.008 + return None diff --git a/helix/constants/module_type_constants/dual_tilt_pseries_constants.py b/helix/constants/module_type_constants/dual_tilt_pseries_constants.py new file mode 100644 index 0000000..542aac0 --- /dev/null +++ b/helix/constants/module_type_constants/dual_tilt_pseries_constants.py @@ -0,0 +1,113 @@ +from numpy import array + +from helix.constants.global_constants import minimum_racking_capacity +from helix.constants.panel_type import PanelType + + +class DualTiltPSeriesConstants(object): + panel_spacing = (88.24, 82.0) # inches + spacing_size_inches = 8.8503937 # This is inches + presenter_spacing = (1.5, 1.5) + panel_area = 22.22 + surface_area = 44.44 # aka tent area + tributary_area = array([3.23, 6.84, 6.54, 13.57]) + ground_coverage_ratio = 0.91 + friction_coefficient = 0.59 + seismic_anchor_interval_constants = (12.6378, 2.94882) + max_psf = 32 + + def c_p_lower_bound(self): + return array([0.06, 0.05, 0.052, 0.039]) + + def edge_factor(self, wind_zone, panel_type): + return 1 + + def racking_capacity(self, panel_type): + if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth: + return minimum_racking_capacity + else: + return 308 + + def c_p_constants(self, A_n, wind_zone): + if wind_zone == 'A': + if A_n < 825: + return -0.144, 1.1451 + else: + return -0.058, 0.5651 + elif wind_zone == 'B': + if A_n < 465: + return -0.117, 0.8824 + elif A_n < 825: + return -0.087, 0.6981 + else: + return -0.033, 0.3336 + elif wind_zone == 'C': + if A_n < 465: + return -0.073, 0.5479 + elif A_n < 825: + return -0.035, 0.3143 + else: + return -0.015, 0.184 + elif wind_zone == 'D': + if A_n < 465: + return -0.039, 0.303 + elif A_n < 825: + return -0.023, 0.2023 + else: + return -0.008, 0.102 + else: + return 1, 1 + + def base_weight(self, panel_type, tray_count): + if panel_type == PanelType.Corner: + return [103.66, 105.96, 107.11, 111.44, 114.62, 117.80, 120.98][tray_count] + elif panel_type == PanelType.NorthSouth: + return [102.58, 104.88, 106.03, 109.21, 112.39, 115.57, 118.75][tray_count] + elif panel_type == PanelType.EastWest: + return [98.19, 100.49, 100.49, 103.67, 106.85, 110.03, 113.21][tray_count] + else: + return [97.11, 99.41, 99.41, 102.59, 105.77, 108.95, 112.13][tray_count] + + def link_tray_thresholds(self, panel_type): + if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth: + return [7.5, 10, 15] + else: + return [5, 7.5, 10] + + def cross_tray_thresholds(self, panel_type): + if panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth: + return [15, 23, 31, 39, 47] + else: + return [10, 18, 26, 34, 42] + + def weighted_average_c_p(self, corner, north_south, east_west, middle): + return (corner * 2. + east_west * 26. + middle * 104. + north_south * 8.) / 140. + + def minimum_a_n_coefficients(self, uplift_c_p, wind_zone): + if wind_zone == 'A': + if uplift_c_p > 0.275: + return 1.5305, 0.187 + else: + return 0.9252, 0.097 + elif wind_zone == 'B': + if uplift_c_p > 0.2292: + return 1.2058, 0.159 + elif uplift_c_p > 0.1537: + return 1.0334, 0.131 + else: + return 0.4411, 0.043 + elif wind_zone == 'C': + if uplift_c_p > 0.151: + return 0.7899, 0.104 + elif uplift_c_p > 0.108: + return 0.5785, 0.07 + else: + return 0.2921, 0.027 + elif wind_zone == 'D': + if uplift_c_p > 0.093: + return 0.3939, 0.049 + elif uplift_c_p > 0.069: + return 0.3043, 0.035 + else: + return 0.122, 0.008 + return None diff --git a/helix/constants/module_type_constants/single_tilt_128_cell_constants.py b/helix/constants/module_type_constants/single_tilt_128_cell_constants.py new file mode 100644 index 0000000..613eb31 --- /dev/null +++ b/helix/constants/module_type_constants/single_tilt_128_cell_constants.py @@ -0,0 +1,236 @@ + +from numpy import array +from numpy.ma import log + +from helix.constants.global_constants import minimum_racking_capacity +from helix.constants.panel_type import PanelType +from helix.constants.system_type_constants.single_tilt_constants import SingleTiltConstants + + +class SingleTilt128CellConstants(object): + panel_spacing = (82.0, 60.0) # inches + presenter_spacing = (1.5, 1) + panel_area = 23.29 + surface_area = 23.29 + tributary_area = array([1.87, 3.24, 3.24, 3.82]) + ground_coverage_ratio = 0.67 + friction_coefficient = 0.42 + seismic_anchor_interval_constants = (9.8784, 2.30496) + system_constants = SingleTiltConstants() + max_psf = 32 + + def c_p_lower_bound(self): + c_p_lower_bound = [] + for index, area in enumerate(self.tributary_area): + if area < 9: + c_p = -0.0273 * log(area) + 0.1401 + else: + c_p = -0.0146 * log(area) + 0.112 + edge_factor = self.edge_factor(self.system_constants.wind_zones[-1], PanelType.from_index(index)) + c_p_lower_bound.append(c_p * edge_factor) + return array(c_p_lower_bound) + + def edge_factor(self, wind_zone, panel_type): + # corner, north/south, east/west, middle + edge_matrix = {'A': [1.2, 1.2, 1.0, 1.0], + 'B': [1.2, 1.2, 1.0, 1.0], + 'C': [1.4, 1.4, 1.0, 1.0], + 'D': [1.4, 1.4, 1.0, 1.0], + 'E': [1.5, 1.0, 1.5, 1.0], + 'F': [1.3, 1.3, 1.0, 1.0], + 'G': [1.0, 1.0, 1.0, 1.0], + 'H': [1.0, 1.0, 1.0, 1.0], + 'I': [1.0, 1.0, 1.0, 1.0], + 'J': [2.0, 2.0, 1.0, 1.0], + 'K': [1.4, 1.4, 1.0, 1.0]} + return edge_matrix[wind_zone][panel_type.index()] + + def racking_capacity(self, panel_type): + if panel_type == PanelType.Corner or panel_type == PanelType.EastWest: + return minimum_racking_capacity + else: + return 308 + + def c_p_constants(self, A_n, wind_zone): + if wind_zone == 'A': + if A_n < 421.5: + return -0.2245, 1.717 + elif A_n < 2294.7: + return -0.1298, 1.1446 + else: + return -0.0308, 0.3787 + elif wind_zone == 'B': + if A_n < 421.5: + return -0.1818, 1.3585 + elif A_n < 2294.7: + return -0.0708, 0.688 + else: + return -0.0308, 0.3787 + elif wind_zone == 'C': + if A_n < 421.5: + return -0.0735, 0.6443 + elif A_n < 2294.7: + return -0.059, 0.5566 + else: + return -0.021, 0.2627 + elif wind_zone == 'D': + if A_n < 187.3: + return -0.1007, 0.6868 + elif A_n < 421.5: + return -0.049, 0.4181 + else: + return -0.0183, 0.2304 + elif wind_zone == 'E': + if A_n < 187.3: + return -0.058, 0.4236 + elif A_n < 421.5: + return -0.0518, 0.391 + else: + return -0.0125, 0.1533 + elif wind_zone == 'F': + if A_n < 187.3: + return -0.0655, 0.4682 + elif A_n < 421.5: + return -0.0518, 0.391 + else: + return -0.0125, 0.1533 + elif wind_zone == 'G': + if A_n < 187.3: + return -0.0341, 0.2786 + elif A_n < 421.5: + return -0.0271, 0.242 + else: + return -0.0125, 0.1533 + elif wind_zone == 'H': + if A_n < 187.3: + return -0.1161, 0.7872 + elif A_n < 421.5: + return -0.074, 0.5672 + elif A_n < 1685.9: + return -0.0433, 0.3816 + else: + return -0.0117, 0.1473 + elif wind_zone == 'I': + if A_n < 187.3: + return -0.3856, 2.258 + elif A_n < 421.5: + return -0.148, 1.0143 + elif A_n < 1685.9: + return -0.0433, 0.3816 + else: + return -0.0117, 0.1473 + elif wind_zone == 'J': + if A_n < 187.3: + return -0.1024, 0.6557 + elif A_n < 421.5: + return -0.0518, 0.391 + else: + return -0.0125, 0.1533 + else: + return 1, 1 + + def base_weight(self, panel_type, tray_count): + if panel_type == PanelType.Corner: + return [71.91, 71.91, 75.09, 78.27][tray_count] + elif panel_type == PanelType.NorthSouth: + return [65.8, 65.8, 68.98, 72.16][tray_count] + elif panel_type == PanelType.EastWest: + return [69.75, 72.05, 75.23, 78.41][tray_count] + else: + return [65.08, 67.38, 70.56, 73.74][tray_count] + + def link_tray_thresholds(self, panel_type): + return [[0, 13.0], + [0, 10.00], + [7, 14.5], + [6, 11.0]][panel_type.index()] + + def cross_tray_thresholds(self, panel_type): + return [[13.0, 21.0, 29.0], + [10.00, 18.0, 26.0], + [14.5, 22.5, 30.5], + [11.0, 19.0, 27.0]][panel_type.index()] + + def weighted_average_c_p(self, corner, north_south, east_west, middle): + """ Based on a 30 by 28 rectangular array """ + return (middle * 182. + north_south * 14. + east_west * 13. + corner) / 210. + + def minimum_a_n_coefficients(self, uplift_c_p, wind_zone): + if wind_zone == 'A': + if uplift_c_p > 0.360: + return 3.1178, 0.3769 + elif uplift_c_p > 0.180: + return 2.4967, 0.274 + else: + return 0.9691, 0.0685 + elif wind_zone == 'B': + if uplift_c_p > 0.260: + return 2.3762, 0.2807 + elif uplift_c_p > 0.160: + return 1.7699, 0.1803 + else: + return 0.7209, 0.0392 + elif wind_zone == 'C': + if uplift_c_p > 0.200: + return 1.7251, 0.215 + elif uplift_c_p > 0.100: + return 1.2668, 0.1274 + else: + return 0.4655, 0.0196 + elif wind_zone == 'D': + if uplift_c_p > 0.120: + return 2.3762, 0.2807 + elif uplift_c_p > 0.094: + return 1.7699, 0.1803 + else: + return 0.7209, 0.0392 + elif wind_zone == 'E': + if uplift_c_p > 0.078: + return 0.8096, 0.0976 + elif uplift_c_p > 0.060: + return 0.438, 0.0361 + else: + return 0.3227, 0.0206 + elif wind_zone == 'F': + if uplift_c_p > 0.078: + return 0.6281, 0.0708 + else: + return 0.328, 0.0212 + elif wind_zone == 'G': + if uplift_c_p > 0.078: + return 0.6281, 0.0708 + else: + return 0.328, 0.0212 + elif wind_zone == 'H': + if uplift_c_p > 0.180: + return 1.8215, 0.2525 + elif uplift_c_p > 0.120: + return 1.4679, 0.185 + elif uplift_c_p > 0.060: + return 1.13479, 0.1298 + else: + return 0.3227, 0.0206 + elif wind_zone == 'I': + if uplift_c_p > 0.240: + return 4.3609, 0.6996 + elif uplift_c_p > 0.120: + return 2.639, 0.3699 + elif uplift_c_p > 0.060: + return 1.4027, 0.1659 + else: + return 0.3277, 0.0206 + elif wind_zone == 'J': + if uplift_c_p > 0.120: + return 0.7131, 0.0891 + elif uplift_c_p > 0.078: + return 0.5503, 0.058 + else: + return 0.328, 0.0212 + elif wind_zone == 'K': + if uplift_c_p > 0.080: + return 0.3, 0.0455 + else: + return 0.2465, 0.0212 + else: + return 1, 1 + diff --git a/helix/constants/module_type_constants/single_tilt_96_cell_constants.py b/helix/constants/module_type_constants/single_tilt_96_cell_constants.py new file mode 100644 index 0000000..5e69666 --- /dev/null +++ b/helix/constants/module_type_constants/single_tilt_96_cell_constants.py @@ -0,0 +1,231 @@ +from numpy import array +from numpy.ma import log + +from helix.constants.global_constants import minimum_racking_capacity +from helix.constants.panel_type import PanelType +from helix.constants.system_type_constants.single_tilt_constants import SingleTiltConstants + + +class SingleTilt96CellConstants(object): + panel_spacing = (62.0, 60.0) # inches + presenter_spacing = (1, 1) + panel_area = 17.58 + surface_area = 17.57 + tributary_area = array([2.07, 4.06, 4.06, 9.79]) + ground_coverage_ratio = 0.67 + friction_coefficient = 0.42 + seismic_anchor_interval_constants = (7.2324, 1.6876) + system_constants = SingleTiltConstants() + max_psf = 48 + + def c_p_lower_bound(self): + c_p_lower_bound = [] + for index, area in enumerate(self.tributary_area): + if area < 9: + c_p = -0.0273 * log(area) + 0.1401 + else: + c_p = -0.0146 * log(area) + 0.112 + edge_factor = self.edge_factor(self.system_constants.wind_zones[-1], PanelType.from_index(index)) + c_p_lower_bound.append(c_p * edge_factor) + return array(c_p_lower_bound) + + def edge_factor(self, wind_zone, panel_type): + # corner, north/south, east/west, middle + edge_matrix = {'A': [1.2, 1.2, 1.0, 1.0], + 'B': [1.2, 1.2, 1.0, 1.0], + 'C': [1.4, 1.4, 1.0, 1.0], + 'D': [1.4, 1.4, 1.0, 1.0], + 'E': [1.5, 1.0, 1.5, 1.0], + 'F': [1.3, 1.3, 1.0, 1.0], + 'G': [1.0, 1.0, 1.0, 1.0], + 'H': [1.0, 1.0, 1.0, 1.0], + 'I': [1.0, 1.0, 1.0, 1.0], + 'J': [2.0, 2.0, 1.0, 1.0], + 'K': [1.4, 1.4, 1.0, 1.0]} + return edge_matrix[wind_zone][panel_type.index()] + + def racking_capacity(self, panel_type): + if panel_type == PanelType.Corner or panel_type == PanelType.EastWest: + return minimum_racking_capacity + else: + return 308 + + def c_p_constants(self, A_n, wind_zone): + if wind_zone == 'A': + if A_n < 421.5: + return -0.2245, 1.717 + elif A_n < 2294.7: + return -0.1298, 1.1446 + else: + return -0.0308, 0.3787 + elif wind_zone == 'B': + if A_n < 421.5: + return -0.1818, 1.3585 + elif A_n < 2294.7: + return -0.0708, 0.688 + else: + return -0.0308, 0.3787 + elif wind_zone == 'C': + if A_n < 421.5: + return -0.0735, 0.6443 + elif A_n < 2294.7: + return -0.059, 0.5566 + else: + return -0.021, 0.2627 + elif wind_zone == 'D': + if A_n < 187.3: + return -0.1007, 0.6868 + elif A_n < 421.5: + return -0.049, 0.4181 + else: + return -0.0183, 0.2304 + elif wind_zone == 'E': + if A_n < 187.3: + return -0.058, 0.4236 + elif A_n < 421.5: + return -0.0518, 0.391 + else: + return -0.0125, 0.1533 + elif wind_zone == 'F': + if A_n < 187.3: + return -0.0655, 0.4682 + elif A_n < 421.5: + return -0.0518, 0.391 + else: + return -0.0125, 0.1533 + elif wind_zone == 'G': + if A_n < 187.3: + return -0.0341, 0.2786 + elif A_n < 421.5: + return -0.0271, 0.242 + else: + return -0.0125, 0.1533 + elif wind_zone == 'H': + if A_n < 187.3: + return -0.1161, 0.7872 + elif A_n < 421.5: + return -0.074, 0.5672 + elif A_n < 1685.9: + return -0.0433, 0.3816 + else: + return -0.0117, 0.1473 + elif wind_zone == 'I': + if A_n < 187.3: + return -0.3856, 2.258 + elif A_n < 421.5: + return -0.148, 1.0143 + elif A_n < 1685.9: + return -0.0433, 0.3816 + else: + return -0.0117, 0.1473 + elif wind_zone == 'J': + if A_n < 187.3: + return -0.1024, 0.6557 + elif A_n < 421.5: + return -0.0518, 0.391 + else: + return -0.0125, 0.1533 + else: + return 1, 1 + + def base_weight(self, panel_type, tray_count): + return [[54.50, 54.50, 56.80, 59.10], + [49.47, 49.47, 51.77, 54.07], + [53.42, 55.72, 58.02, 60.32], + [48.75, 51.05, 53.35, 55.65]][panel_type.index()][tray_count] + + def link_tray_thresholds(self, panel_type): + return [[0, 12.0], + [0, 9.00], + [6, 13.5], + [5, 10.0]][panel_type.index()] + + def cross_tray_thresholds(self, panel_type): + return [[12.0, 21.0, 30.0], + [9.00, 18.0, 27.0], + [13.5, 22.5, 31.5], + [10.0, 19.0, 28.0]][panel_type.index()] + + def weighted_average_c_p(self, corner, north_south, east_west, middle): + """ Based on a 30 by 28 rectangular array """ + return (middle * 182. + north_south * 14. + east_west * 13. + corner) / 210. + + def minimum_a_n_coefficients(self, uplift_c_p, wind_zone): + if wind_zone == 'A': + if uplift_c_p > 0.360: + return 3.1178, 0.3769 + elif uplift_c_p > 0.180: + return 2.4967, 0.274 + else: + return 0.9691, 0.0685 + elif wind_zone == 'B': + if uplift_c_p > 0.260: + return 2.3762, 0.2807 + elif uplift_c_p > 0.160: + return 1.7699, 0.1803 + else: + return 0.7209, 0.0392 + elif wind_zone == 'C': + if uplift_c_p > 0.200: + return 1.7251, 0.215 + elif uplift_c_p > 0.100: + return 1.2668, 0.1274 + else: + return 0.4655, 0.0196 + elif wind_zone == 'D': + if uplift_c_p > 0.120: + return 2.3762, 0.2807 + elif uplift_c_p > 0.094: + return 1.7699, 0.1803 + else: + return 0.7209, 0.0392 + elif wind_zone == 'E': + if uplift_c_p > 0.078: + return 0.8096, 0.0976 + elif uplift_c_p > 0.060: + return 0.438, 0.0361 + else: + return 0.3227, 0.0206 + elif wind_zone == 'F': + if uplift_c_p > 0.078: + return 0.6281, 0.0708 + else: + return 0.328, 0.0212 + elif wind_zone == 'G': + if uplift_c_p > 0.078: + return 0.6281, 0.0708 + else: + return 0.328, 0.0212 + elif wind_zone == 'H': + if uplift_c_p > 0.180: + return 1.8215, 0.2525 + elif uplift_c_p > 0.120: + return 1.4679, 0.185 + elif uplift_c_p > 0.060: + return 1.13479, 0.1298 + else: + return 0.3227, 0.0206 + elif wind_zone == 'I': + if uplift_c_p > 0.240: + return 4.3609, 0.6996 + elif uplift_c_p > 0.120: + return 2.639, 0.3699 + elif uplift_c_p > 0.060: + return 1.4027, 0.1659 + else: + return 0.3277, 0.0206 + elif wind_zone == 'J': + if uplift_c_p > 0.120: + return 0.7131, 0.0891 + elif uplift_c_p > 0.078: + return 0.5503, 0.058 + else: + return 0.328, 0.0212 + elif wind_zone == 'K': + if uplift_c_p > 0.080: + return 0.3, 0.0455 + else: + return 0.2465, 0.0212 + else: + return 1, 1 + diff --git a/helix/constants/module_type_constants/single_tilt_pseries_constants.py b/helix/constants/module_type_constants/single_tilt_pseries_constants.py new file mode 100644 index 0000000..43d316f --- /dev/null +++ b/helix/constants/module_type_constants/single_tilt_pseries_constants.py @@ -0,0 +1,235 @@ +from numpy import array +from numpy.ma import log + +from helix.constants.global_constants import minimum_racking_capacity +from helix.constants.panel_type import PanelType +from helix.constants.system_type_constants.single_tilt_constants import SingleTiltConstants + + +class SingleTiltPSeriesConstants(object): + panel_spacing = (82.0, 61.8755) # inches + presenter_spacing = (1.5, 1) + panel_area = 22.22 + surface_area = 22.22 + tributary_area = array([1.87, 3.24, 3.24, 3.82]) + ground_coverage_ratio = 0.67 + friction_coefficient = 0.42 + seismic_anchor_interval_constants = (8.9964, 2.09916) + max_psf = 32 + system_constants = SingleTiltConstants() + + def c_p_lower_bound(self): + c_p_lower_bound = [] + for index, area in enumerate(self.tributary_area): + if area < 9: + c_p = -0.0273 * log(area) + 0.1401 + else: + c_p = -0.0146 * log(area) + 0.112 + edge_factor = self.edge_factor(self.system_constants.wind_zones[-1], PanelType.from_index(index)) + c_p_lower_bound.append(c_p * edge_factor) + return array(c_p_lower_bound) + + def edge_factor(self, wind_zone, panel_type): + # corner, north/south, east/west, middle + edge_matrix = {'A': [1.2, 1.2, 1.0, 1.0], + 'B': [1.2, 1.2, 1.0, 1.0], + 'C': [1.4, 1.4, 1.0, 1.0], + 'D': [1.4, 1.4, 1.0, 1.0], + 'E': [1.5, 1.0, 1.5, 1.0], + 'F': [1.3, 1.3, 1.0, 1.0], + 'G': [1.0, 1.0, 1.0, 1.0], + 'H': [1.0, 1.0, 1.0, 1.0], + 'I': [1.0, 1.0, 1.0, 1.0], + 'J': [2.0, 2.0, 1.0, 1.0], + 'K': [1.4, 1.4, 1.0, 1.0]} + return edge_matrix[wind_zone][panel_type.index()] + + def racking_capacity(self, panel_type): + if panel_type == PanelType.Corner or panel_type == PanelType.EastWest: + return minimum_racking_capacity + else: + return 308 + + def c_p_constants(self, A_n, wind_zone): + if wind_zone == 'A': + if A_n < 421.5: + return -0.2245, 1.717 + elif A_n < 2294.7: + return -0.1298, 1.1446 + else: + return -0.0308, 0.3787 + elif wind_zone == 'B': + if A_n < 421.5: + return -0.1818, 1.3585 + elif A_n < 2294.7: + return -0.0708, 0.688 + else: + return -0.0308, 0.3787 + elif wind_zone == 'C': + if A_n < 421.5: + return -0.0735, 0.6443 + elif A_n < 2294.7: + return -0.059, 0.5566 + else: + return -0.021, 0.2627 + elif wind_zone == 'D': + if A_n < 187.3: + return -0.1007, 0.6868 + elif A_n < 421.5: + return -0.049, 0.4181 + else: + return -0.0183, 0.2304 + elif wind_zone == 'E': + if A_n < 187.3: + return -0.058, 0.4236 + elif A_n < 421.5: + return -0.0518, 0.391 + else: + return -0.0125, 0.1533 + elif wind_zone == 'F': + if A_n < 187.3: + return -0.0655, 0.4682 + elif A_n < 421.5: + return -0.0518, 0.391 + else: + return -0.0125, 0.1533 + elif wind_zone == 'G': + if A_n < 187.3: + return -0.0341, 0.2786 + elif A_n < 421.5: + return -0.0271, 0.242 + else: + return -0.0125, 0.1533 + elif wind_zone == 'H': + if A_n < 187.3: + return -0.1161, 0.7872 + elif A_n < 421.5: + return -0.074, 0.5672 + elif A_n < 1685.9: + return -0.0433, 0.3816 + else: + return -0.0117, 0.1473 + elif wind_zone == 'I': + if A_n < 187.3: + return -0.3856, 2.258 + elif A_n < 421.5: + return -0.148, 1.0143 + elif A_n < 1685.9: + return -0.0433, 0.3816 + else: + return -0.0117, 0.1473 + elif wind_zone == 'J': + if A_n < 187.3: + return -0.1024, 0.6557 + elif A_n < 421.5: + return -0.0518, 0.391 + else: + return -0.0125, 0.1533 + else: + return 1, 1 + + def base_weight(self, panel_type, tray_count): + if panel_type == PanelType.Corner: + return [66.91, 66.91, 70.09, 73.27][tray_count] + elif panel_type == PanelType.NorthSouth: + return [60.8, 60.8, 63.98, 67.16][tray_count] + elif panel_type == PanelType.EastWest: + return [64.75, 67.05, 70.23, 73.41][tray_count] + else: + return [60.08, 62.38, 65.56, 68.74][tray_count] + + def link_tray_thresholds(self, panel_type): + return [[0, 13.0], + [0, 10.00], + [7, 14.5], + [6, 11.0]][panel_type.index()] + + def cross_tray_thresholds(self, panel_type): + return [[13.0, 21.0, 29.0], + [10.00, 18.0, 26.0], + [14.5, 22.5, 30.5], + [11.0, 19.0, 27.0]][panel_type.index()] + + def weighted_average_c_p(self, corner, north_south, east_west, middle): + """ Based on a 30 by 28 rectangular array """ + return (middle * 182. + north_south * 14. + east_west * 13. + corner) / 210. + + def minimum_a_n_coefficients(self, uplift_c_p, wind_zone): + if wind_zone == 'A': + if uplift_c_p > 0.360: + return 3.1178, 0.3769 + elif uplift_c_p > 0.180: + return 2.4967, 0.274 + else: + return 0.9691, 0.0685 + elif wind_zone == 'B': + if uplift_c_p > 0.260: + return 2.3762, 0.2807 + elif uplift_c_p > 0.160: + return 1.7699, 0.1803 + else: + return 0.7209, 0.0392 + elif wind_zone == 'C': + if uplift_c_p > 0.200: + return 1.7251, 0.215 + elif uplift_c_p > 0.100: + return 1.2668, 0.1274 + else: + return 0.4655, 0.0196 + elif wind_zone == 'D': + if uplift_c_p > 0.120: + return 2.3762, 0.2807 + elif uplift_c_p > 0.094: + return 1.7699, 0.1803 + else: + return 0.7209, 0.0392 + elif wind_zone == 'E': + if uplift_c_p > 0.078: + return 0.8096, 0.0976 + elif uplift_c_p > 0.060: + return 0.438, 0.0361 + else: + return 0.3227, 0.0206 + elif wind_zone == 'F': + if uplift_c_p > 0.078: + return 0.6281, 0.0708 + else: + return 0.328, 0.0212 + elif wind_zone == 'G': + if uplift_c_p > 0.078: + return 0.6281, 0.0708 + else: + return 0.328, 0.0212 + elif wind_zone == 'H': + if uplift_c_p > 0.180: + return 1.8215, 0.2525 + elif uplift_c_p > 0.120: + return 1.4679, 0.185 + elif uplift_c_p > 0.060: + return 1.13479, 0.1298 + else: + return 0.3227, 0.0206 + elif wind_zone == 'I': + if uplift_c_p > 0.240: + return 4.3609, 0.6996 + elif uplift_c_p > 0.120: + return 2.639, 0.3699 + elif uplift_c_p > 0.060: + return 1.4027, 0.1659 + else: + return 0.3277, 0.0206 + elif wind_zone == 'J': + if uplift_c_p > 0.120: + return 0.7131, 0.0891 + elif uplift_c_p > 0.078: + return 0.5503, 0.058 + else: + return 0.328, 0.0212 + elif wind_zone == 'K': + if uplift_c_p > 0.080: + return 0.3, 0.0455 + else: + return 0.2465, 0.0212 + else: + return 1, 1 + diff --git a/helix/constants/panel_type.py b/helix/constants/panel_type.py new file mode 100644 index 0000000..6c09297 --- /dev/null +++ b/helix/constants/panel_type.py @@ -0,0 +1,46 @@ +from enum import Enum + + +class PanelType(Enum): + Corner = 0 + NorthSouth = 1 + EastWest = 2 + Middle = 3 + + @classmethod + def from_number(cls, number): + if number == 1: + return cls.Corner + elif number == 2: + return cls.NorthSouth + elif number == 3: + return cls.EastWest + elif number == 4: + return cls.Middle + return None + + @classmethod + def from_index(cls, index): + return PanelType(index) + + @classmethod + def all(cls): + return [cls.Corner, cls.NorthSouth, cls.EastWest, cls.Middle] + + def number(self): + return self.value + 1 + + def index(self): + return self.value + + def snake_case(self): + return { + PanelType.Corner: 'corner', + PanelType.NorthSouth: 'north_south', + PanelType.EastWest: 'east_west', + PanelType.Middle: 'middle', + }[self] + + def __sub__(self, other): # Used for testing (assert_array_almost_equal requires subtraction) + return self.value - other.value + diff --git a/helix/constants/parts.py b/helix/constants/parts.py new file mode 100644 index 0000000..d7f01a4 --- /dev/null +++ b/helix/constants/parts.py @@ -0,0 +1,241 @@ +# Mechanical Parts + +single_tilt_chassis = ('513831', 'CHASSIS, SINGLE TILT, HELIX ROOF') +dual_tilt_chassis = ('514056', 'BASE, CHASSIS, DUAL TILT, HELIX ROOF') +dual_tilt_platform = ('514057', 'PLATFORM, CHASSIS, DUAL TILT, HELIX ROOF') +platform_bolt = ('515063', 'SCREW, CAP, SH, M6 X 1 X 12, 18-8 SS (DIN 912)') + +left_deflector = ('513841', 'DEFLECTOR, LH, HELIX ROOF') +right_deflector = ('513842', 'DEFLECTOR, RH, HELIX ROOF') +front_skirt = ('515928', 'FRONT SKIRT, HELIX ROOF') +rear_skirt = ('515929', 'REAR SKIRT, HELIX ROOF') +spoiler = ('513836', 'SPOILER, SINGLE TILT, HELIX ROOF') + +rear_skirt_1_1 = ('520301', 'REAR SKIRT, HELIX ROOF V1.1') +spoiler_1_1 = ('520302', 'SPOILER, SINGLE TILT, HELIX ROOF V1.1') +front_skirt_1_1 = ('520303', 'FRONT SKIRT, HELIX ROOF V1.1') +cross_tray_1_1 = ('520306', 'TRAY, OPTIONAL BALLAST, HELIX ROOF V1.1') +left_deflector_1_1 = ('521794', 'DEFLECTOR, LH, HELIX ROOF V1.1') +right_deflector_1_1 = ('521795', 'DEFLECTOR, RH, HELIX ROOF V1.1') + +leading_tray = ('517871', 'TRAY, LEADING, HELIX ROOF, RIVETED VERSION') +following_tray = ('513832', 'TRAY, FOLLOWING, HELIX ROOF') +link_tray = ('513833', 'TRAY, LINK, HELIX ROOF') +cross_tray = ('513844', 'TRAY, OPTIONAL BALLAST, HELIX ROOF') + +anchor_plate = ('513843', 'PLATE, ANCHOR, HELIX ROOF') +anchor = ('TBD', 'Anchors') +anchor_washer = ('518477', 'WASHER, FLAT, 3/8, 1.00 OD, 18-8 SS') + +module = ('TBD', 'Modules') +ballast = ('Contractor Supplied', 'Ballast Blocks') +rubber_foot = ('514265', 'FOOT, RECYCLED RUBBER, HELIX ROOF') + +# Electrical Parts, per inverter + +flat_washer = ('104813', 'WASHER, FLAT, 3/8, .812 OD, 18-8 SS (1500-731)') + +channel_nut = ('106925', 'NUT, CHANNEL, 3/8-16, SS, UNISTRUT P3008 (1507-770)') +hex_nut_three_eighths_16 = ('107551', 'NUT, HEX, 3/8-16, 18-8 SS (5100-086)') +hex_bolt_3_4 = ('513007', 'BOLT, HH, 3/8-16 X 3/4, 316 SS') +hex_bolt_1_2 = ('514865', 'BOLT, HH, 3/8-16 X 1/2, 18-8 SS') + +front_legs = ('512660', 'FRONT LEG, INVERTER RACK, HELIX ROOF') +back_legs = ('512661', 'BACK LEGS, INVERTER RACK, HELIX ROOF') + +harness_2_string_fm = ('514437', 'HARNESS, DC COMBINATION, NO FUSE, 2 STRING, FEMALES TO MALE, HELIX') +harness_2_string_mf = ('514438', 'HARNESS, DC COMBINATION, NO FUSE, 2 STRING, MALES TO FEMALE, HELIX') + +harness_3_string_fm = ('514435', 'HARNESS, DC COMBINATION, W/ FUSE, 3 STRING, FEMALES TO MALE, HELIX') +harness_3_string_mf = ('514436', 'HARNESS, DC COMBINATION, W/ FUSE, 3 STRING, MALES TO FEMALE, HELIX') + +harness_4_string_fm = ('514439', 'HARNESS, DC COMBINATION, W/ FUSE, 4 STRING, FEMALES TO MALE, HELIX') +harness_4_string_mf = ('514440', 'HARNESS, DC COMBINATION, W/ FUSE, 4 STRING, MALES TO FEMALE, HELIX') + +inverter_rail = ('512663', 'RAIL, INVERTER RACK, HELIX ROOF') +inverter_link = ('512662', 'LINK TO ARRAY, INVERTER RACK, HELIX ROOF') +inverter_link_short = ('521798', 'LINK TO ARRAY, LONG, INVERTER RACK, HELIX ROOF V1.1') +inverter_link_long = ('521797', 'LINK TO ARRAY, SHORT, INVERTER RACK, HELIX ROOF V1.1') + +mounting_back_plate = ('518331', 'MOUNTING BACK PLATE, INVERTER/PANEL BOARD, HELIX ROOF/TRACKER') + +sma_12kw_inverter = ('514686', 'INVERTER, SMA, STP, 12000TL-US-10 (SPR-12000m-3 XXX), AFCI, CONNECTORIZED') +sma_15kw_inverter = ('514687', 'INVERTER, SMA, STP, 15000TL-US-10 (SPR-15000m-3 XXX), AFCI, CONNECTORIZED') +sma_20kw_inverter = ('512676', 'INVERTER, SMA, STP, 20000TL-US-10 (SPR-20000m-3 XXX), AFCI, CONNECTORIZED') +sma_24kw_inverter = ('514685', 'INVERTER, SMA, STP, 24000TL-US-10 (SPR-24000m-3 XXX), AFCI, CONNECTORIZED') + +delta_36kw_inverter = ('524952', 'INVERTER, DELTA, M36U_122(MC4), 10INPUT, 36KW, 3PH 480V AC,1000V DC') +delta_42kw_inverter = ('524969', 'INVERTER, DELTA, M42U_122(MC4), 12INPUT, 42KW, 3PH 480V AC,1000V DC') +delta_60kw_inverter = ('524954', 'INVERTER, DELTA, M60U_122 (MC4), 18INPUT, 60KW, 3PH 480V AC,1000V DC') +delta_80kw_inverter = ('524955', '-') + +screw_12_24x1_25 = ('507985', 'SCREW, S-D, HWH, #12-24X 1-1/4", #3 PT, BI-METAL') +# Wire management + +stump = ('512021', 'STUMP, WIRE MANAGEMENT, 50MM ID, HELIX ROOF') + +cable_support = ('512511', 'CABLE SUPPORT, HELIX ROOF') +cable_support_lid = ('512510', 'LID, CABLE SUPPORT, HELIX ROOF') + +wire_clip = ('512200', 'CLIP, WIRE FORMED, CABLE MANAGEMENT, INSIDE, 352MM ^ 2') +wire_clip_large = ('512199', 'CLIP, WIRE FORMED, CABLE MANAGEMENT, INSIDE, 1624MM ^ 2') + +# Panel Boards + +panel_board_4 = ('513299', 'COMBINER BOX, AC, 4 INPUT, NO AUX, W/ CONNECTOR') +panel_board_3 = ('513301', 'COMBINER BOX, AC, 3 INPUT, NO AUX, W/ CONNECTOR') +panel_board_2 = ('513303', 'COMBINER BOX, AC, 2 INPUT, NO AUX, W/ CONNECTOR') + +harness_ac_inner = ('514477', 'HARNESS, AC, INNER, 72", HELIX ROOFTOP') +harness_ac_outer = ('514478', 'HARNESS, AC, OUTER, 108", HELIX ROOFTOP') +whip_tray = ('515059', 'ASSY, WHIP TRAY W/FUSE CLIPS, INVERTER, HELIX') +comm_cable = ('514697', 'COMM CABLE, INVERTER DAISY CHAIN, 118", HELIX ROOF') + +ethernet_plug = ('518058', 'CONNECTOR, ETHERNET, PLUG, RJ-45, WEATHERPROOF, SHIELDED') + +# Aux Plug + +panel_board_4_aux = ('513300', 'COMBINER BOX, AC, 4 INPUT, W/ AUX, W/ CONNECTOR') +panel_board_3_aux = ('513302', 'COMBINER BOX, AC, 3 INPUT, W/ AUX, W/ CONNECTOR') +panel_board_2_aux = ('513304', 'COMBINER BOX, AC, 2 INPUT, W/ AUX, W/ CONNECTOR') + +# Monitoring + +monitor_power_plug = ('519008', 'HARNESS, MONITORING POWER CABLE, SINGLE CONNECTOR, HELIX ROOF') +monitor_controller_480_v = ('518059', 'CONTROLLER, MONITORING, COMMERCIAL, PVS5C BASED, 480VAC, US') +monitor_controller_240_v = ('517463', 'MONITORING SYSTEM, COMMERCIAL, <100KW, PVS5x BASED, 240VAC, US') + +# DC Switch related + +dc_switch_bracket = ('512575', 'BRACKET, DC SWITCH BOX, HELIX') +dc_switch_box = ('514698', 'DC SWITCH BOX, HELIX') + +hex_bolt_quarter_20 = ('114961', 'BOLT, HH, 1/4-20 X 3/4", 18-8SS') +hex_nut_quarter_20 = ('107549', 'NUT, HEX, 1/4-20, 18-8 SS (5100-084)') +flat_washer_quarter_inch = ('107586', 'WASHER, FLAT, 1/4, 0.5 OD, 18-8 SS (5100-144)') + +# Standalone Inverter + +star_washer = ('105317', 'WASHER, STAR, #6, SS (1501-606)') +flat_washer_6 = ('111147', 'WASHER, FLAT, #6, 18-8 SS (1509-097)') +phillips_screw = ('107538', 'SCREW, PH, 6-32 X 1/2, SS (5100-073)') +ac_splice_box = ('516045', 'AC SPLICE BOX, CONNECTORIZED, HELIX ROOF') +ac_switch = ('516043', 'AC SWITCH, CONNECTORIZED, HELIX ROOF') +ac_inverter_bracket = ('513586', 'BRACKET, INVERTER AC SWITCH, HELIX') +delta_kit_inverter_mount = ('524781', 'KIT, INVERTER MOUNT, DELTA, HELIX ROOF') +delta_kit_inverter_mount_dt = ('525772', 'KIT, INVERTER MOUNT, DELTA, DT, HELIX ROOF') +delta_splice_box = ('525651', 'KIT, AC SPLICE, DELTA, HELIX ROOF') +delta_inverter_leg = ('524783', 'INVERTER LEG, DELTA, HELIX ROOF') +delta_branch_connector = ('TBD', 'Branch connector') + +# Other Ebom + +sunshade = ('512910', 'SUN SHADE, INVERTER, HELIX') +sunshade_bolt = ('805615', 'SCREW, HEXAGONAL HEAD, M10X20, SS A2') +sunshade_washer = ('521031', 'WASHER, FLAT, M10 X 20MM OD, SS') +fuseshade = ('521363', 'FUSE SHADE, HELIX ROOF') +fuseshade_brace = ('522020', 'BRACE, FUSE SHADE, HELIX ROOF') + +# Package Sizes + +package_sizes = { + flat_washer: 50, + flat_washer_quarter_inch: 100, + channel_nut: 50, + hex_nut_three_eighths_16: 50, + hex_nut_quarter_20: 100, + hex_bolt_3_4: 50, + hex_bolt_quarter_20: 50, + hex_bolt_1_2: 50, + wire_clip_large: 10, + wire_clip: 30, + platform_bolt: 50, + star_washer: 100, + anchor_washer: 25 +} + +all_parts = [ + single_tilt_chassis, + dual_tilt_chassis, + dual_tilt_platform, + platform_bolt, + left_deflector, + right_deflector, + front_skirt, + rear_skirt, + spoiler, + rear_skirt_1_1, + spoiler_1_1, + front_skirt_1_1, + cross_tray_1_1, + leading_tray, + following_tray, + link_tray, + cross_tray, + anchor_plate, + anchor, + anchor_washer, + module, + ballast, + rubber_foot, + flat_washer, + channel_nut, + hex_nut_three_eighths_16, + hex_bolt_3_4, + hex_bolt_1_2, + front_legs, + back_legs, + harness_2_string_fm, + harness_2_string_mf, + harness_3_string_fm, + harness_3_string_mf, + harness_4_string_fm, + harness_4_string_mf, + inverter_rail, + inverter_link, + mounting_back_plate, + sma_12kw_inverter, + sma_15kw_inverter, + sma_20kw_inverter, + sma_24kw_inverter, + stump, + cable_support, + cable_support_lid, + wire_clip, + wire_clip_large, + panel_board_4, + panel_board_3, + panel_board_2, + harness_ac_inner, + harness_ac_outer, + whip_tray, + comm_cable, + ethernet_plug, + panel_board_4_aux, + panel_board_3_aux, + panel_board_2_aux, + monitor_power_plug, + monitor_controller_480_v, + dc_switch_bracket, + dc_switch_box, + hex_bolt_quarter_20, + hex_nut_quarter_20, + flat_washer_quarter_inch, + star_washer, + flat_washer_6, + phillips_screw, + ac_splice_box, + ac_switch, + ac_inverter_bracket, + sunshade, + sunshade_bolt, + sunshade_washer, + screw_12_24x1_25, + fuseshade_brace, + left_deflector_1_1, + right_deflector_1_1, + inverter_link_short, + inverter_link_long, + fuseshade, + monitor_controller_240_v +] diff --git a/helix/constants/redis_constant.py b/helix/constants/redis_constant.py new file mode 100644 index 0000000..417d5b3 --- /dev/null +++ b/helix/constants/redis_constant.py @@ -0,0 +1,7 @@ +import os + +from helix.db.redis_manager import RedisManager + +vcap_url = os.getenv('VCAP_SERVICES') # pws is best ws. +heroku_url = os.getenv('REDIS_URL') +redis_store = RedisManager.get_redis_connection(vcap_url, heroku_url) diff --git a/helix/constants/seismic_anchor_validation_error.py b/helix/constants/seismic_anchor_validation_error.py new file mode 100644 index 0000000..0ab1437 --- /dev/null +++ b/helix/constants/seismic_anchor_validation_error.py @@ -0,0 +1,5 @@ +from enum import Enum + + +class SeismicAnchorValidationError(Enum): + TooFewAnchors = 'There are too few anchors in one or more subarrays' diff --git a/helix/constants/single_tilt_parts.py b/helix/constants/single_tilt_parts.py new file mode 100644 index 0000000..ade1fcb --- /dev/null +++ b/helix/constants/single_tilt_parts.py @@ -0,0 +1,94 @@ +from helix.constants.module_type import ModuleType +from helix.constants.parts import * + + +class SingleTiltParts(object): + center_panel_parts = { + module: 1, + single_tilt_chassis: 1 + } + + sub_array_parts = { + leading_tray: 1 + } + + north_south_panel_parts = { + module: 1, + single_tilt_chassis: 0.5, + } + + def __init__(self, module_type): + if module_type == ModuleType.PSeries: + self.corner_panel_parts = { + module: 1, + single_tilt_chassis: 1, + left_deflector_1_1: 0.5, + right_deflector_1_1: 0.5, + } + self.east_west_panel_parts = { + module: 1, + single_tilt_chassis: 1.5, + left_deflector_1_1: 0.5, + right_deflector_1_1: 0.5 + } + else: + self.corner_panel_parts = { + module: 1, + single_tilt_chassis: 1, + left_deflector: 0.5, + right_deflector: 0.5 + } + self.east_west_panel_parts = { + module: 1, + single_tilt_chassis: 1.5, + left_deflector: 0.5, + right_deflector: 0.5 + } + + def row_parts(self, _): + return {} + + def column_parts(self, module_type): + if module_type == ModuleType.Cell96: + front_skirt_parts = front_skirt + else: + front_skirt_parts = front_skirt_1_1 + return { + front_skirt_parts: 1, + leading_tray: 1 + } + + def parts_per_panel_type(self): + return [ + self.corner_panel_parts, + self.north_south_panel_parts, + self.east_west_panel_parts, + self.center_panel_parts + ] + + def module(self, module_type): + if module_type == ModuleType.Cell96: + rear_skirt_parts = rear_skirt + spoiler_parts = spoiler + else: + rear_skirt_parts = rear_skirt_1_1 + spoiler_parts = spoiler_1_1 + return { + spoiler_parts: 1, + rear_skirt_parts: 1, + wire_clip: 2, + rubber_foot: 0.1 + } + + def dependent_parts(self, module_type): + return { + module: self.module(module_type), + leading_tray: { + following_tray: 1 + } + } + + def fudge_factors(self, _): + return { + single_tilt_chassis: 1.0525 + } diff --git a/helix/constants/sql_constant.py b/helix/constants/sql_constant.py new file mode 100644 index 0000000..55615c6 --- /dev/null +++ b/helix/constants/sql_constant.py @@ -0,0 +1,6 @@ +import os +from helix.db.sql_manager import SQLManager + + +def sql_session_maker(): + return SQLManager.get_sql_session_maker(os.getenv('DATABASE_URL')) diff --git a/helix/constants/subarray.py b/helix/constants/subarray.py new file mode 100644 index 0000000..8e2e468 --- /dev/null +++ b/helix/constants/subarray.py @@ -0,0 +1,6 @@ +''' +Created on May 22, 2017 + +@author: jvazquez +''' +SUBARRAY_SIZE_BIG = "Array size is too big. Max is 150' by 150'." diff --git a/helix/constants/system_type.py b/helix/constants/system_type.py new file mode 100644 index 0000000..25df255 --- /dev/null +++ b/helix/constants/system_type.py @@ -0,0 +1,60 @@ +from enum import Enum +from helix.constants.module_type import ModuleType +from helix.constants.module_type_constants.dual_tilt_128_cell_constants import DualTilt128CellConstants +from helix.constants.module_type_constants.dual_tilt_96_cell_constants import DualTilt96CellConstants + +from helix.constants.dual_tilt_parts import DualTiltParts +from helix.constants.module_type_constants.dual_tilt_pseries_constants import DualTiltPSeriesConstants +from helix.constants.module_type_constants.single_tilt_128_cell_constants import SingleTilt128CellConstants +from helix.constants.module_type_constants.single_tilt_96_cell_constants import SingleTilt96CellConstants +from helix.constants.module_type_constants.single_tilt_pseries_constants import SingleTiltPSeriesConstants +from helix.constants.single_tilt_parts import SingleTiltParts +from helix.constants.system_type_constants.dual_tilt_constants import DualTiltConstants +from helix.constants.system_type_constants.single_tilt_constants import SingleTiltConstants + + +class SystemType(Enum): + singleTilt = '0' + dualTilt = '1' + + def system_constants(self): + return { + SystemType.singleTilt: SingleTiltConstants(), + SystemType.dualTilt: DualTiltConstants() + }[self] + + def module_constants(self, module_type): + return { + SystemType.singleTilt: self.single_tilt_constants(module_type), + SystemType.dualTilt: self.dual_tilt_constants(module_type) + }[self] + + def parts(self, module_type): + return { + SystemType.singleTilt: SingleTiltParts(module_type), + SystemType.dualTilt: DualTiltParts(module_type) + }[self] + + def display_name(self): + return { + SystemType.singleTilt: 'Single-Tilt', + SystemType.dualTilt: 'Dual-Tilt' + }[self] + + @classmethod + def default_value(cls): + return cls.dualTilt.value + + def single_tilt_constants(self, module_type): + return { + ModuleType.Cell96: SingleTilt96CellConstants(), + ModuleType.Cell128: SingleTilt128CellConstants(), + ModuleType.PSeries: SingleTiltPSeriesConstants() + }[module_type] + + def dual_tilt_constants(self, module_type): + return { + ModuleType.Cell96: DualTilt96CellConstants(), + ModuleType.Cell128: DualTilt128CellConstants(), + ModuleType.PSeries: DualTiltPSeriesConstants() + }[module_type] diff --git a/helix/constants/system_type_constants/__init__.py b/helix/constants/system_type_constants/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/helix/constants/system_type_constants/dual_tilt_constants.py b/helix/constants/system_type_constants/dual_tilt_constants.py new file mode 100644 index 0000000..657132d --- /dev/null +++ b/helix/constants/system_type_constants/dual_tilt_constants.py @@ -0,0 +1,4 @@ +class DualTiltConstants(object): + wind_zones = ['A', 'B', 'C', 'D', 'E'] + module_count = 2 + minimum_corner_module_count = 2 diff --git a/helix/constants/system_type_constants/single_tilt_constants.py b/helix/constants/system_type_constants/single_tilt_constants.py new file mode 100644 index 0000000..fc857bf --- /dev/null +++ b/helix/constants/system_type_constants/single_tilt_constants.py @@ -0,0 +1,4 @@ +class SingleTiltConstants(object): + wind_zones = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K'] + module_count = 1 + minimum_corner_module_count = 4 diff --git a/helix/constants/version.py b/helix/constants/version.py new file mode 100644 index 0000000..31e505e --- /dev/null +++ b/helix/constants/version.py @@ -0,0 +1,7 @@ +import os + + +def version(): + if os.getenv('VERSION'): + return os.getenv('VERSION') + return 'release-9-72-g3e1c132' diff --git a/helix/csv_builder.py b/helix/csv_builder.py new file mode 100644 index 0000000..7645b16 --- /dev/null +++ b/helix/csv_builder.py @@ -0,0 +1,107 @@ +import csv +from enum import Enum +from io import StringIO + + +class PanelDataColumn(Enum): + Handle = 'HANDLE' + Blockname = 'BLOCKNAME' + Subarray = 'SUBARRAY' + PanelType = 'POS' + WindZone = 'WIND' + Ballast = 'BAL' + LinkTray = 'LT_CALCULATED' + CrossTray = 'XTRAY' + WindAnchor = 'ANC' + SeismicAnchor = 'SEISMIC' + Coordinate = 'COORDINATE' + Pressure = 'PSF' + Id = 'ID' + PresentedLinkTray = 'LTRAY' + Xcoord = 'XCOORD' + Ycoord = 'YCOORD' + Rotation = 'ANGLE' + FuzzyWindZone = 'FUZZYWINDZONE' + + +class CSVDataColumn(Enum): + PresentedAnchors = "ANC" + + +class CsvBuilder(object): + def build_cad_output(self, panels): + panels.sort(key=lambda x: x.id) + + + output_columns = [ + PanelDataColumn.Handle, PanelDataColumn.Blockname, PanelDataColumn.WindZone, PanelDataColumn.PanelType, + PanelDataColumn.Subarray, PanelDataColumn.Pressure, PanelDataColumn.Ballast, + PanelDataColumn.PresentedLinkTray, PanelDataColumn.CrossTray, CSVDataColumn.PresentedAnchors, + PanelDataColumn.Id, PanelDataColumn.Xcoord, PanelDataColumn.Ycoord, PanelDataColumn.Rotation + ] + use_fuzzy_wind_zone = False + for panel in panels: + if panel.fuzzy_wind_zone: + output_columns.append(PanelDataColumn.FuzzyWindZone) + use_fuzzy_wind_zone = True + break + + header_row = [col.value for col in output_columns] + matrix = [self.format_panel_for_csv(panel, include_fuzzy_wind_zone=use_fuzzy_wind_zone) for panel in panels] + + return self.output_csv(header_row, matrix) + + def build_bom_output(self, rows): + headers = ['Part #', 'Description', 'Total'] + return self.output_csv(headers, rows) + + def output_csv(self, headers, rows): + output = StringIO() + writer = csv.writer(output, dialect='excel-tab', quoting=csv.QUOTE_NONE, quotechar='') + + writer.writerow(headers) + writer.writerows(rows) + return output.getvalue() + + def format_panel_for_csv(self, panel, include_fuzzy_wind_zone=False): + row = [ + panel.handle or "", + panel.blockname or "", + self.int_to_wind_zone(panel.wind_zone), + panel.panel_type.number() if panel.panel_type else "", + panel.subarray, + round(panel.pressure, 2) if panel.pressure else "", + self.present_value(panel.ballast), + self.present_value(panel.presented_link_tray) or '-', + self.present_value(panel.cross_tray) or '-', + self.present_anchors(panel.wind_anchors, panel.seismic_anchors), + panel.id, + round(panel.original_coordinate.x, 14), + round(panel.original_coordinate.y, 14), + round(panel.original_coordinate.rotation, 14) + ] + if include_fuzzy_wind_zone: + row.append(int(panel.fuzzy_wind_zone)) + return row + + def round_two_digits(self, value): + return round(value, 2) + + def zero_to_dash(self, value): + return int(value) or '-' + + def int_to_wind_zone(self, value): + if value is None: + return "" + return ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"][value] + + def present_value(self, value, default=''): + if value is None: + return default + return int(value) + + def present_anchors(self, wind, seismic): + if not wind and not seismic: + return '-' + wind_string = str(int(wind)) if wind != 0 else '' + return wind_string + 'S' * int(seismic) diff --git a/helix/db/__init__.py b/helix/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/helix/db/redis_manager.py b/helix/db/redis_manager.py new file mode 100644 index 0000000..340654a --- /dev/null +++ b/helix/db/redis_manager.py @@ -0,0 +1,17 @@ +import json + +import redis + + +class RedisManager(object): + @staticmethod + def get_redis_connection(vcap_env, heroku_redis_url): + if vcap_env: + redis_env = json.loads(vcap_env)['rediscloud'][0]['credentials'] + return redis.Redis(host=redis_env['hostname'], + port=int(redis_env['port']), + password=redis_env['password']) + elif heroku_redis_url: + return redis.Redis.from_url(heroku_redis_url) + else: + return redis.Redis(host='localhost', port=6379, db=0) diff --git a/helix/db/sql_manager.py b/helix/db/sql_manager.py new file mode 100644 index 0000000..96b6c07 --- /dev/null +++ b/helix/db/sql_manager.py @@ -0,0 +1,26 @@ +import sqlalchemy +from sqlalchemy.orm import sessionmaker + + +class SQLManager(object): + # Cache the database connection per application process. + # More properly, this should be kept in thread-local state (threading.local()), but should suffice. + # Each passed connection url will have its own pool. + engines = {} + + @classmethod + def get_sql_session_maker(cls, heroku_postgres_url, should_echo=False, cache=True): + if not cache: + return sessionmaker(bind=cls.connect(heroku_postgres_url, should_echo=should_echo))() + else: + engine = cls.engines.setdefault(heroku_postgres_url, + cls.connect(heroku_postgres_url, should_echo=should_echo)) + + return sessionmaker(bind=engine)() + + @staticmethod + def connect(heroku_postgres_url, should_echo=False): + if heroku_postgres_url: + return sqlalchemy.create_engine(heroku_postgres_url, echo=should_echo) + else: + return sqlalchemy.create_engine('postgres://pivotal:@localhost/pivotal', echo=True) diff --git a/helix/doc_gen_params_builder.py b/helix/doc_gen_params_builder.py new file mode 100644 index 0000000..e93a7c1 --- /dev/null +++ b/helix/doc_gen_params_builder.py @@ -0,0 +1,166 @@ +from base64 import b64encode +import os +from helix.constants.system_type import SystemType +from helix.constants.version import version + + +class DocGenParamsBuilder(object): + def __init__(self, user_values, system_type, calculator, image_presenter): + self.user_values = user_values + self.system_type = system_type + self.calculator = calculator + self.image_presenter = image_presenter + + def build(self): + if self.user_values.system_type() == SystemType.singleTilt: + template_name = 'Helix_Single_Tilt_Template' + else: + template_name = 'Helix_Dual_Tilt_Template' + + body = { + **self.site_characterization(), + **self.calculator.documentation_summary_values(), + **self.panel_attributes(), + **self.bom(), + **self.power_stations(), + **self.standalone_inverters(), + **self.subarray_summary(), + } + + image_dicts = self.array_image() + + list_params = self.convert_dict_params_to_list(body) + images = self.convert_dict_params_to_list(image_dicts, key_name='imageKey', value_name='base64encodedImage') + + api_key = os.getenv('SP_DOCGEN_API_KEY', '') + + params = { + "apiKey": api_key, + "templateName": template_name, + "nameValuePairs": list_params, + "dynamicImages": images, + } + return params + + def site_characterization(self): + panels = self.calculator.get_computed_csv_columns() + + return { + 'project_name': self.user_values.project_name(), + 'building_height': self.user_values.building_height(), + 'building_width': self.user_values.building_width(), + 'building_length': self.user_values.building_length(), + 'parapet_height': self.user_values.building_parapet_height(), + 'ballast_block_weight': self.user_values.ballast_block_weight(), + 'max_allowable_system_pressure': self.user_values.max_system_pressure(), + 'anchor_type': self.user_values.anchor_type().value, + 'exposure_category': self.user_values.exposure_category().value, + 'exposure_category_transition_distance': self.user_values.exposure_category_transition_distance(), + 'wind_speed': self.user_values.wind_speed(), + 'spectral_response': self.user_values.spectral_response(), + 'seismic_importance_factor': self.user_values.importance_factor(), + 'module_type': self.user_values.module_type().value, + 'system_type': self.user_values.system_type().display_name(), + + 'total_modules': len(panels), + 'version': version(), + 'lb': round(self.calculator.L_B(), 2), + 'kz': round(self.calculator.k_z(), 2), + 'qz': round(self.calculator.q_z(), 2), + } + + def panel_attributes(self): + summary_table = self.calculator.summary_table() + minimum_array_sizes = self.calculator.minimum_array_sizes() + + result = {} + + for panel_type, values in summary_table.items(): + ballast_blocks = values['ballast blocks'] + pressure = values['pressure'] + anchors = values['anchors'] + for wind_zone_index in range(len(ballast_blocks)): + wind_zone = self.system_type.system_constants().wind_zones[wind_zone_index].lower() + prefix = '%d_%s_' % (panel_type.number(), wind_zone) + result[prefix + 'bb'] = ballast_blocks[wind_zone_index] + result[prefix + 'psf'] = pressure[wind_zone_index] + result[prefix + 'anc'] = anchors[wind_zone_index] + + for idx, array_size in enumerate(minimum_array_sizes): + wind_zone = self.system_type.system_constants().wind_zones[idx].lower() + result['min_' + wind_zone] = array_size + + return result + + def bom(self): + bom_values = self.calculator.documentation_bom() + result = {} + + for row in bom_values: + result['total_' + row[0]] = row[1] + return result + + def power_stations(self): + power_station_string = "" + power_stations = self.user_values.power_stations() + if len(power_stations) > 0: + power_station_string = "\n" + for power_station in self.user_values.power_stations(): + power_station_string += "Description: %s\n" % power_station['power_station_description'] + power_station_string += "\tQuantity: %s\n" % power_station['power_station_quantity'] + power_station_string += "\tAC Run Length: %s\n" % power_station['ac_run_length'] + power_station_string += "\tInverters:\n" + for inverter in power_station['inverters']: + power_station_string += "\t\tModel: %s\n" % inverter['model'].label + power_station_string += "\t\tStrings per inverter: %s\n" % inverter['strings_per_inverter'] + if inverter.get('sunshade'): + power_station_string += "\t\tSunShade: Yes\n" + if inverter.get('dc_switch'): + power_station_string += "\t\tDC Switch: Yes\n" + power_station_string += "\n" + return {'power_station_string': power_station_string} + + def standalone_inverters(self): + inverters_string = "" + for inverter in self.user_values.standalone_inverters(): + inverters_string += "\n" + inverters_string += "\tAC Run Length: %s\n" % inverter['ac_run_length'] + inverters_string += "\tAttachment Point: %s\n" % inverter['attachment_point'][0] + inverters_string += "\tModel: %s\n" % inverter['model'].label + inverters_string += "\tStrings per inverter: %s\n" % inverter['strings_per_inverter'] + if inverter.get('sunshade'): + inverters_string += "\tSunShade: Yes\n" + if inverter.get('dc_switch'): + inverters_string += "\tDC Switch: Yes\n" + return {'standalone_inverter_string': inverters_string} + + def subarray_summary(self): + subarray_string = "" + for subarray in self.calculator.subarray_summary(): + subarray_string += "\n" + subarray_string += "\tSubarray: %d\n" % subarray.subarray_number + subarray_string += "\tSeismic Anchors: %d\n" % subarray.required_seismic_anchors + subarray_string += "\tWeight: %s lbs\n" % "{:,}".format(round(subarray.weight)) + return {'subarrays_string': subarray_string} + + def array_image(self): + png_data = self.image_presenter.generate_image(self.calculator.get_computed_csv_columns(), + self.calculator.subarrays) + return {'array_image': b64encode(png_data).decode('utf-8')} + + @staticmethod + def convert_list_params_to_dict(list_params): + dict_params = {} + for datum in list_params: + dict_params[datum['name']] = datum['value'] + return dict_params + + @staticmethod + def convert_dict_params_to_list(dict_params, key_name='name', value_name='value'): + list_params = [] + for k, v in dict_params.items(): + list_params.append({ + key_name: k, + value_name: v + }) + return sorted(list_params, key=lambda k: k[key_name]) diff --git a/helix/forms/__init__.py b/helix/forms/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/helix/forms/conditional_validator.py b/helix/forms/conditional_validator.py new file mode 100644 index 0000000..e1fe001 --- /dev/null +++ b/helix/forms/conditional_validator.py @@ -0,0 +1,17 @@ +from wtforms.validators import Optional + + +class ConditionalValidator(object): + def __init__(self, other_field_name, other_data_contents, dependent_validator): + self.other_field_name = other_field_name + self.other_data_contents = other_data_contents + self.dependent_validator = dependent_validator + + def __call__(self, form, field): + other_field = form._fields.get(self.other_field_name) + if other_field is None: + raise Exception('no field named "%s" in form' % self.other_field_name) + if other_field.data in self.other_data_contents: + self.dependent_validator.__call__(form, field) + else: + Optional().__call__(form, field) diff --git a/helix/forms/ebom_form.py b/helix/forms/ebom_form.py new file mode 100644 index 0000000..f43b5a6 --- /dev/null +++ b/helix/forms/ebom_form.py @@ -0,0 +1,183 @@ +from wtforms import StringField, SelectField, FormField, BooleanField +from wtforms.fields.html5 import IntegerField +from wtforms.validators import NumberRange, DataRequired + +from helix.constants.inverter_brand import InverterBrand +from helix.constants.inverter_type import InverterType +from helix.constants.system_type import SystemType +from helix.forms.grouped_form import GroupedForm + + +def generate_string_choices(from_i, to_i, only_even=False): + step = 2 if only_even else 1 + return list( + map(lambda x: (x, "%s" % x), range(from_i, to_i + 1, step)) + ) + + +class InverterBrandForm(GroupedForm): + form_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden', 'value': "inverter_brand_form"}) + inverter_brand_id = SelectField('', + choices=[(InverterBrand.SMA.value, InverterBrand.SMA.label), + (InverterBrand.DELTA.value, InverterBrand.DELTA.label)], + coerce=int, + default=InverterBrand.default_value(), + render_kw={'group': 'inverter_brands'}, + ) + + def populate_choices(self, inverter_brands): + if len(inverter_brands) > 0: + self.inverter_brand_id.default = next( + map(lambda x: x['inverter_brand_id'], inverter_brands) + , InverterBrand.default_value()) + self.process() + + def is_delta(self): + return self.inverter_brand_id.data == InverterBrand.DELTA.value + + +class InverterFormSMA(GroupedForm): + quantity = IntegerField('Quantity', + default=1, + render_kw={'group': 'quantity', 'row_class': 'quantity'}, + validators=[NumberRange(0, None)]) + model = SelectField('Model', + choices=[(InverterType.SMA.MODEL_12KW.value, InverterType.SMA.MODEL_12KW.label), + (InverterType.SMA.MODEL_15KW.value, InverterType.SMA.MODEL_15KW.label), + (InverterType.SMA.MODEL_20KW.value, InverterType.SMA.MODEL_20KW.label), + (InverterType.SMA.MODEL_24KW.value, InverterType.SMA.MODEL_24KW.label)], + coerce=int, + default=InverterType.SMA.default_value(), + render_kw={'group': 'non-optional', 'row_class': 'inverter_model'}, + ) + strings_per_inverter = SelectField('# Strings/Inverter', + coerce=int, + choices=generate_string_choices(2, 8), + default=8, + render_kw={'group': 'non-optional', 'row_class': 'inverter_strings'}) + sunshade = BooleanField('Sun Shade', render_kw={'group': 'optional'}) + dc_switch = BooleanField('DC Switch', render_kw={'group': 'optional'}) + + def update_strings(self, system_type): + self.strings_per_inverter.choices = generate_string_choices( + 2, + 8, + system_type != SystemType.singleTilt + ) + + +class InverterFormDelta(GroupedForm): + quantity = IntegerField('Quantity', + default=1, + render_kw={'group': 'quantity', 'row_class': 'quantity'}, + validators=[NumberRange(0, None)]) + model = SelectField('Model', + choices=[(InverterType.DELTA.MODEL_36KW.value, InverterType.DELTA.MODEL_36KW.label), + (InverterType.DELTA.MODEL_42KW.value, InverterType.DELTA.MODEL_42KW.label), + (InverterType.DELTA.MODEL_60KW.value, InverterType.DELTA.MODEL_60KW.label), + # (InverterType.DELTA.MODEL_80KW.value, InverterType.DELTA.MODEL_80KW.label), + ], + coerce=int, + default=InverterType.DELTA.default_value(), + render_kw={'group': 'non-optional', 'row_class': 'inverter_model'}, + ) + strings_per_inverter = SelectField('# Strings/Inverter', + coerce=int, + choices=generate_string_choices(0, 24), + default=8, + render_kw={'group': 'non-optional', 'row_class': 'inverter_strings'}) + splice_box = BooleanField('Splice Box', default=True, render_kw={'group': 'optional'}) + + +class EbomForm(GroupedForm): + power_station_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden'}) + form_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden', 'value': 'power_station_form'}) + power_station_description = StringField('Power Station Description', render_kw={'group': 'header'}, + validators=[DataRequired(message='Power Station Description is required.')], + default='Power Station 1') + power_station_quantity = IntegerField('Power Station Quantity', + default=1, + validators=[NumberRange(0, None)], + render_kw={'group': 'header'}) + ac_run_length = IntegerField('Total AC Run Length for Power Station(s) (ft)', + render_kw={'group': 'header'}, default=0, validators=[NumberRange(0, None)]) + monitor_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden'}) + + inverter_quantity = SelectField('Inverters', choices=[(1, "1"), (2, "2"), (3, "3"), (4, "4")], + coerce=int, + default=4, + render_kw={'group': 'inverter_quantity'}) + + inverter_1 = FormField(InverterFormSMA, 'Inverter 1', render_kw={'group': 'inverters'}) + inverter_2 = FormField(InverterFormSMA, 'Inverter 2', render_kw={'group': 'inverters'}) + inverter_3 = FormField(InverterFormSMA, 'Inverter 3', render_kw={'group': 'inverters'}) + inverter_4 = FormField(InverterFormSMA, 'Inverter 4', render_kw={'group': 'inverters'}) + + def update_inverter_strings_choices(self, system_type): + self.inverter_1.update_strings(system_type) + self.inverter_2.update_strings(system_type) + self.inverter_3.update_strings(system_type) + self.inverter_4.update_strings(system_type) + + +class StandAloneInverterForm(GroupedForm): + standalone_inverter_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden'}) + form_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden', 'value': "standalone_inverter_form"}) + standalone_ac_run_length = IntegerField('AC Run Length for Inverter (ft)', + render_kw={'group': 'power_station'}, + default=0, validators=[NumberRange(0, None)]) + + def update_inverter_strings_choices(self, system_type): + self.inverter.update_strings(system_type) + + def populate_choices(self): + pass + + +class StandAloneInverterFormSMA(StandAloneInverterForm): + inverter = FormField(InverterFormSMA, 'Inverter', render_kw={'group': 'inverters'}) + attachment_point = SelectField('Attachment Point', + choices=[], + default='switch_gear', + render_kw={'group': 'power_station'}) + + def populate_choices(self, power_stations, standalone_inverters): + standalone_inverter_count_per_power_station = {} + for inverter in standalone_inverters: + key = inverter['attachment_point'][1] + standalone_count = standalone_inverter_count_per_power_station.get(key) or 0 + standalone_inverter_count_per_power_station[key] = standalone_count + 1 + + power_stations_with_free_slots = [] + for power_station in power_stations: + standalone_count = standalone_inverter_count_per_power_station.get(power_station['power_station_id']) or 0 + inverter_count = power_station['inverter_quantity'] + standalone_count + if inverter_count < 4 and power_station['power_station_quantity'] == 1: + power_stations_with_free_slots.append(power_station) + + choices = map(lambda x: (str(x['power_station_id']), x['power_station_description']), power_stations_with_free_slots) + self.attachment_point.choices = [('switch_gear', 'Switch Gear')] + list(choices) + + +class StandAloneInverterFormDelta(StandAloneInverterForm): + inverter = FormField(InverterFormDelta, 'Inverter', render_kw={'group': 'inverters'}) + + +class SupervisorForm(GroupedForm): + monitor_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden'}) + form_id = StringField(render_kw={'group': 'hidden', 'class': 'hidden', 'value': "supervisor_form"}) + power_source = SelectField('Power Source', + render_kw={'group': 'power_source'}, + choices=[('switch_gear', 'Switch Gear/External')]) + + +class SupervisorFormSMA(SupervisorForm): + def populate_choices(self, power_stations, supervisors): + supervisor_power_sources = list(map(lambda x: x['power_source'][1], supervisors)) + power_stations_without_supervisors = [] + for power_station in power_stations: + if power_station['power_station_id'] not in supervisor_power_sources: + power_stations_without_supervisors.append(power_station) + + choices = map(lambda x: (str(x['power_station_id']), x['power_station_description']), power_stations_without_supervisors) + self.power_source.choices = self.power_source.choices[:1] + list(choices) diff --git a/helix/forms/grouped_form.py b/helix/forms/grouped_form.py new file mode 100644 index 0000000..267b833 --- /dev/null +++ b/helix/forms/grouped_form.py @@ -0,0 +1,6 @@ +from flask.ext.wtf import Form + +class GroupedForm(Form): + def group(self, label): + return [field for field in self if field.render_kw and field.render_kw['group'] == label] + diff --git a/helix/forms/input_form.py b/helix/forms/input_form.py new file mode 100644 index 0000000..01d925e --- /dev/null +++ b/helix/forms/input_form.py @@ -0,0 +1,95 @@ +from flask.ext.wtf.file import FileField + +from helix.constants.anchor_type import AnchorType +from helix.constants.exposure_category import ExposureCategory +from helix.constants.module_type import ModuleType +from helix.constants.system_type import SystemType +from wtforms import SelectField, StringField, BooleanField +from wtforms.fields.html5 import DecimalField, IntegerField +from wtforms.validators import NumberRange, DataRequired + +from helix.forms.conditional_validator import ConditionalValidator +from helix.forms.grouped_form import GroupedForm + + +class InputForm(GroupedForm): + project_name = StringField('Project Name', validators=[DataRequired(message='Project Name is required.')], + render_kw={'group': 'project_info'}) + building_height = DecimalField('Building Height (ft)', places=1, validators=[NumberRange(0, None)], + render_kw={'group': 'site_info'}) + building_width = DecimalField('Building Width (ft)', places=1, validators=[NumberRange(0, None)], + render_kw={'group': 'site_info'}) + building_length = DecimalField('Building Length (ft)', places=1, validators=[NumberRange(0, None)], + render_kw={'group': 'site_info'}) + building_parapet_height = DecimalField('Parapet Height (ft)', places=1, validators=[NumberRange(0, None)], + render_kw={'group': 'site_info'}) + wind_speed = IntegerField('Wind Speed (ASCE 7-10) (mph)', + validators=[NumberRange(100, 200)], + render_kw={'group': 'site_info', + 'link': {'text': 'Look up', + 'href': 'http://windspeed.atcouncil.org/'}}) + exposure_category = SelectField('Exposure Category', + choices=[(ExposureCategory.B.value, ExposureCategory.B.value), + (ExposureCategory.B_C.value, "B to C"), + (ExposureCategory.C_B.value, "C to B"), + (ExposureCategory.C.value, ExposureCategory.C.value), + (ExposureCategory.D.value, ExposureCategory.D.value)], + default=ExposureCategory.default_value(), + render_kw={'group': 'site_info', + 'link': {'text': 'More info', + 'href': '/exposure_categories'} + }) + exposure_category_transition_distance = IntegerField('Exposure Transition Distance (ft)', + default=0, + validators=[ConditionalValidator('exposure_category', + ['B to C', 'C to B'], + NumberRange(1, None))], + render_kw={'group': 'site_info'}) + ballast_block_weight = DecimalField('Ballast Block Weight (lbs)', + validators=[NumberRange(12, 20)], + default=14, + places=1, + render_kw={'group': 'site_info'}) + max_system_pressure = DecimalField('Max Allowable System Pressure (psf)', + places=1, + validators=[NumberRange(0, None)], + default=12, + render_kw={'group': 'site_info'}) + system_type = SelectField('System Type', + choices=[(SystemType.singleTilt.value, SystemType.singleTilt.display_name()), + (SystemType.dualTilt.value, SystemType.dualTilt.display_name())], + default=SystemType.default_value(), + render_kw={'group': 'project_info'}) + module_type = SelectField('Module Type', + choices=[(ModuleType.Cell128.value, ModuleType.Cell128.value), + (ModuleType.PSeries.value, ModuleType.PSeries.value), + (ModuleType.Cell96.value, ModuleType.Cell96.value)], + default=ModuleType.default_value(), + render_kw={'group': 'project_info'}) + anchor_type = SelectField('Anchor Type', + choices=[(AnchorType.OMG_PowerGrip.value, AnchorType.OMG_PowerGrip.value), + (AnchorType.OMG_PowerGrip_Plus.value, AnchorType.OMG_PowerGrip_Plus.value), + (AnchorType.EcoFasten.value, AnchorType.EcoFasten.value)], + default=AnchorType.default_value(), + render_kw={'group': 'site_info', 'tooltip': 'OMG anchors are compatible with TPO and PVC roof membranes.
EcoFasten anchors are compatible with Built Up Roofing (BUR), Hot Tar, Sips Panels and membrane type roofs.'}) + design_spectral_response = DecimalField('Design Spectral Response Acceleration (SDS) (g)', + places=1, validators=[NumberRange(0, 5)], + render_kw={'group': 'site_info', + 'link': {'text': 'Look up', + 'href': 'http://earthquake.usgs.gov/designmaps/us/application.php' + } + }) + importance_factor = SelectField('Seismic Importance Factor (Ip)', + choices=[('1', 1), ('1.5', 1.5)], + default=1, + render_kw={'group': 'site_info', 'tooltip': 'Use 1.5 for essential facilities such as: Hospitals, Police, Fire & Rescue stations & Designated emergency shelters. All other structures should use 1.0.'}) + + +class ArrayForm(GroupedForm): + file_upload = FileField('System Data (txt)', render_kw={'group': 'array_info', 'class': 'system_upload'}) + dxf_upload = FileField('Cad File (dxf)', render_kw={'group': 'dxf_file', 'class': 'system_upload'}) + + +class TestDXFForm(GroupedForm): + dxf_upload = FileField('Cad File (dxf)', render_kw={'group': 'array_info', 'class': 'system_upload'}) + show_wind_zones = BooleanField('Show Wind Zones', default=True, render_kw={'group': 'array_info'}) diff --git a/helix/functions.py b/helix/functions.py new file mode 100644 index 0000000..1cb0320 --- /dev/null +++ b/helix/functions.py @@ -0,0 +1,4 @@ +def fequal(x, y, delta=1e-6): + if x == y: + return True + return abs(x - y) < delta diff --git a/helix/helpers/__init__.py b/helix/helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/helix/helpers/nodequadtree.py b/helix/helpers/nodequadtree.py new file mode 100644 index 0000000..290fb7c --- /dev/null +++ b/helix/helpers/nodequadtree.py @@ -0,0 +1,152 @@ +from math import floor, ceil + + +# A utility class, a rectangle acting as boundaries of the quad tree +class Bounds: + def __init__(self, left, right, bottom, top): + if top < bottom: + top, bottom = bottom, top + + if right < left: + right, left = left, right + + self.left = left + self.right = right + self.bottom = bottom + self.top = top + self.width = right - left + self.height = top - bottom + + def getWidth(self): + return self.width + + def getHeight(self): + return self.height + + def getLeft(self): + return self.left + + def getRight(self): + return self.right + + def getBottom(self): + return self.bottom + + def getTop(self): + return self.top + + +# A QuadTree implemented to specifically handle panel Nodes +class NodeQuadTree(): + MAX_OBJECTS = 100 + MAX_LEVELS = 5 + + def __init__(self, level, bounds, variance): + if level < 1: + level = 1 + + self.level = level + self.variance = variance + + self.bounds = bounds + if level == 1: + self.bounds = Bounds(floor(bounds.getLeft() - self.variance), + ceil(bounds.getRight() + self.variance), + floor(bounds.getBottom() - self.variance), + ceil(bounds.getTop() + self.variance)) + + self.nodeList = [] + self.quads = [None, None, None, None] + + def clear(self): + self.nodeList = [] + for quad in self.quads: + if quad is not None: + quad.clear() + self.quads = [None, None, None, None] + + def pointInside(self, point): + if point.x - self.variance < self.bounds.getLeft(): + return False + if point.x + self.variance > self.bounds.getRight(): + return False + if point.y - self.variance < self.bounds.getBottom(): + return False + if point.y + self.variance > self.bounds.getTop(): + return False + return True + + def split(self): + left = self.bounds.getLeft() + right = self.bounds.getRight() + midLR = left + self.bounds.getWidth() / 2 + + bottom = self.bounds.getBottom() + top = self.bounds.getTop() + midBT = bottom + self.bounds.getHeight() / 2 + + self.quads[0] = NodeQuadTree(self.level + 1, Bounds(left, midLR, bottom, midBT), self.variance) + self.quads[1] = NodeQuadTree(self.level + 1, Bounds(midLR, right, bottom, midBT), self.variance) + self.quads[2] = NodeQuadTree(self.level + 1, Bounds(left, midLR, midBT, top), self.variance) + self.quads[3] = NodeQuadTree(self.level + 1, Bounds(midLR, right, midBT, top), self.variance) + + # Returns which child index the point belongs in, or -1 if it doesn't fit completely within any + def getIndex(self, point): + for i in range(4): + if self.quads[i] is not None: + if self.quads[i].pointInside(point): + return i + return -1 + + # insert the node into the QuadTree, or one of its children + def insert(self, node): + # add it to our children, if they exist and the point fits + if self.quads[0] is not None: + index = self.getIndex(node.coordinate) + + if index != -1: + self.quads[index].insert(node) + return + + # else, add it to self + self.nodeList.append(node) + + # too big? split into quads + if (len(self.nodeList) > NodeQuadTree.MAX_OBJECTS) and \ + (self.level < NodeQuadTree.MAX_LEVELS) and \ + (self.quads[0] is None): + + self.split() + + toKeep = [] + for n in self.nodeList: + index = self.getIndex(n.coordinate) + + if index != -1: + self.quads[index].insert(n) + else: + toKeep.append(n) + + self.nodeList = toKeep + + # Return a list of all possible nodes that can be near this point + def retrieve(self, nearPoint): + retNodes = list(self.nodeList) + + if self.quads[0] is not None: + index = self.getIndex(nearPoint) + if index != -1: + retNodes += self.quads[index].retrieve(nearPoint) + else: + for quad in self.quads: + retNodes += quad.retrieve(nearPoint) + + return retNodes + + def report(self, outputArray): + if outputArray is not None: + outputArray.append(len(self.nodeList)) + + for quad in self.quads: + if quad is not None: + quad.report(outputArray) diff --git a/helix/helpers/polygon_helper.py b/helix/helpers/polygon_helper.py new file mode 100644 index 0000000..29a766e --- /dev/null +++ b/helix/helpers/polygon_helper.py @@ -0,0 +1,97 @@ +def point_inside_polygon(x, y, points): + n = len(points) + inside = False + + # this does a raytracing algorithm that I don't quite understand. + # See http://stackoverflow.com/questions/217578/how-can-i-determine-whether-a-2d-point-is-within-a-polygon/2922778#2922778 + # for an attempt at an explanation + + p1x, p1y = points[0] + for i in range(n + 1): + p2x, p2y = points[i % n] + if y > min(p1y, p2y): + if y <= max(p1y, p2y): + if x <= max(p1x, p2x): + if p1y != p2y: + xinters = (y - p1y) * (p2x - p1x) / (p2y - p1y) + p1x + if p1x == p2x or x <= xinters: + inside = not inside + p1x, p1y = p2x, p2y + + return inside + +def line_segments_intersect(first_line, second_line): + # returns either None or the point where the line segments intersect + x_0_0 = first_line[0][0] + x_0_1 = first_line[1][0] + y_0_0 = first_line[0][1] + y_0_1 = first_line[1][1] + + x_1_0 = second_line[0][0] + x_1_1 = second_line[1][0] + y_1_0 = second_line[0][1] + y_1_1 = second_line[1][1] + + if max(x_0_0, x_0_1) < min(x_1_0, x_1_1): + return None + + rise_0 = y_0_1 - y_0_0 + run_0 = x_0_1 - x_0_0 + if run_0 == 0: + slope_0 = None + y_intercept_0 = 0 + else: + slope_0 = rise_0 / run_0 + y_intercept_0 = y_0_0 - (slope_0 * x_0_0) + + rise_1 = y_1_1 - y_1_0 + run_1 = x_1_1 - x_1_0 + if run_1 == 0: + slope_1 = None + y_intercept_1 = 0 + else: + slope_1 = rise_1 / run_1 + y_intercept_1 = y_1_0 - (slope_1 * x_1_0) + + if slope_0 is not None and slope_1 is not None and abs(slope_0 - slope_1) < 1e-3: + return None + if slope_0 is None and slope_1 is None: + return None + if slope_0 is None: + x_point = x_0_0 + slope = slope_1 + elif slope_1 is None: + x_point = x_1_0 + slope = slope_0 + else: + x_point = (y_intercept_1 - y_intercept_0) / (slope_0 - slope_1) + slope = slope_0 + + if (x_point < max(min(x_0_0, x_0_1), min(x_1_0, x_1_1))) and (x_point > min(max(x_0_0, x_0_1), max(x_1_0, x_1_1))): + return None + + y_point = slope * x_point + y_intercept_1 + return x_point, y_point + + +def clip_polygon(a, clip_region): + points = [] + for x, y in a: + points.append((x, y, point_inside_polygon(x, y, clip_region))) + + idx = 0 + while idx < len(points): + x0, y0, inside_0 = points[(idx-1) % len(points)] + x1, y1, inside_1 = points[idx] + + first_line = [(x0, y0), (x1, y1)] + + if inside_0 and not inside_1: # intersected between then and now + for index, vertex in enumerate(clip_region): + second_line = [vertex, clip_region[(index + 1) % len(clip_region)]] + intersection = line_segments_intersect(first_line, second_line) + if intersection: + points[idx] = (intersection[0], intersection[1], False) + break + idx += 1 + return [(x, y) for x, y, _ in points] diff --git a/helix/javascript/array_summary/array_visualization.js b/helix/javascript/array_summary/array_visualization.js new file mode 100644 index 0000000..0ecdf0c --- /dev/null +++ b/helix/javascript/array_summary/array_visualization.js @@ -0,0 +1,206 @@ +import '../../../lib/easeljs.js'; +import $ from "jquery"; +import {Panel} from './panel'; +import Colors from "./colors"; + +class ArrayVisualization { + constructor(panelData, isDualTilt, subarrayDisplay, buildings) { + this.panelData = panelData; + this.isDualTilt = isDualTilt; + this.subarrayDisplay = subarrayDisplay; + this.buildings = buildings; + this.scale = 10; + } + + init() { + this.stage = new createjs.Stage("arrayCanvas"); + this.container = new createjs.Container(); + + let background = new createjs.Shape(); + background.graphics.beginFill(Colors.canvas_background).drawRect(0, 0, 850, 850); + this.stage.addChild(background); + this.stage.mouseMoveOutside = true; + this.adjustScale(this.buildings); + + this.drawArray(this.container, this.panelData, this.scale); + this.container.x = 0; + this.container.y = 0; + this.stage.addChild(this.container); + + this.drawBuildings(this.buildings,this.scale); + + this.stage.update(); + + var lastMove = undefined; + + var self = this; + + // Panning + this.stage.on("pressmove", function (event) { + let x = event.stageX; + let y = event.stageY; + if (lastMove != undefined) { + let deltaX = x - lastMove.x; + let deltaY = y - lastMove.y; + + self.container.x += deltaX; + self.container.y += deltaY; + + self.stage.update(); + } + lastMove = {x: x, y: y}; + }); + + this.stage.on("pressup", function () { + lastMove = undefined; + }); + } + + adjustScale(buildings) { + if (!buildings || buildings.length === 0) return; // leave scale as default + // we cannot determine canvas size + // we are in a test or in a very impossible state + // so we better not touch anything + if(!this.stage.canvas) return; + + let all_x = ([].concat(...buildings)).map( (building) => building.x * this.scale ); + let all_y = ([].concat(...buildings)).map( (building) => building.y * this.scale ); + let max_x = Math.max(...all_x); + let max_y = Math.max(...all_y); + + let ratio_y = this.stage.canvas.height * 1.0 / max_y; + let ratio_x = this.stage.canvas.width * 1.0 / max_x; + + const MARGIN = 0.05; + this.scale = this.scale * Math.min(ratio_y, ratio_x) * (1 - MARGIN); + + } + + drawBuildings(buildings, scale) { + if (!buildings) { + console.log("No Buildings!"); + return; + } + + if (!scale) { + console.log("No Scale - don't know how big the buildings should be!"); + return; + } + + let line = new createjs.Shape(); + let color = createjs.Graphics.getRGB(0x010101, 1); + + line.graphics.setStrokeStyle(3); + + for (let i = 0; i < buildings.length; i++) { + let building = buildings[i]; + let firstPoint = building[0]; + let nextPoint = null; + + line.graphics.beginStroke(color); + + line.graphics.moveTo(firstPoint.x * scale, firstPoint.y * scale); + + for (let j = 1; j < building.length; j++ ) { + nextPoint = building[j]; + line.graphics.lineTo(nextPoint.x * scale ,nextPoint.y * scale); + line.graphics.moveTo(nextPoint.x * scale,nextPoint.y * scale); + } + line.graphics.lineTo(firstPoint.x * scale, firstPoint.y * scale ); + line.graphics.endStroke(); + + } + + this.container.addChild(line); + } + + refreshPanels(panelData) { + this.panelData = panelData; + + for (let i = 0; i < this.panels.length; i++) { + this.container.removeChild(this.panels[i]); + } + + this.drawArray(this.container, this.panelData, this.scale); + + let selectedPanel = this.selectedPanel; + this.selectedPanel = undefined; + this.selectPanel(selectedPanel); + this.setOverlay(this.overlay); + + this.subarrayDisplay.didModifyPanels(panelData); + + this.stage.update(); + } + + drawArray(container, panels) { + this.panels = []; + let treatCoordinatesAsCenterpoints = this.buildings && this.buildings.length > 0; + for (let i = 0; i < panels.length; i++) { + let panel = panels[i]; + + let box = new Panel(panel, this.isDualTilt, this.scale, treatCoordinatesAsCenterpoints); + container.addChild(box); + this.panels.push(box); + + let self = this; + box.on("click", function () { + self.selectPanel(i); + }); + } + } + + selectPanel(panelIndex) { + let panel = this.panels[panelIndex]; + if (this.selectedPanel !== undefined) { + let previousPanel = this.panels[this.selectedPanel]; + previousPanel.deselect(); + } + if (this.selectedPanel === panelIndex) { + this.selectedPanel = undefined; + } else { + this.selectedPanel = panelIndex; + panel.select(); + } + this.stage.update(); + } + + setZoom(zoomLevel) { + this.container.scaleX = zoomLevel; + this.container.scaleY = zoomLevel; + + this.stage.update(); + } + + setOverlay(overlay) { + this.overlay = overlay; + this.panels.forEach((panel) => { + panel.setOverlay(overlay); + }); + + this.stage.update(); + } + + addSeismicAnchor() { + if (this.selectedPanel !== undefined) { + this.panelData[this.selectedPanel].data.seismic_anchors++; + this.panels[this.selectedPanel].redrawOverlays(); + this.stage.update(); + this.subarrayDisplay.didModifyPanels(this.panelData); + } + } + + removeSeismicAnchor() { + if (this.selectedPanel !== undefined) { + let seismicAnchors = this.panelData[this.selectedPanel].data.seismic_anchors; + if (seismicAnchors > 0) { + this.panelData[this.selectedPanel].data.seismic_anchors--; + this.panels[this.selectedPanel].redrawOverlays(); + this.stage.update(); + this.subarrayDisplay.didModifyPanels(this.panelData); + } + } + } +} + +export default ArrayVisualization; diff --git a/helix/javascript/array_summary/auto_upload.js b/helix/javascript/array_summary/auto_upload.js new file mode 100644 index 0000000..6e3bd6e --- /dev/null +++ b/helix/javascript/array_summary/auto_upload.js @@ -0,0 +1,23 @@ +let AutoUpload = () => { + $("#file_upload").change((e) => { + var ten_megabyte_max_upload = 10000000; + $("#error_container_txt").empty(); + if(e.currentTarget.files[0].size < ten_megabyte_max_upload){ + e.currentTarget.form.submit(); + }else{ + $("#error_container_txt").append('The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.'); + } + }); + + $("#dxf_upload").change((e) => { + var ten_megabyte_max_upload = 10000000; + $("#error_container_dxf").empty(); + if(e.currentTarget.files[0].size < ten_megabyte_max_upload){ + e.currentTarget.form.submit(); + }else{ + $("#error_container_dxf").append('The system configuration you have uploaded is too large. Try splitting your design into two separate text files and run the tool twice.'); + } + }); +}; + +export default AutoUpload; diff --git a/helix/javascript/array_summary/colors.js b/helix/javascript/array_summary/colors.js new file mode 100644 index 0000000..32dad46 --- /dev/null +++ b/helix/javascript/array_summary/colors.js @@ -0,0 +1,13 @@ +let Colors = { + canvas_background: '#F9F9F9', + + seismic_background: '#F1E8A2', + wind_background: '#B8F3E5', + default_background: '#133256', + selected_background: 'white', + light_text: 'white', + dark_text: '#6490BA', + border: '#537DAA' +}; + +export default Colors \ No newline at end of file diff --git a/helix/javascript/array_summary/index.js b/helix/javascript/array_summary/index.js new file mode 100644 index 0000000..d80113b --- /dev/null +++ b/helix/javascript/array_summary/index.js @@ -0,0 +1,20 @@ +"use strict"; +import ArrayVisualization from './array_visualization'; +import ZoomControl from './zoom_control'; +import OverlayControl from './overlay_control'; +import SeismicControl from './seismic_control'; +import SubarrayDisplay from './subarray_display'; +import AutoUpload from './auto_upload'; + +$(document).ready(function () { + AutoUpload(); + let subarrayDisplay = new SubarrayDisplay(); + subarrayDisplay.init($('#current_anchors'), $('#needed_anchors'), $('#subarray_weight'), panel_data); + let arrayVisualization = new ArrayVisualization(panel_data, is_dual_tilt, subarrayDisplay, buildings_coordinates); + arrayVisualization.init(); + new ZoomControl(arrayVisualization).init($('#zoom_control')); + new OverlayControl(arrayVisualization).init($('#overlay_control'), $('#legend_container')); + new SeismicControl(arrayVisualization, subarrayDisplay).init($('.seismic_anchor_control'), $("#seismic_save")); + window.arrayVisualization = arrayVisualization; + +}); diff --git a/helix/javascript/array_summary/overlay_control.js b/helix/javascript/array_summary/overlay_control.js new file mode 100644 index 0000000..dcc4246 --- /dev/null +++ b/helix/javascript/array_summary/overlay_control.js @@ -0,0 +1,43 @@ +import $ from "jquery"; + +class OverlayControl { + constructor(arrayVisualization) { + this.visualization = arrayVisualization; + } + + init(overlaySelector, legendSelector) { + let self = this; + + this.setOverlay("ANCHOR", overlaySelector, legendSelector); + + overlaySelector.find("#anchor_overlay").click(function () { + self.setOverlay("ANCHOR", overlaySelector, legendSelector); + }); + + overlaySelector.find("#all_overlay").click(function () { + self.setOverlay("ALL", overlaySelector, legendSelector); + }); + } + + setOverlay(overlay, overlaySelector, legendSelector) { + let selectedButton; + let selectedLegend; + if (overlay == "ANCHOR") { + selectedButton = overlaySelector.find("#anchor_overlay"); + selectedLegend = legendSelector.find("img.anchors_mode"); + } else if (overlay == "ALL") { + selectedButton = overlaySelector.find("#all_overlay"); + selectedLegend = legendSelector.find("img.all_mode"); + } + + this.visualization.setOverlay(overlay); + + overlaySelector.find('.overlay_toggle').removeClass("overlay_active"); + selectedButton.addClass("overlay_active"); + + legendSelector.find('.legend').hide(); + selectedLegend.show(); + } +} + +export default OverlayControl; diff --git a/helix/javascript/array_summary/panel.js b/helix/javascript/array_summary/panel.js new file mode 100644 index 0000000..844f863 --- /dev/null +++ b/helix/javascript/array_summary/panel.js @@ -0,0 +1,169 @@ +import "../../../lib/easeljs.js"; +import Colors from "./colors"; +import $ from "jquery"; + +function Panel(panel, isDualTilt, scale = 10, treatCoordinatesAsCenterpoints = true) { + this.Container_constructor(); + this.panel = panel; + this.isDualTilt = isDualTilt; + this.textOverlays = {}; + this.selected = false; + this.scale = scale; + this.treatCoordinatesAsCenterpoints = treatCoordinatesAsCenterpoints; + this.setup(); +} + +const panelMethods = { + setup() { + if (this.treatCoordinatesAsCenterpoints) { + this.x = (this.panel.x - (this.panel.width / 2.0)) * this.scale; + this.y = (this.panel.y - (this.panel.height / 2.0)) * this.scale; + } else { + this.x = this.panel.x * this.scale; + this.y = this.panel.y * this.scale; + } + this.width = this.scale * this.panel.width; + this.height = this.scale * this.panel.height; + this.drawBackground(); + }, + + drawBackground() { + if (this.background === undefined) { + this.background = new createjs.Shape(); + } else { + this.removeChild(this.background); + } + let fillColor; + let borderColor; + let textColor; + let secondFillColor; + if (this.panel.data.seismic_anchors > 0 && this.panel.data.wind_anchors > 0) { + fillColor = Colors.seismic_background; + secondFillColor = Colors.wind_background; + textColor = Colors.dark_text; + borderColor = Colors.border; + } else if (this.panel.data.seismic_anchors > 0) { + fillColor = Colors.seismic_background; + textColor = Colors.dark_text; + borderColor = Colors.border; + } else if (this.panel.data.wind_anchors > 0) { + fillColor = Colors.wind_background; + textColor = Colors.dark_text; + borderColor = Colors.border; + } else { + fillColor = Colors.default_background; + textColor = Colors.light_text; + borderColor = fillColor; + } + + if (this.selected) { + fillColor = Colors.selected_background; + textColor = Colors.dark_text; + borderColor = Colors.border; + } + + this.textColor = textColor; + + let width = this.width; + let height = this.height; + + let borderWidth = 0.5; + + // main background fill rectangle with white border + this.background.graphics.setStrokeStyle(borderWidth) + .beginStroke('white') + .beginFill(fillColor) + .drawRect(0, 0, width, height) + .endFill() + .endStroke(); + + // diagonal split background color for multiple anchor types + if (secondFillColor !== undefined) { + this.background.graphics.beginFill(secondFillColor) + .moveTo(borderWidth, borderWidth) + .lineTo(width - borderWidth, height - borderWidth) + .lineTo(width - borderWidth, borderWidth) + .closePath() + .endFill() + .endStroke(); + } + + // inner border for use with light background colors + this.background.graphics.beginStroke(borderColor) + .drawRect(borderWidth, borderWidth, width - 2 * borderWidth, height - 2 * borderWidth) + .endStroke(); + + // line drawing of dual-tilt indicator (the right-facing triangle) + if (this.isDualTilt) { + this.background.graphics.setStrokeStyle(0.25) + .beginStroke(Colors.border) + .moveTo(width / 2., borderWidth) + .lineTo(width / 2., height - borderWidth) + .lineTo(width - borderWidth, height / 2.) + .closePath() + .endStroke(); + } + this.addChildAt(this.background, 0); + }, + + select() { + this.selected = true; + this.drawBackground(); + this.redrawOverlays(); + }, + + deselect() { + this.selected = false; + this.drawBackground(); + this.redrawOverlays(); + }, + + setOverlay(overlay) { + let data = this.panel.data; + let self = this; + $.each(this.textOverlays, function (idx, overlay) { + self.removeChild(overlay); + }); + this.textOverlays = {}; + if (overlay == "ALL") { + this.addOverlay('panel_id', data.panel_id, "2.5px", 0.3, 0.55); + this.addOverlay('wind_zones', data.wind_zones, '2.5px', 0.7, 0.55); + this.addOverlay('ballast', data.ballast, "1.5px", 0.125, 0.25); + this.addOverlay('wind_anchors', data.wind_anchors, "1.5px", 0.125, 0.8); + this.addOverlay('seismic_anchors', "S".repeat(data.seismic_anchors), "1.5px", 0.35, 0.8); + this.addOverlay('cross_trays', data.cross_trays, "1.5px", 0.6, 0.25); + this.addOverlay('link_trays', data.link_trays, "1.5px", 0.85, 0.25); + this.addOverlay('subarray', data.subarray, "1.5px", 0.6, 0.8); + this.addOverlay('psf', data.psf.toPrecision(3), "1.5px", 0.35, 0.25); + this.addOverlay('panel_type', data.panel_type, "1.5px", 0.85, 0.8); + } else if (overlay == 'ANCHOR') { + this.addOverlay('ballast', data.ballast, "2.5px", 0.3, 0.55); + this.addOverlay('anchors', data.seismic_anchors + data.wind_anchors, '2.5px', 0.7, 0.55); + } + this.currentOverlay = overlay; + }, + + addOverlay(name, text, fontSize, relativeX, relativeY) { + let overlay = new createjs.Text(); + overlay.text = text; + overlay.font = fontSize + " sans-serif"; + overlay.x = this.width * relativeX; + overlay.y = this.height * relativeY; + overlay.textAlign = "center"; + overlay.textBaseline = "middle"; + overlay.color = this.textColor; + overlay.maxWidth = this.width * 0.5; + this.textOverlays[name] = overlay; + this.addChild(overlay); + }, + + redrawOverlays() { + this.setOverlay(this.currentOverlay); + } +}; + +Object.assign(Panel.prototype, createjs.extend(Panel, createjs.Container)); +Object.assign(Panel.prototype, panelMethods); +Panel = createjs.promote(Panel, "Container"); + +export {Panel}; diff --git a/helix/javascript/array_summary/seismic_control.js b/helix/javascript/array_summary/seismic_control.js new file mode 100644 index 0000000..ec08d8a --- /dev/null +++ b/helix/javascript/array_summary/seismic_control.js @@ -0,0 +1,73 @@ +import $ from 'jquery'; + +class SeismicControl { + constructor(arrayVisualization, subarrayDisplay) { + this.visualization = arrayVisualization; + this.subarrayDisplay = subarrayDisplay; + } + + init(seismicSelector, bannerSelector) { + let self = this; + + seismicSelector.find("#add_seismic").click(function () { + self.visualization.addSeismicAnchor(); + }); + + seismicSelector.find("#remove_seismic").click(function () { + self.visualization.removeSeismicAnchor(); + }); + + seismicSelector.find("#save_seismic_changes").click(function () { + let updatedData = self.visualization.panelData.map((panel) => { + return { + panel_id: panel.data.panel_id, + seismic_anchors: panel.data.seismic_anchors + }; + }); + + $.ajax({ + url: "/api/update_panel_data", + data: JSON.stringify(updatedData), + contentType: "application/json; charset=utf-8", + method: 'POST' + }) + .then(function (data) { + bannerSelector.removeClass("seismic_error"); + bannerSelector.removeClass("seismic_success"); + bannerSelector.removeClass("hidden"); + + let seismicClass; + let bannerText; + + bannerSelector.find(".circle").removeClass("icon-ok"); + bannerSelector.find(".circle").text("!"); + if (data.status !== undefined && data.status == "success") { + seismicClass = "seismic_success"; + bannerText = "Changes to the Seismic Anchors have been successfully saved!"; + bannerSelector.find(".circle").addClass("icon-ok"); + bannerSelector.find(".circle").text(""); + self.visualization.refreshPanels(data.panel_data); + self.subarrayDisplay.didUpdateSubarrayData(data.subarray_data); + } else if (data.error !== undefined) { + seismicClass = "seismic_error"; + bannerText = data.error; + self.subarrayDisplay.didUpdateSubarrayData(data.subarray_data); + } else { + seismicClass = "seismic_error"; + bannerText = "Unknown error, please try again."; + } + + bannerSelector.addClass(seismicClass); + bannerSelector.find(".seismic_save_message").text(bannerText); + }); + }); + + bannerSelector.find(".dismiss_button").click(function () { + bannerSelector.addClass("hidden"); + bannerSelector.removeClass("seismic_error"); + bannerSelector.removeClass("seismic_success"); + }); + } +} + +export default SeismicControl; \ No newline at end of file diff --git a/helix/javascript/array_summary/subarray_display.js b/helix/javascript/array_summary/subarray_display.js new file mode 100644 index 0000000..d14473b --- /dev/null +++ b/helix/javascript/array_summary/subarray_display.js @@ -0,0 +1,49 @@ +class SubarrayDisplay { + init(currentSeismicCountSelector, neededAnchorSelector, weightSelector, panelData) { + this.currentSeismicCountSelector = currentSeismicCountSelector; + this.neededAnchorSelector = neededAnchorSelector; + this.weightSelector = weightSelector; + this.didModifyPanels(panelData); + } + + didModifyPanels(panelData) { + let seismicCount = this.computeSeismicAnchorCounts(panelData); + + this.assignSeismicCountItems(seismicCount); + } + + didUpdateSubarrayData(subarrayData) { + for (let i = 0; i < subarrayData.length; i++) { + let data = subarrayData[i]; + let subarray = i + 2; + let neededAnchors = data.required_seismic_anchors; + let weight = data.weight; + this.neededAnchorSelector.find('td:nth-child(' + subarray + ')').text(neededAnchors); + this.weightSelector.find('td:nth-child(' + subarray + ')').text(weight.toLocaleString()); + } + } + + computeSeismicAnchorCounts(panelData) { + let seismicCount = {}; + for (let i = 0; i < panelData.length; i++) { + let panel = panelData[i].data; + if (seismicCount[panel.subarray] === undefined) { + seismicCount[panel.subarray] = 0; + } + seismicCount[panel.subarray] += panel.seismic_anchors; + } + + return seismicCount + } + + assignSeismicCountItems(seismicAnchorCounts) { + for (var subarray in seismicAnchorCounts) { + if (seismicAnchorCounts.hasOwnProperty(subarray)) { + const childId = parseInt(Object.keys(seismicAnchorCounts).indexOf(subarray)) + 2; + this.currentSeismicCountSelector.find(`td:nth-child(${childId})`).text(seismicAnchorCounts[subarray]); + } + } + } +} + +export default SubarrayDisplay; diff --git a/helix/javascript/array_summary/zoom_control.js b/helix/javascript/array_summary/zoom_control.js new file mode 100644 index 0000000..bbd96b6 --- /dev/null +++ b/helix/javascript/array_summary/zoom_control.js @@ -0,0 +1,48 @@ +import $ from "jquery"; + +class ZoomControl { + constructor(arrayVisualization) { + this.visualization = arrayVisualization; + this.zooms = [1, 2, 3, 4, 5, 6, 7]; + this.zoomLevel = 0; + } + + init(zoomSelector) { + let self = this; + zoomSelector.find("#increase_zoom").click(function () { + if (self.zoomLevel != (self.zooms.length - 1)) { + self.setZoom(self.zoomLevel + 1, zoomSelector); + } + }); + + zoomSelector.find("#decrease_zoom").click(function () { + if (self.zoomLevel != 0) { + self.setZoom(self.zoomLevel - 1, zoomSelector); + } + }); + + for (let i = 0; i < self.zooms.length; i++) { + zoomSelector.find("#zoom_indicator_" + i).click(function () { + self.setZoom(i, zoomSelector); + }); + } + + self.setZoom(0, zoomSelector); + } + + setZoom(zoomLevel, zoomSelector) { + const oldZoom = this.zoomLevel; + const totalZooms = this.zooms.length; + this.zoomLevel = zoomLevel % totalZooms; + + const zoom = this.zooms[this.zoomLevel]; + const zoom_percentage = Math.round((100 / (totalZooms - 1)) * this.zoomLevel); + this.visualization.setZoom(zoom); + + zoomSelector.find("#zoom_indicator_" + oldZoom).removeClass("zoom_active"); + zoomSelector.find("#zoom_indicator_" + this.zoomLevel).addClass("zoom_active"); + zoomSelector.find("#zoom_level").text(zoom_percentage + "%"); + } +} + +export default ZoomControl; diff --git a/helix/main.py b/helix/main.py new file mode 100644 index 0000000..180883d --- /dev/null +++ b/helix/main.py @@ -0,0 +1,470 @@ +import os +import requests +import rollbar +import rollbar.contrib.flask +from flask import Flask, request, make_response, session, render_template, \ + redirect, url_for +from flask import got_request_exception +from flask.ext import assets +from webassets.filter import get_filter + +from helix.Services.doc_gen_service import DocGenService +from helix.Services.dxf_helper import DXFHelper +from helix.Services.dxf_service import DXFService +from helix.api.api import api +from helix.calculators.calculator import Calculator +from helix.constants import redis_constant, sql_constant +from helix.constants.inverter_type import InverterType +from helix.constants.system_type import SystemType +from helix.csv_builder import CsvBuilder +from helix.doc_gen_params_builder import DocGenParamsBuilder +from helix.forms.ebom_form import EbomForm, InverterBrandForm, \ + SupervisorForm, SupervisorFormSMA, StandAloneInverterFormSMA, \ + StandAloneInverterFormDelta +from helix.forms.input_form import InputForm, ArrayForm, TestDXFForm +from helix.models.dxf.dxf_error import DXFError, OldDxfFormatException +from helix.presenters.image_presenter import ImagePresenter +from helix.presenters.panel_presenter import ProjectPresenter +from helix.session_manager import SessionManager +from helix.validators.file_validator import FileValidator, FileType +from helix.validators.subarray_validator import SubarrayValidator + +app = Flask(__name__) +app.register_blueprint(api, url_prefix='/api') +app.secret_key = os.getenv('SECRET_KEY', 'verysecretkey') +app.config['PROFILE'] = True + +assets_env = assets.Environment(app) +assets_env.init_app(app) +assets_env.load_path = [ + os.path.join(os.path.dirname(__file__), 'scss') +] +sass = get_filter('scss') +sass.load_paths = [os.path.join(os.path.dirname(os.path.realpath(__file__)), 'scss')] + +assets_env.register( + 'main_css', + assets.Bundle( + '*.scss', + filters=(sass,), + output='css/main.css' + ) +) + + +@app.before_first_request +def init_rollbar(): + # Do nothing unless Rollbar is configured + if not os.getenv("ROLLBAR_ACCESS_TOKEN"): + return + + rollbar.init(os.getenv("ROLLBAR_ACCESS_TOKEN"), + # Setup this var in heroku to distinguish errors from different envs + os.getenv("ROLLBAR_ENV", "development"), + root=os.path.dirname(os.path.realpath(__file__)), + allow_logging_basic_config=False) + + got_request_exception.connect(rollbar.contrib.flask.report_exception, app) + + +@app.route("/") +def index(): + return redirect(url_for('site_characterization')) + + +@app.route("/test_dxf/", methods=['GET', 'POST']) +def test_dxf(): + form = TestDXFForm() + if form.validate_on_submit(): + file = request.files['dxf_upload'] + file_contents = file.read().decode('utf-8') + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + user_values = session_manager.user_values() + calculator = Calculator(user_values, calculate_panel_data=False) + l_b = calculator.L_B() * 12 # convert from feet to inches + try: + dxf_data = DXFService().parse(file_contents, user_values.module_system_constants(), + user_values.system_type(), l_b, DXFHelper(), SubarrayValidator()) + dxf_data['panels'].sort(key=lambda p: p.id) + dxf_data['l_b'] = l_b + if not form.show_wind_zones.data: + dxf_data['lb_polygons'] = [] + except DXFError as error: + form.dxf_upload.errors.append(error.message) + dxf_data = {} + else: + dxf_data = {} + dxf_data['colors'] = [ + 'red', + 'orange', + 'yellow', + 'green', + 'blue', + 'purple', + ] + return render_template('test_dxf.html.jinja', context=dxf_data, form=form) + + +# wizard steps +@app.route("/site_characterization/", methods=['GET', 'POST']) +def site_characterization(): + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + site_info_form = InputForm() + context = session_manager.context() + context['current_step'] = 1 + context['javascripts'] = ['site_characterization'] + + if site_info_form.validate_on_submit(): + session_manager.save_form_submission(request.form) + return redirect(url_for('summary')) + + if request.method != 'POST': + session_manager.fill_saved_values_in_form(site_info_form) + db_session.close() + return render_template('site_characterization.html.jinja', + context=context, + form=site_info_form) + + +@app.route("/summary/") +def summary(): + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + context = session_manager.context() + context['current_step'] = 2 + + if context['site_data_available']: + user_values = session_manager.user_values() + calculator = Calculator(user_values, calculate_panel_data=False) + context['wind_zones'] = user_values.system_type().system_constants().wind_zones + context['summary_table'] = calculator.summary_table() + context['minimum_array_sizes'] = calculator.minimum_array_sizes() + context['l_b'] = round(calculator.L_B(), 2) + context['k_z'] = round(calculator.k_z(), 2) + context['q_z'] = round(calculator.q_z(), 2) + context['warning_messages'] = set() + context['javascripts'] = ['https://cdn.rawgit.com/noelboss/featherlight/1.7.6/release/featherlight.min.js'] + for panel_type, values in context['summary_table'].items(): + for panel_type_warnings in values['warnings']: + for warning in panel_type_warnings: + context['warning_messages'].add(warning) + else: + context['no_proceed'] = True + + db_session.close() + return render_template('site_summary.html.jinja', context=context) + + +@app.route("/array_summary/", methods=['GET', 'POST']) +def array_summary(): + """This endpoint allows you to upload a file. + The content of the file is parsed, and then + several objects are created that aid in the validation + of the uploaded file. + """ + + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + context = session_manager.context() + context['current_step'] = 3 + array_form = ArrayForm() + if array_form.validate_on_submit(): + if 'file_upload' in request.files and request.files['file_upload'].filename: + validator = FileValidator(session_manager.user_values()) + file = request.files['file_upload'] + file_contents = validator.obtain_stream(file) + validation_error = validator.validate(file_contents, file, + FileType.Csv) + if not validation_error: + session_manager.save_uploaded_file(file_contents, + cad_file_name=file.filename) + session_manager.save_buildings_polygons([]) # no buildings in the csv file + session_manager.save_is_drawing_inaccurate(False) + return redirect(url_for('array_summary')) + else: + array_form.file_upload.errors.append(validation_error.format_error_message()) + elif 'dxf_upload' in request.files and request.files['dxf_upload'].filename: + file = request.files['dxf_upload'] + user_values = session_manager.user_values() + calculator = Calculator(user_values, calculate_panel_data=False) + validator = FileValidator(user_values) + file_contents = validator.obtain_stream(file) + validation_error = validator.validate(file_contents, file, + FileType.AuroraDxf) + if validation_error: + error_msg = validation_error.format_error_message() + array_form.dxf_upload.errors.append(error_msg) + else: + try: + module_constants = user_values.module_system_constants() + dxf_data = DXFService().parse(file_contents, + module_constants, + user_values.system_type(), + calculator.L_B() * 12, + DXFHelper(), + SubarrayValidator()) + csv = CsvBuilder().build_cad_output(dxf_data['panels']) + session_manager.save_uploaded_file(csv, dxf_file_name=file.filename) + + buildings = dxf_data['buildings'] + session_manager.save_buildings_polygons(buildings) + session_manager.save_is_drawing_inaccurate(dxf_data['is_panel_drawing_inaccurate']) + + return redirect(url_for('array_summary')) + except DXFError as error: + array_form.dxf_upload.errors.append(error.message) + except OldDxfFormatException as error: + array_form.dxf_upload.errors.append(error.message) + elif context['csv_available']: + return redirect(url_for('power_station_configuration')) + else: + array_form.file_upload.errors.append('Please provide a .txt file!') + + context['javascripts'] = ['array_summary_bundle'] + context['hide_submit'] = True + context['cad_file_name'] = '' + context['dxf_file_name'] = '' + if context['site_data_available'] and context['csv_available']: + user_values = session_manager.user_values() + calculator = Calculator(user_values) + system_type = user_values.system_type() + module_type = user_values.module_type() + project_presenter = ProjectPresenter(system_type, module_type) + + context['wind_zones'] = system_type.system_constants().wind_zones + context['summary_table'] = calculator.summary_table() + context['minimum_array_sizes'] = calculator.minimum_array_sizes() + context['seismic_anchors'] = calculator.subarray_summary() + context['summary_values'] = calculator.summary_values() + + panels = calculator.get_computed_csv_columns() + context['panel_array'] = project_presenter.get_panel_data(panels, + calculator.subarrays, + project_presenter.get_max_y( + calculator.buildings_for_drawing, + panels)) + context['buildings'] = project_presenter.get_buildings(calculator.buildings_for_drawing) + context['override_form'] = True + context['cad_file_name'] = session_manager.site.cad_file_name or 'Upload System Text Data' + context['dxf_file_name'] = session_manager.site.dxf_file_name or 'Upload System DXF' + context['is_drawing_inaccurate'] = session_manager.user_values().is_panel_drawing_inaccurate() + context['inaccurate_drawing_warning'] = 'The subarrays in this design are not parallel to each other, \ + and the graphical representation on this page may not be accurate.' + + + elif not context['site_data_available']: + context['no_proceed'] = True + + db_session.close() + return render_template('array_summary.html.jinja', context=context, form=array_form) + + +@app.route("/power_station_configuration/", methods=['GET', 'POST']) +def power_station_configuration(): + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + context = session_manager.context() + + if session_manager.site: + system_type = session_manager.user_values().system_type() + else: + system_type = None + + inverter_brand_form = InverterBrandForm() + if request.method != 'POST' or request.form['form_id'] != 'inverter_brand_form': + inverter_brand_form.populate_choices(context['inverter_brands']) + is_delta = inverter_brand_form.is_delta() + + if is_delta: + ebom_form = EbomForm() + standalone_inverter_form = StandAloneInverterFormDelta() + standalone_inverter_form.populate_choices() + supervisor_form = SupervisorForm() + else: + ebom_form = EbomForm() + ebom_form.update_inverter_strings_choices(system_type) + standalone_inverter_form = StandAloneInverterFormSMA() + standalone_inverter_form.update_inverter_strings_choices(system_type) + standalone_inverter_form.populate_choices(context['power_stations'], context['standalone_inverters']) + supervisor_form = SupervisorFormSMA() + supervisor_form.populate_choices(context['power_stations'], context['power_monitors']) + + if request.method == 'POST': + if request.form['form_id'] == 'inverter_brand_form' and inverter_brand_form.validate_on_submit(): + session_manager.delete_power_station_config_data() + session_manager.save_or_update_inverter_brands(request.form) + return redirect("/power_station_configuration/") + + elif request.form['form_id'] == 'power_station_form' and ebom_form.validate_on_submit(): + session_manager.save_or_update_power_station(request.form) + return redirect("/power_station_configuration/") + + elif request.form['form_id'] == 'standalone_inverter_form' and standalone_inverter_form.validate_on_submit(): + session_manager.save_or_update_standalone_inverter(request.form) + return redirect("/power_station_configuration/") + + elif request.form['form_id'] == 'supervisor_form' and supervisor_form.validate_on_submit(): + session_manager.save_or_update_supervisor_monitor(request.form) + return redirect("/power_station_configuration") + + ebom_form.power_station_description.data = "Power Station " + str(len(context['power_stations']) + 1) + + inverter_enum = InverterType.DELTA if is_delta else InverterType.SMA + string_limits = {} + string_defaults = {} + for i_e in inverter_enum.all(): + string_limits[i_e.value] = list(i_e.valid_string_ranges) if i_e.valid_string_ranges is not None else [] + string_defaults[i_e.value] = i_e.default_string + + context['standalone_inverter_string_limits'] = string_limits + context['standalone_inverter_string_defaults'] = string_defaults + context['current_step'] = 4 + context['javascripts'] = ['power_station_configuration'] + + db_session.close() + return render_template('power_station_configuration.html.jinja', + context=context, + ebom_form=ebom_form, + is_delta=is_delta, + inverter_brand_form=inverter_brand_form, + standalone_inverter_form=standalone_inverter_form, + supervisor_monitor_form=supervisor_form) + + +@app.route("/download/") +def download(): + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + context = session_manager.context() + context['current_step'] = 5 + db_session.close() + return render_template('download.html.jinja', context=context) + + +@app.route("/delete_power_station/") +def delete_power_station(uuid): + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + session_manager.delete_power_station(uuid) + db_session.close() + return redirect(url_for('power_station_configuration')) + + +@app.route('/delete_standalone_inverter/') +def delete_standalone_inverter(uuid): + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + session_manager.delete_standalone_inverter(uuid) + db_session.close() + return redirect(url_for('power_station_configuration')) + + +@app.route('/delete_supervisor_monitor/') +def delete_supervisor_monitor(uuid): + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + session_manager.delete_supervisor_monitor(uuid) + db_session.close() + return redirect(url_for('power_station_configuration')) + + +@app.route("/result") +def result(): + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + user_values = session_manager.user_values() + calculator = Calculator(user_values) + csv = CsvBuilder().build_cad_output(calculator.get_computed_csv_columns()) + response = make_response(csv) + response.headers["Content-Disposition"] = "attachment; filename=%s_result.txt" % user_values.project_name_no_spaces() + db_session.close() + return response + + +@app.route("/documentation") +def documentation(): + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + user_values = session_manager.user_values() + calculator = Calculator(user_values) + image_presenter = ImagePresenter(user_values.system_type(), user_values.module_type()) + doc_gen_service = DocGenService(requests, DocGenParamsBuilder(user_values, user_values.system_type(), calculator, image_presenter)) + document = doc_gen_service.generate() + + response = make_response(document) + response.headers["Content-Disposition"] = "attachment; filename=%s_documentation.pdf" % user_values.project_name_no_spaces() + db_session.close() + return response + + +@app.route("/bom") +def bom(): + db_session = sql_constant.sql_session_maker() + session_manager = SessionManager(session, redis_constant.redis_store, db_session) + user_values = session_manager.user_values() + calculator = Calculator(user_values) + csv = CsvBuilder().build_bom_output(calculator.compute_bom()) + response = make_response(csv) + response.headers["Content-Disposition"] = "attachment; filename=%s_bom.txt" % user_values.project_name_no_spaces() + db_session.close() + return response + + +@app.route("/exposure_categories") +def exposure_categories(): + db_session = sql_constant.sql_session_maker() + context = SessionManager(session, redis_constant.redis_store, db_session).context() + db_session.close() + return render_template('exposure_categories.html.jinja', context=context) + + +@app.route("/helix_documentation") +def helix_documentation(): + db_session = sql_constant.sql_session_maker() + context = SessionManager(session, redis_constant.redis_store, db_session).context() + db_session.close() + return render_template('helix_documentation.jinja', context=context) + + +@app.template_filter('format_number') +def format_number(number): + return "{:,g}".format(number) + + +@app.template_filter('is_dual_tilt') +def is_dual_tilt(system_type): + return system_type == SystemType.dualTilt + + +@app.context_processor +def power_station_has_monitor(): + def _power_station_has_monitor(power_station, monitors): + for monitor in monitors: + if monitor['power_source'][1] == power_station['power_station_id']: + return True + return False + return dict(power_station_has_monitor=_power_station_has_monitor) + + +@app.context_processor +def enum(): + def _enum(item): + return enumerate(item) + return dict(enum=_enum) + + +def main(): + host = '0.0.0.0' + port = int(os.getenv('PORT', 5000)) + app.run(host=host, port=port, debug=bool(os.getenv("FLASK_DEBUG", False))) + + +@app.route("/fail-test") +def fail_test(): + raise RuntimeError("This is a test failure, ignore it") + + +if __name__ == "__main__": + main() diff --git a/helix/models/__init__.py b/helix/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/helix/models/coordinate.py b/helix/models/coordinate.py new file mode 100644 index 0000000..14a0e5e --- /dev/null +++ b/helix/models/coordinate.py @@ -0,0 +1,87 @@ +import json +from math import hypot +import math +from helix.functions import fequal + + +class Coordinate(object): + def __init__(self, x, y, rotation=0., calculate_rounding=True): + self.x = x + self.y = y + self.rotation = rotation + + if calculate_rounding: + self.__rounded_x = round(x, 3) + self.__rounded_y = round(y, 3) + else: + self.__rounded_x = x + self.__rounded_y = y + + def __eq__(self, other): + if self is other: + return True + if not isinstance(other, self.__class__): + return False + if fequal(self.rotation, other.rotation, delta=1e-3): + return fequal(self.x, other.x, delta=1e-3) and fequal(self.y, other.y, delta=1e-3) + return False + + @property + def dictionary(self): + return {"x": self.x, "y": self.y, "rotation": self.rotation} + + def __repr__(self): + return json.dumps(self.dictionary, sort_keys=True) + + def __sub__(self, other): + if fequal(self.rotation, other.rotation, delta=1e-1): + return Coordinate(self.x - other.x, self.y - other.y, self.rotation) + else: + raise ValueError + + def __add__(self, other): + if abs(self.rotation - other.rotation) < 1e-3: + return Coordinate(self.x + other.x, self.y + other.y, self.rotation, calculate_rounding=False) + else: + raise ValueError + + def __abs__(self): + return self.length() + + def __lt__(self, other): + if self == other: + return False + if self.y < other.y: + return True + elif other.y < self.y: + return False + return self.x < other.x + + def __round__(self, n=0): + return Coordinate(round(self.x, n), round(self.y, n), self.rotation) + + def __hash__(self): + return hash(self.__rounded_x) ^ hash(self.__rounded_y) ^ hash(self.rotation) + + # Returns a Coordinate based on self, rotated by self's rotation + def rotate(self): + rotation = math.radians(self.rotation) + x = self.x * math.cos(rotation) - self.y * math.sin(rotation) + y = self.x * math.sin(rotation) + self.y * math.cos(rotation) + return Coordinate(x, y) + + # Returns a Coordinate based on self, rotated by self's negative rotation + def unrotate(self): + rotation = math.radians(-self.rotation) + x = self.x * math.cos(rotation) - self.y * math.sin(rotation) + y = self.x * math.sin(rotation) + self.y * math.cos(rotation) + return Coordinate(x, y) + + def scale(self, x, y): + return Coordinate(self.x * x, self.y * y, self.rotation) + + def neg_translate(self, other): + return Coordinate(self.x - other.x, self.y - other.y, self.rotation) + + def length(self): + return hypot(self.x, self.y) diff --git a/helix/models/dxf/__init__.py b/helix/models/dxf/__init__.py new file mode 100644 index 0000000..8908269 --- /dev/null +++ b/helix/models/dxf/__init__.py @@ -0,0 +1 @@ +__author__ = 'pivotal' diff --git a/helix/models/dxf/dxf_error.py b/helix/models/dxf/dxf_error.py new file mode 100644 index 0000000..b168ae5 --- /dev/null +++ b/helix/models/dxf/dxf_error.py @@ -0,0 +1,8 @@ +class DXFError(Exception): + def __init__(self, message): + self.message = message + + +class OldDxfFormatException(Exception): + def __init__(self, message): + self.message = message diff --git a/helix/models/dxf/graph_direction.py b/helix/models/dxf/graph_direction.py new file mode 100644 index 0000000..dfc71b1 --- /dev/null +++ b/helix/models/dxf/graph_direction.py @@ -0,0 +1,47 @@ +from enum import Enum + + +class GraphDirection(Enum): + North = (0, 1) + NorthEast = (1, 1) + East = (1, 0) + SouthEast = (1, -1) + South = (0, -1) + SouthWest = (-1, -1) + West = (-1, 0) + NorthWest = (-1, 1) + + @classmethod + def values(cls): + return [ + cls.North.value, + cls.NorthEast.value, + cls.East.value, + cls.SouthEast.value, + cls.South.value, + cls.SouthWest.value, + cls.West.value, + cls.NorthWest.value + ] + + @classmethod + def ordinal_directions(cls): + return [ + cls.North, + cls.East, + cls.South, + cls.West + ] + + def opposite_direction(self): + return { + GraphDirection.North: GraphDirection.South, + GraphDirection.NorthEast: GraphDirection.SouthWest, + GraphDirection.East: GraphDirection.West, + GraphDirection.SouthEast: GraphDirection.NorthWest, + GraphDirection.South: GraphDirection.North, + GraphDirection.SouthWest: GraphDirection.NorthEast, + GraphDirection.West: GraphDirection.East, + GraphDirection.NorthWest: GraphDirection.SouthEast + }[self] + diff --git a/helix/models/dxf/graph_node.py b/helix/models/dxf/graph_node.py new file mode 100644 index 0000000..412612b --- /dev/null +++ b/helix/models/dxf/graph_node.py @@ -0,0 +1,68 @@ +from helix.models.dxf.graph_direction import GraphDirection + + +class GraphNode(object): + def __init__(self, panel, x_spacing, y_spacing): + self.neighbors = {} + self.neighbors = { + GraphDirection.North: None, + GraphDirection.NorthEast: None, + GraphDirection.East: None, + GraphDirection.SouthEast: None, + GraphDirection.South: None, + GraphDirection.SouthWest: None, + GraphDirection.West: None, + GraphDirection.NorthWest: None + } + self.panel = panel + self.x_spacing = x_spacing + self.y_spacing = y_spacing + + @property + def coordinate(self): + return self.panel.coordinate + + def has_existing_neighbor(self, direction): + return self.neighbors.get(direction) is not None + + def add_neighbor(self, other_node, direction): + if other_node is not None: + if direction: + self.neighbors[direction] = other_node + opposite_direction = direction.opposite_direction() + other_node.neighbors[opposite_direction] = self + + def neighboring_nodes(self): + return [neighbor for neighbor in self.neighbors.values() if neighbor is not None] + + def ordinal_neighbors(self): + neighbors = {} + for direction in GraphDirection.ordinal_directions(): + if self.neighbors.get(direction): + neighbors[direction] = self.neighbors[direction] + return neighbors + + def __hash__(self): + return self.panel.coordinate.__hash__() + + def __repr__(self): + neighbors = {} + for key, value in self.neighbors.items(): + if value is not None: + neighbors[key] = value.panel + return str(self.panel) + " - " + str(neighbors) + + def __eq__(self, other): + if self is other: + return True + if not self.panel == other.panel and self.x_spacing != other.x_spacing and self.y_spacing != other.y_spacing: + return False + for key, value in self.neighbors.items(): + other_neighbor = other.neighbors[key] + if value is None and other_neighbor is None: + continue + elif value is None or other_neighbor is None: + return False + elif value.panel != other_neighbor.panel: + return False + return True diff --git a/helix/models/dxf/graph_node_store.py b/helix/models/dxf/graph_node_store.py new file mode 100644 index 0000000..bd5b9c5 --- /dev/null +++ b/helix/models/dxf/graph_node_store.py @@ -0,0 +1,41 @@ +from helix.helpers.nodequadtree import Bounds, NodeQuadTree + +class GraphNodeStore(list): + + def __init__(self): + super().__init__() + self.variance = 0.2 + self.first = True + self.quadTree = None + + def add_node(self, node): + self.append(node) + if self.first: + self.left = self.right = node.coordinate.x + self.top = self.bottom = node.coordinate.y + self.first = False + else: + self.left = min(self.left, node.coordinate.x) + self.right = max(self.right, node.coordinate.x) + self.bottom = min(self.bottom, node.coordinate.y) + self.top = max(self.top, node.coordinate.y) + + def distance_squared(self, node, coordinate): + dx = node.coordinate.x - coordinate.x + dy = node.coordinate.y - coordinate.y + return dx * dx + dy * dy + + def find_coordinate(self, coordinate): + # create and populate the quadtree on first request + if self.quadTree is None: + self.quadTree = NodeQuadTree(1, Bounds(self.left, self.bottom, self.right - self.left, self.top - self.bottom), self.variance) + for node in self: + self.quadTree.insert(node) + del self[:] + + possibilities = self.quadTree.retrieve(coordinate) + for node in possibilities: + if self.distance_squared(node, coordinate) <= self.variance ** 2: + return node + else: + return None diff --git a/helix/models/dxf/polygon.py b/helix/models/dxf/polygon.py new file mode 100644 index 0000000..9371271 --- /dev/null +++ b/helix/models/dxf/polygon.py @@ -0,0 +1,117 @@ +import math +from helix.functions import fequal + + +class Polygon(object): + def __init__(self, line=None, points=()): + if line: + self.points = [line.start, line.end] + elif points: + self.points = points + else: + self.points = [] + + def continues_with_line(self, line): + if not self.closed and line.start == self.points[-1]: + return True + return False + + @property + def closed(self): + return len(self.points) != 1 and self.points[0] == self.points[-1] + + def sorted_points(self): + return sorted(self.points, key=lambda x: x[0]) + + def determine_orientation(self): + points = self.sorted_points() + p1 = points[0] + other_points = sorted(points[1:], key=lambda x: (x[0] - p1[0]) ** 2 + (x[1] - p1[1]) ** 2) + p2 = other_points[1] + # other_points[0] is the point that (along with p1) defines the short edge of this rectangle + # other_points[1] defines the long edge of this rectangle + # other_points[2] is diagonally across from p1. Not useful for defining edges. + x = p2[0] - p1[0] + y = p2[1] - p1[1] + return math.degrees(math.atan2(y, x)) + + def __do_something_with_long_edges__(self, module, fn, pair_spacing): + def cmp_point(p1, p2, pair_spacing): + dx = p1[0] - p2[0] + dy = p1[1] - p2[1] + d = math.sqrt(dx * dx + dy * dy) + allowedVariance = 0.1 + + if pair_spacing is None: + return d < allowedVariance + else: + d = math.fabs(d - pair_spacing) + return d <= allowedVariance + + p1 = self.points[0] + other_points = sorted(self.points[1:], key=lambda x: (x[0] - p1[0]) ** 2 + (x[1] - p1[1]) ** 2) + self_long_edges = [sorted([p1, other_points[1]],), sorted([other_points[0], other_points[2]],)] + + p2 = module.points[0] + other_points = sorted(module.points[1:], key=lambda x: (x[0] - p2[0]) ** 2 + (x[1] - p2[1]) ** 2) + other_long_edges = [sorted([p2, other_points[1]],), sorted([other_points[0], other_points[2]],)] + + for edge in self_long_edges: + for other_edge in other_long_edges: + if cmp_point(edge[0], other_edge[0], pair_spacing) and\ + cmp_point(edge[1], other_edge[1], pair_spacing): + fn(self, module, edge, other_edge) + return True + + return False + + def shares_module_on_long_edge(self, module, pair_spacing): + def on_match_edges(this, other, this_edges, other_edges): + pass + + return self.__do_something_with_long_edges__(module, on_match_edges, pair_spacing) + + def consolidate_with(self, pair, pair_spacing): + def on_match_edges(this, other, this_edges, other_edges): + this.points.remove(this_edges[0]) + this.points.remove(this_edges[1]) + other.points.remove(other_edges[0]) + other.points.remove(other_edges[1]) + this.points += pair.points + + self.__do_something_with_long_edges__(pair, on_match_edges, + pair_spacing) + + def scale(self, scale_x=1, scale_y=1): + polygon = Polygon() + polygon.points = [(x * scale_x, y * scale_y) for x, y in self.points] + return polygon + + def svg_points(self, array_size): + value_string = "" + if len(self.points) == 4: + p0 = self.points[0] + points = sorted(self.points, key=lambda x: (x[0] - p0[0]) ** 2 + (x[1] - p0[1]) ** 2) + sorted_points = [p0, points[1], points[3], points[2]] + else: + sorted_points = self.points + for point in sorted_points: + value_string += "%f,%f " % (point[0], array_size[1] - point[1]) + return value_string + + def __repr__(self): + return str([(round(p[0], 3), round(p[1], 3)) for p in self.points]) + + def __eq__(self, other): + if self is other: + return True + if not isinstance(other, self.__class__): + return False + if len(self.points) != len(other.points): + return False + for idx, point in enumerate(self.points): + other_point = other.points[idx] + for variable_index in range(len(point)): + if not fequal(point[variable_index], other_point[variable_index]): + return False + return True diff --git a/helix/models/panel.py b/helix/models/panel.py new file mode 100644 index 0000000..a416748 --- /dev/null +++ b/helix/models/panel.py @@ -0,0 +1,130 @@ +from enum import Enum +import json +from helix.constants.panel_type import PanelType +from helix.functions import fequal +from helix.models.coordinate import Coordinate + + +class PanelData(Enum): + Handle = 'HANDLE' + Blockname = 'BLOCKNAME' + Subarray = 'SUBARRAY' + PanelType = 'POS' + WindZone = 'WIND' + Ballast = 'BAL' + LinkTray = 'LT_CALCULATED' + CrossTray = 'XTRAY' + WindAnchor = 'ANC' + SeismicAnchor = 'SEISMIC' + Coordinate = 'COORDINATE' + Pressure = 'PSF' + Id = 'ID' + PresentedLinkTray = 'LTRAY' + Xcoord = 'XCOORD' + Ycoord = 'YCOORD' + Rotation = 'ANGLE' + FuzzyWindZone = 'FUZZYWINDZONE' + + +class PanelWarnings(Enum): + MaxPsf = 'The values highlighted are panels that exceed our UL listed load limit. Please do not place panels in these areas to avoid exceeding the listed limit.' + +class Panel(object): + def __init__(self, handle=None, blockname=None, subarray=None, panel_type=None, wind_zone=None, ballast=None, + link_tray=None, cross_tray=None, wind_anchors=None, seismic_anchors=None, coordinate=None, + pressure=None, id=None, presented_link_tray=None, original_coordinate=None, fuzzy_wind_zone=False, + warnings=None): + self.handle = handle + self.blockname = blockname + self.subarray = subarray + self.panel_type = panel_type + self.wind_zone = wind_zone + self.ballast = ballast + self.link_tray = link_tray + self.cross_tray = cross_tray + self.wind_anchors = wind_anchors + self.seismic_anchors = seismic_anchors + # this field after DXF parsing and before serialization into CSV contains translated coordinates (all positive) + # and after deserialization from CSV contains original coordinates - same as original_coordinate field + self.coordinate = coordinate + self.original_coordinate = original_coordinate + self.pressure = pressure + self.id = id + self.presented_link_tray = presented_link_tray + self.fuzzy_wind_zone = fuzzy_wind_zone + self.warnings = warnings or [] + + def merge(self, other): + if not isinstance(other, self.__class__): + return self + + d = {} + for key, data in self.__dict__.items(): + if data is not None: + d[key] = data + else: + d[key] = other.__dict__.get(key) + panel = Panel() + panel.__dict__.update(d) + return panel + + def is_subset(self, other): + if not isinstance(other, self.__class__): + return False + for key, value in self.__dict__.items(): + if value is None: + continue + if other.__dict__[key] != value: + return False + return True + + def almost_equal(self, other, decimal=6): + if not isinstance(other, self.__class__): + return False + if self.pressure is not None and other.pressure is not None: + if not fequal(self.pressure, other.pressure, delta=(10 ** (-decimal))): + print("Pressures are not equal to within %d decimal places, got %f, expected %f" % (decimal, self.pressure, other.pressure)) + return False + elif self.pressure != other.pressure: + return False + for key, value in self.__dict__.items(): + if key == 'pressure': + continue + elif other.__dict__.get(key) != value: + print("Expected %s to be equal, got %s, expected %s" % (key, str(value), str(other.__dict__.get(key)))) + return False + return True + + def __deepcopy__(self, _): + return Panel(handle=self.handle, + blockname=self.blockname, + subarray=self.subarray, + panel_type=self.panel_type, + wind_zone=self.wind_zone, + ballast=self.ballast, + link_tray=self.link_tray, + cross_tray=self.cross_tray, + wind_anchors=self.wind_anchors, + seismic_anchors=self.seismic_anchors, + coordinate=self.coordinate, + original_coordinate=self.original_coordinate, + pressure=self.pressure, + id=self.id, + presented_link_tray=self.presented_link_tray, + fuzzy_wind_zone=self.fuzzy_wind_zone) + + def __eq__(self, other): + if self is other: + return True + return self.almost_equal(other, decimal=3) + + def __repr__(self): + def json_forcer(x): + if isinstance(x, PanelType): + return x.value + if isinstance(x, Coordinate): + return x.dictionary + return x.__dict__ + + d = {key: value for (key, value) in self.__dict__.items() if value is not None} + return json.dumps(d, sort_keys=True, default=json_forcer) diff --git a/helix/models/sql/__init__.py b/helix/models/sql/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/helix/models/sql/inverter_brands.py b/helix/models/sql/inverter_brands.py new file mode 100644 index 0000000..61b27cb --- /dev/null +++ b/helix/models/sql/inverter_brands.py @@ -0,0 +1,14 @@ +from sqlalchemy import Column, Integer, ForeignKey + +from helix.models.sql.shared_sql_base import Base + + +class InverterBrand(Base): + __tablename__ = 'inverter_brands' + id = Column(Integer, primary_key=True) + site_id = Column(Integer, ForeignKey('sites.id'), primary_key=True) + + def to_json(self): + return { + 'inverter_brand_id': self.id, + } diff --git a/helix/models/sql/inverters.py b/helix/models/sql/inverters.py new file mode 100644 index 0000000..ce7a1a8 --- /dev/null +++ b/helix/models/sql/inverters.py @@ -0,0 +1,29 @@ +from sqlalchemy import Column, Integer, Enum, Boolean, ForeignKey, CheckConstraint +from helix.constants.inverter_type import InverterType +from helix.models.sql.shared_sql_base import Base + + +class Inverter(Base): + __tablename__ = 'inverters' + id = Column(Integer, primary_key=True) + model = Column(Enum(*map(lambda x: str(x.value), InverterType.all()), name='invertertype'), nullable=False) + strings_per_inverter = Column(Integer, nullable=False) + sunshade = Column(Boolean) + dc_switch = Column(Boolean) + splice_box = Column(Boolean) + power_station_id = Column(Integer, ForeignKey('power_stations.id')) + standalone_inverter_id = Column(Integer, ForeignKey('standalone_inverters.id', ondelete='CASCADE')) + + __table_args__ = ( + CheckConstraint('(power_station_id IS NULL != standalone_inverter_id IS NULL)'), + ) + + def to_json(self): + inverter_type = InverterType.SMA if int(self.model) in InverterType.SMA.all() else InverterType.DELTA + return { + 'model': inverter_type(int(self.model)), + 'strings_per_inverter': self.strings_per_inverter, + 'sunshade': self.sunshade, + 'dc_switch': self.dc_switch, + 'splice_box': self.splice_box, + } diff --git a/helix/models/sql/power_monitors.py b/helix/models/sql/power_monitors.py new file mode 100644 index 0000000..67c315b --- /dev/null +++ b/helix/models/sql/power_monitors.py @@ -0,0 +1,21 @@ +from sqlalchemy import Column, Integer, ForeignKey +from sqlalchemy.orm import relationship +from helix.models.sql.shared_sql_base import Base + + +class PowerMonitor(Base): + __tablename__ = 'power_monitors' + id = Column(Integer, primary_key=True) + site_id = Column(Integer, ForeignKey('sites.id')) + power_station_id = Column(Integer, ForeignKey('power_stations.id')) + power_station = relationship("PowerStation") + + def to_json(self): + if self.power_station: + power_source = (self.power_station.description, self.power_station.id) + else: + power_source = ('Switch Gear/External', None) + return { + 'monitor_id': self.id, + 'power_source': power_source + } diff --git a/helix/models/sql/power_stations.py b/helix/models/sql/power_stations.py new file mode 100644 index 0000000..fcb1a10 --- /dev/null +++ b/helix/models/sql/power_stations.py @@ -0,0 +1,24 @@ +from sqlalchemy import Column, Integer, Unicode, ForeignKey +from sqlalchemy.orm import relationship +from helix.models.sql.inverters import Inverter +from helix.models.sql.shared_sql_base import Base + + +class PowerStation(Base): + __tablename__ = 'power_stations' + id = Column(Integer, primary_key=True) + site_id = Column(Integer, ForeignKey('sites.id')) + quantity = Column(Integer, nullable=False) + ac_run_length = Column(Integer, nullable=False) + description = Column(Unicode, nullable=False) + inverters = relationship(Inverter.__name__, backref="power_stations", cascade="save-update, merge, delete") + + def to_json(self): + return { + 'inverter_quantity': len(self.inverters), + 'power_station_quantity': self.quantity, + 'power_station_description': self.description, + 'power_station_id': self.id, + 'ac_run_length': self.ac_run_length, + 'inverters': [inverter.to_json() for inverter in self.inverters] + } diff --git a/helix/models/sql/shared_sql_base.py b/helix/models/sql/shared_sql_base.py new file mode 100644 index 0000000..860e542 --- /dev/null +++ b/helix/models/sql/shared_sql_base.py @@ -0,0 +1,3 @@ +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() diff --git a/helix/models/sql/sites.py b/helix/models/sql/sites.py new file mode 100644 index 0000000..1ac96d4 --- /dev/null +++ b/helix/models/sql/sites.py @@ -0,0 +1,41 @@ +from sqlalchemy import Column, Integer, Unicode, Float, Enum, ForeignKey +from sqlalchemy.orm import relationship + +from helix.constants.anchor_type import AnchorType +from helix.constants.module_type import ModuleType +from helix.constants.system_type import SystemType +from helix.models.sql.inverter_brands import InverterBrand +from helix.models.sql.power_monitors import PowerMonitor +from helix.models.sql.power_stations import PowerStation +from helix.models.sql.shared_sql_base import Base +from helix.models.sql.standalone_inverters import StandaloneInverter + + +class Site(Base): + __tablename__ = 'sites' + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey('users.id'), nullable=False) + project_name = Column(Unicode, nullable=False) + building_height = Column(Float, nullable=False) + building_width = Column(Float, nullable=False) + building_length = Column(Float, nullable=False) + parapet_height = Column(Float, nullable=False) + wind_speed = Column(Integer, nullable=False) + exposure_category = Column(Unicode, nullable=False) + exposure_transition_distance = Column(Integer) + ballast_block_weight = Column(Integer, nullable=False) + max_psf = Column(Float, nullable=False) + system_type = Column(Enum(SystemType.singleTilt.value, SystemType.dualTilt.value, name='SystemType'), nullable=False) + module_type = Column(Enum(ModuleType.Cell96.value, ModuleType.Cell128.value, ModuleType.PSeries.value, name='ModuleType'), nullable=False) + anchor_type = Column(Enum(AnchorType.OMG_PowerGrip.value, AnchorType.OMG_PowerGrip_Plus.value, AnchorType.EcoFasten.value, name='AnchorType'), nullable=False) + spectral_response = Column(Float, nullable=False) + seismic_importance_factor = Column(Float, nullable=False) + cad_file = Column(Unicode) + cad_file_name = Column(Unicode) + dxf_file = Column(Unicode) + dxf_file_name = Column(Unicode) + + inverter_brands = relationship(InverterBrand.__name__, backref="site", cascade="save-update, merge, delete") + power_stations = relationship(PowerStation.__name__, backref="site", cascade="save-update, merge, delete") + standalone_inverters = relationship(StandaloneInverter.__name__, backref="site", cascade="save-update, merge, delete") + power_monitors = relationship(PowerMonitor.__name__, backref="site", cascade="save-update, merge, delete") diff --git a/helix/models/sql/standalone_inverters.py b/helix/models/sql/standalone_inverters.py new file mode 100644 index 0000000..b45ee5b --- /dev/null +++ b/helix/models/sql/standalone_inverters.py @@ -0,0 +1,29 @@ +from sqlalchemy import Column, Integer, ForeignKey +from sqlalchemy.orm import relationship, backref +from helix.models.sql.inverters import Inverter +from helix.models.sql.power_stations import PowerStation +from helix.models.sql.shared_sql_base import Base + + +class StandaloneInverter(Base): + __tablename__ = 'standalone_inverters' + id = Column(Integer, primary_key=True) + site_id = Column(Integer, ForeignKey('sites.id')) + ac_run_length = Column(Integer, nullable=False) + inverter = relationship(Inverter.__name__, + backref=backref("standalone_inverters", uselist=False), + cascade="save-update, merge, delete") + attachment_point_id = Column(Integer, ForeignKey('power_stations.id')) + attachment_point = relationship(PowerStation.__name__) + + + def to_json(self): + if self.attachment_point: + attachment_point = (self.attachment_point.description, self.attachment_point.id) + else: + attachment_point = ('Switch Gear', None) + return { **{ + 'standalone_inverter_id': self.id, + 'ac_run_length': self.ac_run_length, + 'attachment_point': attachment_point + }, **(self.inverter[0].to_json()) } diff --git a/helix/models/sql/users.py b/helix/models/sql/users.py new file mode 100644 index 0000000..db1cb69 --- /dev/null +++ b/helix/models/sql/users.py @@ -0,0 +1,9 @@ +from sqlalchemy import Column, Integer, Unicode +from helix.models.sql.shared_sql_base import Base + + +class User(Base): + __tablename__ = 'users' + id = Column(Integer, primary_key=True) + username = Column(Unicode, nullable=False) + password_hash = Column(Unicode, nullable=False) diff --git a/helix/models/subarray.py b/helix/models/subarray.py new file mode 100644 index 0000000..8d23a73 --- /dev/null +++ b/helix/models/subarray.py @@ -0,0 +1,67 @@ +import json +from helix.functions import fequal +from helix.models.coordinate import Coordinate + + +class Subarray(object): + def __init__(self, subarray_number=None, origin=None, required_seismic_anchors=None, start_row=None, size=None, + weight=None, row_count=None, row_counted_geometrically=None, column_count=None, + column_counted_geometrically=None): + self.subarray_number = subarray_number + self.origin = origin + self.required_seismic_anchors = required_seismic_anchors + self.start_row = start_row + self.size = size + self.weight = weight + self.row_count = row_count + self.row_counted_geometrically = row_counted_geometrically + self.column_count = column_count + self.column_counted_geometrically = column_counted_geometrically + + def filter_data(self, required_data): + required_key_names = map(lambda x: x.subarray_key(), required_data) + d = {key: self.__dict__.get(key) for key in required_key_names} + + subarray = Subarray() + subarray.__dict__.update(d) + return subarray + + def is_subset(self, other): + if not isinstance(other, self.__class__): + return False + for key, value in self.__dict__.items(): + if value is None: + continue + if other.__dict__[key] != value: + return False + return True + + def almost_equal(self, other, decimal=6): + if not isinstance(other, self.__class__): + return False + if not fequal(self.weight, other.weight, delta=(10 ** (-decimal))): + print("Weights are not equal to within %d decimal places, got %f, expected %f" % (decimal, self.weight, other.weight)) + return False + for key, value in self.__dict__.items(): + if key == 'weight': + continue + elif other.__dict__.get(key) != value: + print("Expected %s to be equal, got %s, expected %s" % (key, str(value), str(other.__dict__.get(key)))) + return False + return True + + def __eq__(self, other): + if self is other: + return True + if not isinstance(other, self.__class__): + return False + return self.__dict__ == other.__dict__ + + def __repr__(self): + def json_forcer(x): + if isinstance(x, Coordinate): + return x.dictionary + return x.__dict__ + + d = {key: value for (key, value) in self.__dict__.items() if value is not None} + return json.dumps(d, sort_keys=True, default=json_forcer) diff --git a/helix/presenters/__init__.py b/helix/presenters/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/helix/presenters/image_presenter.py b/helix/presenters/image_presenter.py new file mode 100644 index 0000000..4d79bfa --- /dev/null +++ b/helix/presenters/image_presenter.py @@ -0,0 +1,172 @@ +from svgwrite.container import Group +import svgwrite +from svgwrite.shapes import Rect, Polygon +from svgwrite.text import Text +import wand +from wand.api import library +from wand.image import Image +from helix.calculators.subarray_helper import extract_subarray +from helix.constants.color import Color +from helix.constants.system_type import SystemType + + +class ImagePresenter(object): + def __init__(self, system_type, module_type): + self.system_type = system_type + self.module_type = module_type + + def generate_image(self, panels, subarrays): + image_dimensions = (620, 710) + if len(panels) < 3000: + svg_string = self.generate_array_svg(image_dimensions, panels, subarrays) + else: + svg_string = ImagePresenter.svg_too_large_string(image_dimensions) + with Image(blob=svg_string.encode('utf-8'), format='svg') as img: + with wand.color.Color('transparent') as background_color: + library.MagickSetBackgroundColor(img.wand, background_color.resource) + png_image = img.make_blob('png') + return png_image + + @staticmethod + def svg_too_large_string(image_dimensions): + dwg = svgwrite.Drawing(size=image_dimensions) + dwg.add(Text("Array may be too large, please try again or break up array into smaller components", + x=[image_dimensions[0] / 2], + y=[image_dimensions[1] / 2], + text_anchor="middle")) + return dwg.tostring() + + def generate_array_svg(self, image_dimensions, panels, subarrays): + dwg = svgwrite.Drawing(size=image_dimensions) + array_dimensions = self.calculate_max_array_dimensions(panels, subarrays) + + module_constants = self.system_type.module_constants(self.module_type) + + array_width, array_height = array_dimensions + spacing_x, spacing_y = module_constants.presenter_spacing + spacing_x *= 10 + spacing_y *= 10 + + scaling_x = image_dimensions[0] / (array_width * spacing_x) + scaling_y = image_dimensions[1] / (array_height * spacing_y) + scaling_factor = min(scaling_x, scaling_y) + + for subarray in subarrays: + dwg.add(self.draw_subarray(panels, subarray, array_dimensions, scaling_factor)) + return dwg.tostring() + + def draw_subarray(self, all_panels, subarray, array_dimensions, scaling_factor): + module_constants = self.system_type.module_constants(self.module_type) + array_width, array_height = array_dimensions + spacing_x, spacing_y = module_constants.presenter_spacing + spacing_x *= 10 + spacing_y *= 10 + + panels = extract_subarray(all_panels, subarray.subarray_number) + + subarray_group = Group(transform="scale(%f, %f)" % (scaling_factor, scaling_factor)) + + for panel in panels: + group = self.draw_panel(panel, subarray, array_height - 1, (spacing_x, spacing_y)) + subarray_group.add(group) + return subarray_group + + def draw_panel(self, panel, subarray, max_height, panel_dimensions): + origin = subarray.origin + width, height = panel_dimensions + + border_width = 0.5 + + inner_width = width - border_width + inner_height = height - border_width + inner_dimensions = (inner_width - border_width, inner_height - border_width) + + x = (panel.coordinate.x + origin.x) * width + y = (max_height - (panel.coordinate.y + origin.y)) * height + + group = Group(stroke_width=border_width, transform="translate(%f, %f)" % (x, y)) # FIXME: look into not having to do this. + + fill_color, secondary_fill_color, border_color, text_color = self.colors(panel) + + # draw the background, with a white border around everything + background_rect = Rect(insert=(0, 0), size=panel_dimensions, fill=fill_color.value, stroke=Color.array_background.value) + group.add(background_rect) + + # IFF we have both seismic and wind anchors, draw the panel as the "half wind, half seismic" thing. + # This means we add a triangle that divides the panel. + if secondary_fill_color is not None: + points = [ + (border_width, border_width), + (inner_width, inner_height), + (inner_width, border_width) + ] + dual_anchor_indicator = Polygon(points, fill=secondary_fill_color.value, stroke=secondary_fill_color.value) + group.add(dual_anchor_indicator) + + # draw a border rect. + # This doesn't need to be drawn if we don't have any wind or seismic anchors. + if border_color != fill_color: + border_rect = Rect(insert=(border_width, border_width), size=inner_dimensions, stroke=border_color.value, fill_opacity=0) + group.add(border_rect) + + # If dual-tilt, draw the little triangle on the right side of the panel + # Use a hardcoded border value, because this won't change. + if self.system_type == SystemType.dualTilt: + points = [ + (width / 2, border_width), + (width / 2, inner_height), + (inner_width, height / 2), + ] + dual_anchor_indicator = Polygon(points, fill_opacity=0, stroke=Color.border.value, stroke_width=border_width / 2) + group.add(dual_anchor_indicator) + + # add all the text! + ballast_text = self.text_overlay(str(panel.ballast), panel_dimensions, 0.3, 0.55, "2.5px", text_color) + group.add(ballast_text) + + anchors_text = self.text_overlay(str(panel.wind_anchors + panel.seismic_anchors), panel_dimensions, 0.7, 0.55, "2.5px", text_color) + group.add(anchors_text) + + return group + + @staticmethod + def text_overlay(text, dimensions, x, y, font_size, color): + return Text( + text, + x=[dimensions[0] * x], + y=[dimensions[1] * y], + font_size=font_size, + font_family='sans-serif', + text_anchor="middle", + fill=color.value + ) + + @staticmethod + def calculate_max_array_dimensions(all_panels, subarrays): + actual_y_coordinates = [] + actual_x_coordinates = [] + for subarray in subarrays: + panels = extract_subarray(all_panels, subarray.subarray_number) + actual_y_coordinates += [panel.coordinate.y + subarray.origin.y for panel in panels] + actual_x_coordinates += [panel.coordinate.x + subarray.origin.x for panel in panels] + return max(actual_x_coordinates) + 1, max(actual_y_coordinates) + 1 + + @staticmethod + def colors(panel): + fill = Color.default_panel_background + secondary = None + border = Color.border + text = Color.dark_text + + if panel.seismic_anchors > 0 and panel.wind_anchors > 0: + fill = Color.seismic_background + secondary = Color.wind_background + elif panel.seismic_anchors > 0: + fill = Color.seismic_background + elif panel.wind_anchors > 0: + fill = Color.wind_background + else: + border = Color.default_panel_background + text = Color.light_text + + return fill, secondary, border, text diff --git a/helix/presenters/panel_presenter.py b/helix/presenters/panel_presenter.py new file mode 100644 index 0000000..cef976c --- /dev/null +++ b/helix/presenters/panel_presenter.py @@ -0,0 +1,99 @@ +import math + +from helix.models.coordinate import Coordinate +import sys + +class ProjectPresenter(object): + def __init__(self, system_type, module_type): + self.offset = None + self.system_type = system_type + self.module_type = module_type + + '''"This function expects coordinates to be in unit values, processed in coordinates_calculator.py" ''' + def get_panel_data(self, panels, subarrays, max_y = None): + if self.offset is not None: + raise RuntimeError("ProjectPresenter panels must be computed before buildings") + + table_data = [] + + system_constants = self.system_type.system_constants() + module_constants = self.system_type.module_constants(self.module_type) + spacing_x, spacing_y = module_constants.presenter_spacing + wind_zones = system_constants.wind_zones + + + + for panel in panels: + subarray = [x for x in subarrays if x.subarray_number == panel.subarray][0] + origin = subarray.origin + + table_data.append({ + 'x': (panel.coordinate.x + origin.x) * spacing_x, + 'y': (panel.coordinate.y + origin.y) * spacing_y, + 'width': spacing_x, + 'height': spacing_y, + 'data': { + 'panel_id': panel.id, + 'panel_type': panel.panel_type.number(), + 'ballast': panel.ballast, + 'wind_anchors': panel.wind_anchors, + 'seismic_anchors': panel.seismic_anchors, + 'wind_zones': wind_zones[panel.wind_zone], + 'link_trays': panel.presented_link_tray, + 'cross_trays': panel.cross_tray, + 'psf': round(panel.pressure, 2), + 'subarray': panel.subarray + } + }) + + # Move coordinates to reflect origin being at top-left + # (as per canvas) instead of bottom-left + # if max_y is not passed - use the panels instead of buildings + if max_y is None: + height = max(map(lambda row: row['y'], table_data)) + else: + height = max_y + self.offset = height + for panel in table_data: + panel['y'] = height - panel['y'] + return table_data + + def get_buildings(self, buildings): + if self.offset is None: + self.offset = 0 + + module_constants = self.system_type.module_constants(self.module_type) + spacing_x, spacing_y = module_constants.presenter_spacing + # max_y = -sys.maxsize - 1 # for flipping the y coordinate + + result = [] + for building in buildings: + presentable_building = [] + result.append(presentable_building) + # origin = self.find_origin(building) + for point in building: + point.x = point.x * spacing_x + point.y = point.y * spacing_y + presentable_building.append(point.__dict__) + + for presentable_building in result: + for point in presentable_building: + point['y'] = self.offset - point['y'] + + return result + + def get_max_y(self,buildings, panels): + + module_constants = self.system_type.module_constants(self.module_type) + _, spacing_y = module_constants.presenter_spacing + all_y = [] + + if buildings is None or buildings == []: # no buildings in the file, probably CSV coordinates loaded + for panel in panels: + all_y.append(panel.coordinate.y) + + for building in buildings: + for point in building: + all_y.append(point.y) + return max(all_y) * spacing_y + diff --git a/helix/scss/array_visualization.scss b/helix/scss/array_visualization.scss new file mode 100644 index 0000000..a4b743d --- /dev/null +++ b/helix/scss/array_visualization.scss @@ -0,0 +1,285 @@ +@import 'variables'; + +.array_information { + margin: 0 $form-section-margin; + display: flex; + flex-direction: column; + width: calc(100% - 50px); + border: 1px solid $array-visualization-background; + + .item_title { + font-weight: bold; + } + + .item_body { + color: $medium-blue; + font-weight: $semi-bold; + } + + .array_summary { + display: flex; + flex-direction: row; + flex-wrap: wrap; + padding: 10px; + justify-content: flex-start; + + .summary_item { + width: calc(50% - 50px); + margin: 10px 20px; + } + } + + .subarray_summary { + h4 { + padding: 0 25px; + margin-bottom: 0; + } + + #seismic_save { + color: white; + height: 50px; + font-weight: 400; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 20px; + + .left_container { + display: flex; + justify-content: flex-start; + align-items: center; + + .seismic_save_message { + padding-left: 10px; + } + } + + .circle { + border: 2px solid white; + border-radius: 50%; + height: 25px; + width: 25px; + font-weight: 500; + text-align: center; + line-height: 25px; + } + + .dismiss_button { + height: 20px; + } + + &.seismic_error { + background: $error-color; + } + + &.seismic_success { + background: $success-color; + } + } + } +} + +.system_data_upload { + display: flex; + + &.initial_array_upload { + color: $circle-color; + width: 100%; + flex-direction: column; + border: 1px solid $light-border-color; + background: $off-white; + padding: 30px; + margin: 0 40px 50px; + } + + &.change_array_form { + display: flex; + width: 100%; + border-bottom: 1px solid $array-visualization-background; + align-items: flex-end; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; + + .form_row { + flex-grow: 2; + padding: 0; + + label { + font-weight: $semi-bold; + } + } + + .button { + flex-grow: 1; + width: calc(50% - 100px); + margin: 20px; + } + } + + .system_data_title { + align-self: center; + font-size: 20px; + font-weight: 500; + margin-bottom: 30px; + } + + .upload_field { + align-self: center; + margin-top: 30px; + cursor: pointer; + text-align: center; + + position: relative; + overflow: hidden; + width: auto; + + .upload_field_text i { + margin-right: 10px; + } + + .system_upload { + position: absolute; + top: 0; + right: 0; + margin: 0; + padding: 0; + font-size: 20px; + cursor: pointer; + opacity: 0; + filter: alpha(opacity=0); + height: 100%; + width: 100%; + } + } +} + +.grey_line { + border: 1px solid $light-border-color; + width: calc(100% - 50px); + margin: 0 25px; + height: 0; +} + +.array_visualization_header { + margin: 10px $form-section-margin 0; + display: flex; + justify-content: space-between; + width: 100%; + font-weight: 600; + padding-bottom: 10px; + align-items: center; + + #zoom_level { + margin-right: 10px; + } + + .seismic_anchor_control { + display: flex; + + .seismic_control { + padding: 5px 10px; + } + + a { + text-decoration: none; + width: auto; + } + + .cancel_button { + color: $grey-text; + background: white; + font-size: 20px; + } + + #add_seismic { + .icon-seismic-anchor { + color: $add-color; + border: 1px solid $add-color; + border-radius: 50%; + } + } + + #remove_seismic { + .icon-seismic-anchor { + color: $remove-color; + border: 1px solid $remove-color; + border-radius: 50%; + } + } + } + + .overlay_toggle { + border: 1px solid $array-visualization-blue; + padding: 5px 10px; + margin: 0 10px; + + &.overlay_active { + border: 3px solid $array-visualization-blue; + } + } + + .header_action { + display: flex; + justify-content: space-between; + align-items: center; + text-align: center; + color: $array-visualization-blue; + + .zoom_indicator { + background: $zoom-indicator-color; + width: 8px; + margin: 3px; + height: 20px; + + &.zoom_active { + background: $array-visualization-blue; + } + } + + .zoom_button { + $circle_size: 30px; + + -webkit-border-radius: 7px; + -moz-border-radius: 7px; + border-radius: 50%; + border: 1px solid $array-visualization-blue; + width: $circle_size; + height: $circle_size; + font-size: 20px; + display: flex; + margin: 3px; + justify-content: center; + line-height: $circle_size; + } + } +} + +#legend_container { + display: flex; + align-items: center; + justify-content: space-between; + margin: $form-section-margin; +} + +.legend { + width: 50%; +} + +.anchor_colors_legend { + padding: 15px; +} + +canvas { + margin: 0 $form-section-margin; + border: 1px solid $array-visualization-background; +} + +.array_summary_warning { + margin-top: 10px; + margin-bottom: 10px; + width: 100%; + background-color: $warning-color; + border: 1px solid $circle-highlighted; + padding: 7px; +} diff --git a/helix/scss/ebom.scss b/helix/scss/ebom.scss new file mode 100644 index 0000000..fbb229d --- /dev/null +++ b/helix/scss/ebom.scss @@ -0,0 +1,244 @@ +@import 'variables'; + +.ebom_section_content { + width: 100%; +} + +.ebom_form { + margin: 25px 50px; + border: 1px solid $light-border-color; + + &.hidden { + display: none; + } +} + +.ebom_subsection { + width: 100%; + border-top: 1px solid $light-border-color; + + &.submit { + text-align: left; + padding: 25px; + display: flex; + + .button { + width: 100px; + height: 45px; + box-sizing: border-box; + padding: 0; + text-align: center; + line-height: 45px; + } + + .cancel_button { + border: 2px solid $light-border-color; + color: $grey-text; + background: white; + margin-left: 10px; + } + + .delete_button { + color: white; + background: #CE0B24; + } + + a.button { + display: inline-block; + text-decoration: none; + margin-left: 10px; + + &.hidden { + display: none; + } + } + } +} + +.inverter_form_row { + .bold_header { + font-weight: $semi-bold; + font-size: 18px; + margin-left: 25px; + } + + .form_row { + margin-top: 0; + } +} + +#standalone_inverter_form .inverter_form_row { + margin-top: 20px; + width: 100%; +} + +.inverter_inputs_container { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + margin: 0 20px; + + .form_row { + margin-left: 0; + margin-right: 0; + } + + input, select { + font-size: 16px; + letter-spacing: 0; + } +} + +.required_inverter_fields { + display: flex; + width: 68%; + + .inverter_strings { + width: 28%; + } + + .inverter_model { + width: calc(100% - 20px); + flex-grow: 1; + } + + .quantity { + width: 65px; + } +} + +.optional_inverter_fields { + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: center; + padding-top: 5px; + + .checkbox_form_row { + width: 100%; + margin-top: 0; + text-align: center; + } +} + +.checkbox_form_row { + input { + width: auto; + } +} + +#aux_plug_row.checkbox_form_row { + align-self: center; +} + +.checkbox_row { + display: flex; + width: 100%; +} + +.add_button { + font-size: 18px; + font-weight: normal; + + &.disabled { + color: $disabled-text-color; + + .icon-plus { + color: #D6EBF6; + } + } +} + +.power_station_table, .standalone_inverter_table, .supervisor_monitor_table { + margin: 50px; + font-size: 14px; + width: calc(100% - 100px); + border-spacing: 0; + + td { + padding-left: 15px; + padding-right: 20px; + + &.narrow_line { + line-height: 90%; + } + + &.centered { + text-align: center; + } + } + + th { + text-align: left; + font-weight: 500; + color: $medium-blue; + padding-left: 15px; + padding-bottom: 5px; + border-bottom: 2px solid $medium-blue; + } + + .alternating_row_color:nth-child(odd) { + background-color: $table-alternate-row-color; + } + + tr.alternating_row_color.selected { + background: $default-button-color; + color: white; + } + + i { + color: $grey-text; + font-size: 20px; + } + + i.icon-ok { + color: #0075C2; + } + + .trash_can_link { + display: inline-block; + text-decoration: none; + } +} + +.standalone_inverter_table, .supervisor_monitor_table { + td { + padding-top: 10px; + padding-bottom: 10px; + } +} + +.icon-plus { + font-size: 25px; + font-weight: bold; + color: #0075C2; +} + +.empty_state { + height: 200px; + display: flex; + align-items: center; + justify-content: center; + background: $off-white; + font-weight: 500; + color: $grey-text; + font-size: 18px; +} + +.ebom_section_header { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-left: 50px; + margin-right: 50px; + font-size: 20px; + font-weight: $semi-bold; +} + +.monitoring_tag { + background: #FBE7C8; + color: #BA842C; + border: 1px solid #F0D09D; + border-radius: 3px; + padding: 0 5px; +} diff --git a/helix/scss/forms.scss b/helix/scss/forms.scss new file mode 100644 index 0000000..38fe31d --- /dev/null +++ b/helix/scss/forms.scss @@ -0,0 +1,171 @@ +@import "variables"; + +.form_section { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + + h3 { + width: 100%; + margin: $form-section-margin; + font-weight: $semi-bold; + border-bottom: 2px solid $medium-blue; + + span.optional { + color: $grey-text; + font-size: 12px; + padding-left: 10px; + font-weight: normal; + } + } +} + +i.icon-info-circled { + color: $medium-blue; + font-size: 18px; +} + +.tooltip_container { + display: inline-block; + position: relative; +} + +.tooltip { + background: black; + color: white; + border-radius: 5px; + position: absolute; + bottom: 30px; + width: 200px; + left: -100px; + padding: 16px; + + &:after { + top: 100%; + left: 50%; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border: 10px solid transparent; + border-top-color: black; + margin-left: -10px; + } +} + +.form_row { + padding: 5px; + margin: 20px; + width: calc(50% - 50px); + + label { + white-space: nowrap; + margin-bottom: 8px; + } +} + +.error_container { + display: flex; + flex-grow: 1; + width: calc(50% - 20px); + margin: 0 10px; +} + +.error_message { + color: red; + padding-left: 5px; + margin: 0 auto; +} + +input, select { + width: calc(100% - 20px); + padding: 10px; + font-size: 22px; + background: $input-background-color; + border: none; + color: $medium-blue; + letter-spacing: 2px; +} + +select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: $input-background-color url("https://cdn1.iconfinder.com/data/icons/cc_mono_icon_set/blacks/16x16/br_down.png") no-repeat calc(100% - 15px); +} + +.submit, .download { + padding: 20px; + text-align: center; + border-top: 1px solid $light-border-color; +} + +.button, button { + background: $default-button-color; + color: white; + padding: 10px 30px; + width: 50%; + border: none; + border-radius: 5px; + font-size: 20px; + letter-spacing: 2px; +} + +.navigation_buttons { + display: flex; + justify-content: flex-end; + border-top: 1px solid $light-border-color; + border-bottom: 1px solid $light-border-color; + padding: 20px; + + a { + text-decoration: none; + width: auto; + margin: 0 20px; + } + + .button { + width: auto; + } +} + +.back { + border: 2px solid $default-button-color; + color: $default-button-color; + background: white; + margin-left: 10px; +} + +a.back { + text-decoration: none; +} + +.sample_images { + display: flex; + justify-content: flex-end; + border-top: 1px solid $light-border-color; + border-bottom: 1px solid $light-border-color; + padding: 15px; + + a { + text-decoration: none; + width: auto; + margin: 0 5px; + } + + .button { + width: auto; + } +} + +.image_button, button { + background: $default-button-color; + color: white; + padding: 10px 30px; + width: 50%; + border: none; + border-radius: 5px; + font-size: 15px; + letter-spacing: 1px; +} diff --git a/helix/scss/help_page.scss b/helix/scss/help_page.scss new file mode 100644 index 0000000..71d3765 --- /dev/null +++ b/helix/scss/help_page.scss @@ -0,0 +1,40 @@ +@import 'variables'; + +dt { + font-weight: 600; + font-size: 18px; + color: $medium-blue; +} + +dd { + margin-bottom: 20px; +} + +.documentation { + width: 100%; + margin: $form-section-margin; + + img { + width: 100%; + } + + h3 { + font-weight: $semi-bold; + border-bottom: 2px solid $medium-blue; + + span.optional { + color: $grey-text; + font-size: 12px; + padding-left: 10px; + font-weight: normal; + } + } + + p { + font-weight: 500; + } + + a { + text-decoration: none; + } +} diff --git a/helix/scss/main.scss b/helix/scss/main.scss new file mode 100644 index 0000000..f425bb7 --- /dev/null +++ b/helix/scss/main.scss @@ -0,0 +1,61 @@ +@import 'variables'; + +body { + font-family: "Avenir Next", sans-serif; + margin: 0; +} + +header { + display: flex; + justify-content: space-between; + align-items: center; + border-top: 20px solid $medium-blue; + border-bottom: 2px solid $light-border-color; + padding: 20px; + + .group { + display: flex; + justify-content: flex-start; + align-items: center; + + h1 { + width: 100%; + padding-right: 25px; + } + + .back { + padding: 5px 10px; + font-size: 16px; + font-weight: 500; + letter-spacing: 1px; + } + } + + img { + width: 200px; + height: 25px; + } +} + +h1 { + margin: 0; + font-size: 25px; + font-weight: $semi-bold; +} + +.content { + width: 900px; + margin: 40px auto; +} + +.hidden { + display: none; +} + +.clickable { + cursor: pointer; +} + +.spacer { + flex-grow: 1; +} diff --git a/helix/scss/navigation.scss b/helix/scss/navigation.scss new file mode 100644 index 0000000..2476172 --- /dev/null +++ b/helix/scss/navigation.scss @@ -0,0 +1,59 @@ +@import 'variables'; + +.navigation_header { + $circle_size: 30px; + + display: flex; + justify-content: space-between; + + .nav_step { + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: center; + } + + .step_line { + display: flex; + align-items: center; + width: 100%; + margin-bottom: 5px; + + .line { + flex-grow: 1; + height: 0; + border: 2px solid $medium-border-color; + } + } + + .circle { + border: 3px solid $circle-color; + border-radius: 50%; + height: $circle_size; + width: $circle_size; + } + + i.icon-ok { + display: none; + color: $grey-text; + text-align: center; + line-height: $circle_size; + width: $circle_size; + } + + .completed { + i.icon-ok { + display: inline-block; + } + } + + .active { + .circle { + border: 3px solid $circle-highlighted; + } + + i.icon-ok { + color: $checkmark-color; + } + } +} diff --git a/helix/scss/summary_table.scss b/helix/scss/summary_table.scss new file mode 100644 index 0000000..7d10803 --- /dev/null +++ b/helix/scss/summary_table.scss @@ -0,0 +1,128 @@ +@import "variables"; + +#summary_table { + width: calc(100% - 50px); +} + +#seismic_anchor_table { + overflow-x: auto; + width: calc(100% - 50px); + display: inline-block; + margin-top: 5px; + th { + border-bottom: 3px solid #EAEAEA; + } +} + +#seismic_anchor_table, #summary_values_table { + tr { + td:first-child { + border-left: none; + } + + td:last-child { + border-right: none; + } + + td { + padding-left: 10px; + padding-right: 10px; + } + } +} + +.summary_table { + margin: 25px; + text-align: center; + border-spacing: 0; + border-collapse: collapse; + + th { + background: $light-border-color; + padding: 10px; + font-weight: $semi-bold; + font-size: 14px; + border-left: 3px solid $light-border-color; + } + + td { + padding: 7px; + font-weight: 500; + border: 3px solid $light-border-color + } + + .alternating_row_color:nth-child(6n), + .alternating_row_color:nth-child(6n+1), + .alternating_row_color:nth-child(6n+2) { + background-color: $table-alternate-row-color; + } +} + +.summary_warning { + background-color: $warning-color; + border: 1px solid $circle-highlighted; + padding: 10px; +} + +td.table_horizontal_borders { + border: none; + border-bottom: 3px solid $light-border-color; + border-top: 3px solid $light-border-color; + + &.warning { + background-color: $warning-color; + } +} + +td.table_meta_headers { + font-weight: $semi-bold; + border: none; + font-size: 18px; +} + +td.corner_border { + border-left: 3px solid $corner-panel-color; +} + +td.north_south_border { + border-left: 3px solid $northsouth-panel-color; +} + +td.east_west_border { + border-left: 3px solid $eastwest-panel-color; +} + +td.middle_border { + border-left: 3px solid $middle-panel-color; +} + +table .left_text_cell { + text-align: left; + padding-left: 10px; +} + +table .right_border_cell { + border-left: none; +} + +.image_container { + width: 100%; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + justify-content: center; + + img { + width: 90%; + + &.side_by_side { + width: 50%; + } + + &.dual_tilt_panel_types { + width: 65%; + margin-bottom: 25px; + } + } +} diff --git a/helix/scss/variables.scss b/helix/scss/variables.scss new file mode 100644 index 0000000..53a68ae --- /dev/null +++ b/helix/scss/variables.scss @@ -0,0 +1,30 @@ +$semi-bold: 600; + +$corner-panel-color: #9C63CC; +$northsouth-panel-color: #FFBE25; +$eastwest-panel-color: #0BB2EE; +$middle-panel-color: #94CE57; + +$medium-blue: #0075C2; +$default-button-color: #5199F5; +$grey-text: #C7C7C7; +$light-border-color: #EAEAEA; +$medium-border-color: #D8D8D8; +$input-background-color: #F4F4F4; +$disabled-text-color: #CCCCCC; +$zoom-indicator-color: #DDDDDD; +$zoom-active-color: #4A4A4A; +$array-visualization-blue: #2888CB; +$array-visualization-background: #D1D1D1; +$table-alternate-row-color: #ECF9FF; +$off-white: #FDFDFD; +$circle-color: #9B9B9B; +$circle-highlighted: #F59234; +$checkmark-color: #F8A33C; +$error-color: #F06A3F; +$success-color: #6BB24A; +$add-color: #70B72D; +$remove-color: #EF6C6E; +$warning-color: lighten($northsouth-panel-color, 20%); + +$form-section-margin: 25px; diff --git a/helix/seismic_validator_user_values.py b/helix/seismic_validator_user_values.py new file mode 100644 index 0000000..b5ab644 --- /dev/null +++ b/helix/seismic_validator_user_values.py @@ -0,0 +1,14 @@ +from helix.models.panel import Panel +from helix.user_values import UserValues + + +class SeismicValidatorUserValues(UserValues): + def __init__(self, user_values, user_seismic): + super().__init__(user_values.store, user_values.site) + self.user_seismic = user_seismic + + def user_override_seismic_anchors(self): + return True + + def get_user_provided_seismic_anchors(self): + return [Panel(id=x['panel_id'], seismic_anchors=x['seismic_anchors']) for x in self.user_seismic] diff --git a/helix/session_manager.py b/helix/session_manager.py new file mode 100644 index 0000000..388e2e4 --- /dev/null +++ b/helix/session_manager.py @@ -0,0 +1,339 @@ +import json +import uuid + +import os + +from helix.constants.anchor_type import AnchorType +from helix.constants.module_type import ModuleType +from helix.constants.system_type import SystemType +from helix.models.sql.inverter_brands import InverterBrand +from helix.models.sql.inverters import Inverter +from helix.models.sql.power_monitors import PowerMonitor +from helix.models.sql.inverter_brands import InverterBrand +from helix.models.sql.power_stations import PowerStation +from helix.models.sql.sites import Site +from helix.models.sql.standalone_inverters import StandaloneInverter +from helix.models.sql.users import User +from helix.store import Store +from helix.user_values import UserValues + + +class SessionManager(object): + def __init__(self, session, raw_redis_store, db_session): + self.session = session + if 'id' not in session: + session['id'] = uuid.uuid4().hex + self.db_session = db_session + self.user = self.db_session.query(User).filter_by(username=session['id']).first() + if not self.user: + self.user = User(username=session['id'], password_hash='blah') + self.db_session.add(self.user) + self.db_session.commit() + self.site = None + else: + self.site = self.db_session.query(Site).filter_by(user_id=self.user.id).first() + + self.store = Store(raw_redis_store, session['id']) + + def context(self): + if self.site: + inverter_brands = [inverter_brands.to_json() for inverter_brands in self.db_session.query(InverterBrand).filter_by(site_id=self.site.id).order_by(InverterBrand.id).all()] + power_stations = [power_station.to_json() for power_station in self.db_session.query(PowerStation).filter_by(site_id=self.site.id).order_by(PowerStation.id).all()] + standalone_inverters = [standalone_inverter.to_json() for standalone_inverter in self.db_session.query(StandaloneInverter).filter_by(site_id=self.site.id).order_by(StandaloneInverter.id).all()] + monitors = [monitor.to_json() for monitor in self.db_session.query(PowerMonitor).filter_by(site_id=self.site.id).order_by(PowerMonitor.id).all()] + else: + inverter_brands = [] + power_stations = [] + standalone_inverters = [] + monitors = [] + + site_data_available = self.site is not None + csv_available = self.site.cad_file is not None if self.site else False + + return { + 'id': self.session['id'], + 'google_analytics_token': os.getenv('GOOGLE_ANALYTICS', ''), + 'csv_available': csv_available, + 'site_data_available': site_data_available, + 'inverter_brands': inverter_brands, + 'power_stations': power_stations, + 'standalone_inverters': standalone_inverters, + 'power_monitors': monitors, + 'system_type': SystemType(self.site.system_type) if self.site else SystemType.dualTilt, + 'steps': [ + (1, 'Site Characterization', '/site_characterization/', site_data_available), + (2, 'Site Summary', '/summary/', site_data_available), + (3, 'Array Summary', '/array_summary/', csv_available), + (4, 'Power Station Configuration', '/power_station_configuration/', + True if power_stations or standalone_inverters else False), + (5, 'Download', '/download/', None) + ] + } + + def user_values(self): + return UserValues(self.store, self.site) + + def save_form_submission(self, form_data): + if not self.site: + self.site = Site( + user_id=self.user.id, + project_name=form_data.get('project_name', ''), + building_height=form_data.get('building_height', 0), + building_width=form_data.get('building_width', 0), + building_length=form_data.get('building_length', 0), + parapet_height=form_data.get('building_parapet_height', 0), + wind_speed=form_data.get('wind_speed', 0), + exposure_category=form_data.get('exposure_category', 'C'), + exposure_transition_distance=form_data.get('exposure_category_transition_distance', 0), + ballast_block_weight=form_data.get('ballast_block_weight', 0), + max_psf=form_data.get('max_system_pressure', 0), + system_type=form_data.get('system_type', SystemType.dualTilt.value), + module_type=form_data.get('module_type', ModuleType.Cell96.value), + anchor_type=form_data.get('anchor_type', AnchorType.OMG_PowerGrip_Plus.value), + spectral_response=form_data.get('design_spectral_response', 1), + seismic_importance_factor=form_data.get('importance_factor', 1) + ) + self.db_session.add(self.site) + self.db_session.commit() + else: + old_system_type = self.site.system_type + old_module_type = self.site.module_type + self.site.project_name = form_data.get('project_name', self.site.project_name) + self.site.building_height = form_data.get('building_height', self.site.building_height) + self.site.building_width = form_data.get('building_width', self.site.building_width) + self.site.building_length = form_data.get('building_length', self.site.building_length) + self.site.parapet_height = form_data.get('building_parapet_height', self.site.parapet_height) + self.site.wind_speed = form_data.get('wind_speed', self.site.wind_speed) + self.site.exposure_category = form_data.get('exposure_category', self.site.exposure_category) + self.site.exposure_transition_distance = form_data.get('exposure_category_transition_distance', + self.site.exposure_transition_distance) + self.site.ballast_block_weight = form_data.get('ballast_block_weight', self.site.ballast_block_weight) + self.site.max_psf = form_data.get('max_system_pressure', self.site.max_psf) + self.site.system_type = form_data.get('system_type', self.site.system_type) + self.site.module_type = form_data.get('module_type', self.site.module_type) + self.site.anchor_type = form_data.get('anchor_type', self.site.anchor_type) + self.site.spectral_response = form_data.get('design_spectral_response', self.site.spectral_response) + self.site.seismic_importance_factor = form_data.get('importance_factor', self.site.seismic_importance_factor) + if old_system_type != self.site.system_type or old_module_type != self.site.module_type: + self.site.cad_file = None + self.site.cad_file_name = None + self.site.dxf_file = None + self.site.dxf_file_name = None + self.db_session.commit() + + def save_or_update_power_station(self, form_data): + power_station = None + if form_data['power_station_id']: + power_station = self.db_session.query(PowerStation).get(int(form_data['power_station_id'])) + + if not power_station: + power_station = PowerStation( + site_id=self.site.id, + quantity=int(form_data.get('power_station_quantity', 0)), + ac_run_length=int(form_data.get('ac_run_length', 0)), + description=form_data.get('power_station_description', ''), + ) + self.db_session.add(power_station) + + else: + power_station.quantity = int(form_data.get('power_station_quantity', power_station.quantity)) + power_station.ac_run_length = int(form_data.get('ac_run_length', power_station.ac_run_length)) + power_station.description = form_data.get('power_station_description', power_station.description) + + # Delete all inverters for now in order to create new ones on update + for inverter in power_station.inverters: + self.db_session.delete(inverter) + + self.db_session.commit() + + inverter_quantity = int(form_data['inverter_quantity']) + + if inverter_quantity == 0: + return + + for index in range(inverter_quantity): + prefix = 'inverter_' + str(index + 1) + + inverter = Inverter( + model=str(form_data[prefix + '-model']), + strings_per_inverter=int(form_data[prefix + '-strings_per_inverter']), + sunshade=bool(form_data.get(prefix + '-sunshade', False)), + dc_switch=bool(form_data.get(prefix + '-dc_switch', False)), + power_station_id=power_station.id, + standalone_inverter_id=None, + ) + self.db_session.add(inverter) + self.db_session.commit() + + def delete_power_station(self, power_station_id): + power_station = self.db_session.query(PowerStation).get(power_station_id) + if power_station: + attached_standalone_inverters = self.db_session.query(StandaloneInverter).filter(StandaloneInverter.attachment_point_id==power_station.id).all() + for standalone_inverter in attached_standalone_inverters: + standalone_inverter.attachment_point_id = None + + attached_power_monitors = self.db_session.query(PowerMonitor).filter(PowerMonitor.power_station_id==power_station.id).all() + for power_monitor in attached_power_monitors: + power_monitor.power_station_id = None + + self.db_session.delete(power_station) + self.db_session.commit() + + def delete_power_station_config_data(self): + site_id = self.site.id + power_stations = self.db_session.query(PowerStation).filter(PowerStation.site_id==site_id).all() + + for power_station in power_stations: + self.db_session\ + .query(StandaloneInverter)\ + .filter(StandaloneInverter.attachment_point_id==power_station.id)\ + .delete() + self.db_session\ + .query(PowerMonitor)\ + .filter(PowerMonitor.power_station_id==power_station.id)\ + .delete() + self.db_session.delete(power_station) + + self.db_session.query(StandaloneInverter).filter(StandaloneInverter.site_id==site_id).delete() + self.db_session.query(PowerMonitor).filter(PowerMonitor.site_id==site_id).delete() + self.db_session.commit() + + def save_or_update_inverter_brands(self, form_data): + inverter_brand = None + + if form_data.get('inverter_brand_id'): + inverter_brand = self.db_session.query(InverterBrand).filter_by( + site_id=self.site.id + ).first() + + if not inverter_brand: + inverter_brand = InverterBrand( + id=int(form_data.get('inverter_brand_id')), + site_id=self.site.id, + ) + + self.db_session.add(inverter_brand) + self.db_session.commit() + else: + inverter_brand.id = int(form_data.get('inverter_brand_id', inverter_brand.id)) + + self.db_session.commit() + + + def save_or_update_standalone_inverter(self, form_data): + standalone_inverter = None + prefix = 'inverter-' + + if form_data.get('standalone_inverter_id'): + standalone_inverter = self.db_session.query(StandaloneInverter).get(int(form_data.get('standalone_inverter_id'))) + + if not standalone_inverter: + quantity = int(form_data[prefix + 'quantity']) + + for n in range(quantity): + standalone_inverter = StandaloneInverter( + site_id=self.site.id, + attachment_point_id=form_data.get('attachment_point') if form_data.get('attachment_point') != 'switch_gear' else None, + ac_run_length=int(form_data.get('standalone_ac_run_length', 0)) + ) + self.db_session.add(standalone_inverter) + self.db_session.commit() + inverter = Inverter( + model=str(form_data[prefix + 'model']), + strings_per_inverter=int(form_data[prefix + 'strings_per_inverter']), + sunshade=bool(form_data.get(prefix + 'sunshade', False)), + dc_switch=bool(form_data.get(prefix + 'dc_switch', False)), + splice_box=bool(form_data.get(prefix + 'splice_box', False)), + power_station_id=None, + standalone_inverter_id=standalone_inverter.id, + ) + self.db_session.add(inverter) + self.db_session.commit() + else: + standalone_inverter.ac_run_length = int(form_data.get('standalone_ac_run_length', standalone_inverter.ac_run_length)) + standalone_inverter.attachment_point_id = form_data.get('attachment_point') if form_data.get('attachment_point', standalone_inverter.attachment_point_id) != 'switch_gear' else None + + standalone_inverter.inverter[0].model = str(form_data[prefix + 'model']) + standalone_inverter.inverter[0].strings_per_inverter = int(form_data[prefix + 'strings_per_inverter']) + standalone_inverter.inverter[0].sunshade = bool(form_data.get(prefix + 'sunshade', False)) + standalone_inverter.inverter[0].dc_switch = bool(form_data.get(prefix + 'dc_switch', False)) + standalone_inverter.inverter[0].splice_box = bool(form_data.get(prefix + 'splice_box', False)) + self.db_session.commit() + + def delete_standalone_inverter(self, inverter_id): + standalone_inverter = self.db_session.query(StandaloneInverter).get(inverter_id) + if standalone_inverter: + self.db_session.delete(standalone_inverter) + self.db_session.commit() + + def save_or_update_supervisor_monitor(self, form_data): + monitor_id = form_data.get('monitor_id') + if monitor_id: + monitor = self.db_session.query(PowerMonitor).get(monitor_id) + monitor.power_station_id = self.interpret_power_source(form_data.get('power_source')) + else: + monitor = PowerMonitor( + site_id=self.site.id, + power_station_id=self.interpret_power_source(form_data['power_source']) + ) + self.db_session.add(monitor) + self.db_session.commit() + + def delete_supervisor_monitor(self, monitor_id): + monitor = self.db_session.query(PowerMonitor).get(monitor_id) + if monitor: + self.db_session.delete(monitor) + self.db_session.commit() + + def save_uploaded_file(self, contents, cad_file_name=None, dxf_file_name=None): + self.site.cad_file = contents + self.site.cad_file_name = cad_file_name + self.site.dxf_file_name = dxf_file_name + self.store.delete('user_override_seismic_anchors') + self.db_session.commit() + + def fill_saved_values_in_form(self, form): + if self.site: + form.project_name.data = self.site.project_name + form.building_height.data = self.site.building_height + form.building_width.data = self.site.building_width + form.building_length.data = self.site.building_length + form.building_parapet_height.data = self.site.parapet_height + form.wind_speed.data = self.site.wind_speed + form.exposure_category.data = self.site.exposure_category + form.exposure_category_transition_distance.data = self.site.exposure_transition_distance + form.ballast_block_weight.data = self.site.ballast_block_weight + form.max_system_pressure.data = self.site.max_psf + form.system_type.data = self.site.system_type + form.module_type.data = self.site.module_type + form.anchor_type.data = self.site.anchor_type + form.design_spectral_response.data = self.site.spectral_response + form.importance_factor.data = str(self.site.seismic_importance_factor) + + def save_user_provided_seismic_anchors(self, seismic_anchor_json): + self.store.set("user_override_seismic_anchors", json.dumps(seismic_anchor_json)) + + def save_buildings_polygons(self, buildings): + # buildings is a + # list of lists of points presented by list of two numbers + # [ [ [123.1, 111.1] , [ 222.1, 223.2 ] , [ 212.1, -225.2 ] ] ] + polygons_primitive = list(map(lambda x: x.points, buildings)) + self.store.set("buildings_polygons", json.dumps(polygons_primitive)) + + def save_is_drawing_inaccurate(self, is_drawing_inaccurate): + self.store.set("is_drawing_inaccurate", json.dumps(is_drawing_inaccurate)) + + def get_map_index(self, list_id, identifier_key, identifier): + maps = self.store.get_list_of_maps(list_id) + for idx, val in enumerate(maps): + if val[identifier_key] == identifier: + return idx + + def interpret_power_source(self, power_source): + if power_source != 'switch_gear': + power_station = self.db_session.query(PowerStation).get(power_source) + if power_station: + return power_station.id + + return None + diff --git a/helix/static/css/fontello.css b/helix/static/css/fontello.css new file mode 100755 index 0000000..92ce5c5 --- /dev/null +++ b/helix/static/css/fontello.css @@ -0,0 +1,72 @@ +@font-face { + font-family: 'fontello'; + src: url('../font/fontello.eot?10976371'); + src: url('../font/fontello.eot?10976371#iefix') format('embedded-opentype'), + url('../font/fontello.woff2?10976371') format('woff2'), + url('../font/fontello.woff?10976371') format('woff'), + url('../font/fontello.ttf?10976371') format('truetype'), + url('../font/fontello.svg?10976371#fontello') format('svg'); + font-weight: normal; + font-style: normal; +} +/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ +/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ +/* +@media screen and (-webkit-min-device-pixel-ratio:0) { + @font-face { + font-family: 'fontello'; + src: url('../font/fontello.svg?10976371#fontello') format('svg'); + } +} +*/ + + [class^="icon-"]:before, [class*=" icon-"]:before { + font-family: "fontello"; + font-style: normal; + font-weight: normal; + speak: none; + + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: .2em; + text-align: center; + /* opacity: .8; */ + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + + /* fix buttons height, for twitter bootstrap */ + line-height: 1em; + + /* Animation center compensation - margins should be symmetric */ + /* remove if not needed */ + margin-left: .2em; + + /* you can be more comfortable with increased icons size */ + /* font-size: 120%; */ + + /* Font smoothing. That was taken from TWBS */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + /* Uncomment for 3D effect */ + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ +} + +.icon-info-circled:before { content: '\e800'; } /* '' */ +.icon-seismic-anchor:before { content: '\e801'; } /* '' */ +.icon-trash:before { content: '\e802'; } /* '' */ +.icon-down-open-big:before { content: '\e803'; } /* '' */ +.icon-ok:before { content: '\e804'; } /* '' */ +.icon-pencil:before { content: '\e805'; } /* '' */ +.icon-right-open:before { content: '\e806'; } /* '' */ +.icon-left-open:before { content: '\e807'; } /* '' */ +.icon-down-open:before { content: '\e808'; } /* '' */ +.icon-up-open:before { content: '\e809'; } /* '' */ +.icon-plus:before { content: '\e80a'; } /* '' */ +.icon-info:before { content: '\e80b'; } /* '' */ +.icon-close:before { content: '\e80c'; } /* '' */ +.icon-sunpower-logo:before { content: '\e80d'; } /* '' */ +.icon-upload-cloud:before { content: '\e80e'; } /* '' */ \ No newline at end of file diff --git a/helix/static/css/main.css b/helix/static/css/main.css new file mode 100644 index 0000000..258f70e --- /dev/null +++ b/helix/static/css/main.css @@ -0,0 +1,1025 @@ +/* line 3 */ +.array_information { + margin: 0 25px; + display: flex; + flex-direction: column; + width: calc(100% - 50px); + border: 1px solid #D1D1D1; +} +/* line 10 */ +.array_information .item_title { + font-weight: bold; +} +/* line 14 */ +.array_information .item_body { + color: #0075C2; + font-weight: 600; +} +/* line 19 */ +.array_information .array_summary { + display: flex; + flex-direction: row; + flex-wrap: wrap; + padding: 10px; + justify-content: flex-start; +} +/* line 26 */ +.array_information .array_summary .summary_item { + width: calc(50% - 50px); + margin: 10px 20px; +} +/* line 33 */ +.array_information .subarray_summary h4 { + padding: 0 25px; + margin-bottom: 0; +} +/* line 38 */ +.array_information .subarray_summary #seismic_save { + color: white; + height: 50px; + font-weight: 400; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 20px; +} +/* line 47 */ +.array_information .subarray_summary #seismic_save .left_container { + display: flex; + justify-content: flex-start; + align-items: center; +} +/* line 52 */ +.array_information .subarray_summary #seismic_save .left_container .seismic_save_message { + padding-left: 10px; +} +/* line 57 */ +.array_information .subarray_summary #seismic_save .circle { + border: 2px solid white; + border-radius: 50%; + height: 25px; + width: 25px; + font-weight: 500; + text-align: center; + line-height: 25px; +} +/* line 67 */ +.array_information .subarray_summary #seismic_save .dismiss_button { + height: 20px; +} +/* line 71 */ +.array_information .subarray_summary #seismic_save.seismic_error { + background: #F06A3F; +} +/* line 75 */ +.array_information .subarray_summary #seismic_save.seismic_success { + background: #6BB24A; +} + +/* line 82 */ +.system_data_upload { + display: flex; +} +/* line 85 */ +.system_data_upload.initial_array_upload { + color: #9B9B9B; + width: 100%; + flex-direction: column; + border: 1px solid #EAEAEA; + background: #FDFDFD; + padding: 30px; + margin: 0 40px 50px; +} +/* line 95 */ +.system_data_upload.change_array_form { + display: flex; + width: 100%; + border-bottom: 1px solid #D1D1D1; + align-items: flex-end; + flex-direction: row; + flex-wrap: wrap; + justify-content: flex-start; +} +/* line 104 */ +.system_data_upload.change_array_form .form_row { + flex-grow: 2; + padding: 0; +} +/* line 108 */ +.system_data_upload.change_array_form .form_row label { + font-weight: 600; +} +/* line 113 */ +.system_data_upload.change_array_form .button { + flex-grow: 1; + width: calc(50% - 100px); + margin: 20px; +} +/* line 120 */ +.system_data_upload .system_data_title { + align-self: center; + font-size: 20px; + font-weight: 500; + margin-bottom: 30px; +} +/* line 127 */ +.system_data_upload .upload_field { + align-self: center; + margin-top: 30px; + cursor: pointer; + text-align: center; + position: relative; + overflow: hidden; + width: auto; +} +/* line 137 */ +.system_data_upload .upload_field .upload_field_text i { + margin-right: 10px; +} +/* line 141 */ +.system_data_upload .upload_field .system_upload { + position: absolute; + top: 0; + right: 0; + margin: 0; + padding: 0; + font-size: 20px; + cursor: pointer; + opacity: 0; + filter: alpha(opacity=0); + height: 100%; + width: 100%; +} + +/* line 157 */ +.grey_line { + border: 1px solid #EAEAEA; + width: calc(100% - 50px); + margin: 0 25px; + height: 0; +} + +/* line 164 */ +.array_visualization_header { + margin: 10px 25px 0; + display: flex; + justify-content: space-between; + width: 100%; + font-weight: 600; + padding-bottom: 10px; + align-items: center; +} +/* line 173 */ +.array_visualization_header #zoom_level { + margin-right: 10px; +} +/* line 177 */ +.array_visualization_header .seismic_anchor_control { + display: flex; +} +/* line 180 */ +.array_visualization_header .seismic_anchor_control .seismic_control { + padding: 5px 10px; +} +/* line 184 */ +.array_visualization_header .seismic_anchor_control a { + text-decoration: none; + width: auto; +} +/* line 189 */ +.array_visualization_header .seismic_anchor_control .cancel_button { + color: #C7C7C7; + background: white; + font-size: 20px; +} +/* line 196 */ +.array_visualization_header .seismic_anchor_control #add_seismic .icon-seismic-anchor { + color: #70B72D; + border: 1px solid #70B72D; + border-radius: 50%; +} +/* line 204 */ +.array_visualization_header .seismic_anchor_control #remove_seismic .icon-seismic-anchor { + color: #EF6C6E; + border: 1px solid #EF6C6E; + border-radius: 50%; +} +/* line 212 */ +.array_visualization_header .overlay_toggle { + border: 1px solid #2888CB; + padding: 5px 10px; + margin: 0 10px; +} +/* line 217 */ +.array_visualization_header .overlay_toggle.overlay_active { + border: 3px solid #2888CB; +} +/* line 222 */ +.array_visualization_header .header_action { + display: flex; + justify-content: space-between; + align-items: center; + text-align: center; + color: #2888CB; +} +/* line 229 */ +.array_visualization_header .header_action .zoom_indicator { + background: #DDDDDD; + width: 8px; + margin: 3px; + height: 20px; +} +/* line 235 */ +.array_visualization_header .header_action .zoom_indicator.zoom_active { + background: #2888CB; +} +/* line 240 */ +.array_visualization_header .header_action .zoom_button { + -webkit-border-radius: 7px; + -moz-border-radius: 7px; + border-radius: 50%; + border: 1px solid #2888CB; + width: 30px; + height: 30px; + font-size: 20px; + display: flex; + margin: 3px; + justify-content: center; + line-height: 30px; +} + +/* line 258 */ +#legend_container { + display: flex; + align-items: center; + justify-content: space-between; + margin: 25px; +} + +/* line 265 */ +.legend { + width: 50%; +} + +/* line 269 */ +.anchor_colors_legend { + padding: 15px; +} + +/* line 273 */ +canvas { + margin: 0 25px; + border: 1px solid #D1D1D1; +} + +/* line 278 */ +.array_summary_warning { + margin-top: 10px; + margin-bottom: 10px; + width: 100%; + background-color: #ffdc8b; + border: 1px solid #F59234; + padding: 7px; +} + +/* line 3 */ +.ebom_section_content { + width: 100%; +} + +/* line 7 */ +.ebom_form { + margin: 25px 50px; + border: 1px solid #EAEAEA; +} +/* line 11 */ +.ebom_form.hidden { + display: none; +} + +/* line 16 */ +.ebom_subsection { + width: 100%; + border-top: 1px solid #EAEAEA; +} +/* line 20 */ +.ebom_subsection.submit { + text-align: left; + padding: 25px; + display: flex; +} +/* line 25 */ +.ebom_subsection.submit .button { + width: 100px; + height: 45px; + box-sizing: border-box; + padding: 0; + text-align: center; + line-height: 45px; +} +/* line 34 */ +.ebom_subsection.submit .cancel_button { + border: 2px solid #EAEAEA; + color: #C7C7C7; + background: white; + margin-left: 10px; +} +/* line 41 */ +.ebom_subsection.submit .delete_button { + color: white; + background: #CE0B24; +} +/* line 46 */ +.ebom_subsection.submit a.button { + display: inline-block; + text-decoration: none; + margin-left: 10px; +} +/* line 51 */ +.ebom_subsection.submit a.button.hidden { + display: none; +} + +/* line 59 */ +.inverter_form_row .bold_header { + font-weight: 600; + font-size: 18px; + margin-left: 25px; +} +/* line 65 */ +.inverter_form_row .form_row { + margin-top: 0; +} + +/* line 70 */ +#standalone_inverter_form .inverter_form_row { + margin-top: 20px; + width: 100%; +} + +/* line 75 */ +.inverter_inputs_container { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + margin: 0 20px; +} +/* line 81 */ +.inverter_inputs_container .form_row { + margin-left: 0; + margin-right: 0; +} +/* line 86 */ +.inverter_inputs_container input, .inverter_inputs_container select { + font-size: 16px; + letter-spacing: 0; +} + +/* line 92 */ +.required_inverter_fields { + display: flex; + width: 68%; +} +/* line 96 */ +.required_inverter_fields .inverter_strings { + width: 28%; +} +/* line 100 */ +.required_inverter_fields .inverter_model { + width: calc(100% - 20px); + flex-grow: 1; +} +/* line 105 */ +.required_inverter_fields .quantity { + width: 65px; +} + +/* line 110 */ +.optional_inverter_fields { + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: center; + padding-top: 5px; +} +/* line 117 */ +.optional_inverter_fields .checkbox_form_row { + width: 100%; + margin-top: 0; + text-align: center; +} + +/* line 125 */ +.checkbox_form_row input { + width: auto; +} + +/* line 130 */ +#aux_plug_row.checkbox_form_row { + align-self: center; +} + +/* line 134 */ +.checkbox_row { + display: flex; + width: 100%; +} + +/* line 139 */ +.add_button { + font-size: 18px; + font-weight: normal; +} +/* line 143 */ +.add_button.disabled { + color: #CCCCCC; +} +/* line 146 */ +.add_button.disabled .icon-plus { + color: #D6EBF6; +} + +/* line 152 */ +.power_station_table, .standalone_inverter_table, .supervisor_monitor_table { + margin: 50px; + font-size: 14px; + width: calc(100% - 100px); + border-spacing: 0; +} +/* line 158 */ +.power_station_table td, .standalone_inverter_table td, .supervisor_monitor_table td { + padding-left: 15px; + padding-right: 20px; +} +/* line 162 */ +.power_station_table td.narrow_line, .standalone_inverter_table td.narrow_line, .supervisor_monitor_table td.narrow_line { + line-height: 90%; +} +/* line 166 */ +.power_station_table td.centered, .standalone_inverter_table td.centered, .supervisor_monitor_table td.centered { + text-align: center; +} +/* line 171 */ +.power_station_table th, .standalone_inverter_table th, .supervisor_monitor_table th { + text-align: left; + font-weight: 500; + color: #0075C2; + padding-left: 15px; + padding-bottom: 5px; + border-bottom: 2px solid #0075C2; +} +/* line 180 */ +.power_station_table .alternating_row_color:nth-child(odd), .standalone_inverter_table .alternating_row_color:nth-child(odd), .supervisor_monitor_table .alternating_row_color:nth-child(odd) { + background-color: #ECF9FF; +} +/* line 184 */ +.power_station_table tr.alternating_row_color.selected, .standalone_inverter_table tr.alternating_row_color.selected, .supervisor_monitor_table tr.alternating_row_color.selected { + background: #5199F5; + color: white; +} +/* line 189 */ +.power_station_table i, .standalone_inverter_table i, .supervisor_monitor_table i { + color: #C7C7C7; + font-size: 20px; +} +/* line 194 */ +.power_station_table i.icon-ok, .standalone_inverter_table i.icon-ok, .supervisor_monitor_table i.icon-ok { + color: #0075C2; +} +/* line 198 */ +.power_station_table .trash_can_link, .standalone_inverter_table .trash_can_link, .supervisor_monitor_table .trash_can_link { + display: inline-block; + text-decoration: none; +} + +/* line 205 */ +.standalone_inverter_table td, .supervisor_monitor_table td { + padding-top: 10px; + padding-bottom: 10px; +} + +/* line 211 */ +.icon-plus { + font-size: 25px; + font-weight: bold; + color: #0075C2; +} + +/* line 217 */ +.empty_state { + height: 200px; + display: flex; + align-items: center; + justify-content: center; + background: #FDFDFD; + font-weight: 500; + color: #C7C7C7; + font-size: 18px; +} + +/* line 228 */ +.ebom_section_header { + display: flex; + justify-content: space-between; + align-items: baseline; + margin-left: 50px; + margin-right: 50px; + font-size: 20px; + font-weight: 600; +} + +/* line 238 */ +.monitoring_tag { + background: #FBE7C8; + color: #BA842C; + border: 1px solid #F0D09D; + border-radius: 3px; + padding: 0 5px; +} + +/* line 3 */ +.form_section { + display: flex; + flex-wrap: wrap; + align-items: flex-start; +} +/* line 8 */ +.form_section h3 { + width: 100%; + margin: 25px; + font-weight: 600; + border-bottom: 2px solid #0075C2; +} +/* line 14 */ +.form_section h3 span.optional { + color: #C7C7C7; + font-size: 12px; + padding-left: 10px; + font-weight: normal; +} + +/* line 23 */ +i.icon-info-circled { + color: #0075C2; + font-size: 18px; +} + +/* line 28 */ +.tooltip_container { + display: inline-block; + position: relative; +} + +/* line 33 */ +.tooltip { + background: black; + color: white; + border-radius: 5px; + position: absolute; + bottom: 30px; + width: 200px; + left: -100px; + padding: 16px; +} +/* line 43 */ +.tooltip:after { + top: 100%; + left: 50%; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border: 10px solid transparent; + border-top-color: black; + margin-left: -10px; +} + +/* line 57 */ +.form_row { + padding: 5px; + margin: 20px; + width: calc(50% - 50px); +} +/* line 62 */ +.form_row label { + white-space: nowrap; + margin-bottom: 8px; +} + +/* line 68 */ +.error_container { + display: flex; + flex-grow: 1; + width: calc(50% - 20px); + margin: 0 10px; +} + +/* line 75 */ +.error_message { + color: red; + padding-left: 5px; + margin: 0 auto; +} + +/* line 81 */ +input, select { + width: calc(100% - 20px); + padding: 10px; + font-size: 22px; + background: #F4F4F4; + border: none; + color: #0075C2; + letter-spacing: 2px; +} + +/* line 91 */ +select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: #F4F4F4 url("https://cdn1.iconfinder.com/data/icons/cc_mono_icon_set/blacks/16x16/br_down.png") no-repeat calc(100% - 15px); +} + +/* line 98 */ +.submit, .download { + padding: 20px; + text-align: center; + border-top: 1px solid #EAEAEA; +} + +/* line 104 */ +.button, button { + background: #5199F5; + color: white; + padding: 10px 30px; + width: 50%; + border: none; + border-radius: 5px; + font-size: 20px; + letter-spacing: 2px; +} + +/* line 115 */ +.navigation_buttons { + display: flex; + justify-content: flex-end; + border-top: 1px solid #EAEAEA; + border-bottom: 1px solid #EAEAEA; + padding: 20px; +} +/* line 122 */ +.navigation_buttons a { + text-decoration: none; + width: auto; + margin: 0 20px; +} +/* line 128 */ +.navigation_buttons .button { + width: auto; +} + +/* line 133 */ +.back { + border: 2px solid #5199F5; + color: #5199F5; + background: white; + margin-left: 10px; +} + +/* line 140 */ +a.back { + text-decoration: none; +} + +/* line 144 */ +.sample_images { + display: flex; + justify-content: flex-end; + border-top: 1px solid #EAEAEA; + border-bottom: 1px solid #EAEAEA; + padding: 15px; +} +/* line 151 */ +.sample_images a { + text-decoration: none; + width: auto; + margin: 2px 5px; +} +/* line 157 */ +.sample_images .button { + width: auto; +} + +/* line 162 */ +.image_button, button { + background: #5199F5; + color: white; + padding: 10px 30px; + width: 50%; + border: none; + border-radius: 5px; + font-size: 15px; + letter-spacing: 1px; +} + +/* line 3 */ +dt { + font-weight: 600; + font-size: 18px; + color: #0075C2; +} + +/* line 9 */ +dd { + margin-bottom: 20px; +} + +/* line 13 */ +.documentation { + width: 100%; + margin: 25px; +} +/* line 17 */ +.documentation img { + width: 100%; +} +/* line 21 */ +.documentation h3 { + font-weight: 600; + border-bottom: 2px solid #0075C2; +} +/* line 25 */ +.documentation h3 span.optional { + color: #C7C7C7; + font-size: 12px; + padding-left: 10px; + font-weight: normal; +} +/* line 33 */ +.documentation p { + font-weight: 500; +} +/* line 37 */ +.documentation a { + text-decoration: none; +} + +/* line 3 */ +body { + font-family: "Avenir Next", sans-serif; + margin: 0; +} + +/* line 8 */ +header { + display: flex; + justify-content: space-between; + align-items: center; + border-top: 20px solid #0075C2; + border-bottom: 2px solid #EAEAEA; + padding: 20px; +} +/* line 16 */ +header .group { + display: flex; + justify-content: flex-start; + align-items: center; +} +/* line 21 */ +header .group h1 { + width: 100%; + padding-right: 25px; +} +/* line 26 */ +header .group .back { + padding: 5px 10px; + font-size: 16px; + font-weight: 500; + letter-spacing: 1px; +} +/* line 34 */ +header img { + width: 200px; + height: 25px; +} + +/* line 40 */ +h1 { + margin: 0; + font-size: 25px; + font-weight: 600; +} + +/* line 46 */ +.content { + width: 900px; + margin: 40px auto; +} + +/* line 51 */ +.hidden { + display: none; +} + +/* line 55 */ +.clickable { + cursor: pointer; +} + +/* line 59 */ +.spacer { + flex-grow: 1; +} + +/* line 3 */ +.navigation_header { + display: flex; + justify-content: space-between; +} +/* line 9 */ +.navigation_header .nav_step { + flex-grow: 1; + display: flex; + flex-direction: column; + align-items: center; +} +/* line 16 */ +.navigation_header .step_line { + display: flex; + align-items: center; + width: 100%; + margin-bottom: 5px; +} +/* line 22 */ +.navigation_header .step_line .line { + flex-grow: 1; + height: 0; + border: 2px solid #D8D8D8; +} +/* line 29 */ +.navigation_header .circle { + border: 3px solid #9B9B9B; + border-radius: 50%; + height: 30px; + width: 30px; +} +/* line 36 */ +.navigation_header i.icon-ok { + display: none; + color: #C7C7C7; + text-align: center; + line-height: 30px; + width: 30px; +} +/* line 45 */ +.navigation_header .completed i.icon-ok { + display: inline-block; +} +/* line 51 */ +.navigation_header .active .circle { + border: 3px solid #F59234; +} +/* line 55 */ +.navigation_header .active i.icon-ok { + color: #F8A33C; +} + +/* line 3 */ +#summary_table { + width: calc(100% - 50px); +} + +/* line 7 */ +#seismic_anchor_table { + overflow-x: auto; + width: calc(100% - 50px); + display: inline-block; + margin-top: 5px; +} +/* line 12 */ +#seismic_anchor_table th { + border-bottom: 3px solid #EAEAEA; +} + +/* line 19 */ +#seismic_anchor_table tr td:first-child, #summary_values_table tr td:first-child { + border-left: none; +} +/* line 23 */ +#seismic_anchor_table tr td:last-child, #summary_values_table tr td:last-child { + border-right: none; +} +/* line 27 */ +#seismic_anchor_table tr td, #summary_values_table tr td { + padding-left: 10px; + padding-right: 10px; +} + +/* line 34 */ +.summary_table { + margin: 25px; + text-align: center; + border-spacing: 0; + border-collapse: collapse; +} +/* line 40 */ +.summary_table th { + background: #EAEAEA; + padding: 10px; + font-weight: 600; + font-size: 14px; + border-left: 3px solid #EAEAEA; +} +/* line 48 */ +.summary_table td { + padding: 7px; + font-weight: 500; + border: 3px solid #EAEAEA; +} +/* line 54 */ +.summary_table .alternating_row_color:nth-child(6n), +.summary_table .alternating_row_color:nth-child(6n+1), +.summary_table .alternating_row_color:nth-child(6n+2) { + background-color: #ECF9FF; +} + +/* line 61 */ +.summary_warning { + background-color: #ffdc8b; + border: 1px solid #F59234; + padding: 10px; +} + +/* line 67 */ +td.table_horizontal_borders { + border: none; + border-bottom: 3px solid #EAEAEA; + border-top: 3px solid #EAEAEA; +} +/* line 72 */ +td.table_horizontal_borders.warning { + background-color: #ffdc8b; +} + +/* line 77 */ +td.table_meta_headers { + font-weight: 600; + border: none; + font-size: 18px; +} + +/* line 83 */ +td.corner_border { + border-left: 3px solid #9C63CC; +} + +/* line 87 */ +td.north_south_border { + border-left: 3px solid #FFBE25; +} + +/* line 91 */ +td.east_west_border { + border-left: 3px solid #0BB2EE; +} + +/* line 95 */ +td.middle_border { + border-left: 3px solid #94CE57; +} + +/* line 99 */ +table .left_text_cell { + text-align: left; + padding-left: 10px; +} + +/* line 104 */ +table .right_border_cell { + border-left: none; +} + +/* line 108 */ +.image_container { + width: 100%; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: center; + justify-content: center; +} +/* line 116 */ +.image_container img { + width: 90%; +} +/* line 119 */ +.image_container img.side_by_side { + width: 50%; +} +/* line 123 */ +.image_container img.dual_tilt_panel_types { + width: 65%; + margin-bottom: 25px; +} + diff --git a/helix/static/font/fontello.eot b/helix/static/font/fontello.eot new file mode 100755 index 0000000..7e6f481 Binary files /dev/null and b/helix/static/font/fontello.eot differ diff --git a/helix/static/font/fontello.svg b/helix/static/font/fontello.svg new file mode 100755 index 0000000..b486eee --- /dev/null +++ b/helix/static/font/fontello.svg @@ -0,0 +1,40 @@ + + + +Copyright (C) 2016 by original authors @ fontello.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/helix/static/font/fontello.ttf b/helix/static/font/fontello.ttf new file mode 100755 index 0000000..7ffe2a9 Binary files /dev/null and b/helix/static/font/fontello.ttf differ diff --git a/helix/static/font/fontello.woff b/helix/static/font/fontello.woff new file mode 100755 index 0000000..ed3436e Binary files /dev/null and b/helix/static/font/fontello.woff differ diff --git a/helix/static/font/fontello.woff2 b/helix/static/font/fontello.woff2 new file mode 100755 index 0000000..65bfcc8 Binary files /dev/null and b/helix/static/font/fontello.woff2 differ diff --git a/helix/static/images/arrow_down_blue.png b/helix/static/images/arrow_down_blue.png new file mode 100644 index 0000000..0656a17 Binary files /dev/null and b/helix/static/images/arrow_down_blue.png differ diff --git a/helix/static/images/arrow_right_blue.png b/helix/static/images/arrow_right_blue.png new file mode 100644 index 0000000..4f64d83 Binary files /dev/null and b/helix/static/images/arrow_right_blue.png differ diff --git a/helix/static/images/panel_types_dual_tilt@2x.png b/helix/static/images/panel_types_dual_tilt@2x.png new file mode 100644 index 0000000..34050d9 Binary files /dev/null and b/helix/static/images/panel_types_dual_tilt@2x.png differ diff --git a/helix/static/images/panel_types_single_tilt@2x.png b/helix/static/images/panel_types_single_tilt@2x.png new file mode 100644 index 0000000..9c9f3c3 Binary files /dev/null and b/helix/static/images/panel_types_single_tilt@2x.png differ diff --git a/helix/static/images/sp-helix-doc-header.png b/helix/static/images/sp-helix-doc-header.png new file mode 100644 index 0000000..25b0299 Binary files /dev/null and b/helix/static/images/sp-helix-doc-header.png differ diff --git a/helix/static/images/sp-helix-doc-header@2x.png b/helix/static/images/sp-helix-doc-header@2x.png new file mode 100644 index 0000000..1bee879 Binary files /dev/null and b/helix/static/images/sp-helix-doc-header@2x.png differ diff --git a/helix/static/images/sp-helix-legend-anchor-types.png b/helix/static/images/sp-helix-legend-anchor-types.png new file mode 100644 index 0000000..d18b31f Binary files /dev/null and b/helix/static/images/sp-helix-legend-anchor-types.png differ diff --git a/helix/static/images/sp-helix-legend-dual-all.png b/helix/static/images/sp-helix-legend-dual-all.png new file mode 100644 index 0000000..41e8e26 Binary files /dev/null and b/helix/static/images/sp-helix-legend-dual-all.png differ diff --git a/helix/static/images/sp-helix-legend-dual-anchors.png b/helix/static/images/sp-helix-legend-dual-anchors.png new file mode 100644 index 0000000..4b13a1f Binary files /dev/null and b/helix/static/images/sp-helix-legend-dual-anchors.png differ diff --git a/helix/static/images/sp-helix-legend-single-all.png b/helix/static/images/sp-helix-legend-single-all.png new file mode 100644 index 0000000..51689f8 Binary files /dev/null and b/helix/static/images/sp-helix-legend-single-all.png differ diff --git a/helix/static/images/sp-helix-legend-single-anchors.png b/helix/static/images/sp-helix-legend-single-anchors.png new file mode 100644 index 0000000..5b3524f Binary files /dev/null and b/helix/static/images/sp-helix-legend-single-anchors.png differ diff --git a/helix/static/images/sp_logo.png b/helix/static/images/sp_logo.png new file mode 100644 index 0000000..2cbfb30 Binary files /dev/null and b/helix/static/images/sp_logo.png differ diff --git a/helix/static/images/wind_zones_dual_tilt@2x.png b/helix/static/images/wind_zones_dual_tilt@2x.png new file mode 100644 index 0000000..60f77ac Binary files /dev/null and b/helix/static/images/wind_zones_dual_tilt@2x.png differ diff --git a/helix/static/images/wind_zones_single_tilt_north@2x.png b/helix/static/images/wind_zones_single_tilt_north@2x.png new file mode 100644 index 0000000..49af5e3 Binary files /dev/null and b/helix/static/images/wind_zones_single_tilt_north@2x.png differ diff --git a/helix/static/images/wind_zones_single_tilt_south@2x.png b/helix/static/images/wind_zones_single_tilt_south@2x.png new file mode 100644 index 0000000..e01b451 Binary files /dev/null and b/helix/static/images/wind_zones_single_tilt_south@2x.png differ diff --git a/helix/static/javascripts/array_summary_bundle.js b/helix/static/javascripts/array_summary_bundle.js new file mode 100644 index 0000000..070323f --- /dev/null +++ b/helix/static/javascripts/array_summary_bundle.js @@ -0,0 +1,24761 @@ +/******/ (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] = { +/******/ exports: {}, +/******/ id: moduleId, +/******/ loaded: false +/******/ }; + +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + +/******/ // Flag the module as loaded +/******/ module.loaded = 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; + +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; + +/******/ // Load entry module and return exports +/******/ return __webpack_require__(0); +/******/ }) +/************************************************************************/ +/******/ ([ +/* 0 */ +/***/ (function(module, exports, __webpack_require__) { + + "use strict"; + + var _array_visualization = __webpack_require__(1); + + var _array_visualization2 = _interopRequireDefault(_array_visualization); + + var _zoom_control = __webpack_require__(6); + + var _zoom_control2 = _interopRequireDefault(_zoom_control); + + var _overlay_control = __webpack_require__(7); + + var _overlay_control2 = _interopRequireDefault(_overlay_control); + + var _seismic_control = __webpack_require__(8); + + var _seismic_control2 = _interopRequireDefault(_seismic_control); + + var _subarray_display = __webpack_require__(9); + + var _subarray_display2 = _interopRequireDefault(_subarray_display); + + var _auto_upload = __webpack_require__(10); + + var _auto_upload2 = _interopRequireDefault(_auto_upload); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + $(document).ready(function () { + (0, _auto_upload2.default)(); + var subarrayDisplay = new _subarray_display2.default(); + subarrayDisplay.init($('#current_anchors'), $('#needed_anchors'), $('#subarray_weight'), panel_data); + var arrayVisualization = new _array_visualization2.default(panel_data, is_dual_tilt, subarrayDisplay, buildings_coordinates); + arrayVisualization.init(); + new _zoom_control2.default(arrayVisualization).init($('#zoom_control')); + new _overlay_control2.default(arrayVisualization).init($('#overlay_control'), $('#legend_container')); + new _seismic_control2.default(arrayVisualization, subarrayDisplay).init($('.seismic_anchor_control'), $("#seismic_save")); + window.arrayVisualization = arrayVisualization; + }); + +/***/ }), +/* 1 */ +/***/ (function(module, exports, __webpack_require__) { + + 'use strict'; + + Object.defineProperty(exports, "__esModule", { + value: true + }); + + var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); + + __webpack_require__(2); + + var _jquery = __webpack_require__(3); + + var _jquery2 = _interopRequireDefault(_jquery); + + var _panel = __webpack_require__(4); + + var _colors = __webpack_require__(5); + + var _colors2 = _interopRequireDefault(_colors); + + function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } + + function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } + + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } + + var ArrayVisualization = function () { + function ArrayVisualization(panelData, isDualTilt, subarrayDisplay, buildings) { + _classCallCheck(this, ArrayVisualization); + + this.panelData = panelData; + this.isDualTilt = isDualTilt; + this.subarrayDisplay = subarrayDisplay; + this.buildings = buildings; + this.scale = 10; + } + + _createClass(ArrayVisualization, [{ + key: 'init', + value: function init() { + this.stage = new createjs.Stage("arrayCanvas"); + this.container = new createjs.Container(); + + var background = new createjs.Shape(); + background.graphics.beginFill(_colors2.default.canvas_background).drawRect(0, 0, 850, 850); + this.stage.addChild(background); + this.stage.mouseMoveOutside = true; + this.adjustScale(this.buildings); + + this.drawArray(this.container, this.panelData, this.scale); + this.container.x = 0; + this.container.y = 0; + this.stage.addChild(this.container); + + this.drawBuildings(this.buildings, this.scale); + + this.stage.update(); + + var lastMove = undefined; + + var self = this; + + // Panning + this.stage.on("pressmove", function (event) { + var x = event.stageX; + var y = event.stageY; + if (lastMove != undefined) { + var deltaX = x - lastMove.x; + var deltaY = y - lastMove.y; + + self.container.x += deltaX; + self.container.y += deltaY; + + self.stage.update(); + } + lastMove = { x: x, y: y }; + }); + + this.stage.on("pressup", function () { + lastMove = undefined; + }); + } + }, { + key: 'adjustScale', + value: function adjustScale(buildings) { + var _ref, + _this = this, + _ref2; + + if (!buildings || buildings.length === 0) return; // leave scale as default + // we cannot determine canvas size + // we are in a test or in a very impossible state + // so we better not touch anything + if (!this.stage.canvas) return; + + var all_x = (_ref = []).concat.apply(_ref, _toConsumableArray(buildings)).map(function (building) { + return building.x * _this.scale; + }); + var all_y = (_ref2 = []).concat.apply(_ref2, _toConsumableArray(buildings)).map(function (building) { + return building.y * _this.scale; + }); + var max_x = Math.max.apply(Math, _toConsumableArray(all_x)); + var max_y = Math.max.apply(Math, _toConsumableArray(all_y)); + + var ratio_y = this.stage.canvas.height * 1.0 / max_y; + var ratio_x = this.stage.canvas.width * 1.0 / max_x; + + var MARGIN = 0.05; + this.scale = this.scale * Math.min(ratio_y, ratio_x) * (1 - MARGIN); + } + }, { + key: 'drawBuildings', + value: function drawBuildings(buildings, scale) { + if (!buildings) { + console.log("No Buildings!"); + return; + } + + if (!scale) { + console.log("No Scale - don't know how big the buildings should be!"); + return; + } + + var line = new createjs.Shape(); + var color = createjs.Graphics.getRGB(0x010101, 1); + + line.graphics.setStrokeStyle(3); + + for (var i = 0; i < buildings.length; i++) { + var building = buildings[i]; + var firstPoint = building[0]; + var nextPoint = null; + + line.graphics.beginStroke(color); + + line.graphics.moveTo(firstPoint.x * scale, firstPoint.y * scale); + + for (var j = 1; j < building.length; j++) { + nextPoint = building[j]; + line.graphics.lineTo(nextPoint.x * scale, nextPoint.y * scale); + line.graphics.moveTo(nextPoint.x * scale, nextPoint.y * scale); + } + line.graphics.lineTo(firstPoint.x * scale, firstPoint.y * scale); + line.graphics.endStroke(); + } + + this.container.addChild(line); + } + }, { + key: 'refreshPanels', + value: function refreshPanels(panelData) { + this.panelData = panelData; + + for (var i = 0; i < this.panels.length; i++) { + this.container.removeChild(this.panels[i]); + } + + this.drawArray(this.container, this.panelData, this.scale); + + var selectedPanel = this.selectedPanel; + this.selectedPanel = undefined; + this.selectPanel(selectedPanel); + this.setOverlay(this.overlay); + + this.subarrayDisplay.didModifyPanels(panelData); + + this.stage.update(); + } + }, { + key: 'drawArray', + value: function drawArray(container, panels) { + var _this2 = this; + + this.panels = []; + var treatCoordinatesAsCenterpoints = this.buildings && this.buildings.length > 0; + + var _loop = function _loop(i) { + var panel = panels[i]; + + var box = new _panel.Panel(panel, _this2.isDualTilt, _this2.scale, treatCoordinatesAsCenterpoints); + container.addChild(box); + _this2.panels.push(box); + + var self = _this2; + box.on("click", function () { + self.selectPanel(i); + }); + }; + + for (var i = 0; i < panels.length; i++) { + _loop(i); + } + } + }, { + key: 'selectPanel', + value: function selectPanel(panelIndex) { + var panel = this.panels[panelIndex]; + if (this.selectedPanel !== undefined) { + var previousPanel = this.panels[this.selectedPanel]; + previousPanel.deselect(); + } + if (this.selectedPanel === panelIndex) { + this.selectedPanel = undefined; + } else { + this.selectedPanel = panelIndex; + panel.select(); + } + this.stage.update(); + } + }, { + key: 'setZoom', + value: function setZoom(zoomLevel) { + this.container.scaleX = zoomLevel; + this.container.scaleY = zoomLevel; + + this.stage.update(); + } + }, { + key: 'setOverlay', + value: function setOverlay(overlay) { + this.overlay = overlay; + this.panels.forEach(function (panel) { + panel.setOverlay(overlay); + }); + + this.stage.update(); + } + }, { + key: 'addSeismicAnchor', + value: function addSeismicAnchor() { + if (this.selectedPanel !== undefined) { + this.panelData[this.selectedPanel].data.seismic_anchors++; + this.panels[this.selectedPanel].redrawOverlays(); + this.stage.update(); + this.subarrayDisplay.didModifyPanels(this.panelData); + } + } + }, { + key: 'removeSeismicAnchor', + value: function removeSeismicAnchor() { + if (this.selectedPanel !== undefined) { + var seismicAnchors = this.panelData[this.selectedPanel].data.seismic_anchors; + if (seismicAnchors > 0) { + this.panelData[this.selectedPanel].data.seismic_anchors--; + this.panels[this.selectedPanel].redrawOverlays(); + this.stage.update(); + this.subarrayDisplay.didModifyPanels(this.panelData); + } + } + } + }]); + + return ArrayVisualization; + }(); + + exports.default = ArrayVisualization; + +/***/ }), +/* 2 */ +/***/ (function(module, exports) { + + "use strict"; + + /*** IMPORTS FROM imports-loader ***/ + (function () { + + /*! + * EaselJS + * Visit http://createjs.com/ for documentation, updates and examples. + * + * Copyright (c) 2010 gskinner.com, inc. + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + + //############################################################################## + // extend.js + //############################################################################## + + this.createjs = this.createjs || {}; + + /** + * @class Utility Methods + */ + + /** + * Sets up the prototype chain and constructor property for a new class. + * + * This should be called right after creating the class constructor. + * + * function MySubClass() {} + * createjs.extend(MySubClass, MySuperClass); + * MySubClass.prototype.doSomething = function() { } + * + * var foo = new MySubClass(); + * console.log(foo instanceof MySuperClass); // true + * console.log(foo.prototype.constructor === MySubClass); // true + * + * @method extend + * @param {Function} subclass The subclass. + * @param {Function} superclass The superclass to extend. + * @return {Function} Returns the subclass's new prototype. + */ + createjs.extend = function (subclass, superclass) { + "use strict"; + + function o() { + this.constructor = subclass; + } + o.prototype = superclass.prototype; + return subclass.prototype = new o(); + }; + + //############################################################################## + // promote.js + //############################################################################## + + this.createjs = this.createjs || {}; + + /** + * @class Utility Methods + */ + + /** + * Promotes any methods on the super class that were overridden, by creating an alias in the format `prefix_methodName`. + * It is recommended to use the super class's name as the prefix. + * An alias to the super class's constructor is always added in the format `prefix_constructor`. + * This allows the subclass to call super class methods without using `function.call`, providing better performance. + * + * For example, if `MySubClass` extends `MySuperClass`, and both define a `draw` method, then calling `promote(MySubClass, "MySuperClass")` + * would add a `MySuperClass_constructor` method to MySubClass and promote the `draw` method on `MySuperClass` to the + * prototype of `MySubClass` as `MySuperClass_draw`. + * + * This should be called after the class's prototype is fully defined. + * + * function ClassA(name) { + * this.name = name; + * } + * ClassA.prototype.greet = function() { + * return "Hello "+this.name; + * } + * + * function ClassB(name, punctuation) { + * this.ClassA_constructor(name); + * this.punctuation = punctuation; + * } + * createjs.extend(ClassB, ClassA); + * ClassB.prototype.greet = function() { + * return this.ClassA_greet()+this.punctuation; + * } + * createjs.promote(ClassB, "ClassA"); + * + * var foo = new ClassB("World", "!?!"); + * console.log(foo.greet()); // Hello World!?! + * + * @method promote + * @param {Function} subclass The class to promote super class methods on. + * @param {String} prefix The prefix to add to the promoted method names. Usually the name of the superclass. + * @return {Function} Returns the subclass. + */ + createjs.promote = function (subclass, prefix) { + "use strict"; + + var subP = subclass.prototype, + supP = Object.getPrototypeOf && Object.getPrototypeOf(subP) || subP.__proto__; + if (supP) { + subP[(prefix += "_") + "constructor"] = supP.constructor; // constructor is not always innumerable + for (var n in supP) { + if (subP.hasOwnProperty(n) && typeof supP[n] == "function") { + subP[prefix + n] = supP[n]; + } + } + } + return subclass; + }; + + //############################################################################## + // indexOf.js + //############################################################################## + + this.createjs = this.createjs || {}; + + /** + * @class Utility Methods + */ + + /** + * Finds the first occurrence of a specified value searchElement in the passed in array, and returns the index of + * that value. Returns -1 if value is not found. + * + * var i = createjs.indexOf(myArray, myElementToFind); + * + * @method indexOf + * @param {Array} array Array to search for searchElement + * @param searchElement Element to find in array. + * @return {Number} The first index of searchElement in array. + */ + createjs.indexOf = function (array, searchElement) { + "use strict"; + + for (var i = 0, l = array.length; i < l; i++) { + if (searchElement === array[i]) { + return i; + } + } + return -1; + }; + + //############################################################################## + // Event.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Contains properties and methods shared by all events for use with + * {{#crossLink "EventDispatcher"}}{{/crossLink}}. + * + * Note that Event objects are often reused, so you should never + * rely on an event object's state outside of the call stack it was received in. + * @class Event + * @param {String} type The event type. + * @param {Boolean} bubbles Indicates whether the event will bubble through the display list. + * @param {Boolean} cancelable Indicates whether the default behaviour of this event can be cancelled. + * @constructor + **/ + + function Event(type, bubbles, cancelable) { + + // public properties: + /** + * The type of event. + * @property type + * @type String + **/ + this.type = type; + + /** + * The object that generated an event. + * @property target + * @type Object + * @default null + * @readonly + */ + this.target = null; + + /** + * The current target that a bubbling event is being dispatched from. For non-bubbling events, this will + * always be the same as target. For example, if childObj.parent = parentObj, and a bubbling event + * is generated from childObj, then a listener on parentObj would receive the event with + * target=childObj (the original target) and currentTarget=parentObj (where the listener was added). + * @property currentTarget + * @type Object + * @default null + * @readonly + */ + this.currentTarget = null; + + /** + * For bubbling events, this indicates the current event phase:
    + *
  1. capture phase: starting from the top parent to the target
  2. + *
  3. at target phase: currently being dispatched from the target
  4. + *
  5. bubbling phase: from the target to the top parent
  6. + *
+ * @property eventPhase + * @type Number + * @default 0 + * @readonly + */ + this.eventPhase = 0; + + /** + * Indicates whether the event will bubble through the display list. + * @property bubbles + * @type Boolean + * @default false + * @readonly + */ + this.bubbles = !!bubbles; + + /** + * Indicates whether the default behaviour of this event can be cancelled via + * {{#crossLink "Event/preventDefault"}}{{/crossLink}}. This is set via the Event constructor. + * @property cancelable + * @type Boolean + * @default false + * @readonly + */ + this.cancelable = !!cancelable; + + /** + * The epoch time at which this event was created. + * @property timeStamp + * @type Number + * @default 0 + * @readonly + */ + this.timeStamp = new Date().getTime(); + + /** + * Indicates if {{#crossLink "Event/preventDefault"}}{{/crossLink}} has been called + * on this event. + * @property defaultPrevented + * @type Boolean + * @default false + * @readonly + */ + this.defaultPrevented = false; + + /** + * Indicates if {{#crossLink "Event/stopPropagation"}}{{/crossLink}} or + * {{#crossLink "Event/stopImmediatePropagation"}}{{/crossLink}} has been called on this event. + * @property propagationStopped + * @type Boolean + * @default false + * @readonly + */ + this.propagationStopped = false; + + /** + * Indicates if {{#crossLink "Event/stopImmediatePropagation"}}{{/crossLink}} has been called + * on this event. + * @property immediatePropagationStopped + * @type Boolean + * @default false + * @readonly + */ + this.immediatePropagationStopped = false; + + /** + * Indicates if {{#crossLink "Event/remove"}}{{/crossLink}} has been called on this event. + * @property removed + * @type Boolean + * @default false + * @readonly + */ + this.removed = false; + } + var p = Event.prototype; + + /** + * REMOVED. Removed in favor of using `MySuperClass_constructor`. + * See {{#crossLink "Utility Methods/extend"}}{{/crossLink}} and {{#crossLink "Utility Methods/promote"}}{{/crossLink}} + * for details. + * + * There is an inheritance tutorial distributed with EaselJS in /tutorials/Inheritance. + * + * @method initialize + * @protected + * @deprecated + */ + // p.initialize = function() {}; // searchable for devs wondering where it is. + + // public methods: + /** + * Sets {{#crossLink "Event/defaultPrevented"}}{{/crossLink}} to true if the event is cancelable. + * Mirrors the DOM level 2 event standard. In general, cancelable events that have `preventDefault()` called will + * cancel the default behaviour associated with the event. + * @method preventDefault + **/ + p.preventDefault = function () { + this.defaultPrevented = this.cancelable && true; + }; + + /** + * Sets {{#crossLink "Event/propagationStopped"}}{{/crossLink}} to true. + * Mirrors the DOM event standard. + * @method stopPropagation + **/ + p.stopPropagation = function () { + this.propagationStopped = true; + }; + + /** + * Sets {{#crossLink "Event/propagationStopped"}}{{/crossLink}} and + * {{#crossLink "Event/immediatePropagationStopped"}}{{/crossLink}} to true. + * Mirrors the DOM event standard. + * @method stopImmediatePropagation + **/ + p.stopImmediatePropagation = function () { + this.immediatePropagationStopped = this.propagationStopped = true; + }; + + /** + * Causes the active listener to be removed via removeEventListener(); + * + * myBtn.addEventListener("click", function(evt) { + * // do stuff... + * evt.remove(); // removes this listener. + * }); + * + * @method remove + **/ + p.remove = function () { + this.removed = true; + }; + + /** + * Returns a clone of the Event instance. + * @method clone + * @return {Event} a clone of the Event instance. + **/ + p.clone = function () { + return new Event(this.type, this.bubbles, this.cancelable); + }; + + /** + * Provides a chainable shortcut method for setting a number of properties on the instance. + * + * @method set + * @param {Object} props A generic object containing properties to copy to the instance. + * @return {Event} Returns the instance the method is called on (useful for chaining calls.) + * @chainable + */ + p.set = function (props) { + for (var n in props) { + this[n] = props[n]; + } + return this; + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[Event (type=" + this.type + ")]"; + }; + + createjs.Event = Event; + })(); + + //############################################################################## + // EventDispatcher.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * EventDispatcher provides methods for managing queues of event listeners and dispatching events. + * + * You can either extend EventDispatcher or mix its methods into an existing prototype or instance by using the + * EventDispatcher {{#crossLink "EventDispatcher/initialize"}}{{/crossLink}} method. + * + * Together with the CreateJS Event class, EventDispatcher provides an extended event model that is based on the + * DOM Level 2 event model, including addEventListener, removeEventListener, and dispatchEvent. It supports + * bubbling / capture, preventDefault, stopPropagation, stopImmediatePropagation, and handleEvent. + * + * EventDispatcher also exposes a {{#crossLink "EventDispatcher/on"}}{{/crossLink}} method, which makes it easier + * to create scoped listeners, listeners that only run once, and listeners with associated arbitrary data. The + * {{#crossLink "EventDispatcher/off"}}{{/crossLink}} method is merely an alias to + * {{#crossLink "EventDispatcher/removeEventListener"}}{{/crossLink}}. + * + * Another addition to the DOM Level 2 model is the {{#crossLink "EventDispatcher/removeAllEventListeners"}}{{/crossLink}} + * method, which can be used to listeners for all events, or listeners for a specific event. The Event object also + * includes a {{#crossLink "Event/remove"}}{{/crossLink}} method which removes the active listener. + * + *

Example

+ * Add EventDispatcher capabilities to the "MyClass" class. + * + * EventDispatcher.initialize(MyClass.prototype); + * + * Add an event (see {{#crossLink "EventDispatcher/addEventListener"}}{{/crossLink}}). + * + * instance.addEventListener("eventName", handlerMethod); + * function handlerMethod(event) { + * console.log(event.target + " Was Clicked"); + * } + * + * Maintaining proper scope
+ * Scope (ie. "this") can be be a challenge with events. Using the {{#crossLink "EventDispatcher/on"}}{{/crossLink}} + * method to subscribe to events simplifies this. + * + * instance.addEventListener("click", function(event) { + * console.log(instance == this); // false, scope is ambiguous. + * }); + * + * instance.on("click", function(event) { + * console.log(instance == this); // true, "on" uses dispatcher scope by default. + * }); + * + * If you want to use addEventListener instead, you may want to use function.bind() or a similar proxy to manage + * scope. + * + * Browser support + * The event model in CreateJS can be used separately from the suite in any project, however the inheritance model + * requires modern browsers (IE9+). + * + * + * @class EventDispatcher + * @constructor + **/ + + function EventDispatcher() { + + // private properties: + /** + * @protected + * @property _listeners + * @type Object + **/ + this._listeners = null; + + /** + * @protected + * @property _captureListeners + * @type Object + **/ + this._captureListeners = null; + } + var p = EventDispatcher.prototype; + + /** + * REMOVED. Removed in favor of using `MySuperClass_constructor`. + * See {{#crossLink "Utility Methods/extend"}}{{/crossLink}} and {{#crossLink "Utility Methods/promote"}}{{/crossLink}} + * for details. + * + * There is an inheritance tutorial distributed with EaselJS in /tutorials/Inheritance. + * + * @method initialize + * @protected + * @deprecated + */ + // p.initialize = function() {}; // searchable for devs wondering where it is. + + + // static public methods: + /** + * Static initializer to mix EventDispatcher methods into a target object or prototype. + * + * EventDispatcher.initialize(MyClass.prototype); // add to the prototype of the class + * EventDispatcher.initialize(myObject); // add to a specific instance + * + * @method initialize + * @static + * @param {Object} target The target object to inject EventDispatcher methods into. This can be an instance or a + * prototype. + **/ + EventDispatcher.initialize = function (target) { + target.addEventListener = p.addEventListener; + target.on = p.on; + target.removeEventListener = target.off = p.removeEventListener; + target.removeAllEventListeners = p.removeAllEventListeners; + target.hasEventListener = p.hasEventListener; + target.dispatchEvent = p.dispatchEvent; + target._dispatchEvent = p._dispatchEvent; + target.willTrigger = p.willTrigger; + }; + + // public methods: + /** + * Adds the specified event listener. Note that adding multiple listeners to the same function will result in + * multiple callbacks getting fired. + * + *

Example

+ * + * displayObject.addEventListener("click", handleClick); + * function handleClick(event) { + * // Click happened. + * } + * + * @method addEventListener + * @param {String} type The string type of the event. + * @param {Function | Object} listener An object with a handleEvent method, or a function that will be called when + * the event is dispatched. + * @param {Boolean} [useCapture] For events that bubble, indicates whether to listen for the event in the capture or bubbling/target phase. + * @return {Function | Object} Returns the listener for chaining or assignment. + **/ + p.addEventListener = function (type, listener, useCapture) { + var listeners; + if (useCapture) { + listeners = this._captureListeners = this._captureListeners || {}; + } else { + listeners = this._listeners = this._listeners || {}; + } + var arr = listeners[type]; + if (arr) { + this.removeEventListener(type, listener, useCapture); + } + arr = listeners[type]; // remove may have deleted the array + if (!arr) { + listeners[type] = [listener]; + } else { + arr.push(listener); + } + return listener; + }; + + /** + * A shortcut method for using addEventListener that makes it easier to specify an execution scope, have a listener + * only run once, associate arbitrary data with the listener, and remove the listener. + * + * This method works by creating an anonymous wrapper function and subscribing it with addEventListener. + * The wrapper function is returned for use with `removeEventListener` (or `off`). + * + * IMPORTANT: To remove a listener added with `on`, you must pass in the returned wrapper function as the listener, or use + * {{#crossLink "Event/remove"}}{{/crossLink}}. Likewise, each time you call `on` a NEW wrapper function is subscribed, so multiple calls + * to `on` with the same params will create multiple listeners. + * + *

Example

+ * + * var listener = myBtn.on("click", handleClick, null, false, {count:3}); + * function handleClick(evt, data) { + * data.count -= 1; + * console.log(this == myBtn); // true - scope defaults to the dispatcher + * if (data.count == 0) { + * alert("clicked 3 times!"); + * myBtn.off("click", listener); + * // alternately: evt.remove(); + * } + * } + * + * @method on + * @param {String} type The string type of the event. + * @param {Function | Object} listener An object with a handleEvent method, or a function that will be called when + * the event is dispatched. + * @param {Object} [scope] The scope to execute the listener in. Defaults to the dispatcher/currentTarget for function listeners, and to the listener itself for object listeners (ie. using handleEvent). + * @param {Boolean} [once=false] If true, the listener will remove itself after the first time it is triggered. + * @param {*} [data] Arbitrary data that will be included as the second parameter when the listener is called. + * @param {Boolean} [useCapture=false] For events that bubble, indicates whether to listen for the event in the capture or bubbling/target phase. + * @return {Function} Returns the anonymous function that was created and assigned as the listener. This is needed to remove the listener later using .removeEventListener. + **/ + p.on = function (type, listener, scope, once, data, useCapture) { + if (listener.handleEvent) { + scope = scope || listener; + listener = listener.handleEvent; + } + scope = scope || this; + return this.addEventListener(type, function (evt) { + listener.call(scope, evt, data); + once && evt.remove(); + }, useCapture); + }; + + /** + * Removes the specified event listener. + * + * Important Note: that you must pass the exact function reference used when the event was added. If a proxy + * function, or function closure is used as the callback, the proxy/closure reference must be used - a new proxy or + * closure will not work. + * + *

Example

+ * + * displayObject.removeEventListener("click", handleClick); + * + * @method removeEventListener + * @param {String} type The string type of the event. + * @param {Function | Object} listener The listener function or object. + * @param {Boolean} [useCapture] For events that bubble, indicates whether to listen for the event in the capture or bubbling/target phase. + **/ + p.removeEventListener = function (type, listener, useCapture) { + var listeners = useCapture ? this._captureListeners : this._listeners; + if (!listeners) { + return; + } + var arr = listeners[type]; + if (!arr) { + return; + } + for (var i = 0, l = arr.length; i < l; i++) { + if (arr[i] == listener) { + if (l == 1) { + delete listeners[type]; + } // allows for faster checks. + else { + arr.splice(i, 1); + } + break; + } + } + }; + + /** + * A shortcut to the removeEventListener method, with the same parameters and return value. This is a companion to the + * .on method. + * + * IMPORTANT: To remove a listener added with `on`, you must pass in the returned wrapper function as the listener. See + * {{#crossLink "EventDispatcher/on"}}{{/crossLink}} for an example. + * + * @method off + * @param {String} type The string type of the event. + * @param {Function | Object} listener The listener function or object. + * @param {Boolean} [useCapture] For events that bubble, indicates whether to listen for the event in the capture or bubbling/target phase. + **/ + p.off = p.removeEventListener; + + /** + * Removes all listeners for the specified type, or all listeners of all types. + * + *

Example

+ * + * // Remove all listeners + * displayObject.removeAllEventListeners(); + * + * // Remove all click listeners + * displayObject.removeAllEventListeners("click"); + * + * @method removeAllEventListeners + * @param {String} [type] The string type of the event. If omitted, all listeners for all types will be removed. + **/ + p.removeAllEventListeners = function (type) { + if (!type) { + this._listeners = this._captureListeners = null; + } else { + if (this._listeners) { + delete this._listeners[type]; + } + if (this._captureListeners) { + delete this._captureListeners[type]; + } + } + }; + + /** + * Dispatches the specified event to all listeners. + * + *

Example

+ * + * // Use a string event + * this.dispatchEvent("complete"); + * + * // Use an Event instance + * var event = new createjs.Event("progress"); + * this.dispatchEvent(event); + * + * @method dispatchEvent + * @param {Object | String | Event} eventObj An object with a "type" property, or a string type. + * While a generic object will work, it is recommended to use a CreateJS Event instance. If a string is used, + * dispatchEvent will construct an Event instance if necessary with the specified type. This latter approach can + * be used to avoid event object instantiation for non-bubbling events that may not have any listeners. + * @param {Boolean} [bubbles] Specifies the `bubbles` value when a string was passed to eventObj. + * @param {Boolean} [cancelable] Specifies the `cancelable` value when a string was passed to eventObj. + * @return {Boolean} Returns false if `preventDefault()` was called on a cancelable event, true otherwise. + **/ + p.dispatchEvent = function (eventObj, bubbles, cancelable) { + if (typeof eventObj == "string") { + // skip everything if there's no listeners and it doesn't bubble: + var listeners = this._listeners; + if (!bubbles && (!listeners || !listeners[eventObj])) { + return true; + } + eventObj = new createjs.Event(eventObj, bubbles, cancelable); + } else if (eventObj.target && eventObj.clone) { + // redispatching an active event object, so clone it: + eventObj = eventObj.clone(); + } + + // TODO: it would be nice to eliminate this. Maybe in favour of evtObj instanceof Event? Or !!evtObj.createEvent + try { + eventObj.target = this; + } catch (e) {} // try/catch allows redispatching of native events + + if (!eventObj.bubbles || !this.parent) { + this._dispatchEvent(eventObj, 2); + } else { + var top = this, + list = [top]; + while (top.parent) { + list.push(top = top.parent); + } + var i, + l = list.length; + + // capture & atTarget + for (i = l - 1; i >= 0 && !eventObj.propagationStopped; i--) { + list[i]._dispatchEvent(eventObj, 1 + (i == 0)); + } + // bubbling + for (i = 1; i < l && !eventObj.propagationStopped; i++) { + list[i]._dispatchEvent(eventObj, 3); + } + } + return !eventObj.defaultPrevented; + }; + + /** + * Indicates whether there is at least one listener for the specified event type. + * @method hasEventListener + * @param {String} type The string type of the event. + * @return {Boolean} Returns true if there is at least one listener for the specified event. + **/ + p.hasEventListener = function (type) { + var listeners = this._listeners, + captureListeners = this._captureListeners; + return !!(listeners && listeners[type] || captureListeners && captureListeners[type]); + }; + + /** + * Indicates whether there is at least one listener for the specified event type on this object or any of its + * ancestors (parent, parent's parent, etc). A return value of true indicates that if a bubbling event of the + * specified type is dispatched from this object, it will trigger at least one listener. + * + * This is similar to {{#crossLink "EventDispatcher/hasEventListener"}}{{/crossLink}}, but it searches the entire + * event flow for a listener, not just this object. + * @method willTrigger + * @param {String} type The string type of the event. + * @return {Boolean} Returns `true` if there is at least one listener for the specified event. + **/ + p.willTrigger = function (type) { + var o = this; + while (o) { + if (o.hasEventListener(type)) { + return true; + } + o = o.parent; + } + return false; + }; + + /** + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[EventDispatcher]"; + }; + + // private methods: + /** + * @method _dispatchEvent + * @param {Object | String | Event} eventObj + * @param {Object} eventPhase + * @protected + **/ + p._dispatchEvent = function (eventObj, eventPhase) { + var l, + listeners = eventPhase == 1 ? this._captureListeners : this._listeners; + if (eventObj && listeners) { + var arr = listeners[eventObj.type]; + if (!arr || !(l = arr.length)) { + return; + } + try { + eventObj.currentTarget = this; + } catch (e) {} + try { + eventObj.eventPhase = eventPhase; + } catch (e) {} + eventObj.removed = false; + + arr = arr.slice(); // to avoid issues with items being removed or added during the dispatch + for (var i = 0; i < l && !eventObj.immediatePropagationStopped; i++) { + var o = arr[i]; + if (o.handleEvent) { + o.handleEvent(eventObj); + } else { + o(eventObj); + } + if (eventObj.removed) { + this.off(eventObj.type, o, eventPhase == 1); + eventObj.removed = false; + } + } + } + }; + + createjs.EventDispatcher = EventDispatcher; + })(); + + //############################################################################## + // Ticker.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * The Ticker provides a centralized tick or heartbeat broadcast at a set interval. Listeners can subscribe to the tick + * event to be notified when a set time interval has elapsed. + * + * Note that the interval that the tick event is called is a target interval, and may be broadcast at a slower interval + * when under high CPU load. The Ticker class uses a static interface (ex. `Ticker.framerate = 30;`) and + * can not be instantiated. + * + *

Example

+ * + * createjs.Ticker.addEventListener("tick", handleTick); + * function handleTick(event) { + * // Actions carried out each tick (aka frame) + * if (!event.paused) { + * // Actions carried out when the Ticker is not paused. + * } + * } + * + * @class Ticker + * @uses EventDispatcher + * @static + **/ + + function Ticker() { + throw "Ticker cannot be instantiated."; + } + + // constants: + /** + * In this mode, Ticker uses the requestAnimationFrame API, but attempts to synch the ticks to target framerate. It + * uses a simple heuristic that compares the time of the RAF return to the target time for the current frame and + * dispatches the tick when the time is within a certain threshold. + * + * This mode has a higher variance for time between frames than {{#crossLink "Ticker/TIMEOUT:property"}}{{/crossLink}}, + * but does not require that content be time based as with {{#crossLink "Ticker/RAF:property"}}{{/crossLink}} while + * gaining the benefits of that API (screen synch, background throttling). + * + * Variance is usually lowest for framerates that are a divisor of the RAF frequency. This is usually 60, so + * framerates of 10, 12, 15, 20, and 30 work well. + * + * Falls back to {{#crossLink "Ticker/TIMEOUT:property"}}{{/crossLink}} if the requestAnimationFrame API is not + * supported. + * @property RAF_SYNCHED + * @static + * @type {String} + * @default "synched" + * @readonly + **/ + Ticker.RAF_SYNCHED = "synched"; + + /** + * In this mode, Ticker passes through the requestAnimationFrame heartbeat, ignoring the target framerate completely. + * Because requestAnimationFrame frequency is not deterministic, any content using this mode should be time based. + * You can leverage {{#crossLink "Ticker/getTime"}}{{/crossLink}} and the {{#crossLink "Ticker/tick:event"}}{{/crossLink}} + * event object's "delta" properties to make this easier. + * + * Falls back on {{#crossLink "Ticker/TIMEOUT:property"}}{{/crossLink}} if the requestAnimationFrame API is not + * supported. + * @property RAF + * @static + * @type {String} + * @default "raf" + * @readonly + **/ + Ticker.RAF = "raf"; + + /** + * In this mode, Ticker uses the setTimeout API. This provides predictable, adaptive frame timing, but does not + * provide the benefits of requestAnimationFrame (screen synch, background throttling). + * @property TIMEOUT + * @static + * @type {String} + * @default "timeout" + * @readonly + **/ + Ticker.TIMEOUT = "timeout"; + + // static events: + /** + * Dispatched each tick. The event will be dispatched to each listener even when the Ticker has been paused using + * {{#crossLink "Ticker/setPaused"}}{{/crossLink}}. + * + *

Example

+ * + * createjs.Ticker.addEventListener("tick", handleTick); + * function handleTick(event) { + * console.log("Paused:", event.paused, event.delta); + * } + * + * @event tick + * @param {Object} target The object that dispatched the event. + * @param {String} type The event type. + * @param {Boolean} paused Indicates whether the ticker is currently paused. + * @param {Number} delta The time elapsed in ms since the last tick. + * @param {Number} time The total time in ms since Ticker was initialized. + * @param {Number} runTime The total time in ms that Ticker was not paused since it was initialized. For example, + * you could determine the amount of time that the Ticker has been paused since initialization with `time-runTime`. + * @since 0.6.0 + */ + + // public static properties: + /** + * Deprecated in favour of {{#crossLink "Ticker/timingMode"}}{{/crossLink}}, and will be removed in a future version. If true, timingMode will + * use {{#crossLink "Ticker/RAF_SYNCHED"}}{{/crossLink}} by default. + * @deprecated Deprecated in favour of {{#crossLink "Ticker/timingMode"}}{{/crossLink}}. + * @property useRAF + * @static + * @type {Boolean} + * @default false + **/ + Ticker.useRAF = false; + + /** + * Specifies the timing api (setTimeout or requestAnimationFrame) and mode to use. See + * {{#crossLink "Ticker/TIMEOUT"}}{{/crossLink}}, {{#crossLink "Ticker/RAF"}}{{/crossLink}}, and + * {{#crossLink "Ticker/RAF_SYNCHED"}}{{/crossLink}} for mode details. + * @property timingMode + * @static + * @type {String} + * @default Ticker.TIMEOUT + **/ + Ticker.timingMode = null; + + /** + * Specifies a maximum value for the delta property in the tick event object. This is useful when building time + * based animations and systems to prevent issues caused by large time gaps caused by background tabs, system sleep, + * alert dialogs, or other blocking routines. Double the expected frame duration is often an effective value + * (ex. maxDelta=50 when running at 40fps). + * + * This does not impact any other values (ex. time, runTime, etc), so you may experience issues if you enable maxDelta + * when using both delta and other values. + * + * If 0, there is no maximum. + * @property maxDelta + * @static + * @type {number} + * @default 0 + */ + Ticker.maxDelta = 0; + + /** + * When the ticker is paused, all listeners will still receive a tick event, but the paused property + * of the event will be `true`. Also, while paused the `runTime` will not increase. See {{#crossLink "Ticker/tick:event"}}{{/crossLink}}, + * {{#crossLink "Ticker/getTime"}}{{/crossLink}}, and {{#crossLink "Ticker/getEventTime"}}{{/crossLink}} for more + * info. + * + *

Example

+ * + * createjs.Ticker.addEventListener("tick", handleTick); + * createjs.Ticker.paused = true; + * function handleTick(event) { + * console.log(event.paused, + * createjs.Ticker.getTime(false), + * createjs.Ticker.getTime(true)); + * } + * + * @property paused + * @static + * @type {Boolean} + * @default false + **/ + Ticker.paused = false; + + // mix-ins: + // EventDispatcher methods: + Ticker.removeEventListener = null; + Ticker.removeAllEventListeners = null; + Ticker.dispatchEvent = null; + Ticker.hasEventListener = null; + Ticker._listeners = null; + createjs.EventDispatcher.initialize(Ticker); // inject EventDispatcher methods. + Ticker._addEventListener = Ticker.addEventListener; + Ticker.addEventListener = function () { + !Ticker._inited && Ticker.init(); + return Ticker._addEventListener.apply(Ticker, arguments); + }; + + // private static properties: + /** + * @property _inited + * @static + * @type {Boolean} + * @protected + **/ + Ticker._inited = false; + + /** + * @property _startTime + * @static + * @type {Number} + * @protected + **/ + Ticker._startTime = 0; + + /** + * @property _pausedTime + * @static + * @type {Number} + * @protected + **/ + Ticker._pausedTime = 0; + + /** + * The number of ticks that have passed + * @property _ticks + * @static + * @type {Number} + * @protected + **/ + Ticker._ticks = 0; + + /** + * The number of ticks that have passed while Ticker has been paused + * @property _pausedTicks + * @static + * @type {Number} + * @protected + **/ + Ticker._pausedTicks = 0; + + /** + * @property _interval + * @static + * @type {Number} + * @protected + **/ + Ticker._interval = 50; + + /** + * @property _lastTime + * @static + * @type {Number} + * @protected + **/ + Ticker._lastTime = 0; + + /** + * @property _times + * @static + * @type {Array} + * @protected + **/ + Ticker._times = null; + + /** + * @property _tickTimes + * @static + * @type {Array} + * @protected + **/ + Ticker._tickTimes = null; + + /** + * Stores the timeout or requestAnimationFrame id. + * @property _timerId + * @static + * @type {Number} + * @protected + **/ + Ticker._timerId = null; + + /** + * True if currently using requestAnimationFrame, false if using setTimeout. This may be different than timingMode + * if that property changed and a tick hasn't fired. + * @property _raf + * @static + * @type {Boolean} + * @protected + **/ + Ticker._raf = true; + + // static getter / setters: + /** + * Use the {{#crossLink "Ticker/interval:property"}}{{/crossLink}} property instead. + * @method setInterval + * @static + * @param {Number} interval + * @deprecated + **/ + Ticker.setInterval = function (interval) { + Ticker._interval = interval; + if (!Ticker._inited) { + return; + } + Ticker._setupTick(); + }; + + /** + * Use the {{#crossLink "Ticker/interval:property"}}{{/crossLink}} property instead. + * @method getInterval + * @static + * @return {Number} + * @deprecated + **/ + Ticker.getInterval = function () { + return Ticker._interval; + }; + + /** + * Use the {{#crossLink "Ticker/framerate:property"}}{{/crossLink}} property instead. + * @method setFPS + * @static + * @param {Number} value + * @deprecated + **/ + Ticker.setFPS = function (value) { + Ticker.setInterval(1000 / value); + }; + + /** + * Use the {{#crossLink "Ticker/framerate:property"}}{{/crossLink}} property instead. + * @method getFPS + * @static + * @return {Number} + * @deprecated + **/ + Ticker.getFPS = function () { + return 1000 / Ticker._interval; + }; + + /** + * Indicates the target time (in milliseconds) between ticks. Default is 50 (20 FPS). + * Note that actual time between ticks may be more than specified depending on CPU load. + * This property is ignored if the ticker is using the `RAF` timing mode. + * @property interval + * @static + * @type {Number} + **/ + + /** + * Indicates the target frame rate in frames per second (FPS). Effectively just a shortcut to `interval`, where + * `framerate == 1000/interval`. + * @property framerate + * @static + * @type {Number} + **/ + try { + Object.defineProperties(Ticker, { + interval: { get: Ticker.getInterval, set: Ticker.setInterval }, + framerate: { get: Ticker.getFPS, set: Ticker.setFPS } + }); + } catch (e) { + console.log(e); + } + + // public static methods: + /** + * Starts the tick. This is called automatically when the first listener is added. + * @method init + * @static + **/ + Ticker.init = function () { + if (Ticker._inited) { + return; + } + Ticker._inited = true; + Ticker._times = []; + Ticker._tickTimes = []; + Ticker._startTime = Ticker._getTime(); + Ticker._times.push(Ticker._lastTime = 0); + Ticker.interval = Ticker._interval; + }; + + /** + * Stops the Ticker and removes all listeners. Use init() to restart the Ticker. + * @method reset + * @static + **/ + Ticker.reset = function () { + if (Ticker._raf) { + var f = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.oCancelAnimationFrame || window.msCancelAnimationFrame; + f && f(Ticker._timerId); + } else { + clearTimeout(Ticker._timerId); + } + Ticker.removeAllEventListeners("tick"); + Ticker._timerId = Ticker._times = Ticker._tickTimes = null; + Ticker._startTime = Ticker._lastTime = Ticker._ticks = 0; + Ticker._inited = false; + }; + + /** + * Returns the average time spent within a tick. This can vary significantly from the value provided by getMeasuredFPS + * because it only measures the time spent within the tick execution stack. + * + * Example 1: With a target FPS of 20, getMeasuredFPS() returns 20fps, which indicates an average of 50ms between + * the end of one tick and the end of the next. However, getMeasuredTickTime() returns 15ms. This indicates that + * there may be up to 35ms of "idle" time between the end of one tick and the start of the next. + * + * Example 2: With a target FPS of 30, getFPS() returns 10fps, which indicates an average of 100ms between the end of + * one tick and the end of the next. However, getMeasuredTickTime() returns 20ms. This would indicate that something + * other than the tick is using ~80ms (another script, DOM rendering, etc). + * @method getMeasuredTickTime + * @static + * @param {Number} [ticks] The number of previous ticks over which to measure the average time spent in a tick. + * Defaults to the number of ticks per second. To get only the last tick's time, pass in 1. + * @return {Number} The average time spent in a tick in milliseconds. + **/ + Ticker.getMeasuredTickTime = function (ticks) { + var ttl = 0, + times = Ticker._tickTimes; + if (!times || times.length < 1) { + return -1; + } + + // by default, calculate average for the past ~1 second: + ticks = Math.min(times.length, ticks || Ticker.getFPS() | 0); + for (var i = 0; i < ticks; i++) { + ttl += times[i]; + } + return ttl / ticks; + }; + + /** + * Returns the actual frames / ticks per second. + * @method getMeasuredFPS + * @static + * @param {Number} [ticks] The number of previous ticks over which to measure the actual frames / ticks per second. + * Defaults to the number of ticks per second. + * @return {Number} The actual frames / ticks per second. Depending on performance, this may differ + * from the target frames per second. + **/ + Ticker.getMeasuredFPS = function (ticks) { + var times = Ticker._times; + if (!times || times.length < 2) { + return -1; + } + + // by default, calculate fps for the past ~1 second: + ticks = Math.min(times.length - 1, ticks || Ticker.getFPS() | 0); + return 1000 / ((times[0] - times[ticks]) / ticks); + }; + + /** + * Use the {{#crossLink "Ticker/paused:property"}}{{/crossLink}} property instead. + * @method setPaused + * @static + * @param {Boolean} value + * @deprecated + **/ + Ticker.setPaused = function (value) { + // TODO: deprecated. + Ticker.paused = value; + }; + + /** + * Use the {{#crossLink "Ticker/paused:property"}}{{/crossLink}} property instead. + * @method getPaused + * @static + * @return {Boolean} + * @deprecated + **/ + Ticker.getPaused = function () { + // TODO: deprecated. + return Ticker.paused; + }; + + /** + * Returns the number of milliseconds that have elapsed since Ticker was initialized via {{#crossLink "Ticker/init"}}. + * Returns -1 if Ticker has not been initialized. For example, you could use + * this in a time synchronized animation to determine the exact amount of time that has elapsed. + * @method getTime + * @static + * @param {Boolean} [runTime=false] If true only time elapsed while Ticker was not paused will be returned. + * If false, the value returned will be total time elapsed since the first tick event listener was added. + * @return {Number} Number of milliseconds that have elapsed since Ticker was initialized or -1. + **/ + Ticker.getTime = function (runTime) { + return Ticker._startTime ? Ticker._getTime() - (runTime ? Ticker._pausedTime : 0) : -1; + }; + + /** + * Similar to the {{#crossLink "Ticker/getTime"}}{{/crossLink}} method, but returns the time on the most recent {{#crossLink "Ticker/tick:event"}}{{/crossLink}} + * event object. + * @method getEventTime + * @static + * @param runTime {Boolean} [runTime=false] If true, the runTime property will be returned instead of time. + * @returns {number} The time or runTime property from the most recent tick event or -1. + */ + Ticker.getEventTime = function (runTime) { + return Ticker._startTime ? (Ticker._lastTime || Ticker._startTime) - (runTime ? Ticker._pausedTime : 0) : -1; + }; + + /** + * Returns the number of ticks that have been broadcast by Ticker. + * @method getTicks + * @static + * @param {Boolean} pauseable Indicates whether to include ticks that would have been broadcast + * while Ticker was paused. If true only tick events broadcast while Ticker is not paused will be returned. + * If false, tick events that would have been broadcast while Ticker was paused will be included in the return + * value. The default value is false. + * @return {Number} of ticks that have been broadcast. + **/ + Ticker.getTicks = function (pauseable) { + return Ticker._ticks - (pauseable ? Ticker._pausedTicks : 0); + }; + + // private static methods: + /** + * @method _handleSynch + * @static + * @protected + **/ + Ticker._handleSynch = function () { + Ticker._timerId = null; + Ticker._setupTick(); + + // run if enough time has elapsed, with a little bit of flexibility to be early: + if (Ticker._getTime() - Ticker._lastTime >= (Ticker._interval - 1) * 0.97) { + Ticker._tick(); + } + }; + + /** + * @method _handleRAF + * @static + * @protected + **/ + Ticker._handleRAF = function () { + Ticker._timerId = null; + Ticker._setupTick(); + Ticker._tick(); + }; + + /** + * @method _handleTimeout + * @static + * @protected + **/ + Ticker._handleTimeout = function () { + Ticker._timerId = null; + Ticker._setupTick(); + Ticker._tick(); + }; + + /** + * @method _setupTick + * @static + * @protected + **/ + Ticker._setupTick = function () { + if (Ticker._timerId != null) { + return; + } // avoid duplicates + + var mode = Ticker.timingMode || Ticker.useRAF && Ticker.RAF_SYNCHED; + if (mode == Ticker.RAF_SYNCHED || mode == Ticker.RAF) { + var f = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame; + if (f) { + Ticker._timerId = f(mode == Ticker.RAF ? Ticker._handleRAF : Ticker._handleSynch); + Ticker._raf = true; + return; + } + } + Ticker._raf = false; + Ticker._timerId = setTimeout(Ticker._handleTimeout, Ticker._interval); + }; + + /** + * @method _tick + * @static + * @protected + **/ + Ticker._tick = function () { + var paused = Ticker.paused; + var time = Ticker._getTime(); + var elapsedTime = time - Ticker._lastTime; + Ticker._lastTime = time; + Ticker._ticks++; + + if (paused) { + Ticker._pausedTicks++; + Ticker._pausedTime += elapsedTime; + } + + if (Ticker.hasEventListener("tick")) { + var event = new createjs.Event("tick"); + var maxDelta = Ticker.maxDelta; + event.delta = maxDelta && elapsedTime > maxDelta ? maxDelta : elapsedTime; + event.paused = paused; + event.time = time; + event.runTime = time - Ticker._pausedTime; + Ticker.dispatchEvent(event); + } + + Ticker._tickTimes.unshift(Ticker._getTime() - time); + while (Ticker._tickTimes.length > 100) { + Ticker._tickTimes.pop(); + } + + Ticker._times.unshift(time); + while (Ticker._times.length > 100) { + Ticker._times.pop(); + } + }; + + /** + * @method _getTime + * @static + * @protected + **/ + var now = window.performance && (performance.now || performance.mozNow || performance.msNow || performance.oNow || performance.webkitNow); + Ticker._getTime = function () { + return (now && now.call(performance) || new Date().getTime()) - Ticker._startTime; + }; + + createjs.Ticker = Ticker; + })(); + + //############################################################################## + // UID.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Global utility for generating sequential unique ID numbers. The UID class uses a static interface (ex. UID.get()) + * and should not be instantiated. + * @class UID + * @static + **/ + + function UID() { + throw "UID cannot be instantiated"; + } + + // private static properties: + /** + * @property _nextID + * @type Number + * @protected + **/ + UID._nextID = 0; + + // public static methods: + /** + * Returns the next unique id. + * @method get + * @return {Number} The next unique id + * @static + **/ + UID.get = function () { + return UID._nextID++; + }; + + createjs.UID = UID; + })(); + + //############################################################################## + // MouseEvent.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Passed as the parameter to all mouse/pointer/touch related events. For a listing of mouse events and their properties, + * see the {{#crossLink "DisplayObject"}}{{/crossLink}} and {{#crossLink "Stage"}}{{/crossLink}} event listings. + * @class MouseEvent + * @param {String} type The event type. + * @param {Boolean} bubbles Indicates whether the event will bubble through the display list. + * @param {Boolean} cancelable Indicates whether the default behaviour of this event can be cancelled. + * @param {Number} stageX The normalized x position relative to the stage. + * @param {Number} stageY The normalized y position relative to the stage. + * @param {MouseEvent} nativeEvent The native DOM event related to this mouse event. + * @param {Number} pointerID The unique id for the pointer. + * @param {Boolean} primary Indicates whether this is the primary pointer in a multitouch environment. + * @param {Number} rawX The raw x position relative to the stage. + * @param {Number} rawY The raw y position relative to the stage. + * @param {DisplayObject} relatedTarget The secondary target for the event. + * @extends Event + * @constructor + **/ + + function MouseEvent(type, bubbles, cancelable, stageX, stageY, nativeEvent, pointerID, primary, rawX, rawY, relatedTarget) { + this.Event_constructor(type, bubbles, cancelable); + + // public properties: + /** + * The normalized x position on the stage. This will always be within the range 0 to stage width. + * @property stageX + * @type Number + */ + this.stageX = stageX; + + /** + * The normalized y position on the stage. This will always be within the range 0 to stage height. + * @property stageY + * @type Number + **/ + this.stageY = stageY; + + /** + * The raw x position relative to the stage. Normally this will be the same as the stageX value, unless + * stage.mouseMoveOutside is true and the pointer is outside of the stage bounds. + * @property rawX + * @type Number + */ + this.rawX = rawX == null ? stageX : rawX; + + /** + * The raw y position relative to the stage. Normally this will be the same as the stageY value, unless + * stage.mouseMoveOutside is true and the pointer is outside of the stage bounds. + * @property rawY + * @type Number + */ + this.rawY = rawY == null ? stageY : rawY; + + /** + * The native MouseEvent generated by the browser. The properties and API for this + * event may differ between browsers. This property will be null if the + * EaselJS property was not directly generated from a native MouseEvent. + * @property nativeEvent + * @type HtmlMouseEvent + * @default null + **/ + this.nativeEvent = nativeEvent; + + /** + * The unique id for the pointer (touch point or cursor). This will be either -1 for the mouse, or the system + * supplied id value. + * @property pointerID + * @type {Number} + */ + this.pointerID = pointerID; + + /** + * Indicates whether this is the primary pointer in a multitouch environment. This will always be true for the mouse. + * For touch pointers, the first pointer in the current stack will be considered the primary pointer. + * @property primary + * @type {Boolean} + */ + this.primary = !!primary; + + /** + * The secondary target for the event, if applicable. This is used for mouseout/rollout + * events to indicate the object that the mouse entered from, mouseover/rollover for the object the mouse exited, + * and stagemousedown/stagemouseup events for the object that was the under the cursor, if any. + * + * Only valid interaction targets will be returned (ie. objects with mouse listeners or a cursor set). + * @property relatedTarget + * @type {DisplayObject} + */ + this.relatedTarget = relatedTarget; + } + var p = createjs.extend(MouseEvent, createjs.Event); + + // TODO: deprecated + // p.initialize = function() {}; // searchable for devs wondering where it is. REMOVED. See docs for details. + + + // getter / setters: + /** + * Returns the x position of the mouse in the local coordinate system of the current target (ie. the dispatcher). + * @property localX + * @type {Number} + * @readonly + */ + p._get_localX = function () { + return this.currentTarget.globalToLocal(this.rawX, this.rawY).x; + }; + + /** + * Returns the y position of the mouse in the local coordinate system of the current target (ie. the dispatcher). + * @property localY + * @type {Number} + * @readonly + */ + p._get_localY = function () { + return this.currentTarget.globalToLocal(this.rawX, this.rawY).y; + }; + + /** + * Indicates whether the event was generated by a touch input (versus a mouse input). + * @property isTouch + * @type {Boolean} + * @readonly + */ + p._get_isTouch = function () { + return this.pointerID !== -1; + }; + + try { + Object.defineProperties(p, { + localX: { get: p._get_localX }, + localY: { get: p._get_localY }, + isTouch: { get: p._get_isTouch } + }); + } catch (e) {} // TODO: use Log + + + // public methods: + /** + * Returns a clone of the MouseEvent instance. + * @method clone + * @return {MouseEvent} a clone of the MouseEvent instance. + **/ + p.clone = function () { + return new MouseEvent(this.type, this.bubbles, this.cancelable, this.stageX, this.stageY, this.nativeEvent, this.pointerID, this.primary, this.rawX, this.rawY); + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[MouseEvent (type=" + this.type + " stageX=" + this.stageX + " stageY=" + this.stageY + ")]"; + }; + + createjs.MouseEvent = createjs.promote(MouseEvent, "Event"); + })(); + + //############################################################################## + // Matrix2D.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Represents an affine transformation matrix, and provides tools for constructing and concatenating matrices. + * + * This matrix can be visualized as: + * + * [ a c tx + * b d ty + * 0 0 1 ] + * + * Note the locations of b and c. + * + * @class Matrix2D + * @param {Number} [a=1] Specifies the a property for the new matrix. + * @param {Number} [b=0] Specifies the b property for the new matrix. + * @param {Number} [c=0] Specifies the c property for the new matrix. + * @param {Number} [d=1] Specifies the d property for the new matrix. + * @param {Number} [tx=0] Specifies the tx property for the new matrix. + * @param {Number} [ty=0] Specifies the ty property for the new matrix. + * @constructor + **/ + + function Matrix2D(a, b, c, d, tx, ty) { + this.setValues(a, b, c, d, tx, ty); + + // public properties: + // assigned in the setValues method. + /** + * Position (0, 0) in a 3x3 affine transformation matrix. + * @property a + * @type Number + **/ + + /** + * Position (0, 1) in a 3x3 affine transformation matrix. + * @property b + * @type Number + **/ + + /** + * Position (1, 0) in a 3x3 affine transformation matrix. + * @property c + * @type Number + **/ + + /** + * Position (1, 1) in a 3x3 affine transformation matrix. + * @property d + * @type Number + **/ + + /** + * Position (2, 0) in a 3x3 affine transformation matrix. + * @property tx + * @type Number + **/ + + /** + * Position (2, 1) in a 3x3 affine transformation matrix. + * @property ty + * @type Number + **/ + } + var p = Matrix2D.prototype; + + /** + * REMOVED. Removed in favor of using `MySuperClass_constructor`. + * See {{#crossLink "Utility Methods/extend"}}{{/crossLink}} and {{#crossLink "Utility Methods/promote"}}{{/crossLink}} + * for details. + * + * There is an inheritance tutorial distributed with EaselJS in /tutorials/Inheritance. + * + * @method initialize + * @protected + * @deprecated + */ + // p.initialize = function() {}; // searchable for devs wondering where it is. + + + // constants: + /** + * Multiplier for converting degrees to radians. Used internally by Matrix2D. + * @property DEG_TO_RAD + * @static + * @final + * @type Number + * @readonly + **/ + Matrix2D.DEG_TO_RAD = Math.PI / 180; + + // static public properties: + /** + * An identity matrix, representing a null transformation. + * @property identity + * @static + * @type Matrix2D + * @readonly + **/ + Matrix2D.identity = null; // set at bottom of class definition. + + + // public methods: + /** + * Sets the specified values on this instance. + * @method setValues + * @param {Number} [a=1] Specifies the a property for the new matrix. + * @param {Number} [b=0] Specifies the b property for the new matrix. + * @param {Number} [c=0] Specifies the c property for the new matrix. + * @param {Number} [d=1] Specifies the d property for the new matrix. + * @param {Number} [tx=0] Specifies the tx property for the new matrix. + * @param {Number} [ty=0] Specifies the ty property for the new matrix. + * @return {Matrix2D} This instance. Useful for chaining method calls. + */ + p.setValues = function (a, b, c, d, tx, ty) { + // don't forget to update docs in the constructor if these change: + this.a = a == null ? 1 : a; + this.b = b || 0; + this.c = c || 0; + this.d = d == null ? 1 : d; + this.tx = tx || 0; + this.ty = ty || 0; + return this; + }; + + /** + * Appends the specified matrix properties to this matrix. All parameters are required. + * This is the equivalent of multiplying `(this matrix) * (specified matrix)`. + * @method append + * @param {Number} a + * @param {Number} b + * @param {Number} c + * @param {Number} d + * @param {Number} tx + * @param {Number} ty + * @return {Matrix2D} This matrix. Useful for chaining method calls. + **/ + p.append = function (a, b, c, d, tx, ty) { + var a1 = this.a; + var b1 = this.b; + var c1 = this.c; + var d1 = this.d; + if (a != 1 || b != 0 || c != 0 || d != 1) { + this.a = a1 * a + c1 * b; + this.b = b1 * a + d1 * b; + this.c = a1 * c + c1 * d; + this.d = b1 * c + d1 * d; + } + this.tx = a1 * tx + c1 * ty + this.tx; + this.ty = b1 * tx + d1 * ty + this.ty; + return this; + }; + + /** + * Prepends the specified matrix properties to this matrix. + * This is the equivalent of multiplying `(specified matrix) * (this matrix)`. + * All parameters are required. + * @method prepend + * @param {Number} a + * @param {Number} b + * @param {Number} c + * @param {Number} d + * @param {Number} tx + * @param {Number} ty + * @return {Matrix2D} This matrix. Useful for chaining method calls. + **/ + p.prepend = function (a, b, c, d, tx, ty) { + var a1 = this.a; + var c1 = this.c; + var tx1 = this.tx; + + this.a = a * a1 + c * this.b; + this.b = b * a1 + d * this.b; + this.c = a * c1 + c * this.d; + this.d = b * c1 + d * this.d; + this.tx = a * tx1 + c * this.ty + tx; + this.ty = b * tx1 + d * this.ty + ty; + return this; + }; + + /** + * Appends the specified matrix to this matrix. + * This is the equivalent of multiplying `(this matrix) * (specified matrix)`. + * @method appendMatrix + * @param {Matrix2D} matrix + * @return {Matrix2D} This matrix. Useful for chaining method calls. + **/ + p.appendMatrix = function (matrix) { + return this.append(matrix.a, matrix.b, matrix.c, matrix.d, matrix.tx, matrix.ty); + }; + + /** + * Prepends the specified matrix to this matrix. + * This is the equivalent of multiplying `(specified matrix) * (this matrix)`. + * For example, you could calculate the combined transformation for a child object using: + * + * var o = myDisplayObject; + * var mtx = o.getMatrix(); + * while (o = o.parent) { + * // prepend each parent's transformation in turn: + * o.prependMatrix(o.getMatrix()); + * } + * @method prependMatrix + * @param {Matrix2D} matrix + * @return {Matrix2D} This matrix. Useful for chaining method calls. + **/ + p.prependMatrix = function (matrix) { + return this.prepend(matrix.a, matrix.b, matrix.c, matrix.d, matrix.tx, matrix.ty); + }; + + /** + * Generates matrix properties from the specified display object transform properties, and appends them to this matrix. + * For example, you can use this to generate a matrix representing the transformations of a display object: + * + * var mtx = new createjs.Matrix2D(); + * mtx.appendTransform(o.x, o.y, o.scaleX, o.scaleY, o.rotation); + * @method appendTransform + * @param {Number} x + * @param {Number} y + * @param {Number} scaleX + * @param {Number} scaleY + * @param {Number} rotation + * @param {Number} skewX + * @param {Number} skewY + * @param {Number} regX Optional. + * @param {Number} regY Optional. + * @return {Matrix2D} This matrix. Useful for chaining method calls. + **/ + p.appendTransform = function (x, y, scaleX, scaleY, rotation, skewX, skewY, regX, regY) { + if (rotation % 360) { + var r = rotation * Matrix2D.DEG_TO_RAD; + var cos = Math.cos(r); + var sin = Math.sin(r); + } else { + cos = 1; + sin = 0; + } + + if (skewX || skewY) { + // TODO: can this be combined into a single append operation? + skewX *= Matrix2D.DEG_TO_RAD; + skewY *= Matrix2D.DEG_TO_RAD; + this.append(Math.cos(skewY), Math.sin(skewY), -Math.sin(skewX), Math.cos(skewX), x, y); + this.append(cos * scaleX, sin * scaleX, -sin * scaleY, cos * scaleY, 0, 0); + } else { + this.append(cos * scaleX, sin * scaleX, -sin * scaleY, cos * scaleY, x, y); + } + + if (regX || regY) { + // append the registration offset: + this.tx -= regX * this.a + regY * this.c; + this.ty -= regX * this.b + regY * this.d; + } + return this; + }; + + /** + * Generates matrix properties from the specified display object transform properties, and prepends them to this matrix. + * For example, you could calculate the combined transformation for a child object using: + * + * var o = myDisplayObject; + * var mtx = new createjs.Matrix2D(); + * do { + * // prepend each parent's transformation in turn: + * mtx.prependTransform(o.x, o.y, o.scaleX, o.scaleY, o.rotation, o.skewX, o.skewY, o.regX, o.regY); + * } while (o = o.parent); + * + * Note that the above example would not account for {{#crossLink "DisplayObject/transformMatrix:property"}}{{/crossLink}} + * values. See {{#crossLink "Matrix2D/prependMatrix"}}{{/crossLink}} for an example that does. + * @method prependTransform + * @param {Number} x + * @param {Number} y + * @param {Number} scaleX + * @param {Number} scaleY + * @param {Number} rotation + * @param {Number} skewX + * @param {Number} skewY + * @param {Number} regX Optional. + * @param {Number} regY Optional. + * @return {Matrix2D} This matrix. Useful for chaining method calls. + **/ + p.prependTransform = function (x, y, scaleX, scaleY, rotation, skewX, skewY, regX, regY) { + if (rotation % 360) { + var r = rotation * Matrix2D.DEG_TO_RAD; + var cos = Math.cos(r); + var sin = Math.sin(r); + } else { + cos = 1; + sin = 0; + } + + if (regX || regY) { + // prepend the registration offset: + this.tx -= regX;this.ty -= regY; + } + if (skewX || skewY) { + // TODO: can this be combined into a single prepend operation? + skewX *= Matrix2D.DEG_TO_RAD; + skewY *= Matrix2D.DEG_TO_RAD; + this.prepend(cos * scaleX, sin * scaleX, -sin * scaleY, cos * scaleY, 0, 0); + this.prepend(Math.cos(skewY), Math.sin(skewY), -Math.sin(skewX), Math.cos(skewX), x, y); + } else { + this.prepend(cos * scaleX, sin * scaleX, -sin * scaleY, cos * scaleY, x, y); + } + return this; + }; + + /** + * Applies a clockwise rotation transformation to the matrix. + * @method rotate + * @param {Number} angle The angle to rotate by, in degrees. To use a value in radians, multiply it by `180/Math.PI`. + * @return {Matrix2D} This matrix. Useful for chaining method calls. + **/ + p.rotate = function (angle) { + angle = angle * Matrix2D.DEG_TO_RAD; + var cos = Math.cos(angle); + var sin = Math.sin(angle); + + var a1 = this.a; + var b1 = this.b; + + this.a = a1 * cos + this.c * sin; + this.b = b1 * cos + this.d * sin; + this.c = -a1 * sin + this.c * cos; + this.d = -b1 * sin + this.d * cos; + return this; + }; + + /** + * Applies a skew transformation to the matrix. + * @method skew + * @param {Number} skewX The amount to skew horizontally in degrees. To use a value in radians, multiply it by `180/Math.PI`. + * @param {Number} skewY The amount to skew vertically in degrees. + * @return {Matrix2D} This matrix. Useful for chaining method calls. + */ + p.skew = function (skewX, skewY) { + skewX = skewX * Matrix2D.DEG_TO_RAD; + skewY = skewY * Matrix2D.DEG_TO_RAD; + this.append(Math.cos(skewY), Math.sin(skewY), -Math.sin(skewX), Math.cos(skewX), 0, 0); + return this; + }; + + /** + * Applies a scale transformation to the matrix. + * @method scale + * @param {Number} x The amount to scale horizontally. E.G. a value of 2 will double the size in the X direction, and 0.5 will halve it. + * @param {Number} y The amount to scale vertically. + * @return {Matrix2D} This matrix. Useful for chaining method calls. + **/ + p.scale = function (x, y) { + this.a *= x; + this.b *= x; + this.c *= y; + this.d *= y; + //this.tx *= x; + //this.ty *= y; + return this; + }; + + /** + * Translates the matrix on the x and y axes. + * @method translate + * @param {Number} x + * @param {Number} y + * @return {Matrix2D} This matrix. Useful for chaining method calls. + **/ + p.translate = function (x, y) { + this.tx += this.a * x + this.c * y; + this.ty += this.b * x + this.d * y; + return this; + }; + + /** + * Sets the properties of the matrix to those of an identity matrix (one that applies a null transformation). + * @method identity + * @return {Matrix2D} This matrix. Useful for chaining method calls. + **/ + p.identity = function () { + this.a = this.d = 1; + this.b = this.c = this.tx = this.ty = 0; + return this; + }; + + /** + * Inverts the matrix, causing it to perform the opposite transformation. + * @method invert + * @return {Matrix2D} This matrix. Useful for chaining method calls. + **/ + p.invert = function () { + var a1 = this.a; + var b1 = this.b; + var c1 = this.c; + var d1 = this.d; + var tx1 = this.tx; + var n = a1 * d1 - b1 * c1; + + this.a = d1 / n; + this.b = -b1 / n; + this.c = -c1 / n; + this.d = a1 / n; + this.tx = (c1 * this.ty - d1 * tx1) / n; + this.ty = -(a1 * this.ty - b1 * tx1) / n; + return this; + }; + + /** + * Returns true if the matrix is an identity matrix. + * @method isIdentity + * @return {Boolean} + **/ + p.isIdentity = function () { + return this.tx === 0 && this.ty === 0 && this.a === 1 && this.b === 0 && this.c === 0 && this.d === 1; + }; + + /** + * Returns true if this matrix is equal to the specified matrix (all property values are equal). + * @method equals + * @param {Matrix2D} matrix The matrix to compare. + * @return {Boolean} + **/ + p.equals = function (matrix) { + return this.tx === matrix.tx && this.ty === matrix.ty && this.a === matrix.a && this.b === matrix.b && this.c === matrix.c && this.d === matrix.d; + }; + + /** + * Transforms a point according to this matrix. + * @method transformPoint + * @param {Number} x The x component of the point to transform. + * @param {Number} y The y component of the point to transform. + * @param {Point | Object} [pt] An object to copy the result into. If omitted a generic object with x/y properties will be returned. + * @return {Point} This matrix. Useful for chaining method calls. + **/ + p.transformPoint = function (x, y, pt) { + pt = pt || {}; + pt.x = x * this.a + y * this.c + this.tx; + pt.y = x * this.b + y * this.d + this.ty; + return pt; + }; + + /** + * Decomposes the matrix into transform properties (x, y, scaleX, scaleY, and rotation). Note that these values + * may not match the transform properties you used to generate the matrix, though they will produce the same visual + * results. + * @method decompose + * @param {Object} target The object to apply the transform properties to. If null, then a new object will be returned. + * @return {Object} The target, or a new generic object with the transform properties applied. + */ + p.decompose = function (target) { + // TODO: it would be nice to be able to solve for whether the matrix can be decomposed into only scale/rotation even when scale is negative + if (target == null) { + target = {}; + } + target.x = this.tx; + target.y = this.ty; + target.scaleX = Math.sqrt(this.a * this.a + this.b * this.b); + target.scaleY = Math.sqrt(this.c * this.c + this.d * this.d); + + var skewX = Math.atan2(-this.c, this.d); + var skewY = Math.atan2(this.b, this.a); + + var delta = Math.abs(1 - skewX / skewY); + if (delta < 0.00001) { + // effectively identical, can use rotation: + target.rotation = skewY / Matrix2D.DEG_TO_RAD; + if (this.a < 0 && this.d >= 0) { + target.rotation += target.rotation <= 0 ? 180 : -180; + } + target.skewX = target.skewY = 0; + } else { + target.skewX = skewX / Matrix2D.DEG_TO_RAD; + target.skewY = skewY / Matrix2D.DEG_TO_RAD; + } + return target; + }; + + /** + * Copies all properties from the specified matrix to this matrix. + * @method copy + * @param {Matrix2D} matrix The matrix to copy properties from. + * @return {Matrix2D} This matrix. Useful for chaining method calls. + */ + p.copy = function (matrix) { + return this.setValues(matrix.a, matrix.b, matrix.c, matrix.d, matrix.tx, matrix.ty); + }; + + /** + * Returns a clone of the Matrix2D instance. + * @method clone + * @return {Matrix2D} a clone of the Matrix2D instance. + **/ + p.clone = function () { + return new Matrix2D(this.a, this.b, this.c, this.d, this.tx, this.ty); + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[Matrix2D (a=" + this.a + " b=" + this.b + " c=" + this.c + " d=" + this.d + " tx=" + this.tx + " ty=" + this.ty + ")]"; + }; + + // this has to be populated after the class is defined: + Matrix2D.identity = new Matrix2D(); + + createjs.Matrix2D = Matrix2D; + })(); + + //############################################################################## + // DisplayProps.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + /** + * Used for calculating and encapsulating display related properties. + * @class DisplayProps + * @param {Number} [visible=true] Visible value. + * @param {Number} [alpha=1] Alpha value. + * @param {Number} [shadow=null] A Shadow instance or null. + * @param {Number} [compositeOperation=null] A compositeOperation value or null. + * @param {Number} [matrix] A transformation matrix. Defaults to a new identity matrix. + * @constructor + **/ + + function DisplayProps(visible, alpha, shadow, compositeOperation, matrix) { + this.setValues(visible, alpha, shadow, compositeOperation, matrix); + + // public properties: + // assigned in the setValues method. + /** + * Property representing the alpha that will be applied to a display object. + * @property alpha + * @type Number + **/ + + /** + * Property representing the shadow that will be applied to a display object. + * @property shadow + * @type Shadow + **/ + + /** + * Property representing the compositeOperation that will be applied to a display object. + * You can find a list of valid composite operations at: + * https://developer.mozilla.org/en/Canvas_tutorial/Compositing + * @property compositeOperation + * @type String + **/ + + /** + * Property representing the value for visible that will be applied to a display object. + * @property visible + * @type Boolean + **/ + + /** + * The transformation matrix that will be applied to a display object. + * @property matrix + * @type Matrix2D + **/ + } + var p = DisplayProps.prototype; + + // initialization: + /** + * Reinitializes the instance with the specified values. + * @method setValues + * @param {Number} [visible=true] Visible value. + * @param {Number} [alpha=1] Alpha value. + * @param {Number} [shadow=null] A Shadow instance or null. + * @param {Number} [compositeOperation=null] A compositeOperation value or null. + * @param {Number} [matrix] A transformation matrix. Defaults to an identity matrix. + * @return {DisplayProps} This instance. Useful for chaining method calls. + * @chainable + */ + p.setValues = function (visible, alpha, shadow, compositeOperation, matrix) { + this.visible = visible == null ? true : !!visible; + this.alpha = alpha == null ? 1 : alpha; + this.shadow = shadow; + this.compositeOperation = compositeOperation; + this.matrix = matrix || this.matrix && this.matrix.identity() || new createjs.Matrix2D(); + return this; + }; + + // public methods: + /** + * Appends the specified display properties. This is generally used to apply a child's properties its parent's. + * @method append + * @param {Boolean} visible desired visible value + * @param {Number} alpha desired alpha value + * @param {Shadow} shadow desired shadow value + * @param {String} compositeOperation desired composite operation value + * @param {Matrix2D} [matrix] a Matrix2D instance + * @return {DisplayProps} This instance. Useful for chaining method calls. + * @chainable + */ + p.append = function (visible, alpha, shadow, compositeOperation, matrix) { + this.alpha *= alpha; + this.shadow = shadow || this.shadow; + this.compositeOperation = compositeOperation || this.compositeOperation; + this.visible = this.visible && visible; + matrix && this.matrix.appendMatrix(matrix); + return this; + }; + + /** + * Prepends the specified display properties. This is generally used to apply a parent's properties to a child's. + * For example, to get the combined display properties that would be applied to a child, you could use: + * + * var o = myDisplayObject; + * var props = new createjs.DisplayProps(); + * do { + * // prepend each parent's props in turn: + * props.prepend(o.visible, o.alpha, o.shadow, o.compositeOperation, o.getMatrix()); + * } while (o = o.parent); + * + * @method prepend + * @param {Boolean} visible desired visible value + * @param {Number} alpha desired alpha value + * @param {Shadow} shadow desired shadow value + * @param {String} compositeOperation desired composite operation value + * @param {Matrix2D} [matrix] a Matrix2D instance + * @return {DisplayProps} This instance. Useful for chaining method calls. + * @chainable + */ + p.prepend = function (visible, alpha, shadow, compositeOperation, matrix) { + this.alpha *= alpha; + this.shadow = this.shadow || shadow; + this.compositeOperation = this.compositeOperation || compositeOperation; + this.visible = this.visible && visible; + matrix && this.matrix.prependMatrix(matrix); + return this; + }; + + /** + * Resets this instance and its matrix to default values. + * @method identity + * @return {DisplayProps} This instance. Useful for chaining method calls. + * @chainable + */ + p.identity = function () { + this.visible = true; + this.alpha = 1; + this.shadow = this.compositeOperation = null; + this.matrix.identity(); + return this; + }; + + /** + * Returns a clone of the DisplayProps instance. Clones the associated matrix. + * @method clone + * @return {DisplayProps} a clone of the DisplayProps instance. + **/ + p.clone = function () { + return new DisplayProps(this.alpha, this.shadow, this.compositeOperation, this.visible, this.matrix.clone()); + }; + + // private methods: + + createjs.DisplayProps = DisplayProps; + })(); + + //############################################################################## + // Point.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Represents a point on a 2 dimensional x / y coordinate system. + * + *

Example

+ * + * var point = new createjs.Point(0, 100); + * + * @class Point + * @param {Number} [x=0] X position. + * @param {Number} [y=0] Y position. + * @constructor + **/ + + function Point(x, y) { + this.setValues(x, y); + + // public properties: + // assigned in the setValues method. + /** + * X position. + * @property x + * @type Number + **/ + + /** + * Y position. + * @property y + * @type Number + **/ + } + var p = Point.prototype; + + /** + * REMOVED. Removed in favor of using `MySuperClass_constructor`. + * See {{#crossLink "Utility Methods/extend"}}{{/crossLink}} and {{#crossLink "Utility Methods/promote"}}{{/crossLink}} + * for details. + * + * There is an inheritance tutorial distributed with EaselJS in /tutorials/Inheritance. + * + * @method initialize + * @protected + * @deprecated + */ + // p.initialize = function() {}; // searchable for devs wondering where it is. + + + // public methods: + /** + * Sets the specified values on this instance. + * @method setValues + * @param {Number} [x=0] X position. + * @param {Number} [y=0] Y position. + * @return {Point} This instance. Useful for chaining method calls. + * @chainable + */ + p.setValues = function (x, y) { + this.x = x || 0; + this.y = y || 0; + return this; + }; + + /** + * Copies all properties from the specified point to this point. + * @method copy + * @param {Point} point The point to copy properties from. + * @return {Point} This point. Useful for chaining method calls. + * @chainable + */ + p.copy = function (point) { + this.x = point.x; + this.y = point.y; + return this; + }; + + /** + * Returns a clone of the Point instance. + * @method clone + * @return {Point} a clone of the Point instance. + **/ + p.clone = function () { + return new Point(this.x, this.y); + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[Point (x=" + this.x + " y=" + this.y + ")]"; + }; + + createjs.Point = Point; + })(); + + //############################################################################## + // Rectangle.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Represents a rectangle as defined by the points (x, y) and (x+width, y+height). + * + *

Example

+ * + * var rect = new createjs.Rectangle(0, 0, 100, 100); + * + * @class Rectangle + * @param {Number} [x=0] X position. + * @param {Number} [y=0] Y position. + * @param {Number} [width=0] The width of the Rectangle. + * @param {Number} [height=0] The height of the Rectangle. + * @constructor + **/ + + function Rectangle(x, y, width, height) { + this.setValues(x, y, width, height); + + // public properties: + // assigned in the setValues method. + /** + * X position. + * @property x + * @type Number + **/ + + /** + * Y position. + * @property y + * @type Number + **/ + + /** + * Width. + * @property width + * @type Number + **/ + + /** + * Height. + * @property height + * @type Number + **/ + } + var p = Rectangle.prototype; + + /** + * REMOVED. Removed in favor of using `MySuperClass_constructor`. + * See {{#crossLink "Utility Methods/extend"}}{{/crossLink}} and {{#crossLink "Utility Methods/promote"}}{{/crossLink}} + * for details. + * + * There is an inheritance tutorial distributed with EaselJS in /tutorials/Inheritance. + * + * @method initialize + * @protected + * @deprecated + */ + // p.initialize = function() {}; // searchable for devs wondering where it is. + + + // public methods: + /** + * Sets the specified values on this instance. + * @method setValues + * @param {Number} [x=0] X position. + * @param {Number} [y=0] Y position. + * @param {Number} [width=0] The width of the Rectangle. + * @param {Number} [height=0] The height of the Rectangle. + * @return {Rectangle} This instance. Useful for chaining method calls. + * @chainable + */ + p.setValues = function (x, y, width, height) { + // don't forget to update docs in the constructor if these change: + this.x = x || 0; + this.y = y || 0; + this.width = width || 0; + this.height = height || 0; + return this; + }; + + /** + * Extends the rectangle's bounds to include the described point or rectangle. + * @method extend + * @param {Number} x X position of the point or rectangle. + * @param {Number} y Y position of the point or rectangle. + * @param {Number} [width=0] The width of the rectangle. + * @param {Number} [height=0] The height of the rectangle. + * @return {Rectangle} This instance. Useful for chaining method calls. + * @chainable + */ + p.extend = function (x, y, width, height) { + width = width || 0; + height = height || 0; + if (x + width > this.x + this.width) { + this.width = x + width - this.x; + } + if (y + height > this.y + this.height) { + this.height = y + height - this.y; + } + if (x < this.x) { + this.width += this.x - x;this.x = x; + } + if (y < this.y) { + this.height += this.y - y;this.y = y; + } + return this; + }; + + /** + * Adds the specified padding to the rectangle's bounds. + * @method pad + * @param {Number} top + * @param {Number} left + * @param {Number} right + * @param {Number} bottom + * @return {Rectangle} This instance. Useful for chaining method calls. + * @chainable + */ + p.pad = function (top, left, bottom, right) { + this.x -= left; + this.y -= top; + this.width += left + right; + this.height += top + bottom; + return this; + }; + + /** + * Copies all properties from the specified rectangle to this rectangle. + * @method copy + * @param {Rectangle} rectangle The rectangle to copy properties from. + * @return {Rectangle} This rectangle. Useful for chaining method calls. + * @chainable + */ + p.copy = function (rectangle) { + return this.setValues(rectangle.x, rectangle.y, rectangle.width, rectangle.height); + }; + + /** + * Returns true if this rectangle fully encloses the described point or rectangle. + * @method contains + * @param {Number} x X position of the point or rectangle. + * @param {Number} y Y position of the point or rectangle. + * @param {Number} [width=0] The width of the rectangle. + * @param {Number} [height=0] The height of the rectangle. + * @return {Boolean} True if the described point or rectangle is contained within this rectangle. + */ + p.contains = function (x, y, width, height) { + width = width || 0; + height = height || 0; + return x >= this.x && x + width <= this.x + this.width && y >= this.y && y + height <= this.y + this.height; + }; + + /** + * Returns a new rectangle which contains this rectangle and the specified rectangle. + * @method union + * @param {Rectangle} rect The rectangle to calculate a union with. + * @return {Rectangle} A new rectangle describing the union. + */ + p.union = function (rect) { + return this.clone().extend(rect.x, rect.y, rect.width, rect.height); + }; + + /** + * Returns a new rectangle which describes the intersection (overlap) of this rectangle and the specified rectangle, + * or null if they do not intersect. + * @method intersection + * @param {Rectangle} rect The rectangle to calculate an intersection with. + * @return {Rectangle} A new rectangle describing the intersection or null. + */ + p.intersection = function (rect) { + var x1 = rect.x, + y1 = rect.y, + x2 = x1 + rect.width, + y2 = y1 + rect.height; + if (this.x > x1) { + x1 = this.x; + } + if (this.y > y1) { + y1 = this.y; + } + if (this.x + this.width < x2) { + x2 = this.x + this.width; + } + if (this.y + this.height < y2) { + y2 = this.y + this.height; + } + return x2 <= x1 || y2 <= y1 ? null : new Rectangle(x1, y1, x2 - x1, y2 - y1); + }; + + /** + * Returns true if the specified rectangle intersects (has any overlap) with this rectangle. + * @method intersects + * @param {Rectangle} rect The rectangle to compare. + * @return {Boolean} True if the rectangles intersect. + */ + p.intersects = function (rect) { + return rect.x <= this.x + this.width && this.x <= rect.x + rect.width && rect.y <= this.y + this.height && this.y <= rect.y + rect.height; + }; + + /** + * Returns true if the width or height are equal or less than 0. + * @method isEmpty + * @return {Boolean} True if the rectangle is empty. + */ + p.isEmpty = function () { + return this.width <= 0 || this.height <= 0; + }; + + /** + * Returns a clone of the Rectangle instance. + * @method clone + * @return {Rectangle} a clone of the Rectangle instance. + **/ + p.clone = function () { + return new Rectangle(this.x, this.y, this.width, this.height); + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[Rectangle (x=" + this.x + " y=" + this.y + " width=" + this.width + " height=" + this.height + ")]"; + }; + + createjs.Rectangle = Rectangle; + })(); + + //############################################################################## + // ButtonHelper.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * The ButtonHelper is a helper class to create interactive buttons from {{#crossLink "MovieClip"}}{{/crossLink}} or + * {{#crossLink "Sprite"}}{{/crossLink}} instances. This class will intercept mouse events from an object, and + * automatically call {{#crossLink "Sprite/gotoAndStop"}}{{/crossLink}} or {{#crossLink "Sprite/gotoAndPlay"}}{{/crossLink}}, + * to the respective animation labels, add a pointer cursor, and allows the user to define a hit state frame. + * + * The ButtonHelper instance does not need to be added to the stage, but a reference should be maintained to prevent + * garbage collection. + * + * Note that over states will not work unless you call {{#crossLink "Stage/enableMouseOver"}}{{/crossLink}}. + * + *

Example

+ * + * var helper = new createjs.ButtonHelper(myInstance, "out", "over", "down", false, myInstance, "hit"); + * myInstance.addEventListener("click", handleClick); + * function handleClick(event) { + * // Click Happened. + * } + * + * @class ButtonHelper + * @param {Sprite|MovieClip} target The instance to manage. + * @param {String} [outLabel="out"] The label or animation to go to when the user rolls out of the button. + * @param {String} [overLabel="over"] The label or animation to go to when the user rolls over the button. + * @param {String} [downLabel="down"] The label or animation to go to when the user presses the button. + * @param {Boolean} [play=false] If the helper should call "gotoAndPlay" or "gotoAndStop" on the button when changing + * states. + * @param {DisplayObject} [hitArea] An optional item to use as the hit state for the button. If this is not defined, + * then the button's visible states will be used instead. Note that the same instance as the "target" argument can be + * used for the hitState. + * @param {String} [hitLabel] The label or animation on the hitArea instance that defines the hitArea bounds. If this is + * null, then the default state of the hitArea will be used. * + * @constructor + */ + + function ButtonHelper(target, outLabel, overLabel, downLabel, play, hitArea, hitLabel) { + if (!target.addEventListener) { + return; + } + + // public properties: + /** + * The target for this button helper. + * @property target + * @type MovieClip | Sprite + * @readonly + **/ + this.target = target; + + /** + * The label name or frame number to display when the user mouses out of the target. Defaults to "over". + * @property overLabel + * @type String | Number + **/ + this.overLabel = overLabel == null ? "over" : overLabel; + + /** + * The label name or frame number to display when the user mouses over the target. Defaults to "out". + * @property outLabel + * @type String | Number + **/ + this.outLabel = outLabel == null ? "out" : outLabel; + + /** + * The label name or frame number to display when the user presses on the target. Defaults to "down". + * @property downLabel + * @type String | Number + **/ + this.downLabel = downLabel == null ? "down" : downLabel; + + /** + * If true, then ButtonHelper will call gotoAndPlay, if false, it will use gotoAndStop. Default is false. + * @property play + * @default false + * @type Boolean + **/ + this.play = play; + + // private properties + /** + * @property _isPressed + * @type Boolean + * @protected + **/ + this._isPressed = false; + + /** + * @property _isOver + * @type Boolean + * @protected + **/ + this._isOver = false; + + /** + * @property _enabled + * @type Boolean + * @protected + **/ + this._enabled = false; + + // setup: + target.mouseChildren = false; // prevents issues when children are removed from the display list when state changes. + this.enabled = true; + this.handleEvent({}); + if (hitArea) { + if (hitLabel) { + hitArea.actionsEnabled = false; + hitArea.gotoAndStop && hitArea.gotoAndStop(hitLabel); + } + target.hitArea = hitArea; + } + } + var p = ButtonHelper.prototype; + + /** + * REMOVED. Removed in favor of using `MySuperClass_constructor`. + * See {{#crossLink "Utility Methods/extend"}}{{/crossLink}} and {{#crossLink "Utility Methods/promote"}}{{/crossLink}} + * for details. + * + * There is an inheritance tutorial distributed with EaselJS in /tutorials/Inheritance. + * + * @method initialize + * @protected + * @deprecated + */ + // p.initialize = function() {}; // searchable for devs wondering where it is. + + + // getter / setters: + /** + * Use the {{#crossLink "ButtonHelper/enabled:property"}}{{/crossLink}} property instead. + * @method setEnabled + * @param {Boolean} value + * @deprecated + **/ + p.setEnabled = function (value) { + // TODO: deprecated. + if (value == this._enabled) { + return; + } + var o = this.target; + this._enabled = value; + if (value) { + o.cursor = "pointer"; + o.addEventListener("rollover", this); + o.addEventListener("rollout", this); + o.addEventListener("mousedown", this); + o.addEventListener("pressup", this); + if (o._reset) { + o.__reset = o._reset;o._reset = this._reset; + } + } else { + o.cursor = null; + o.removeEventListener("rollover", this); + o.removeEventListener("rollout", this); + o.removeEventListener("mousedown", this); + o.removeEventListener("pressup", this); + if (o.__reset) { + o._reset = o.__reset;delete o.__reset; + } + } + }; + /** + * Use the {{#crossLink "ButtonHelper/enabled:property"}}{{/crossLink}} property instead. + * @method getEnabled + * @return {Boolean} + * @deprecated + **/ + p.getEnabled = function () { + return this._enabled; + }; + + /** + * Enables or disables the button functionality on the target. + * @property enabled + * @type {Boolean} + **/ + try { + Object.defineProperties(p, { + enabled: { get: p.getEnabled, set: p.setEnabled } + }); + } catch (e) {} // TODO: use Log + + + // public methods: + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[ButtonHelper]"; + }; + + // private methods: + /** + * @method handleEvent + * @param {Object} evt The mouse event to handle. + * @protected + **/ + p.handleEvent = function (evt) { + var label, + t = this.target, + type = evt.type; + if (type == "mousedown") { + this._isPressed = true; + label = this.downLabel; + } else if (type == "pressup") { + this._isPressed = false; + label = this._isOver ? this.overLabel : this.outLabel; + } else if (type == "rollover") { + this._isOver = true; + label = this._isPressed ? this.downLabel : this.overLabel; + } else { + // rollout and default + this._isOver = false; + label = this._isPressed ? this.overLabel : this.outLabel; + } + if (this.play) { + t.gotoAndPlay && t.gotoAndPlay(label); + } else { + t.gotoAndStop && t.gotoAndStop(label); + } + }; + + /** + * Injected into target. Preserves the paused state through a reset. + * @method _reset + * @protected + **/ + p._reset = function () { + // TODO: explore better ways to handle this issue. This is hacky & disrupts object signatures. + var p = this.paused; + this.__reset(); + this.paused = p; + }; + + createjs.ButtonHelper = ButtonHelper; + })(); + + //############################################################################## + // Shadow.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * This class encapsulates the properties required to define a shadow to apply to a {{#crossLink "DisplayObject"}}{{/crossLink}} + * via its shadow property. + * + *

Example

+ * + * myImage.shadow = new createjs.Shadow("#000000", 5, 5, 10); + * + * @class Shadow + * @constructor + * @param {String} color The color of the shadow. This can be any valid CSS color value. + * @param {Number} offsetX The x offset of the shadow in pixels. + * @param {Number} offsetY The y offset of the shadow in pixels. + * @param {Number} blur The size of the blurring effect. + **/ + + function Shadow(color, offsetX, offsetY, blur) { + + // public properties: + /** + * The color of the shadow. This can be any valid CSS color value. + * @property color + * @type String + * @default null + */ + this.color = color || "black"; + + /** The x offset of the shadow. + * @property offsetX + * @type Number + * @default 0 + */ + this.offsetX = offsetX || 0; + + /** The y offset of the shadow. + * @property offsetY + * @type Number + * @default 0 + */ + this.offsetY = offsetY || 0; + + /** The blur of the shadow. + * @property blur + * @type Number + * @default 0 + */ + this.blur = blur || 0; + } + var p = Shadow.prototype; + + /** + * REMOVED. Removed in favor of using `MySuperClass_constructor`. + * See {{#crossLink "Utility Methods/extend"}}{{/crossLink}} and {{#crossLink "Utility Methods/promote"}}{{/crossLink}} + * for details. + * + * There is an inheritance tutorial distributed with EaselJS in /tutorials/Inheritance. + * + * @method initialize + * @protected + * @deprecated + */ + // p.initialize = function() {}; // searchable for devs wondering where it is. + + + // static public properties: + /** + * An identity shadow object (all properties are set to 0). + * @property identity + * @type Shadow + * @static + * @final + * @readonly + **/ + Shadow.identity = new Shadow("transparent", 0, 0, 0); + + // public methods: + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[Shadow]"; + }; + + /** + * Returns a clone of this Shadow instance. + * @method clone + * @return {Shadow} A clone of the current Shadow instance. + **/ + p.clone = function () { + return new Shadow(this.color, this.offsetX, this.offsetY, this.blur); + }; + + createjs.Shadow = Shadow; + })(); + + //############################################################################## + // SpriteSheet.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Encapsulates the properties and methods associated with a sprite sheet. A sprite sheet is a series of images (usually + * animation frames) combined into a larger image (or images). For example, an animation consisting of eight 100x100 + * images could be combined into a single 400x200 sprite sheet (4 frames across by 2 high). + * + * The data passed to the SpriteSheet constructor defines: + *
    + *
  1. The source image or images to use.
  2. + *
  3. The positions of individual image frames.
  4. + *
  5. Sequences of frames that form named animations. Optional.
  6. + *
  7. The target playback framerate. Optional.
  8. + *
+ *

SpriteSheet Format

+ * SpriteSheets are an object with two required properties (`images` and `frames`), and two optional properties + * (`framerate` and `animations`). This makes them easy to define in javascript code, or in JSON. + * + *

images

+ * An array of source images. Images can be either an HTMlimage + * instance, or a uri to an image. The former is recommended to control preloading. + * + * images: [image1, "path/to/image2.png"], + * + *

frames

+ * Defines the individual frames. There are two supported formats for frame data: + * When all of the frames are the same size (in a grid), use an object with `width`, `height`, `regX`, `regY`, + * and `count` properties. + * + *
    + *
  • `width` & `height` are required and specify the dimensions of the frames
  • + *
  • `regX` & `regY` indicate the registration point or "origin" of the frames
  • + *
  • `spacing` indicate the spacing between frames
  • + *
  • `margin` specify the margin around the image(s)
  • + *
  • `count` allows you to specify the total number of frames in the spritesheet; if omitted, this will + * be calculated based on the dimensions of the source images and the frames. Frames will be assigned + * indexes based on their position in the source images (left to right, top to bottom).
  • + *
+ * + * frames: {width:64, height:64, count:20, regX: 32, regY:64, spacing:0, margin:0} + * + * If the frames are of different sizes, use an array of frame definitions. Each definition is itself an array + * with 4 required and 3 optional entries, in the order: + * + *
    + *
  • The first four, `x`, `y`, `width`, and `height` are required and define the frame rectangle.
  • + *
  • The fifth, `imageIndex`, specifies the index of the source image (defaults to 0)
  • + *
  • The last two, `regX` and `regY` specify the registration point of the frame
  • + *
+ * + * frames: [ + * // x, y, width, height, imageIndex*, regX*, regY* + * [64, 0, 96, 64], + * [0, 0, 64, 64, 1, 32, 32] + * // etc. + * ] + * + *

animations

+ * Optional. An object defining sequences of frames to play as named animations. Each property corresponds to an + * animation of the same name. Each animation must specify the frames to play, and may + * also include a relative playback `speed` (ex. 2 would playback at double speed, 0.5 at half), and + * the name of the `next` animation to sequence to after it completes. + * + * There are three formats supported for defining the frames in an animation, which can be mixed and matched as appropriate: + *
    + *
  1. for a single frame animation, you can simply specify the frame index + * + * animations: { + * sit: 7 + * } + * + *
  2. + *
  3. + * for an animation of consecutive frames, you can use an array with two required, and two optional entries + * in the order: `start`, `end`, `next`, and `speed`. This will play the frames from start to end inclusive. + * + * animations: { + * // start, end, next*, speed* + * run: [0, 8], + * jump: [9, 12, "run", 2] + * } + * + *
  4. + *
  5. + * for non-consecutive frames, you can use an object with a `frames` property defining an array of frame + * indexes to play in order. The object can also specify `next` and `speed` properties. + * + * animations: { + * walk: { + * frames: [1,2,3,3,2,1] + * }, + * shoot: { + * frames: [1,4,5,6], + * next: "walk", + * speed: 0.5 + * } + * } + * + *
  6. + *
+ * Note: the `speed` property was added in EaselJS 0.7.0. Earlier versions had a `frequency` + * property instead, which was the inverse of `speed`. For example, a value of "4" would be 1/4 normal speed in + * earlier versions, but is 4x normal speed in EaselJS 0.7.0+. + * + *

framerate

+ * Optional. Indicates the default framerate to play this spritesheet at in frames per second. See + * {{#crossLink "SpriteSheet/framerate:property"}}{{/crossLink}} for more information. + * + * framerate: 20 + * + * Note that the Sprite framerate will only work if the stage update method is provided with the {{#crossLink "Ticker/tick:event"}}{{/crossLink}} + * event generated by the {{#crossLink "Ticker"}}{{/crossLink}}. + * + * createjs.Ticker.on("tick", handleTick); + * function handleTick(event) { + * stage.update(event); + * } + * + *

Example

+ * To define a simple sprite sheet, with a single image "sprites.jpg" arranged in a regular 50x50 grid with three + * animations: "stand" showing the first frame, "run" looping frame 1-5 inclusive, and "jump" playing frame 6-8 and + * sequencing back to run. + * + * var data = { + * images: ["sprites.jpg"], + * frames: {width:50, height:50}, + * animations: { + * stand:0, + * run:[1,5], + * jump:[6,8,"run"] + * } + * }; + * var spriteSheet = new createjs.SpriteSheet(data); + * var animation = new createjs.Sprite(spriteSheet, "run"); + * + *

Generating SpriteSheet Images

+ * Spritesheets can be created manually by combining images in PhotoShop, and specifying the frame size or + * coordinates manually, however there are a number of tools that facilitate this. + *
    + *
  • Exporting SpriteSheets or HTML5 content from Flash Pro supports the EaselJS SpriteSheet format.
  • + *
  • The popular Texture Packer has + * EaselJS support. + *
  • SWF animations in Flash can be exported to SpriteSheets using
  • + *
+ * + *

Cross Origin Issues

+ * Warning: Images loaded cross-origin will throw cross-origin security errors when interacted with + * using: + *
    + *
  • a mouse
  • + *
  • methods such as {{#crossLink "Container/getObjectUnderPoint"}}{{/crossLink}}
  • + *
  • Filters (see {{#crossLink "Filter"}}{{/crossLink}})
  • + *
  • caching (see {{#crossLink "DisplayObject/cache"}}{{/crossLink}})
  • + *
+ * You can get around this by setting `crossOrigin` property on your images before passing them to EaselJS, or + * setting the `crossOrigin` property on PreloadJS' LoadQueue or LoadItems. + * + * var image = new Image(); + * img.crossOrigin="Anonymous"; + * img.src = "http://server-with-CORS-support.com/path/to/image.jpg"; + * + * If you pass string paths to SpriteSheets, they will not work cross-origin. The server that stores the image must + * support cross-origin requests, or this will not work. For more information, check out + * CORS overview on MDN. + * + * @class SpriteSheet + * @constructor + * @param {Object} data An object describing the SpriteSheet data. + * @extends EventDispatcher + **/ + + function SpriteSheet(data) { + this.EventDispatcher_constructor(); + + // public properties: + /** + * Indicates whether all images are finished loading. + * @property complete + * @type Boolean + * @readonly + **/ + this.complete = true; + + /** + * Specifies the framerate to use by default for Sprite instances using the SpriteSheet. See the Sprite class + * {{#crossLink "Sprite/framerate:property"}}{{/crossLink}} for more information. + * @property framerate + * @type Number + **/ + this.framerate = 0; + + // private properties: + /** + * @property _animations + * @protected + * @type Array + **/ + this._animations = null; + + /** + * @property _frames + * @protected + * @type Array + **/ + this._frames = null; + + /** + * @property _images + * @protected + * @type Array + **/ + this._images = null; + + /** + * @property _data + * @protected + * @type Object + **/ + this._data = null; + + /** + * @property _loadCount + * @protected + * @type Number + **/ + this._loadCount = 0; + + // only used for simple frame defs: + /** + * @property _frameHeight + * @protected + * @type Number + **/ + this._frameHeight = 0; + + /** + * @property _frameWidth + * @protected + * @type Number + **/ + this._frameWidth = 0; + + /** + * @property _numFrames + * @protected + * @type Number + **/ + this._numFrames = 0; + + /** + * @property _regX + * @protected + * @type Number + **/ + this._regX = 0; + + /** + * @property _regY + * @protected + * @type Number + **/ + this._regY = 0; + + /** + * @property _spacing + * @protected + * @type Number + **/ + this._spacing = 0; + + /** + * @property _margin + * @protected + * @type Number + **/ + this._margin = 0; + + // setup: + this._parseData(data); + } + var p = createjs.extend(SpriteSheet, createjs.EventDispatcher); + + // TODO: deprecated + // p.initialize = function() {}; // searchable for devs wondering where it is. REMOVED. See docs for details. + + + // events: + /** + * Dispatched when all images are loaded. Note that this only fires if the images + * were not fully loaded when the sprite sheet was initialized. You should check the complete property + * to prior to adding a listener. Ex. + * + * var sheet = new createjs.SpriteSheet(data); + * if (!sheet.complete) { + * // not preloaded, listen for the complete event: + * sheet.addEventListener("complete", handler); + * } + * + * @event complete + * @param {Object} target The object that dispatched the event. + * @param {String} type The event type. + * @since 0.6.0 + */ + + /** + * Dispatched when getFrame is called with a valid frame index. This is primarily intended for use by {{#crossLink "SpriteSheetBuilder"}}{{/crossLink}} + * when doing on-demand rendering. + * @event getframe + * @param {Number} index The frame index. + * @param {Object} frame The frame object that getFrame will return. + */ + + /** + * Dispatched when an image encounters an error. A SpriteSheet will dispatch an error event for each image that + * encounters an error, and will still dispatch a {{#crossLink "SpriteSheet/complete:event"}}{{/crossLink}} + * event once all images are finished processing, even if an error is encountered. + * @event error + * @param {String} src The source of the image that failed to load. + * @since 0.8.2 + */ + + // getter / setters: + /** + * Use the {{#crossLink "SpriteSheet/animations:property"}}{{/crossLink}} property instead. + * @method getAnimations + * @return {Array} + * @deprecated + **/ + p.getAnimations = function () { + return this._animations.slice(); + }; + + /** + * Returns an array of all available animation names available on this sprite sheet as strings. + * @property animations + * @type {Array} + * @readonly + **/ + try { + Object.defineProperties(p, { + animations: { get: p.getAnimations } + }); + } catch (e) {} + + // public methods: + /** + * Returns the total number of frames in the specified animation, or in the whole sprite + * sheet if the animation param is omitted. Returns 0 if the spritesheet relies on calculated frame counts, and + * the images have not been fully loaded. + * @method getNumFrames + * @param {String} animation The name of the animation to get a frame count for. + * @return {Number} The number of frames in the animation, or in the entire sprite sheet if the animation param is omitted. + */ + p.getNumFrames = function (animation) { + if (animation == null) { + return this._frames ? this._frames.length : this._numFrames || 0; + } else { + var data = this._data[animation]; + if (data == null) { + return 0; + } else { + return data.frames.length; + } + } + }; + + /** + * Returns an object defining the specified animation. The returned object contains:
    + *
  • frames: an array of the frame ids in the animation
  • + *
  • speed: the playback speed for this animation
  • + *
  • name: the name of the animation
  • + *
  • next: the default animation to play next. If the animation loops, the name and next property will be the + * same.
  • + *
+ * @method getAnimation + * @param {String} name The name of the animation to get. + * @return {Object} a generic object with frames, speed, name, and next properties. + **/ + p.getAnimation = function (name) { + return this._data[name]; + }; + + /** + * Returns an object specifying the image and source rect of the specified frame. The returned object has:
    + *
  • an image property holding a reference to the image object in which the frame is found
  • + *
  • a rect property containing a Rectangle instance which defines the boundaries for the frame within that + * image.
  • + *
  • A regX and regY property corresponding to the regX/Y values for the frame. + *
+ * @method getFrame + * @param {Number} frameIndex The index of the frame. + * @return {Object} a generic object with image and rect properties. Returns null if the frame does not exist. + **/ + p.getFrame = function (frameIndex) { + var frame; + if (this._frames && (frame = this._frames[frameIndex])) { + return frame; + } + return null; + }; + + /** + * Returns a {{#crossLink "Rectangle"}}{{/crossLink}} instance defining the bounds of the specified frame relative + * to the origin. For example, a 90 x 70 frame with a regX of 50 and a regY of 40 would return: + * + * [x=-50, y=-40, width=90, height=70] + * + * @method getFrameBounds + * @param {Number} frameIndex The index of the frame. + * @param {Rectangle} [rectangle] A Rectangle instance to copy the values into. By default a new instance is created. + * @return {Rectangle} A Rectangle instance. Returns null if the frame does not exist, or the image is not fully loaded. + **/ + p.getFrameBounds = function (frameIndex, rectangle) { + var frame = this.getFrame(frameIndex); + return frame ? (rectangle || new createjs.Rectangle()).setValues(-frame.regX, -frame.regY, frame.rect.width, frame.rect.height) : null; + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[SpriteSheet]"; + }; + + /** + * SpriteSheet cannot be cloned. A SpriteSheet can be shared by multiple Sprite instances without cloning it. + * @method clone + **/ + p.clone = function () { + throw "SpriteSheet cannot be cloned."; + }; + + // private methods: + /** + * @method _parseData + * @param {Object} data An object describing the SpriteSheet data. + * @protected + **/ + p._parseData = function (data) { + var i, l, o, a; + if (data == null) { + return; + } + + this.framerate = data.framerate || 0; + + // parse images: + if (data.images && (l = data.images.length) > 0) { + a = this._images = []; + for (i = 0; i < l; i++) { + var img = data.images[i]; + if (typeof img == "string") { + var src = img; + img = document.createElement("img"); + img.src = src; + } + a.push(img); + if (!img.getContext && !img.naturalWidth) { + this._loadCount++; + this.complete = false; + (function (o, src) { + img.onload = function () { + o._handleImageLoad(src); + }; + })(this, src); + (function (o, src) { + img.onerror = function () { + o._handleImageError(src); + }; + })(this, src); + } + } + } + + // parse frames: + if (data.frames == null) {// nothing + } else if (Array.isArray(data.frames)) { + this._frames = []; + a = data.frames; + for (i = 0, l = a.length; i < l; i++) { + var arr = a[i]; + this._frames.push({ image: this._images[arr[4] ? arr[4] : 0], rect: new createjs.Rectangle(arr[0], arr[1], arr[2], arr[3]), regX: arr[5] || 0, regY: arr[6] || 0 }); + } + } else { + o = data.frames; + this._frameWidth = o.width; + this._frameHeight = o.height; + this._regX = o.regX || 0; + this._regY = o.regY || 0; + this._spacing = o.spacing || 0; + this._margin = o.margin || 0; + this._numFrames = o.count; + if (this._loadCount == 0) { + this._calculateFrames(); + } + } + + // parse animations: + this._animations = []; + if ((o = data.animations) != null) { + this._data = {}; + var name; + for (name in o) { + var anim = { name: name }; + var obj = o[name]; + if (typeof obj == "number") { + // single frame + a = anim.frames = [obj]; + } else if (Array.isArray(obj)) { + // simple + if (obj.length == 1) { + anim.frames = [obj[0]]; + } else { + anim.speed = obj[3]; + anim.next = obj[2]; + a = anim.frames = []; + for (i = obj[0]; i <= obj[1]; i++) { + a.push(i); + } + } + } else { + // complex + anim.speed = obj.speed; + anim.next = obj.next; + var frames = obj.frames; + a = anim.frames = typeof frames == "number" ? [frames] : frames.slice(0); + } + if (anim.next === true || anim.next === undefined) { + anim.next = name; + } // loop + if (anim.next === false || a.length < 2 && anim.next == name) { + anim.next = null; + } // stop + if (!anim.speed) { + anim.speed = 1; + } + this._animations.push(name); + this._data[name] = anim; + } + } + }; + + /** + * @method _handleImageLoad + * @protected + **/ + p._handleImageLoad = function (src) { + if (--this._loadCount == 0) { + this._calculateFrames(); + this.complete = true; + this.dispatchEvent("complete"); + } + }; + + /** + * @method _handleImageError + * @protected + */ + p._handleImageError = function (src) { + var errorEvent = new createjs.Event("error"); + errorEvent.src = src; + this.dispatchEvent(errorEvent); + + // Complete is still dispatched. + if (--this._loadCount == 0) { + this.dispatchEvent("complete"); + } + }; + + /** + * @method _calculateFrames + * @protected + **/ + p._calculateFrames = function () { + if (this._frames || this._frameWidth == 0) { + return; + } + + this._frames = []; + + var maxFrames = this._numFrames || 100000; // if we go over this, something is wrong. + var frameCount = 0, + frameWidth = this._frameWidth, + frameHeight = this._frameHeight; + var spacing = this._spacing, + margin = this._margin; + + imgLoop: for (var i = 0, imgs = this._images; i < imgs.length; i++) { + var img = imgs[i], + imgW = img.width, + imgH = img.height; + + var y = margin; + while (y <= imgH - margin - frameHeight) { + var x = margin; + while (x <= imgW - margin - frameWidth) { + if (frameCount >= maxFrames) { + break imgLoop; + } + frameCount++; + this._frames.push({ + image: img, + rect: new createjs.Rectangle(x, y, frameWidth, frameHeight), + regX: this._regX, + regY: this._regY + }); + x += frameWidth + spacing; + } + y += frameHeight + spacing; + } + } + this._numFrames = frameCount; + }; + + createjs.SpriteSheet = createjs.promote(SpriteSheet, "EventDispatcher"); + })(); + + //############################################################################## + // Graphics.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * The Graphics class exposes an easy to use API for generating vector drawing instructions and drawing them to a + * specified context. Note that you can use Graphics without any dependency on the EaselJS framework by calling {{#crossLink "Graphics/draw"}}{{/crossLink}} + * directly, or it can be used with the {{#crossLink "Shape"}}{{/crossLink}} object to draw vector graphics within the + * context of an EaselJS display list. + * + * There are two approaches to working with Graphics object: calling methods on a Graphics instance (the "Graphics API"), or + * instantiating Graphics command objects and adding them to the graphics queue via {{#crossLink "Graphics/append"}}{{/crossLink}}. + * The former abstracts the latter, simplifying beginning and ending paths, fills, and strokes. + * + * var g = new createjs.Graphics(); + * g.setStrokeStyle(1); + * g.beginStroke("#000000"); + * g.beginFill("red"); + * g.drawCircle(0,0,30); + * + * All drawing methods in Graphics return the Graphics instance, so they can be chained together. For example, + * the following line of code would generate the instructions to draw a rectangle with a red stroke and blue fill: + * + * myGraphics.beginStroke("red").beginFill("blue").drawRect(20, 20, 100, 50); + * + * Each graphics API call generates a command object (see below). The last command to be created can be accessed via + * {{#crossLink "Graphics/command:property"}}{{/crossLink}}: + * + * var fillCommand = myGraphics.beginFill("red").command; + * // ... later, update the fill style/color: + * fillCommand.style = "blue"; + * // or change it to a bitmap fill: + * fillCommand.bitmap(myImage); + * + * For more direct control of rendering, you can instantiate and append command objects to the graphics queue directly. In this case, you + * need to manage path creation manually, and ensure that fill/stroke is applied to a defined path: + * + * // start a new path. Graphics.beginCmd is a reusable BeginPath instance: + * myGraphics.append(createjs.Graphics.beginCmd); + * // we need to define the path before applying the fill: + * var circle = new createjs.Graphics.Circle(0,0,30); + * myGraphics.append(circle); + * // fill the path we just defined: + * var fill = new createjs.Graphics.Fill("red"); + * myGraphics.append(fill); + * + * These approaches can be used together, for example to insert a custom command: + * + * myGraphics.beginFill("red"); + * var customCommand = new CustomSpiralCommand(etc); + * myGraphics.append(customCommand); + * myGraphics.beginFill("blue"); + * myGraphics.drawCircle(0, 0, 30); + * + * See {{#crossLink "Graphics/append"}}{{/crossLink}} for more info on creating custom commands. + * + *

Tiny API

+ * The Graphics class also includes a "tiny API", which is one or two-letter methods that are shortcuts for all of the + * Graphics methods. These methods are great for creating compact instructions, and is used by the Toolkit for CreateJS + * to generate readable code. All tiny methods are marked as protected, so you can view them by enabling protected + * descriptions in the docs. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
TinyMethodTinyMethod
mt{{#crossLink "Graphics/moveTo"}}{{/crossLink}} lt {{#crossLink "Graphics/lineTo"}}{{/crossLink}}
a/at{{#crossLink "Graphics/arc"}}{{/crossLink}} / {{#crossLink "Graphics/arcTo"}}{{/crossLink}} bt{{#crossLink "Graphics/bezierCurveTo"}}{{/crossLink}}
qt{{#crossLink "Graphics/quadraticCurveTo"}}{{/crossLink}} (also curveTo)r{{#crossLink "Graphics/rect"}}{{/crossLink}}
cp{{#crossLink "Graphics/closePath"}}{{/crossLink}} c{{#crossLink "Graphics/clear"}}{{/crossLink}}
f{{#crossLink "Graphics/beginFill"}}{{/crossLink}} lf{{#crossLink "Graphics/beginLinearGradientFill"}}{{/crossLink}}
rf{{#crossLink "Graphics/beginRadialGradientFill"}}{{/crossLink}} bf{{#crossLink "Graphics/beginBitmapFill"}}{{/crossLink}}
ef{{#crossLink "Graphics/endFill"}}{{/crossLink}} ss / sd{{#crossLink "Graphics/setStrokeStyle"}}{{/crossLink}} / {{#crossLink "Graphics/setStrokeDash"}}{{/crossLink}}
s{{#crossLink "Graphics/beginStroke"}}{{/crossLink}} ls{{#crossLink "Graphics/beginLinearGradientStroke"}}{{/crossLink}}
rs{{#crossLink "Graphics/beginRadialGradientStroke"}}{{/crossLink}} bs{{#crossLink "Graphics/beginBitmapStroke"}}{{/crossLink}}
es{{#crossLink "Graphics/endStroke"}}{{/crossLink}} dr{{#crossLink "Graphics/drawRect"}}{{/crossLink}}
rr{{#crossLink "Graphics/drawRoundRect"}}{{/crossLink}} rc{{#crossLink "Graphics/drawRoundRectComplex"}}{{/crossLink}}
dc{{#crossLink "Graphics/drawCircle"}}{{/crossLink}} de{{#crossLink "Graphics/drawEllipse"}}{{/crossLink}}
dp{{#crossLink "Graphics/drawPolyStar"}}{{/crossLink}} p{{#crossLink "Graphics/decodePath"}}{{/crossLink}}
+ * + * Here is the above example, using the tiny API instead. + * + * myGraphics.s("red").f("blue").r(20, 20, 100, 50); + * + * @class Graphics + * @constructor + **/ + + function Graphics() { + + // public properties + /** + * Holds a reference to the last command that was created or appended. For example, you could retain a reference + * to a Fill command in order to dynamically update the color later by using: + * + * var myFill = myGraphics.beginFill("red").command; + * // update color later: + * myFill.style = "yellow"; + * + * @property command + * @type Object + **/ + this.command = null; + + // private properties + /** + * @property _stroke + * @protected + * @type {Stroke} + **/ + this._stroke = null; + + /** + * @property _strokeStyle + * @protected + * @type {StrokeStyle} + **/ + this._strokeStyle = null; + + /** + * @property _oldStrokeStyle + * @protected + * @type {StrokeStyle} + **/ + this._oldStrokeStyle = null; + + /** + * @property _strokeDash + * @protected + * @type {StrokeDash} + **/ + this._strokeDash = null; + + /** + * @property _oldStrokeDash + * @protected + * @type {StrokeDash} + **/ + this._oldStrokeDash = null; + + /** + * @property _strokeIgnoreScale + * @protected + * @type Boolean + **/ + this._strokeIgnoreScale = false; + + /** + * @property _fill + * @protected + * @type {Fill} + **/ + this._fill = null; + + /** + * @property _instructions + * @protected + * @type {Array} + **/ + this._instructions = []; + + /** + * Indicates the last instruction index that was committed. + * @property _commitIndex + * @protected + * @type {Number} + **/ + this._commitIndex = 0; + + /** + * Uncommitted instructions. + * @property _activeInstructions + * @protected + * @type {Array} + **/ + this._activeInstructions = []; + + /** + * This indicates that there have been changes to the activeInstruction list since the last updateInstructions call. + * @property _dirty + * @protected + * @type {Boolean} + * @default false + **/ + this._dirty = false; + + /** + * Index to draw from if a store operation has happened. + * @property _storeIndex + * @protected + * @type {Number} + * @default 0 + **/ + this._storeIndex = 0; + + // setup: + this.clear(); + } + var p = Graphics.prototype; + var G = Graphics; // shortcut + + /** + * REMOVED. Removed in favor of using `MySuperClass_constructor`. + * See {{#crossLink "Utility Methods/extend"}}{{/crossLink}} and {{#crossLink "Utility Methods/promote"}}{{/crossLink}} + * for details. + * + * There is an inheritance tutorial distributed with EaselJS in /tutorials/Inheritance. + * + * @method initialize + * @protected + * @deprecated + */ + // p.initialize = function() {}; // searchable for devs wondering where it is. + + + // static public methods: + /** + * Returns a CSS compatible color string based on the specified RGB numeric color values in the format + * "rgba(255,255,255,1.0)", or if alpha is null then in the format "rgb(255,255,255)". For example, + * + * createjs.Graphics.getRGB(50, 100, 150, 0.5); + * // Returns "rgba(50,100,150,0.5)" + * + * It also supports passing a single hex color value as the first param, and an optional alpha value as the second + * param. For example, + * + * createjs.Graphics.getRGB(0xFF00FF, 0.2); + * // Returns "rgba(255,0,255,0.2)" + * + * @method getRGB + * @static + * @param {Number} r The red component for the color, between 0 and 0xFF (255). + * @param {Number} g The green component for the color, between 0 and 0xFF (255). + * @param {Number} b The blue component for the color, between 0 and 0xFF (255). + * @param {Number} [alpha] The alpha component for the color where 0 is fully transparent and 1 is fully opaque. + * @return {String} A CSS compatible color string based on the specified RGB numeric color values in the format + * "rgba(255,255,255,1.0)", or if alpha is null then in the format "rgb(255,255,255)". + **/ + Graphics.getRGB = function (r, g, b, alpha) { + if (r != null && b == null) { + alpha = g; + b = r & 0xFF; + g = r >> 8 & 0xFF; + r = r >> 16 & 0xFF; + } + if (alpha == null) { + return "rgb(" + r + "," + g + "," + b + ")"; + } else { + return "rgba(" + r + "," + g + "," + b + "," + alpha + ")"; + } + }; + + /** + * Returns a CSS compatible color string based on the specified HSL numeric color values in the format "hsla(360,100,100,1.0)", + * or if alpha is null then in the format "hsl(360,100,100)". + * + * createjs.Graphics.getHSL(150, 100, 70); + * // Returns "hsl(150,100,70)" + * + * @method getHSL + * @static + * @param {Number} hue The hue component for the color, between 0 and 360. + * @param {Number} saturation The saturation component for the color, between 0 and 100. + * @param {Number} lightness The lightness component for the color, between 0 and 100. + * @param {Number} [alpha] The alpha component for the color where 0 is fully transparent and 1 is fully opaque. + * @return {String} A CSS compatible color string based on the specified HSL numeric color values in the format + * "hsla(360,100,100,1.0)", or if alpha is null then in the format "hsl(360,100,100)". + **/ + Graphics.getHSL = function (hue, saturation, lightness, alpha) { + if (alpha == null) { + return "hsl(" + hue % 360 + "," + saturation + "%," + lightness + "%)"; + } else { + return "hsla(" + hue % 360 + "," + saturation + "%," + lightness + "%," + alpha + ")"; + } + }; + + // static properties: + /** + * A reusable instance of {{#crossLink "Graphics/BeginPath"}}{{/crossLink}} to avoid + * unnecessary instantiation. + * @property beginCmd + * @type {Graphics.BeginPath} + * @static + **/ + // defined at the bottom of this file. + + /** + * Map of Base64 characters to values. Used by {{#crossLink "Graphics/decodePath"}}{{/crossLink}}. + * @property BASE_64 + * @static + * @final + * @readonly + * @type {Object} + **/ + Graphics.BASE_64 = { "A": 0, "B": 1, "C": 2, "D": 3, "E": 4, "F": 5, "G": 6, "H": 7, "I": 8, "J": 9, "K": 10, "L": 11, "M": 12, "N": 13, "O": 14, "P": 15, "Q": 16, "R": 17, "S": 18, "T": 19, "U": 20, "V": 21, "W": 22, "X": 23, "Y": 24, "Z": 25, "a": 26, "b": 27, "c": 28, "d": 29, "e": 30, "f": 31, "g": 32, "h": 33, "i": 34, "j": 35, "k": 36, "l": 37, "m": 38, "n": 39, "o": 40, "p": 41, "q": 42, "r": 43, "s": 44, "t": 45, "u": 46, "v": 47, "w": 48, "x": 49, "y": 50, "z": 51, "0": 52, "1": 53, "2": 54, "3": 55, "4": 56, "5": 57, "6": 58, "7": 59, "8": 60, "9": 61, "+": 62, "/": 63 }; + + /** + * Maps numeric values for the caps parameter of {{#crossLink "Graphics/setStrokeStyle"}}{{/crossLink}} to + * corresponding string values. This is primarily for use with the tiny API. The mappings are as follows: 0 to + * "butt", 1 to "round", and 2 to "square". + * For example, to set the line caps to "square": + * + * myGraphics.ss(16, 2); + * + * @property STROKE_CAPS_MAP + * @static + * @final + * @readonly + * @type {Array} + **/ + Graphics.STROKE_CAPS_MAP = ["butt", "round", "square"]; + + /** + * Maps numeric values for the joints parameter of {{#crossLink "Graphics/setStrokeStyle"}}{{/crossLink}} to + * corresponding string values. This is primarily for use with the tiny API. The mappings are as follows: 0 to + * "miter", 1 to "round", and 2 to "bevel". + * For example, to set the line joints to "bevel": + * + * myGraphics.ss(16, 0, 2); + * + * @property STROKE_JOINTS_MAP + * @static + * @final + * @readonly + * @type {Array} + **/ + Graphics.STROKE_JOINTS_MAP = ["miter", "round", "bevel"]; + + /** + * @property _ctx + * @static + * @protected + * @type {CanvasRenderingContext2D} + **/ + var canvas = createjs.createCanvas ? createjs.createCanvas() : document.createElement("canvas"); + if (canvas.getContext) { + Graphics._ctx = canvas.getContext("2d"); + canvas.width = canvas.height = 1; + } + + // getter / setters: + /** + * Use the {{#crossLink "Graphics/instructions:property"}}{{/crossLink}} property instead. + * @method getInstructions + * @return {Array} + * @deprecated + **/ + p.getInstructions = function () { + this._updateInstructions(); + return this._instructions; + }; + + /** + * Returns the graphics instructions array. Each entry is a graphics command object (ex. Graphics.Fill, Graphics.Rect) + * Modifying the returned array directly is not recommended, and is likely to result in unexpected behaviour. + * + * This property is mainly intended for introspection of the instructions (ex. for graphics export). + * @property instructions + * @type {Array} + * @readonly + **/ + try { + Object.defineProperties(p, { + instructions: { get: p.getInstructions } + }); + } catch (e) {} + + // public methods: + /** + * Returns true if this Graphics instance has no drawing commands. + * @method isEmpty + * @return {Boolean} Returns true if this Graphics instance has no drawing commands. + **/ + p.isEmpty = function () { + return !(this._instructions.length || this._activeInstructions.length); + }; + + /** + * Draws the display object into the specified context ignoring its visible, alpha, shadow, and transform. + * Returns true if the draw was handled (useful for overriding functionality). + * + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method draw + * @param {CanvasRenderingContext2D} ctx The canvas 2D context object to draw into. + * @param {Object} data Optional data that is passed to graphics command exec methods. When called from a Shape instance, the shape passes itself as the data parameter. This can be used by custom graphic commands to insert contextual data. + **/ + p.draw = function (ctx, data) { + this._updateInstructions(); + var instr = this._instructions; + for (var i = this._storeIndex, l = instr.length; i < l; i++) { + instr[i].exec(ctx, data); + } + }; + + /** + * Draws only the path described for this Graphics instance, skipping any non-path instructions, including fill and + * stroke descriptions. Used for DisplayObject.mask to draw the clipping path, for example. + * + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method drawAsPath + * @param {CanvasRenderingContext2D} ctx The canvas 2D context object to draw into. + **/ + p.drawAsPath = function (ctx) { + this._updateInstructions(); + var instr, + instrs = this._instructions; + for (var i = this._storeIndex, l = instrs.length; i < l; i++) { + // the first command is always a beginPath command. + if ((instr = instrs[i]).path !== false) { + instr.exec(ctx); + } + } + }; + + // public methods that map directly to context 2D calls: + /** + * Moves the drawing point to the specified position. A tiny API method "mt" also exists. + * @method moveTo + * @param {Number} x The x coordinate the drawing point should move to. + * @param {Number} y The y coordinate the drawing point should move to. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls). + * @chainable + **/ + p.moveTo = function (x, y) { + return this.append(new G.MoveTo(x, y), true); + }; + + /** + * Draws a line from the current drawing point to the specified position, which become the new current drawing + * point. Note that you *must* call {{#crossLink "Graphics/moveTo"}}{{/crossLink}} before the first `lineTo()`. + * A tiny API method "lt" also exists. + * + * For detailed information, read the + * + * whatwg spec. + * @method lineTo + * @param {Number} x The x coordinate the drawing point should draw to. + * @param {Number} y The y coordinate the drawing point should draw to. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.lineTo = function (x, y) { + return this.append(new G.LineTo(x, y)); + }; + + /** + * Draws an arc with the specified control points and radius. For detailed information, read the + * + * whatwg spec. A tiny API method "at" also exists. + * @method arcTo + * @param {Number} x1 + * @param {Number} y1 + * @param {Number} x2 + * @param {Number} y2 + * @param {Number} radius + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.arcTo = function (x1, y1, x2, y2, radius) { + return this.append(new G.ArcTo(x1, y1, x2, y2, radius)); + }; + + /** + * Draws an arc defined by the radius, startAngle and endAngle arguments, centered at the position (x, y). For + * example, to draw a full circle with a radius of 20 centered at (100, 100): + * + * arc(100, 100, 20, 0, Math.PI*2); + * + * For detailed information, read the + * whatwg spec. + * A tiny API method "a" also exists. + * @method arc + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @param {Number} startAngle Measured in radians. + * @param {Number} endAngle Measured in radians. + * @param {Boolean} anticlockwise + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.arc = function (x, y, radius, startAngle, endAngle, anticlockwise) { + return this.append(new G.Arc(x, y, radius, startAngle, endAngle, anticlockwise)); + }; + + /** + * Draws a quadratic curve from the current drawing point to (x, y) using the control point (cpx, cpy). For detailed + * information, read the + * whatwg spec. A tiny API method "qt" also exists. + * @method quadraticCurveTo + * @param {Number} cpx + * @param {Number} cpy + * @param {Number} x + * @param {Number} y + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.quadraticCurveTo = function (cpx, cpy, x, y) { + return this.append(new G.QuadraticCurveTo(cpx, cpy, x, y)); + }; + + /** + * Draws a bezier curve from the current drawing point to (x, y) using the control points (cp1x, cp1y) and (cp2x, + * cp2y). For detailed information, read the + * + * whatwg spec. A tiny API method "bt" also exists. + * @method bezierCurveTo + * @param {Number} cp1x + * @param {Number} cp1y + * @param {Number} cp2x + * @param {Number} cp2y + * @param {Number} x + * @param {Number} y + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.bezierCurveTo = function (cp1x, cp1y, cp2x, cp2y, x, y) { + return this.append(new G.BezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)); + }; + + /** + * Draws a rectangle at (x, y) with the specified width and height using the current fill and/or stroke. + * For detailed information, read the + * + * whatwg spec. A tiny API method "r" also exists. + * @method rect + * @param {Number} x + * @param {Number} y + * @param {Number} w Width of the rectangle + * @param {Number} h Height of the rectangle + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.rect = function (x, y, w, h) { + return this.append(new G.Rect(x, y, w, h)); + }; + + /** + * Closes the current path, effectively drawing a line from the current drawing point to the first drawing point specified + * since the fill or stroke was last set. A tiny API method "cp" also exists. + * @method closePath + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.closePath = function () { + return this._activeInstructions.length ? this.append(new G.ClosePath()) : this; + }; + + // public methods that roughly map to Flash graphics APIs: + /** + * Clears all drawing instructions, effectively resetting this Graphics instance. Any line and fill styles will need + * to be redefined to draw shapes following a clear call. A tiny API method "c" also exists. + * @method clear + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.clear = function () { + this._instructions.length = this._activeInstructions.length = this._commitIndex = 0; + this._strokeStyle = this._oldStrokeStyle = this._stroke = this._fill = this._strokeDash = this._oldStrokeDash = null; + this._dirty = this._strokeIgnoreScale = false; + return this; + }; + + /** + * Begins a fill with the specified color. This ends the current sub-path. A tiny API method "f" also exists. + * @method beginFill + * @param {String} color A CSS compatible color value (ex. "red", "#FF0000", or "rgba(255,0,0,0.5)"). Setting to + * null will result in no fill. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.beginFill = function (color) { + return this._setFill(color ? new G.Fill(color) : null); + }; + + /** + * Begins a linear gradient fill defined by the line (x0, y0) to (x1, y1). This ends the current sub-path. For + * example, the following code defines a black to white vertical gradient ranging from 20px to 120px, and draws a + * square to display it: + * + * myGraphics.beginLinearGradientFill(["#000","#FFF"], [0, 1], 0, 20, 0, 120).drawRect(20, 20, 120, 120); + * + * A tiny API method "lf" also exists. + * @method beginLinearGradientFill + * @param {Array} colors An array of CSS compatible color values. For example, ["#F00","#00F"] would define a gradient + * drawing from red to blue. + * @param {Array} ratios An array of gradient positions which correspond to the colors. For example, [0.1, 0.9] would draw + * the first color to 10% then interpolating to the second color at 90%. + * @param {Number} x0 The position of the first point defining the line that defines the gradient direction and size. + * @param {Number} y0 The position of the first point defining the line that defines the gradient direction and size. + * @param {Number} x1 The position of the second point defining the line that defines the gradient direction and size. + * @param {Number} y1 The position of the second point defining the line that defines the gradient direction and size. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.beginLinearGradientFill = function (colors, ratios, x0, y0, x1, y1) { + return this._setFill(new G.Fill().linearGradient(colors, ratios, x0, y0, x1, y1)); + }; + + /** + * Begins a radial gradient fill. This ends the current sub-path. For example, the following code defines a red to + * blue radial gradient centered at (100, 100), with a radius of 50, and draws a circle to display it: + * + * myGraphics.beginRadialGradientFill(["#F00","#00F"], [0, 1], 100, 100, 0, 100, 100, 50).drawCircle(100, 100, 50); + * + * A tiny API method "rf" also exists. + * @method beginRadialGradientFill + * @param {Array} colors An array of CSS compatible color values. For example, ["#F00","#00F"] would define + * a gradient drawing from red to blue. + * @param {Array} ratios An array of gradient positions which correspond to the colors. For example, [0.1, + * 0.9] would draw the first color to 10% then interpolating to the second color at 90%. + * @param {Number} x0 Center position of the inner circle that defines the gradient. + * @param {Number} y0 Center position of the inner circle that defines the gradient. + * @param {Number} r0 Radius of the inner circle that defines the gradient. + * @param {Number} x1 Center position of the outer circle that defines the gradient. + * @param {Number} y1 Center position of the outer circle that defines the gradient. + * @param {Number} r1 Radius of the outer circle that defines the gradient. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.beginRadialGradientFill = function (colors, ratios, x0, y0, r0, x1, y1, r1) { + return this._setFill(new G.Fill().radialGradient(colors, ratios, x0, y0, r0, x1, y1, r1)); + }; + + /** + * Begins a pattern fill using the specified image. This ends the current sub-path. A tiny API method "bf" also + * exists. + * @method beginBitmapFill + * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement} image The Image, Canvas, or Video object to use + * as the pattern. Must be loaded prior to creating a bitmap fill, or the fill will be empty. + * @param {String} repetition Optional. Indicates whether to repeat the image in the fill area. One of "repeat", + * "repeat-x", "repeat-y", or "no-repeat". Defaults to "repeat". Note that Firefox does not support "repeat-x" or + * "repeat-y" (latest tests were in FF 20.0), and will default to "repeat". + * @param {Matrix2D} matrix Optional. Specifies a transformation matrix for the bitmap fill. This transformation + * will be applied relative to the parent transform. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.beginBitmapFill = function (image, repetition, matrix) { + return this._setFill(new G.Fill(null, matrix).bitmap(image, repetition)); + }; + + /** + * Ends the current sub-path, and begins a new one with no fill. Functionally identical to beginFill(null). + * A tiny API method "ef" also exists. + * @method endFill + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.endFill = function () { + return this.beginFill(); + }; + + /** + * Sets the stroke style. Like all drawing methods, this can be chained, so you can define + * the stroke style and color in a single line of code like so: + * + * myGraphics.setStrokeStyle(8,"round").beginStroke("#F00"); + * + * A tiny API method "ss" also exists. + * @method setStrokeStyle + * @param {Number} thickness The width of the stroke. + * @param {String | Number} [caps=0] Indicates the type of caps to use at the end of lines. One of butt, + * round, or square. Defaults to "butt". Also accepts the values 0 (butt), 1 (round), and 2 (square) for use with + * the tiny API. + * @param {String | Number} [joints=0] Specifies the type of joints that should be used where two lines meet. + * One of bevel, round, or miter. Defaults to "miter". Also accepts the values 0 (miter), 1 (round), and 2 (bevel) + * for use with the tiny API. + * @param {Number} [miterLimit=10] If joints is set to "miter", then you can specify a miter limit ratio which + * controls at what point a mitered joint will be clipped. + * @param {Boolean} [ignoreScale=false] If true, the stroke will be drawn at the specified thickness regardless + * of active transformations. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.setStrokeStyle = function (thickness, caps, joints, miterLimit, ignoreScale) { + this._updateInstructions(true); + this._strokeStyle = this.command = new G.StrokeStyle(thickness, caps, joints, miterLimit, ignoreScale); + + // ignoreScale lives on Stroke, not StrokeStyle, so we do a little trickery: + if (this._stroke) { + this._stroke.ignoreScale = ignoreScale; + } + this._strokeIgnoreScale = ignoreScale; + return this; + }; + + /** + * Sets or clears the stroke dash pattern. + * + * myGraphics.setStrokeDash([20, 10], 0); + * + * A tiny API method `sd` also exists. + * @method setStrokeDash + * @param {Array} [segments] An array specifying the dash pattern, alternating between line and gap. + * For example, `[20,10]` would create a pattern of 20 pixel lines with 10 pixel gaps between them. + * Passing null or an empty array will clear the existing stroke dash. + * @param {Number} [offset=0] The offset of the dash pattern. For example, you could increment this value to create a "marching ants" effect. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.setStrokeDash = function (segments, offset) { + this._updateInstructions(true); + this._strokeDash = this.command = new G.StrokeDash(segments, offset); + return this; + }; + + /** + * Begins a stroke with the specified color. This ends the current sub-path. A tiny API method "s" also exists. + * @method beginStroke + * @param {String} color A CSS compatible color value (ex. "#FF0000", "red", or "rgba(255,0,0,0.5)"). Setting to + * null will result in no stroke. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.beginStroke = function (color) { + return this._setStroke(color ? new G.Stroke(color) : null); + }; + + /** + * Begins a linear gradient stroke defined by the line (x0, y0) to (x1, y1). This ends the current sub-path. For + * example, the following code defines a black to white vertical gradient ranging from 20px to 120px, and draws a + * square to display it: + * + * myGraphics.setStrokeStyle(10). + * beginLinearGradientStroke(["#000","#FFF"], [0, 1], 0, 20, 0, 120).drawRect(20, 20, 120, 120); + * + * A tiny API method "ls" also exists. + * @method beginLinearGradientStroke + * @param {Array} colors An array of CSS compatible color values. For example, ["#F00","#00F"] would define + * a gradient drawing from red to blue. + * @param {Array} ratios An array of gradient positions which correspond to the colors. For example, [0.1, + * 0.9] would draw the first color to 10% then interpolating to the second color at 90%. + * @param {Number} x0 The position of the first point defining the line that defines the gradient direction and size. + * @param {Number} y0 The position of the first point defining the line that defines the gradient direction and size. + * @param {Number} x1 The position of the second point defining the line that defines the gradient direction and size. + * @param {Number} y1 The position of the second point defining the line that defines the gradient direction and size. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.beginLinearGradientStroke = function (colors, ratios, x0, y0, x1, y1) { + return this._setStroke(new G.Stroke().linearGradient(colors, ratios, x0, y0, x1, y1)); + }; + + /** + * Begins a radial gradient stroke. This ends the current sub-path. For example, the following code defines a red to + * blue radial gradient centered at (100, 100), with a radius of 50, and draws a rectangle to display it: + * + * myGraphics.setStrokeStyle(10) + * .beginRadialGradientStroke(["#F00","#00F"], [0, 1], 100, 100, 0, 100, 100, 50) + * .drawRect(50, 90, 150, 110); + * + * A tiny API method "rs" also exists. + * @method beginRadialGradientStroke + * @param {Array} colors An array of CSS compatible color values. For example, ["#F00","#00F"] would define + * a gradient drawing from red to blue. + * @param {Array} ratios An array of gradient positions which correspond to the colors. For example, [0.1, + * 0.9] would draw the first color to 10% then interpolating to the second color at 90%, then draw the second color + * to 100%. + * @param {Number} x0 Center position of the inner circle that defines the gradient. + * @param {Number} y0 Center position of the inner circle that defines the gradient. + * @param {Number} r0 Radius of the inner circle that defines the gradient. + * @param {Number} x1 Center position of the outer circle that defines the gradient. + * @param {Number} y1 Center position of the outer circle that defines the gradient. + * @param {Number} r1 Radius of the outer circle that defines the gradient. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.beginRadialGradientStroke = function (colors, ratios, x0, y0, r0, x1, y1, r1) { + return this._setStroke(new G.Stroke().radialGradient(colors, ratios, x0, y0, r0, x1, y1, r1)); + }; + + /** + * Begins a pattern fill using the specified image. This ends the current sub-path. Note that unlike bitmap fills, + * strokes do not currently support a matrix parameter due to limitations in the canvas API. A tiny API method "bs" + * also exists. + * @method beginBitmapStroke + * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement} image The Image, Canvas, or Video object to use + * as the pattern. Must be loaded prior to creating a bitmap fill, or the fill will be empty. + * @param {String} [repetition=repeat] Optional. Indicates whether to repeat the image in the fill area. One of + * "repeat", "repeat-x", "repeat-y", or "no-repeat". Defaults to "repeat". + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.beginBitmapStroke = function (image, repetition) { + // NOTE: matrix is not supported for stroke because transforms on strokes also affect the drawn stroke width. + return this._setStroke(new G.Stroke().bitmap(image, repetition)); + }; + + /** + * Ends the current sub-path, and begins a new one with no stroke. Functionally identical to beginStroke(null). + * A tiny API method "es" also exists. + * @method endStroke + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.endStroke = function () { + return this.beginStroke(); + }; + + /** + * Maps the familiar ActionScript curveTo() method to the functionally similar {{#crossLink "Graphics/quadraticCurveTo"}}{{/crossLink}} + * method. + * @method quadraticCurveTo + * @param {Number} cpx + * @param {Number} cpy + * @param {Number} x + * @param {Number} y + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.curveTo = p.quadraticCurveTo; + + /** + * + * Maps the familiar ActionScript drawRect() method to the functionally similar {{#crossLink "Graphics/rect"}}{{/crossLink}} + * method. + * @method drawRect + * @param {Number} x + * @param {Number} y + * @param {Number} w Width of the rectangle + * @param {Number} h Height of the rectangle + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.drawRect = p.rect; + + /** + * Draws a rounded rectangle with all corners with the specified radius. + * @method drawRoundRect + * @param {Number} x + * @param {Number} y + * @param {Number} w + * @param {Number} h + * @param {Number} radius Corner radius. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.drawRoundRect = function (x, y, w, h, radius) { + return this.drawRoundRectComplex(x, y, w, h, radius, radius, radius, radius); + }; + + /** + * Draws a rounded rectangle with different corner radii. Supports positive and negative corner radii. A tiny API + * method "rc" also exists. + * @method drawRoundRectComplex + * @param {Number} x The horizontal coordinate to draw the round rect. + * @param {Number} y The vertical coordinate to draw the round rect. + * @param {Number} w The width of the round rect. + * @param {Number} h The height of the round rect. + * @param {Number} radiusTL Top left corner radius. + * @param {Number} radiusTR Top right corner radius. + * @param {Number} radiusBR Bottom right corner radius. + * @param {Number} radiusBL Bottom left corner radius. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.drawRoundRectComplex = function (x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL) { + return this.append(new G.RoundRect(x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL)); + }; + + /** + * Draws a circle with the specified radius at (x, y). + * + * var g = new createjs.Graphics(); + * g.setStrokeStyle(1); + * g.beginStroke(createjs.Graphics.getRGB(0,0,0)); + * g.beginFill(createjs.Graphics.getRGB(255,0,0)); + * g.drawCircle(0,0,3); + * + * var s = new createjs.Shape(g); + * s.x = 100; + * s.y = 100; + * + * stage.addChild(s); + * stage.update(); + * + * A tiny API method "dc" also exists. + * @method drawCircle + * @param {Number} x x coordinate center point of circle. + * @param {Number} y y coordinate center point of circle. + * @param {Number} radius Radius of circle. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.drawCircle = function (x, y, radius) { + return this.append(new G.Circle(x, y, radius)); + }; + + /** + * Draws an ellipse (oval) with a specified width (w) and height (h). Similar to {{#crossLink "Graphics/drawCircle"}}{{/crossLink}}, + * except the width and height can be different. A tiny API method "de" also exists. + * @method drawEllipse + * @param {Number} x The left coordinate point of the ellipse. Note that this is different from {{#crossLink "Graphics/drawCircle"}}{{/crossLink}} + * which draws from center. + * @param {Number} y The top coordinate point of the ellipse. Note that this is different from {{#crossLink "Graphics/drawCircle"}}{{/crossLink}} + * which draws from the center. + * @param {Number} w The height (horizontal diameter) of the ellipse. The horizontal radius will be half of this + * number. + * @param {Number} h The width (vertical diameter) of the ellipse. The vertical radius will be half of this number. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.drawEllipse = function (x, y, w, h) { + return this.append(new G.Ellipse(x, y, w, h)); + }; + + /** + * Draws a star if pointSize is greater than 0, or a regular polygon if pointSize is 0 with the specified number of + * points. For example, the following code will draw a familiar 5 pointed star shape centered at 100, 100 and with a + * radius of 50: + * + * myGraphics.beginFill("#FF0").drawPolyStar(100, 100, 50, 5, 0.6, -90); + * // Note: -90 makes the first point vertical + * + * A tiny API method "dp" also exists. + * + * @method drawPolyStar + * @param {Number} x Position of the center of the shape. + * @param {Number} y Position of the center of the shape. + * @param {Number} radius The outer radius of the shape. + * @param {Number} sides The number of points on the star or sides on the polygon. + * @param {Number} pointSize The depth or "pointy-ness" of the star points. A pointSize of 0 will draw a regular + * polygon (no points), a pointSize of 1 will draw nothing because the points are infinitely pointy. + * @param {Number} angle The angle of the first point / corner. For example a value of 0 will draw the first point + * directly to the right of the center. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.drawPolyStar = function (x, y, radius, sides, pointSize, angle) { + return this.append(new G.PolyStar(x, y, radius, sides, pointSize, angle)); + }; + + // TODO: deprecated. + /** + * Removed in favour of using custom command objects with {{#crossLink "Graphics/append"}}{{/crossLink}}. + * @method inject + * @deprecated + **/ + + /** + * Appends a graphics command object to the graphics queue. Command objects expose an "exec" method + * that accepts two parameters: the Context2D to operate on, and an arbitrary data object passed into + * {{#crossLink "Graphics/draw"}}{{/crossLink}}. The latter will usually be the Shape instance that called draw. + * + * This method is used internally by Graphics methods, such as drawCircle, but can also be used directly to insert + * built-in or custom graphics commands. For example: + * + * // attach data to our shape, so we can access it during the draw: + * myShape.color = "red"; + * + * // append a Circle command object: + * myShape.graphics.append(new createjs.Graphics.Circle(50, 50, 30)); + * + * // append a custom command object with an exec method that sets the fill style + * // based on the shape's data, and then fills the circle. + * myShape.graphics.append({exec:function(ctx, shape) { + * ctx.fillStyle = shape.color; + * ctx.fill(); + * }}); + * + * @method append + * @param {Object} command A graphics command object exposing an "exec" method. + * @param {boolean} clean The clean param is primarily for internal use. A value of true indicates that a command does not generate a path that should be stroked or filled. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.append = function (command, clean) { + this._activeInstructions.push(command); + this.command = command; + if (!clean) { + this._dirty = true; + } + return this; + }; + + /** + * Decodes a compact encoded path string into a series of draw instructions. + * This format is not intended to be human readable, and is meant for use by authoring tools. + * The format uses a base64 character set, with each character representing 6 bits, to define a series of draw + * commands. + * + * Each command is comprised of a single "header" character followed by a variable number of alternating x and y + * position values. Reading the header bits from left to right (most to least significant): bits 1 to 3 specify the + * type of operation (0-moveTo, 1-lineTo, 2-quadraticCurveTo, 3-bezierCurveTo, 4-closePath, 5-7 unused). Bit 4 + * indicates whether position values use 12 bits (2 characters) or 18 bits (3 characters), with a one indicating the + * latter. Bits 5 and 6 are currently unused. + * + * Following the header is a series of 0 (closePath), 2 (moveTo, lineTo), 4 (quadraticCurveTo), or 6 (bezierCurveTo) + * parameters. These parameters are alternating x/y positions represented by 2 or 3 characters (as indicated by the + * 4th bit in the command char). These characters consist of a 1 bit sign (1 is negative, 0 is positive), followed + * by an 11 (2 char) or 17 (3 char) bit integer value. All position values are in tenths of a pixel. Except in the + * case of move operations which are absolute, this value is a delta from the previous x or y position (as + * appropriate). + * + * For example, the string "A3cAAMAu4AAA" represents a line starting at -150,0 and ending at 150,0. + *
A - bits 000000. First 3 bits (000) indicate a moveTo operation. 4th bit (0) indicates 2 chars per + * parameter. + *
n0 - 110111011100. Absolute x position of -150.0px. First bit indicates a negative value, remaining bits + * indicate 1500 tenths of a pixel. + *
AA - 000000000000. Absolute y position of 0. + *
I - 001100. First 3 bits (001) indicate a lineTo operation. 4th bit (1) indicates 3 chars per parameter. + *
Au4 - 000000101110111000. An x delta of 300.0px, which is added to the previous x value of -150.0px to + * provide an absolute position of +150.0px. + *
AAA - 000000000000000000. A y delta value of 0. + * + * A tiny API method "p" also exists. + * @method decodePath + * @param {String} str The path string to decode. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.decodePath = function (str) { + var instructions = [this.moveTo, this.lineTo, this.quadraticCurveTo, this.bezierCurveTo, this.closePath]; + var paramCount = [2, 2, 4, 6, 0]; + var i = 0, + l = str.length; + var params = []; + var x = 0, + y = 0; + var base64 = Graphics.BASE_64; + + while (i < l) { + var c = str.charAt(i); + var n = base64[c]; + var fi = n >> 3; // highest order bits 1-3 code for operation. + var f = instructions[fi]; + // check that we have a valid instruction & that the unused bits are empty: + if (!f || n & 3) { + throw "bad path data (@" + i + "): " + c; + } + var pl = paramCount[fi]; + if (!fi) { + x = y = 0; + } // move operations reset the position. + params.length = 0; + i++; + var charCount = (n >> 2 & 1) + 2; // 4th header bit indicates number size for this operation. + for (var p = 0; p < pl; p++) { + var num = base64[str.charAt(i)]; + var sign = num >> 5 ? -1 : 1; + num = (num & 31) << 6 | base64[str.charAt(i + 1)]; + if (charCount == 3) { + num = num << 6 | base64[str.charAt(i + 2)]; + } + num = sign * num / 10; + if (p % 2) { + x = num += x; + } else { + y = num += y; + } + params[p] = num; + i += charCount; + } + f.apply(this, params); + } + return this; + }; + + /** + * Stores all graphics commands so they won't be executed in future draws. Calling store() a second time adds to + * the existing store. This also affects `drawAsPath()`. + * + * This is useful in cases where you are creating vector graphics in an iterative manner (ex. generative art), so + * that only new graphics need to be drawn (which can provide huge performance benefits), but you wish to retain all + * of the vector instructions for later use (ex. scaling, modifying, or exporting). + * + * Note that calling store() will force the active path (if any) to be ended in a manner similar to changing + * the fill or stroke. + * + * For example, consider a application where the user draws lines with the mouse. As each line segment (or collection of + * segments) are added to a Shape, it can be rasterized using {{#crossLink "DisplayObject/updateCache"}}{{/crossLink}}, + * and then stored, so that it can be redrawn at a different scale when the application is resized, or exported to SVG. + * + * // set up cache: + * myShape.cache(0,0,500,500,scale); + * + * // when the user drags, draw a new line: + * myShape.graphics.moveTo(oldX,oldY).lineTo(newX,newY); + * // then draw it into the existing cache: + * myShape.updateCache("source-over"); + * // store the new line, so it isn't redrawn next time: + * myShape.store(); + * + * // then, when the window resizes, we can re-render at a different scale: + * // first, unstore all our lines: + * myShape.unstore(); + * // then cache using the new scale: + * myShape.cache(0,0,500,500,newScale); + * // finally, store the existing commands again: + * myShape.store(); + * + * @method store + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.store = function () { + this._updateInstructions(true); + this._storeIndex = this._instructions.length; + return this; + }; + + /** + * Unstores any graphics commands that were previously stored using {{#crossLink "Graphics/store"}}{{/crossLink}} + * so that they will be executed in subsequent draw calls. + * + * @method unstore + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.unstore = function () { + this._storeIndex = 0; + return this; + }; + + /** + * Returns a clone of this Graphics instance. Note that the individual command objects are not cloned. + * @method clone + * @return {Graphics} A clone of the current Graphics instance. + **/ + p.clone = function () { + var o = new Graphics(); + o.command = this.command; + o._stroke = this._stroke; + o._strokeStyle = this._strokeStyle; + o._strokeDash = this._strokeDash; + o._strokeIgnoreScale = this._strokeIgnoreScale; + o._fill = this._fill; + o._instructions = this._instructions.slice(); + o._commitIndex = this._commitIndex; + o._activeInstructions = this._activeInstructions.slice(); + o._dirty = this._dirty; + o._storeIndex = this._storeIndex; + return o; + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[Graphics]"; + }; + + // tiny API: + /** + * Shortcut to moveTo. + * @method mt + * @param {Number} x The x coordinate the drawing point should move to. + * @param {Number} y The y coordinate the drawing point should move to. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls). + * @chainable + * @protected + **/ + p.mt = p.moveTo; + + /** + * Shortcut to lineTo. + * @method lt + * @param {Number} x The x coordinate the drawing point should draw to. + * @param {Number} y The y coordinate the drawing point should draw to. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.lt = p.lineTo; + + /** + * Shortcut to arcTo. + * @method at + * @param {Number} x1 + * @param {Number} y1 + * @param {Number} x2 + * @param {Number} y2 + * @param {Number} radius + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.at = p.arcTo; + + /** + * Shortcut to bezierCurveTo. + * @method bt + * @param {Number} cp1x + * @param {Number} cp1y + * @param {Number} cp2x + * @param {Number} cp2y + * @param {Number} x + * @param {Number} y + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.bt = p.bezierCurveTo; + + /** + * Shortcut to quadraticCurveTo / curveTo. + * @method qt + * @param {Number} cpx + * @param {Number} cpy + * @param {Number} x + * @param {Number} y + * @protected + * @chainable + **/ + p.qt = p.quadraticCurveTo; + + /** + * Shortcut to arc. + * @method a + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @param {Number} startAngle Measured in radians. + * @param {Number} endAngle Measured in radians. + * @param {Boolean} anticlockwise + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @protected + * @chainable + **/ + p.a = p.arc; + + /** + * Shortcut to rect. + * @method r + * @param {Number} x + * @param {Number} y + * @param {Number} w Width of the rectangle + * @param {Number} h Height of the rectangle + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.r = p.rect; + + /** + * Shortcut to closePath. + * @method cp + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.cp = p.closePath; + + /** + * Shortcut to clear. + * @method c + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.c = p.clear; + + /** + * Shortcut to beginFill. + * @method f + * @param {String} color A CSS compatible color value (ex. "red", "#FF0000", or "rgba(255,0,0,0.5)"). Setting to + * null will result in no fill. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.f = p.beginFill; + + /** + * Shortcut to beginLinearGradientFill. + * @method lf + * @param {Array} colors An array of CSS compatible color values. For example, ["#F00","#00F"] would define a gradient + * drawing from red to blue. + * @param {Array} ratios An array of gradient positions which correspond to the colors. For example, [0.1, 0.9] would draw + * the first color to 10% then interpolating to the second color at 90%. + * @param {Number} x0 The position of the first point defining the line that defines the gradient direction and size. + * @param {Number} y0 The position of the first point defining the line that defines the gradient direction and size. + * @param {Number} x1 The position of the second point defining the line that defines the gradient direction and size. + * @param {Number} y1 The position of the second point defining the line that defines the gradient direction and size. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.lf = p.beginLinearGradientFill; + + /** + * Shortcut to beginRadialGradientFill. + * @method rf + * @param {Array} colors An array of CSS compatible color values. For example, ["#F00","#00F"] would define + * a gradient drawing from red to blue. + * @param {Array} ratios An array of gradient positions which correspond to the colors. For example, [0.1, + * 0.9] would draw the first color to 10% then interpolating to the second color at 90%. + * @param {Number} x0 Center position of the inner circle that defines the gradient. + * @param {Number} y0 Center position of the inner circle that defines the gradient. + * @param {Number} r0 Radius of the inner circle that defines the gradient. + * @param {Number} x1 Center position of the outer circle that defines the gradient. + * @param {Number} y1 Center position of the outer circle that defines the gradient. + * @param {Number} r1 Radius of the outer circle that defines the gradient. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.rf = p.beginRadialGradientFill; + + /** + * Shortcut to beginBitmapFill. + * @method bf + * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement} image The Image, Canvas, or Video object to use + * as the pattern. + * @param {String} repetition Optional. Indicates whether to repeat the image in the fill area. One of "repeat", + * "repeat-x", "repeat-y", or "no-repeat". Defaults to "repeat". Note that Firefox does not support "repeat-x" or + * "repeat-y" (latest tests were in FF 20.0), and will default to "repeat". + * @param {Matrix2D} matrix Optional. Specifies a transformation matrix for the bitmap fill. This transformation + * will be applied relative to the parent transform. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.bf = p.beginBitmapFill; + + /** + * Shortcut to endFill. + * @method ef + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.ef = p.endFill; + + /** + * Shortcut to setStrokeStyle. + * @method ss + * @param {Number} thickness The width of the stroke. + * @param {String | Number} [caps=0] Indicates the type of caps to use at the end of lines. One of butt, + * round, or square. Defaults to "butt". Also accepts the values 0 (butt), 1 (round), and 2 (square) for use with + * the tiny API. + * @param {String | Number} [joints=0] Specifies the type of joints that should be used where two lines meet. + * One of bevel, round, or miter. Defaults to "miter". Also accepts the values 0 (miter), 1 (round), and 2 (bevel) + * for use with the tiny API. + * @param {Number} [miterLimit=10] If joints is set to "miter", then you can specify a miter limit ratio which + * controls at what point a mitered joint will be clipped. + * @param {Boolean} [ignoreScale=false] If true, the stroke will be drawn at the specified thickness regardless + * of active transformations. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.ss = p.setStrokeStyle; + + /** + * Shortcut to setStrokeDash. + * @method sd + * @param {Array} [segments] An array specifying the dash pattern, alternating between line and gap. + * For example, [20,10] would create a pattern of 20 pixel lines with 10 pixel gaps between them. + * Passing null or an empty array will clear any existing dash. + * @param {Number} [offset=0] The offset of the dash pattern. For example, you could increment this value to create a "marching ants" effect. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.sd = p.setStrokeDash; + + /** + * Shortcut to beginStroke. + * @method s + * @param {String} color A CSS compatible color value (ex. "#FF0000", "red", or "rgba(255,0,0,0.5)"). Setting to + * null will result in no stroke. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.s = p.beginStroke; + + /** + * Shortcut to beginLinearGradientStroke. + * @method ls + * @param {Array} colors An array of CSS compatible color values. For example, ["#F00","#00F"] would define + * a gradient drawing from red to blue. + * @param {Array} ratios An array of gradient positions which correspond to the colors. For example, [0.1, + * 0.9] would draw the first color to 10% then interpolating to the second color at 90%. + * @param {Number} x0 The position of the first point defining the line that defines the gradient direction and size. + * @param {Number} y0 The position of the first point defining the line that defines the gradient direction and size. + * @param {Number} x1 The position of the second point defining the line that defines the gradient direction and size. + * @param {Number} y1 The position of the second point defining the line that defines the gradient direction and size. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.ls = p.beginLinearGradientStroke; + + /** + * Shortcut to beginRadialGradientStroke. + * @method rs + * @param {Array} colors An array of CSS compatible color values. For example, ["#F00","#00F"] would define + * a gradient drawing from red to blue. + * @param {Array} ratios An array of gradient positions which correspond to the colors. For example, [0.1, + * 0.9] would draw the first color to 10% then interpolating to the second color at 90%, then draw the second color + * to 100%. + * @param {Number} x0 Center position of the inner circle that defines the gradient. + * @param {Number} y0 Center position of the inner circle that defines the gradient. + * @param {Number} r0 Radius of the inner circle that defines the gradient. + * @param {Number} x1 Center position of the outer circle that defines the gradient. + * @param {Number} y1 Center position of the outer circle that defines the gradient. + * @param {Number} r1 Radius of the outer circle that defines the gradient. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.rs = p.beginRadialGradientStroke; + + /** + * Shortcut to beginBitmapStroke. + * @method bs + * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement} image The Image, Canvas, or Video object to use + * as the pattern. + * @param {String} [repetition=repeat] Optional. Indicates whether to repeat the image in the fill area. One of + * "repeat", "repeat-x", "repeat-y", or "no-repeat". Defaults to "repeat". + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.bs = p.beginBitmapStroke; + + /** + * Shortcut to endStroke. + * @method es + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.es = p.endStroke; + + /** + * Shortcut to drawRect. + * @method dr + * @param {Number} x + * @param {Number} y + * @param {Number} w Width of the rectangle + * @param {Number} h Height of the rectangle + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.dr = p.drawRect; + + /** + * Shortcut to drawRoundRect. + * @method rr + * @param {Number} x + * @param {Number} y + * @param {Number} w + * @param {Number} h + * @param {Number} radius Corner radius. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.rr = p.drawRoundRect; + + /** + * Shortcut to drawRoundRectComplex. + * @method rc + * @param {Number} x The horizontal coordinate to draw the round rect. + * @param {Number} y The vertical coordinate to draw the round rect. + * @param {Number} w The width of the round rect. + * @param {Number} h The height of the round rect. + * @param {Number} radiusTL Top left corner radius. + * @param {Number} radiusTR Top right corner radius. + * @param {Number} radiusBR Bottom right corner radius. + * @param {Number} radiusBL Bottom left corner radius. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.rc = p.drawRoundRectComplex; + + /** + * Shortcut to drawCircle. + * @method dc + * @param {Number} x x coordinate center point of circle. + * @param {Number} y y coordinate center point of circle. + * @param {Number} radius Radius of circle. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.dc = p.drawCircle; + + /** + * Shortcut to drawEllipse. + * @method de + * @param {Number} x The left coordinate point of the ellipse. Note that this is different from {{#crossLink "Graphics/drawCircle"}}{{/crossLink}} + * which draws from center. + * @param {Number} y The top coordinate point of the ellipse. Note that this is different from {{#crossLink "Graphics/drawCircle"}}{{/crossLink}} + * which draws from the center. + * @param {Number} w The height (horizontal diameter) of the ellipse. The horizontal radius will be half of this + * number. + * @param {Number} h The width (vertical diameter) of the ellipse. The vertical radius will be half of this number. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.de = p.drawEllipse; + + /** + * Shortcut to drawPolyStar. + * @method dp + * @param {Number} x Position of the center of the shape. + * @param {Number} y Position of the center of the shape. + * @param {Number} radius The outer radius of the shape. + * @param {Number} sides The number of points on the star or sides on the polygon. + * @param {Number} pointSize The depth or "pointy-ness" of the star points. A pointSize of 0 will draw a regular + * polygon (no points), a pointSize of 1 will draw nothing because the points are infinitely pointy. + * @param {Number} angle The angle of the first point / corner. For example a value of 0 will draw the first point + * directly to the right of the center. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.dp = p.drawPolyStar; + + /** + * Shortcut to decodePath. + * @method p + * @param {String} str The path string to decode. + * @return {Graphics} The Graphics instance the method is called on (useful for chaining calls.) + * @chainable + * @protected + **/ + p.p = p.decodePath; + + // private methods: + /** + * @method _updateInstructions + * @param commit + * @protected + **/ + p._updateInstructions = function (commit) { + var instr = this._instructions, + active = this._activeInstructions, + commitIndex = this._commitIndex; + + if (this._dirty && active.length) { + instr.length = commitIndex; // remove old, uncommitted commands + instr.push(Graphics.beginCmd); + + var l = active.length, + ll = instr.length; + instr.length = ll + l; + for (var i = 0; i < l; i++) { + instr[i + ll] = active[i]; + } + + if (this._fill) { + instr.push(this._fill); + } + if (this._stroke) { + // doesn't need to be re-applied if it hasn't changed. + if (this._strokeDash !== this._oldStrokeDash) { + this._oldStrokeDash = this._strokeDash; + instr.push(this._strokeDash); + } + if (this._strokeStyle !== this._oldStrokeStyle) { + this._oldStrokeStyle = this._strokeStyle; + instr.push(this._strokeStyle); + } + instr.push(this._stroke); + } + + this._dirty = false; + } + + if (commit) { + active.length = 0; + this._commitIndex = instr.length; + } + }; + + /** + * @method _setFill + * @param fill + * @protected + **/ + p._setFill = function (fill) { + this._updateInstructions(true); + this.command = this._fill = fill; + return this; + }; + + /** + * @method _setStroke + * @param stroke + * @protected + **/ + p._setStroke = function (stroke) { + this._updateInstructions(true); + if (this.command = this._stroke = stroke) { + stroke.ignoreScale = this._strokeIgnoreScale; + } + return this; + }; + + // Command Objects: + /** + * @namespace Graphics + */ + /** + * Graphics command object. See {{#crossLink "Graphics/lineTo"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. See {{#crossLink "Graphics"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class LineTo + * @constructor + * @param {Number} x + * @param {Number} y + **/ + /** + * @property x + * @type Number + */ + /** + * @property y + * @type Number + */ + /** + * Execute the Graphics command in the provided Canvas context. + * @method exec + * @param {CanvasRenderingContext2D} ctx The canvas rendering context + */ + (G.LineTo = function (x, y) { + this.x = x;this.y = y; + }).prototype.exec = function (ctx) { + ctx.lineTo(this.x, this.y); + }; + + /** + * Graphics command object. See {{#crossLink "Graphics/moveTo"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class MoveTo + * @constructor + * @param {Number} x + * @param {Number} y + **/ + /** + * @property x + * @type Number + */ + /** + * @property y + * @type Number + */ + /** + * @method exec + * @param {CanvasRenderingContext2D} ctx + */ + (G.MoveTo = function (x, y) { + this.x = x;this.y = y; + }).prototype.exec = function (ctx) { + ctx.moveTo(this.x, this.y); + }; + + /** + * Graphics command object. See {{#crossLink "Graphics/arcTo"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class ArcTo + * @constructor + * @param {Number} x1 + * @param {Number} y1 + * @param {Number} x2 + * @param {Number} y2 + * @param {Number} radius + **/ + /** + * @property x1 + * @type Number + */ + /** + * @property y1 + * @type Number + */ + /** + * @property x2 + * @type Number + */ + /** + * @property y2 + * @type Number + */ + /** + * @property radius + * @type Number + */ + /** + * Execute the Graphics command in the provided Canvas context. + * @method exec + * @param {CanvasRenderingContext2D} ctx The canvas rendering context + */ + (G.ArcTo = function (x1, y1, x2, y2, radius) { + this.x1 = x1;this.y1 = y1; + this.x2 = x2;this.y2 = y2; + this.radius = radius; + }).prototype.exec = function (ctx) { + ctx.arcTo(this.x1, this.y1, this.x2, this.y2, this.radius); + }; + + /** + * Graphics command object. See {{#crossLink "Graphics/arc"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class Arc + * @constructor + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @param {Number} startAngle + * @param {Number} endAngle + * @param {Number} anticlockwise + **/ + /** + * @property x + * @type Number + */ + /** + * @property y + * @type Number + */ + /** + * @property radius + * @type Number + */ + /** + * @property startAngle + * @type Number + */ + /** + * @property endAngle + * @type Number + */ + /** + * @property anticlockwise + * @type Number + */ + /** + * Execute the Graphics command in the provided Canvas context. + * @method exec + * @param {CanvasRenderingContext2D} ctx The canvas rendering context + */ + (G.Arc = function (x, y, radius, startAngle, endAngle, anticlockwise) { + this.x = x;this.y = y; + this.radius = radius; + this.startAngle = startAngle;this.endAngle = endAngle; + this.anticlockwise = !!anticlockwise; + }).prototype.exec = function (ctx) { + ctx.arc(this.x, this.y, this.radius, this.startAngle, this.endAngle, this.anticlockwise); + }; + + /** + * Graphics command object. See {{#crossLink "Graphics/quadraticCurveTo"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class QuadraticCurveTo + * @constructor + * @param {Number} cpx + * @param {Number} cpy + * @param {Number} x + * @param {Number} y + **/ + /** + * @property cpx + * @type Number + */ + /** + * @property cpy + * @type Number + */ + /** + * @property x + * @type Number + */ + /** + * @property y + * @type Number + */ + /** + * Execute the Graphics command in the provided Canvas context. + * @method exec + * @param {CanvasRenderingContext2D} ctx The canvas rendering context + */ + (G.QuadraticCurveTo = function (cpx, cpy, x, y) { + this.cpx = cpx;this.cpy = cpy; + this.x = x;this.y = y; + }).prototype.exec = function (ctx) { + ctx.quadraticCurveTo(this.cpx, this.cpy, this.x, this.y); + }; + + /** + * Graphics command object. See {{#crossLink "Graphics/bezierCurveTo"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class BezierCurveTo + * @constructor + * @param {Number} cp1x + * @param {Number} cp1y + * @param {Number} cp2x + * @param {Number} cp2y + * @param {Number} x + * @param {Number} y + **/ + /** + * @property cp1x + * @type Number + */ + /** + * @property cp1y + * @type Number + */ + /** + * @property cp2x + * @type Number + */ + /** + * @property cp2y + * @type Number + */ + /** + * @property x + * @type Number + */ + /** + * @property y + * @type Number + */ + /** + * Execute the Graphics command in the provided Canvas context. + * @method exec + * @param {CanvasRenderingContext2D} ctx The canvas rendering context + */ + (G.BezierCurveTo = function (cp1x, cp1y, cp2x, cp2y, x, y) { + this.cp1x = cp1x;this.cp1y = cp1y; + this.cp2x = cp2x;this.cp2y = cp2y; + this.x = x;this.y = y; + }).prototype.exec = function (ctx) { + ctx.bezierCurveTo(this.cp1x, this.cp1y, this.cp2x, this.cp2y, this.x, this.y); + }; + + /** + * Graphics command object. See {{#crossLink "Graphics/rect"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class Rect + * @constructor + * @param {Number} x + * @param {Number} y + * @param {Number} w + * @param {Number} h + **/ + /** + * @property x + * @type Number + */ + /** + * @property y + * @type Number + */ + /** + * @property w + * @type Number + */ + /** + * @property h + * @type Number + */ + /** + * Execute the Graphics command in the provided Canvas context. + * @method exec + * @param {CanvasRenderingContext2D} ctx The canvas rendering context + */ + (G.Rect = function (x, y, w, h) { + this.x = x;this.y = y; + this.w = w;this.h = h; + }).prototype.exec = function (ctx) { + ctx.rect(this.x, this.y, this.w, this.h); + }; + + /** + * Graphics command object. See {{#crossLink "Graphics/closePath"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class ClosePath + * @constructor + **/ + /** + * Execute the Graphics command in the provided Canvas context. + * @method exec + * @param {CanvasRenderingContext2D} ctx The canvas rendering context + */ + (G.ClosePath = function () {}).prototype.exec = function (ctx) { + ctx.closePath(); + }; + + /** + * Graphics command object to begin a new path. See {{#crossLink "Graphics"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class BeginPath + * @constructor + **/ + /** + * Execute the Graphics command in the provided Canvas context. + * @method exec + * @param {CanvasRenderingContext2D} ctx The canvas rendering context + */ + (G.BeginPath = function () {}).prototype.exec = function (ctx) { + ctx.beginPath(); + }; + + /** + * Graphics command object. See {{#crossLink "Graphics/beginFill"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class Fill + * @constructor + * @param {Object} style A valid Context2D fillStyle. + * @param {Matrix2D} matrix + **/ + /** + * A valid Context2D fillStyle. + * @property style + * @type Object + */ + /** + * @property matrix + * @type Matrix2D + */ + /** + * Execute the Graphics command in the provided Canvas context. + * @method exec + * @param {CanvasRenderingContext2D} ctx The canvas rendering context + */ + p = (G.Fill = function (style, matrix) { + this.style = style; + this.matrix = matrix; + }).prototype; + p.exec = function (ctx) { + if (!this.style) { + return; + } + ctx.fillStyle = this.style; + var mtx = this.matrix; + if (mtx) { + ctx.save();ctx.transform(mtx.a, mtx.b, mtx.c, mtx.d, mtx.tx, mtx.ty); + } + ctx.fill(); + if (mtx) { + ctx.restore(); + } + }; + /** + * Creates a linear gradient style and assigns it to {{#crossLink "Fill/style:property"}}{{/crossLink}}. + * See {{#crossLink "Graphics/beginLinearGradientFill"}}{{/crossLink}} for more information. + * @method linearGradient + * @param {Array} colors + * + * @param {Array} ratios + * @param {Number} x0 + * @param {Number} y0 + * @param {Number} x1 + * @param {Number} y1 + * @return {Fill} Returns this Fill object for chaining or assignment. + */ + p.linearGradient = function (colors, ratios, x0, y0, x1, y1) { + var o = this.style = Graphics._ctx.createLinearGradient(x0, y0, x1, y1); + for (var i = 0, l = colors.length; i < l; i++) { + o.addColorStop(ratios[i], colors[i]); + } + o.props = { colors: colors, ratios: ratios, x0: x0, y0: y0, x1: x1, y1: y1, type: "linear" }; + return this; + }; + /** + * Creates a radial gradient style and assigns it to {{#crossLink "Fill/style:property"}}{{/crossLink}}. + * See {{#crossLink "Graphics/beginRadialGradientFill"}}{{/crossLink}} for more information. + * @method radialGradient + * @param {Array} colors + * @param {Array} ratios + * @param {Number} x0 + * @param {Number} y0 + * @param {Number} r0 + * @param {Number} x1 + * @param {Number} y1 + * @param {Number} r1 + * @return {Fill} Returns this Fill object for chaining or assignment. + */ + p.radialGradient = function (colors, ratios, x0, y0, r0, x1, y1, r1) { + var o = this.style = Graphics._ctx.createRadialGradient(x0, y0, r0, x1, y1, r1); + for (var i = 0, l = colors.length; i < l; i++) { + o.addColorStop(ratios[i], colors[i]); + } + o.props = { colors: colors, ratios: ratios, x0: x0, y0: y0, r0: r0, x1: x1, y1: y1, r1: r1, type: "radial" }; + return this; + }; + /** + * Creates a bitmap fill style and assigns it to the {{#crossLink "Fill/style:property"}}{{/crossLink}}. + * See {{#crossLink "Graphics/beginBitmapFill"}}{{/crossLink}} for more information. + * @method bitmap + * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement} image Must be loaded prior to creating a bitmap fill, or the fill will be empty. + * @param {String} [repetition] One of: repeat, repeat-x, repeat-y, or no-repeat. + * @return {Fill} Returns this Fill object for chaining or assignment. + */ + p.bitmap = function (image, repetition) { + if (image.naturalWidth || image.getContext || image.readyState >= 2) { + var o = this.style = Graphics._ctx.createPattern(image, repetition || ""); + o.props = { image: image, repetition: repetition, type: "bitmap" }; + } + return this; + }; + p.path = false; + + /** + * Graphics command object. See {{#crossLink "Graphics/beginStroke"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class Stroke + * @constructor + * @param {Object} style A valid Context2D fillStyle. + * @param {Boolean} ignoreScale + **/ + /** + * A valid Context2D strokeStyle. + * @property style + * @type Object + */ + /** + * @property ignoreScale + * @type Boolean + */ + /** + * Execute the Graphics command in the provided Canvas context. + * @method exec + * @param {CanvasRenderingContext2D} ctx The canvas rendering context + */ + p = (G.Stroke = function (style, ignoreScale) { + this.style = style; + this.ignoreScale = ignoreScale; + }).prototype; + p.exec = function (ctx) { + if (!this.style) { + return; + } + ctx.strokeStyle = this.style; + if (this.ignoreScale) { + ctx.save();ctx.setTransform(1, 0, 0, 1, 0, 0); + } + ctx.stroke(); + if (this.ignoreScale) { + ctx.restore(); + } + }; + /** + * Creates a linear gradient style and assigns it to {{#crossLink "Stroke/style:property"}}{{/crossLink}}. + * See {{#crossLink "Graphics/beginLinearGradientStroke"}}{{/crossLink}} for more information. + * @method linearGradient + * @param {Array} colors + * @param {Array} ratios + * @param {Number} x0 + * @param {Number} y0 + * @param {Number} x1 + * @param {Number} y1 + * @return {Fill} Returns this Stroke object for chaining or assignment. + */ + p.linearGradient = G.Fill.prototype.linearGradient; + /** + * Creates a radial gradient style and assigns it to {{#crossLink "Stroke/style:property"}}{{/crossLink}}. + * See {{#crossLink "Graphics/beginRadialGradientStroke"}}{{/crossLink}} for more information. + * @method radialGradient + * @param {Array} colors + * @param {Array} ratios + * @param {Number} x0 + * @param {Number} y0 + * @param {Number} r0 + * @param {Number} x1 + * @param {Number} y1 + * @param {Number} r1 + * @return {Fill} Returns this Stroke object for chaining or assignment. + */ + p.radialGradient = G.Fill.prototype.radialGradient; + /** + * Creates a bitmap fill style and assigns it to {{#crossLink "Stroke/style:property"}}{{/crossLink}}. + * See {{#crossLink "Graphics/beginBitmapStroke"}}{{/crossLink}} for more information. + * @method bitmap + * @param {HTMLImageElement} image + * @param {String} [repetition] One of: repeat, repeat-x, repeat-y, or no-repeat. + * @return {Fill} Returns this Stroke object for chaining or assignment. + */ + p.bitmap = G.Fill.prototype.bitmap; + p.path = false; + + /** + * Graphics command object. See {{#crossLink "Graphics/setStrokeStyle"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class StrokeStyle + * @constructor + * @param {Number} width + * @param {String} [caps=butt] + * @param {String} [joints=miter] + * @param {Number} [miterLimit=10] + * @param {Boolean} [ignoreScale=false] + **/ + /** + * @property width + * @type Number + */ + /** + * One of: butt, round, square + * @property caps + * @type String + */ + /** + * One of: round, bevel, miter + * @property joints + * @type String + */ + /** + * @property miterLimit + * @type Number + */ + /** + * Execute the Graphics command in the provided Canvas context. + * @method exec + * @param {CanvasRenderingContext2D} ctx The canvas rendering context + */ + p = (G.StrokeStyle = function (width, caps, joints, miterLimit, ignoreScale) { + this.width = width; + this.caps = caps; + this.joints = joints; + this.miterLimit = miterLimit; + this.ignoreScale = ignoreScale; + }).prototype; + p.exec = function (ctx) { + ctx.lineWidth = this.width == null ? "1" : this.width; + ctx.lineCap = this.caps == null ? "butt" : isNaN(this.caps) ? this.caps : Graphics.STROKE_CAPS_MAP[this.caps]; + ctx.lineJoin = this.joints == null ? "miter" : isNaN(this.joints) ? this.joints : Graphics.STROKE_JOINTS_MAP[this.joints]; + ctx.miterLimit = this.miterLimit == null ? "10" : this.miterLimit; + ctx.ignoreScale = this.ignoreScale == null ? false : this.ignoreScale; + }; + p.path = false; + + /** + * Graphics command object. See {{#crossLink "Graphics/setStrokeDash"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class StrokeDash + * @constructor + * @param {Array} [segments] + * @param {Number} [offset=0] + **/ + /** + * @property segments + * @type Array + */ + /** + * @property offset + * @type Number + */ + /** + * Execute the Graphics command in the provided Canvas context. + * @method exec + * @param {CanvasRenderingContext2D} ctx The canvas rendering context + */ + (G.StrokeDash = function (segments, offset) { + this.segments = segments; + this.offset = offset || 0; + }).prototype.exec = function (ctx) { + if (ctx.setLineDash) { + // feature detection. + ctx.setLineDash(this.segments || G.StrokeDash.EMPTY_SEGMENTS); // instead of [] to reduce churn. + ctx.lineDashOffset = this.offset || 0; + } + }; + /** + * The default value for segments (ie. no dash). + * @property EMPTY_SEGMENTS + * @static + * @final + * @readonly + * @protected + * @type {Array} + **/ + G.StrokeDash.EMPTY_SEGMENTS = []; + + /** + * Graphics command object. See {{#crossLink "Graphics/drawRoundRectComplex"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class RoundRect + * @constructor + * @param {Number} x + * @param {Number} y + * @param {Number} w + * @param {Number} h + * @param {Number} radiusTL + * @param {Number} radiusTR + * @param {Number} radiusBR + * @param {Number} radiusBL + **/ + /** + * @property x + * @type Number + */ + /** + * @property y + * @type Number + */ + /** + * @property w + * @type Number + */ + /** + * @property h + * @type Number + */ + /** + * @property radiusTL + * @type Number + */ + /** + * @property radiusTR + * @type Number + */ + /** + * @property radiusBR + * @type Number + */ + /** + * @property radiusBL + * @type Number + */ + /** + * Execute the Graphics command in the provided Canvas context. + * @method exec + * @param {CanvasRenderingContext2D} ctx The canvas rendering context + */ + (G.RoundRect = function (x, y, w, h, radiusTL, radiusTR, radiusBR, radiusBL) { + this.x = x;this.y = y; + this.w = w;this.h = h; + this.radiusTL = radiusTL;this.radiusTR = radiusTR; + this.radiusBR = radiusBR;this.radiusBL = radiusBL; + }).prototype.exec = function (ctx) { + var max = (w < h ? w : h) / 2; + var mTL = 0, + mTR = 0, + mBR = 0, + mBL = 0; + var x = this.x, + y = this.y, + w = this.w, + h = this.h; + var rTL = this.radiusTL, + rTR = this.radiusTR, + rBR = this.radiusBR, + rBL = this.radiusBL; + + if (rTL < 0) { + rTL *= mTL = -1; + } + if (rTL > max) { + rTL = max; + } + if (rTR < 0) { + rTR *= mTR = -1; + } + if (rTR > max) { + rTR = max; + } + if (rBR < 0) { + rBR *= mBR = -1; + } + if (rBR > max) { + rBR = max; + } + if (rBL < 0) { + rBL *= mBL = -1; + } + if (rBL > max) { + rBL = max; + } + + ctx.moveTo(x + w - rTR, y); + ctx.arcTo(x + w + rTR * mTR, y - rTR * mTR, x + w, y + rTR, rTR); + ctx.lineTo(x + w, y + h - rBR); + ctx.arcTo(x + w + rBR * mBR, y + h + rBR * mBR, x + w - rBR, y + h, rBR); + ctx.lineTo(x + rBL, y + h); + ctx.arcTo(x - rBL * mBL, y + h + rBL * mBL, x, y + h - rBL, rBL); + ctx.lineTo(x, y + rTL); + ctx.arcTo(x - rTL * mTL, y - rTL * mTL, x + rTL, y, rTL); + ctx.closePath(); + }; + + /** + * Graphics command object. See {{#crossLink "Graphics/drawCircle"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class Circle + * @constructor + * @param {Number} x + * @param {Number} y + * @param {Number} radius + **/ + /** + * @property x + * @type Number + */ + /** + * @property y + * @type Number + */ + /** + * @property radius + * @type Number + */ + /** + * Execute the Graphics command in the provided Canvas context. + * @method exec + * @param {CanvasRenderingContext2D} ctx The canvas rendering context + */ + (G.Circle = function (x, y, radius) { + this.x = x;this.y = y; + this.radius = radius; + }).prototype.exec = function (ctx) { + ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); + }; + + /** + * Graphics command object. See {{#crossLink "Graphics/drawEllipse"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class Ellipse + * @constructor + * @param {Number} x + * @param {Number} y + * @param {Number} w + * @param {Number} h + **/ + /** + * @property x + * @type Number + */ + /** + * @property y + * @type Number + */ + /** + * @property w + * @type Number + */ + /** + * @property h + * @type Number + */ + /** + * Execute the Graphics command in the provided Canvas context. + * @method exec + * @param {CanvasRenderingContext2D} ctx The canvas rendering context + */ + (G.Ellipse = function (x, y, w, h) { + this.x = x;this.y = y; + this.w = w;this.h = h; + }).prototype.exec = function (ctx) { + var x = this.x, + y = this.y; + var w = this.w, + h = this.h; + + var k = 0.5522848; + var ox = w / 2 * k; + var oy = h / 2 * k; + var xe = x + w; + var ye = y + h; + var xm = x + w / 2; + var ym = y + h / 2; + + ctx.moveTo(x, ym); + ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y); + ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym); + ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye); + ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym); + }; + + /** + * Graphics command object. See {{#crossLink "Graphics/drawPolyStar"}}{{/crossLink}} and {{#crossLink "Graphics/append"}}{{/crossLink}} for more information. + * @class PolyStar + * @constructor + * @param {Number} x + * @param {Number} y + * @param {Number} radius + * @param {Number} sides + * @param {Number} pointSize + * @param {Number} angle + **/ + /** + * @property x + * @type Number + */ + /** + * @property y + * @type Number + */ + /** + * @property radius + * @type Number + */ + /** + * @property sides + * @type Number + */ + /** + * @property pointSize + * @type Number + */ + /** + * @property angle + * @type Number + */ + /** + * Execute the Graphics command in the provided Canvas context. + * @method exec + * @param {CanvasRenderingContext2D} ctx The canvas rendering context + */ + (G.PolyStar = function (x, y, radius, sides, pointSize, angle) { + this.x = x;this.y = y; + this.radius = radius; + this.sides = sides; + this.pointSize = pointSize; + this.angle = angle; + }).prototype.exec = function (ctx) { + var x = this.x, + y = this.y; + var radius = this.radius; + var angle = (this.angle || 0) / 180 * Math.PI; + var sides = this.sides; + var ps = 1 - (this.pointSize || 0); + var a = Math.PI / sides; + + ctx.moveTo(x + Math.cos(angle) * radius, y + Math.sin(angle) * radius); + for (var i = 0; i < sides; i++) { + angle += a; + if (ps != 1) { + ctx.lineTo(x + Math.cos(angle) * radius * ps, y + Math.sin(angle) * radius * ps); + } + angle += a; + ctx.lineTo(x + Math.cos(angle) * radius, y + Math.sin(angle) * radius); + } + ctx.closePath(); + }; + + // docced above. + Graphics.beginCmd = new G.BeginPath(); // so we don't have to instantiate multiple instances. + + + createjs.Graphics = Graphics; + })(); + + //############################################################################## + // DisplayObject.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * DisplayObject is an abstract class that should not be constructed directly. Instead construct subclasses such as + * {{#crossLink "Container"}}{{/crossLink}}, {{#crossLink "Bitmap"}}{{/crossLink}}, and {{#crossLink "Shape"}}{{/crossLink}}. + * DisplayObject is the base class for all display classes in the EaselJS library. It defines the core properties and + * methods that are shared between all display objects, such as transformation properties (x, y, scaleX, scaleY, etc), + * caching, and mouse handlers. + * @class DisplayObject + * @extends EventDispatcher + * @constructor + **/ + + function DisplayObject() { + this.EventDispatcher_constructor(); + + // public properties: + /** + * The alpha (transparency) for this display object. 0 is fully transparent, 1 is fully opaque. + * @property alpha + * @type {Number} + * @default 1 + **/ + this.alpha = 1; + + /** + * If a cache is active, this returns the canvas that holds the cached version of this display object. See {{#crossLink "cache"}}{{/crossLink}} + * for more information. + * @property cacheCanvas + * @type {HTMLCanvasElement | Object} + * @default null + * @readonly + **/ + this.cacheCanvas = null; + + /** + * Returns an ID number that uniquely identifies the current cache for this display object. This can be used to + * determine if the cache has changed since a previous check. + * @property cacheID + * @type {Number} + * @default 0 + */ + this.cacheID = 0; + + /** + * Unique ID for this display object. Makes display objects easier for some uses. + * @property id + * @type {Number} + * @default -1 + **/ + this.id = createjs.UID.get(); + + /** + * Indicates whether to include this object when running mouse interactions. Setting this to `false` for children + * of a {{#crossLink "Container"}}{{/crossLink}} will cause events on the Container to not fire when that child is + * clicked. Setting this property to `false` does not prevent the {{#crossLink "Container/getObjectsUnderPoint"}}{{/crossLink}} + * method from returning the child. + * + * Note: In EaselJS 0.7.0, the mouseEnabled property will not work properly with nested Containers. Please + * check out the latest NEXT version in GitHub for an updated version with this issue resolved. The fix will be + * provided in the next release of EaselJS. + * @property mouseEnabled + * @type {Boolean} + * @default true + **/ + this.mouseEnabled = true; + + /** + * If false, the tick will not run on this display object (or its children). This can provide some performance benefits. + * In addition to preventing the "tick" event from being dispatched, it will also prevent tick related updates + * on some display objects (ex. Sprite & MovieClip frame advancing, DOMElement visibility handling). + * @property tickEnabled + * @type Boolean + * @default true + **/ + this.tickEnabled = true; + + /** + * An optional name for this display object. Included in {{#crossLink "DisplayObject/toString"}}{{/crossLink}} . Useful for + * debugging. + * @property name + * @type {String} + * @default null + **/ + this.name = null; + + /** + * A reference to the {{#crossLink "Container"}}{{/crossLink}} or {{#crossLink "Stage"}}{{/crossLink}} object that + * contains this display object, or null if it has not been added + * to one. + * @property parent + * @final + * @type {Container} + * @default null + * @readonly + **/ + this.parent = null; + + /** + * The left offset for this display object's registration point. For example, to make a 100x100px Bitmap rotate + * around its center, you would set regX and {{#crossLink "DisplayObject/regY:property"}}{{/crossLink}} to 50. + * @property regX + * @type {Number} + * @default 0 + **/ + this.regX = 0; + + /** + * The y offset for this display object's registration point. For example, to make a 100x100px Bitmap rotate around + * its center, you would set {{#crossLink "DisplayObject/regX:property"}}{{/crossLink}} and regY to 50. + * @property regY + * @type {Number} + * @default 0 + **/ + this.regY = 0; + + /** + * The rotation in degrees for this display object. + * @property rotation + * @type {Number} + * @default 0 + **/ + this.rotation = 0; + + /** + * The factor to stretch this display object horizontally. For example, setting scaleX to 2 will stretch the display + * object to twice its nominal width. To horizontally flip an object, set the scale to a negative number. + * @property scaleX + * @type {Number} + * @default 1 + **/ + this.scaleX = 1; + + /** + * The factor to stretch this display object vertically. For example, setting scaleY to 0.5 will stretch the display + * object to half its nominal height. To vertically flip an object, set the scale to a negative number. + * @property scaleY + * @type {Number} + * @default 1 + **/ + this.scaleY = 1; + + /** + * The factor to skew this display object horizontally. + * @property skewX + * @type {Number} + * @default 0 + **/ + this.skewX = 0; + + /** + * The factor to skew this display object vertically. + * @property skewY + * @type {Number} + * @default 0 + **/ + this.skewY = 0; + + /** + * A shadow object that defines the shadow to render on this display object. Set to `null` to remove a shadow. If + * null, this property is inherited from the parent container. + * @property shadow + * @type {Shadow} + * @default null + **/ + this.shadow = null; + + /** + * Indicates whether this display object should be rendered to the canvas and included when running the Stage + * {{#crossLink "Stage/getObjectsUnderPoint"}}{{/crossLink}} method. + * @property visible + * @type {Boolean} + * @default true + **/ + this.visible = true; + + /** + * The x (horizontal) position of the display object, relative to its parent. + * @property x + * @type {Number} + * @default 0 + **/ + this.x = 0; + + /** The y (vertical) position of the display object, relative to its parent. + * @property y + * @type {Number} + * @default 0 + **/ + this.y = 0; + + /** + * If set, defines the transformation for this display object, overriding all other transformation properties + * (x, y, rotation, scale, skew). + * @property transformMatrix + * @type {Matrix2D} + * @default null + **/ + this.transformMatrix = null; + + /** + * The composite operation indicates how the pixels of this display object will be composited with the elements + * behind it. If `null`, this property is inherited from the parent container. For more information, read the + * + * whatwg spec on compositing. + * @property compositeOperation + * @type {String} + * @default null + **/ + this.compositeOperation = null; + + /** + * Indicates whether the display object should be drawn to a whole pixel when + * {{#crossLink "Stage/snapToPixelEnabled"}}{{/crossLink}} is true. To enable/disable snapping on whole + * categories of display objects, set this value on the prototype (Ex. Text.prototype.snapToPixel = true). + * @property snapToPixel + * @type {Boolean} + * @default true + **/ + this.snapToPixel = true; + + /** + * An array of Filter objects to apply to this display object. Filters are only applied / updated when {{#crossLink "cache"}}{{/crossLink}} + * or {{#crossLink "updateCache"}}{{/crossLink}} is called on the display object, and only apply to the area that is + * cached. + * @property filters + * @type {Array} + * @default null + **/ + this.filters = null; + + /** + * A Shape instance that defines a vector mask (clipping path) for this display object. The shape's transformation + * will be applied relative to the display object's parent coordinates (as if it were a child of the parent). + * @property mask + * @type {Shape} + * @default null + */ + this.mask = null; + + /** + * A display object that will be tested when checking mouse interactions or testing {{#crossLink "Container/getObjectsUnderPoint"}}{{/crossLink}}. + * The hit area will have its transformation applied relative to this display object's coordinate space (as though + * the hit test object were a child of this display object and relative to its regX/Y). The hitArea will be tested + * using only its own `alpha` value regardless of the alpha value on the target display object, or the target's + * ancestors (parents). + * + * If set on a {{#crossLink "Container"}}{{/crossLink}}, children of the Container will not receive mouse events. + * This is similar to setting {{#crossLink "mouseChildren"}}{{/crossLink}} to false. + * + * Note that hitArea is NOT currently used by the `hitTest()` method, nor is it supported for {{#crossLink "Stage"}}{{/crossLink}}. + * @property hitArea + * @type {DisplayObject} + * @default null + */ + this.hitArea = null; + + /** + * A CSS cursor (ex. "pointer", "help", "text", etc) that will be displayed when the user hovers over this display + * object. You must enable mouseover events using the {{#crossLink "Stage/enableMouseOver"}}{{/crossLink}} method to + * use this property. Setting a non-null cursor on a Container will override the cursor set on its descendants. + * @property cursor + * @type {String} + * @default null + */ + this.cursor = null; + + // private properties: + /** + * @property _cacheOffsetX + * @protected + * @type {Number} + * @default 0 + **/ + this._cacheOffsetX = 0; + + /** + * @property _cacheOffsetY + * @protected + * @type {Number} + * @default 0 + **/ + this._cacheOffsetY = 0; + + /** + * @property _filterOffsetX + * @protected + * @type {Number} + * @default 0 + **/ + this._filterOffsetX = 0; + + /** + * @property _filterOffsetY + * @protected + * @type {Number} + * @default 0 + **/ + this._filterOffsetY = 0; + + /** + * @property _cacheScale + * @protected + * @type {Number} + * @default 1 + **/ + this._cacheScale = 1; + + /** + * @property _cacheDataURLID + * @protected + * @type {Number} + * @default 0 + */ + this._cacheDataURLID = 0; + + /** + * @property _cacheDataURL + * @protected + * @type {String} + * @default null + */ + this._cacheDataURL = null; + + /** + * @property _props + * @protected + * @type {DisplayObject} + * @default null + **/ + this._props = new createjs.DisplayProps(); + + /** + * @property _rectangle + * @protected + * @type {Rectangle} + * @default null + **/ + this._rectangle = new createjs.Rectangle(); + + /** + * @property _bounds + * @protected + * @type {Rectangle} + * @default null + **/ + this._bounds = null; + } + var p = createjs.extend(DisplayObject, createjs.EventDispatcher); + + // TODO: deprecated + // p.initialize = function() {}; // searchable for devs wondering where it is. REMOVED. See docs for details. + + // static properties: + /** + * Listing of mouse event names. Used in _hasMouseEventListener. + * @property _MOUSE_EVENTS + * @protected + * @static + * @type {Array} + **/ + DisplayObject._MOUSE_EVENTS = ["click", "dblclick", "mousedown", "mouseout", "mouseover", "pressmove", "pressup", "rollout", "rollover"]; + + /** + * Suppresses errors generated when using features like hitTest, mouse events, and {{#crossLink "getObjectsUnderPoint"}}{{/crossLink}} + * with cross domain content. + * @property suppressCrossDomainErrors + * @static + * @type {Boolean} + * @default false + **/ + DisplayObject.suppressCrossDomainErrors = false; + + /** + * @property _snapToPixelEnabled + * @protected + * @static + * @type {Boolean} + * @default false + **/ + DisplayObject._snapToPixelEnabled = false; // stage.snapToPixelEnabled is temporarily copied here during a draw to provide global access. + + /** + * @property _hitTestCanvas + * @type {HTMLCanvasElement | Object} + * @static + * @protected + **/ + /** + * @property _hitTestContext + * @type {CanvasRenderingContext2D} + * @static + * @protected + **/ + var canvas = createjs.createCanvas ? createjs.createCanvas() : document.createElement("canvas"); // prevent errors on load in browsers without canvas. + if (canvas.getContext) { + DisplayObject._hitTestCanvas = canvas; + DisplayObject._hitTestContext = canvas.getContext("2d"); + canvas.width = canvas.height = 1; + } + + /** + * @property _nextCacheID + * @type {Number} + * @static + * @protected + **/ + DisplayObject._nextCacheID = 1; + + // events: + /** + * Dispatched when the user presses their left mouse button over the display object. See the + * {{#crossLink "MouseEvent"}}{{/crossLink}} class for a listing of event properties. + * @event mousedown + * @since 0.6.0 + */ + + /** + * Dispatched when the user presses their left mouse button and then releases it while over the display object. + * See the {{#crossLink "MouseEvent"}}{{/crossLink}} class for a listing of event properties. + * @event click + * @since 0.6.0 + */ + + /** + * Dispatched when the user double clicks their left mouse button over this display object. + * See the {{#crossLink "MouseEvent"}}{{/crossLink}} class for a listing of event properties. + * @event dblclick + * @since 0.6.0 + */ + + /** + * Dispatched when the user's mouse enters this display object. This event must be enabled using + * {{#crossLink "Stage/enableMouseOver"}}{{/crossLink}}. See also {{#crossLink "DisplayObject/rollover:event"}}{{/crossLink}}. + * See the {{#crossLink "MouseEvent"}}{{/crossLink}} class for a listing of event properties. + * @event mouseover + * @since 0.6.0 + */ + + /** + * Dispatched when the user's mouse leaves this display object. This event must be enabled using + * {{#crossLink "Stage/enableMouseOver"}}{{/crossLink}}. See also {{#crossLink "DisplayObject/rollout:event"}}{{/crossLink}}. + * See the {{#crossLink "MouseEvent"}}{{/crossLink}} class for a listing of event properties. + * @event mouseout + * @since 0.6.0 + */ + + /** + * This event is similar to {{#crossLink "DisplayObject/mouseover:event"}}{{/crossLink}}, with the following + * differences: it does not bubble, and it considers {{#crossLink "Container"}}{{/crossLink}} instances as an + * aggregate of their content. + * + * For example, myContainer contains two overlapping children: shapeA and shapeB. The user moves their mouse over + * shapeA and then directly on to shapeB. With a listener for {{#crossLink "mouseover:event"}}{{/crossLink}} on + * myContainer, two events would be received, each targeting a child element:
    + *
  1. when the mouse enters shapeA (target=shapeA)
  2. + *
  3. when the mouse enters shapeB (target=shapeB)
  4. + *
+ * However, with a listener for "rollover" instead, only a single event is received when the mouse first enters + * the aggregate myContainer content (target=myContainer). + * + * This event must be enabled using {{#crossLink "Stage/enableMouseOver"}}{{/crossLink}}. + * See the {{#crossLink "MouseEvent"}}{{/crossLink}} class for a listing of event properties. + * @event rollover + * @since 0.7.0 + */ + + /** + * This event is similar to {{#crossLink "DisplayObject/mouseout:event"}}{{/crossLink}}, with the following + * differences: it does not bubble, and it considers {{#crossLink "Container"}}{{/crossLink}} instances as an + * aggregate of their content. + * + * For example, myContainer contains two overlapping children: shapeA and shapeB. The user moves their mouse over + * shapeA, then directly on to shapeB, then off both. With a listener for {{#crossLink "mouseout:event"}}{{/crossLink}} + * on myContainer, two events would be received, each targeting a child element:
    + *
  1. when the mouse leaves shapeA (target=shapeA)
  2. + *
  3. when the mouse leaves shapeB (target=shapeB)
  4. + *
+ * However, with a listener for "rollout" instead, only a single event is received when the mouse leaves + * the aggregate myContainer content (target=myContainer). + * + * This event must be enabled using {{#crossLink "Stage/enableMouseOver"}}{{/crossLink}}. + * See the {{#crossLink "MouseEvent"}}{{/crossLink}} class for a listing of event properties. + * @event rollout + * @since 0.7.0 + */ + + /** + * After a {{#crossLink "DisplayObject/mousedown:event"}}{{/crossLink}} occurs on a display object, a pressmove + * event will be generated on that object whenever the mouse moves until the mouse press is released. This can be + * useful for dragging and similar operations. + * @event pressmove + * @since 0.7.0 + */ + + /** + * After a {{#crossLink "DisplayObject/mousedown:event"}}{{/crossLink}} occurs on a display object, a pressup event + * will be generated on that object when that mouse press is released. This can be useful for dragging and similar + * operations. + * @event pressup + * @since 0.7.0 + */ + + /** + * Dispatched when the display object is added to a parent container. + * @event added + */ + + /** + * Dispatched when the display object is removed from its parent container. + * @event removed + */ + + /** + * Dispatched on each display object on a stage whenever the stage updates. This occurs immediately before the + * rendering (draw) pass. When {{#crossLink "Stage/update"}}{{/crossLink}} is called, first all display objects on + * the stage dispatch the tick event, then all of the display objects are drawn to stage. Children will have their + * {{#crossLink "tick:event"}}{{/crossLink}} event dispatched in order of their depth prior to the event being + * dispatched on their parent. + * @event tick + * @param {Object} target The object that dispatched the event. + * @param {String} type The event type. + * @param {Array} params An array containing any arguments that were passed to the Stage.update() method. For + * example if you called stage.update("hello"), then the params would be ["hello"]. + * @since 0.6.0 + */ + + // getter / setters: + /** + * Use the {{#crossLink "DisplayObject/stage:property"}}{{/crossLink}} property instead. + * @method getStage + * @return {Stage} + * @deprecated + **/ + p.getStage = function () { + // uses dynamic access to avoid circular dependencies; + var o = this, + _Stage = createjs["Stage"]; + while (o.parent) { + o = o.parent; + } + if (o instanceof _Stage) { + return o; + } + return null; + }; + + /** + * Returns the Stage instance that this display object will be rendered on, or null if it has not been added to one. + * @property stage + * @type {Stage} + * @readonly + **/ + try { + Object.defineProperties(p, { + stage: { get: p.getStage } + }); + } catch (e) {} + + // public methods: + /** + * Returns true or false indicating whether the display object would be visible if drawn to a canvas. + * This does not account for whether it would be visible within the boundaries of the stage. + * + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method isVisible + * @return {Boolean} Boolean indicating whether the display object would be visible if drawn to a canvas + **/ + p.isVisible = function () { + return !!(this.visible && this.alpha > 0 && this.scaleX != 0 && this.scaleY != 0); + }; + + /** + * Draws the display object into the specified context ignoring its visible, alpha, shadow, and transform. + * Returns true if the draw was handled (useful for overriding functionality). + * + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method draw + * @param {CanvasRenderingContext2D} ctx The canvas 2D context object to draw into. + * @param {Boolean} [ignoreCache=false] Indicates whether the draw operation should ignore any current cache. For example, + * used for drawing the cache (to prevent it from simply drawing an existing cache back into itself). + * @return {Boolean} + **/ + p.draw = function (ctx, ignoreCache) { + var cacheCanvas = this.cacheCanvas; + if (ignoreCache || !cacheCanvas) { + return false; + } + var scale = this._cacheScale; + ctx.drawImage(cacheCanvas, this._cacheOffsetX + this._filterOffsetX, this._cacheOffsetY + this._filterOffsetY, cacheCanvas.width / scale, cacheCanvas.height / scale); + return true; + }; + + /** + * Applies this display object's transformation, alpha, globalCompositeOperation, clipping path (mask), and shadow + * to the specified context. This is typically called prior to {{#crossLink "DisplayObject/draw"}}{{/crossLink}}. + * @method updateContext + * @param {CanvasRenderingContext2D} ctx The canvas 2D to update. + **/ + p.updateContext = function (ctx) { + var o = this, + mask = o.mask, + mtx = o._props.matrix; + + if (mask && mask.graphics && !mask.graphics.isEmpty()) { + mask.getMatrix(mtx); + ctx.transform(mtx.a, mtx.b, mtx.c, mtx.d, mtx.tx, mtx.ty); + + mask.graphics.drawAsPath(ctx); + ctx.clip(); + + mtx.invert(); + ctx.transform(mtx.a, mtx.b, mtx.c, mtx.d, mtx.tx, mtx.ty); + } + + this.getMatrix(mtx); + var tx = mtx.tx, + ty = mtx.ty; + if (DisplayObject._snapToPixelEnabled && o.snapToPixel) { + tx = tx + (tx < 0 ? -0.5 : 0.5) | 0; + ty = ty + (ty < 0 ? -0.5 : 0.5) | 0; + } + ctx.transform(mtx.a, mtx.b, mtx.c, mtx.d, tx, ty); + ctx.globalAlpha *= o.alpha; + if (o.compositeOperation) { + ctx.globalCompositeOperation = o.compositeOperation; + } + if (o.shadow) { + this._applyShadow(ctx, o.shadow); + } + }; + + /** + * Draws the display object into a new canvas, which is then used for subsequent draws. For complex content + * that does not change frequently (ex. a Container with many children that do not move, or a complex vector Shape), + * this can provide for much faster rendering because the content does not need to be re-rendered each tick. The + * cached display object can be moved, rotated, faded, etc freely, however if its content changes, you must + * manually update the cache by calling updateCache() or cache() again. You must specify + * the cache area via the x, y, w, and h parameters. This defines the rectangle that will be rendered and cached + * using this display object's coordinates. + * + *

Example

+ * For example if you defined a Shape that drew a circle at 0, 0 with a radius of 25: + * + * var shape = new createjs.Shape(); + * shape.graphics.beginFill("#ff0000").drawCircle(0, 0, 25); + * myShape.cache(-25, -25, 50, 50); + * + * Note that filters need to be defined before the cache is applied. Check out the {{#crossLink "Filter"}}{{/crossLink}} + * class for more information. Some filters (ex. BlurFilter) will not work as expected in conjunction with the scale param. + * + * Usually, the resulting cacheCanvas will have the dimensions width*scale by height*scale, however some filters (ex. BlurFilter) + * will add padding to the canvas dimensions. + * + * @method cache + * @param {Number} x The x coordinate origin for the cache region. + * @param {Number} y The y coordinate origin for the cache region. + * @param {Number} width The width of the cache region. + * @param {Number} height The height of the cache region. + * @param {Number} [scale=1] The scale at which the cache will be created. For example, if you cache a vector shape using + * myShape.cache(0,0,100,100,2) then the resulting cacheCanvas will be 200x200 px. This lets you scale and rotate + * cached elements with greater fidelity. Default is 1. + **/ + p.cache = function (x, y, width, height, scale) { + // draw to canvas. + scale = scale || 1; + if (!this.cacheCanvas) { + this.cacheCanvas = createjs.createCanvas ? createjs.createCanvas() : document.createElement("canvas"); + } + this._cacheWidth = width; + this._cacheHeight = height; + this._cacheOffsetX = x; + this._cacheOffsetY = y; + this._cacheScale = scale; + this.updateCache(); + }; + + /** + * Redraws the display object to its cache. Calling updateCache without an active cache will throw an error. + * If compositeOperation is null the current cache will be cleared prior to drawing. Otherwise the display object + * will be drawn over the existing cache using the specified compositeOperation. + * + *

Example

+ * Clear the current graphics of a cached shape, draw some new instructions, and then update the cache. The new line + * will be drawn on top of the old one. + * + * // Not shown: Creating the shape, and caching it. + * shapeInstance.clear(); + * shapeInstance.setStrokeStyle(3).beginStroke("#ff0000").moveTo(100, 100).lineTo(200,200); + * shapeInstance.updateCache(); + * + * @method updateCache + * @param {String} compositeOperation The compositeOperation to use, or null to clear the cache and redraw it. + * + * whatwg spec on compositing. + **/ + p.updateCache = function (compositeOperation) { + var cacheCanvas = this.cacheCanvas; + if (!cacheCanvas) { + throw "cache() must be called before updateCache()"; + } + var scale = this._cacheScale, + offX = this._cacheOffsetX * scale, + offY = this._cacheOffsetY * scale; + var w = this._cacheWidth, + h = this._cacheHeight, + ctx = cacheCanvas.getContext("2d"); + + var fBounds = this._getFilterBounds(); + offX += this._filterOffsetX = fBounds.x; + offY += this._filterOffsetY = fBounds.y; + + w = Math.ceil(w * scale) + fBounds.width; + h = Math.ceil(h * scale) + fBounds.height; + if (w != cacheCanvas.width || h != cacheCanvas.height) { + // TODO: it would be nice to preserve the content if there is a compositeOperation. + cacheCanvas.width = w; + cacheCanvas.height = h; + } else if (!compositeOperation) { + ctx.clearRect(0, 0, w + 1, h + 1); + } + + ctx.save(); + ctx.globalCompositeOperation = compositeOperation; + ctx.setTransform(scale, 0, 0, scale, -offX, -offY); + this.draw(ctx, true); + // TODO: filters and cache scale don't play well together at present. + this._applyFilters(); + ctx.restore(); + this.cacheID = DisplayObject._nextCacheID++; + }; + + /** + * Clears the current cache. See {{#crossLink "DisplayObject/cache"}}{{/crossLink}} for more information. + * @method uncache + **/ + p.uncache = function () { + this._cacheDataURL = this.cacheCanvas = null; + this.cacheID = this._cacheOffsetX = this._cacheOffsetY = this._filterOffsetX = this._filterOffsetY = 0; + this._cacheScale = 1; + }; + + /** + * Returns a data URL for the cache, or null if this display object is not cached. + * Uses cacheID to ensure a new data URL is not generated if the cache has not changed. + * @method getCacheDataURL + * @return {String} The image data url for the cache. + **/ + p.getCacheDataURL = function () { + if (!this.cacheCanvas) { + return null; + } + if (this.cacheID != this._cacheDataURLID) { + this._cacheDataURL = this.cacheCanvas.toDataURL(); + } + return this._cacheDataURL; + }; + + /** + * Transforms the specified x and y position from the coordinate space of the display object + * to the global (stage) coordinate space. For example, this could be used to position an HTML label + * over a specific point on a nested display object. Returns a Point instance with x and y properties + * correlating to the transformed coordinates on the stage. + * + *

Example

+ * + * displayObject.x = 300; + * displayObject.y = 200; + * stage.addChild(displayObject); + * var point = displayObject.localToGlobal(100, 100); + * // Results in x=400, y=300 + * + * @method localToGlobal + * @param {Number} x The x position in the source display object to transform. + * @param {Number} y The y position in the source display object to transform. + * @param {Point | Object} [pt] An object to copy the result into. If omitted a new Point object with x/y properties will be returned. + * @return {Point} A Point instance with x and y properties correlating to the transformed coordinates + * on the stage. + **/ + p.localToGlobal = function (x, y, pt) { + return this.getConcatenatedMatrix(this._props.matrix).transformPoint(x, y, pt || new createjs.Point()); + }; + + /** + * Transforms the specified x and y position from the global (stage) coordinate space to the + * coordinate space of the display object. For example, this could be used to determine + * the current mouse position within the display object. Returns a Point instance with x and y properties + * correlating to the transformed position in the display object's coordinate space. + * + *

Example

+ * + * displayObject.x = 300; + * displayObject.y = 200; + * stage.addChild(displayObject); + * var point = displayObject.globalToLocal(100, 100); + * // Results in x=-200, y=-100 + * + * @method globalToLocal + * @param {Number} x The x position on the stage to transform. + * @param {Number} y The y position on the stage to transform. + * @param {Point | Object} [pt] An object to copy the result into. If omitted a new Point object with x/y properties will be returned. + * @return {Point} A Point instance with x and y properties correlating to the transformed position in the + * display object's coordinate space. + **/ + p.globalToLocal = function (x, y, pt) { + return this.getConcatenatedMatrix(this._props.matrix).invert().transformPoint(x, y, pt || new createjs.Point()); + }; + + /** + * Transforms the specified x and y position from the coordinate space of this display object to the coordinate + * space of the target display object. Returns a Point instance with x and y properties correlating to the + * transformed position in the target's coordinate space. Effectively the same as using the following code with + * {{#crossLink "DisplayObject/localToGlobal"}}{{/crossLink}} and {{#crossLink "DisplayObject/globalToLocal"}}{{/crossLink}}. + * + * var pt = this.localToGlobal(x, y); + * pt = target.globalToLocal(pt.x, pt.y); + * + * @method localToLocal + * @param {Number} x The x position in the source display object to transform. + * @param {Number} y The y position on the source display object to transform. + * @param {DisplayObject} target The target display object to which the coordinates will be transformed. + * @param {Point | Object} [pt] An object to copy the result into. If omitted a new Point object with x/y properties will be returned. + * @return {Point} Returns a Point instance with x and y properties correlating to the transformed position + * in the target's coordinate space. + **/ + p.localToLocal = function (x, y, target, pt) { + pt = this.localToGlobal(x, y, pt); + return target.globalToLocal(pt.x, pt.y, pt); + }; + + /** + * Shortcut method to quickly set the transform properties on the display object. All parameters are optional. + * Omitted parameters will have the default value set. + * + *

Example

+ * + * displayObject.setTransform(100, 100, 2, 2); + * + * @method setTransform + * @param {Number} [x=0] The horizontal translation (x position) in pixels + * @param {Number} [y=0] The vertical translation (y position) in pixels + * @param {Number} [scaleX=1] The horizontal scale, as a percentage of 1 + * @param {Number} [scaleY=1] the vertical scale, as a percentage of 1 + * @param {Number} [rotation=0] The rotation, in degrees + * @param {Number} [skewX=0] The horizontal skew factor + * @param {Number} [skewY=0] The vertical skew factor + * @param {Number} [regX=0] The horizontal registration point in pixels + * @param {Number} [regY=0] The vertical registration point in pixels + * @return {DisplayObject} Returns this instance. Useful for chaining commands. + * @chainable + */ + p.setTransform = function (x, y, scaleX, scaleY, rotation, skewX, skewY, regX, regY) { + this.x = x || 0; + this.y = y || 0; + this.scaleX = scaleX == null ? 1 : scaleX; + this.scaleY = scaleY == null ? 1 : scaleY; + this.rotation = rotation || 0; + this.skewX = skewX || 0; + this.skewY = skewY || 0; + this.regX = regX || 0; + this.regY = regY || 0; + return this; + }; + + /** + * Returns a matrix based on this object's current transform. + * @method getMatrix + * @param {Matrix2D} matrix Optional. A Matrix2D object to populate with the calculated values. If null, a new + * Matrix object is returned. + * @return {Matrix2D} A matrix representing this display object's transform. + **/ + p.getMatrix = function (matrix) { + var o = this, + mtx = matrix && matrix.identity() || new createjs.Matrix2D(); + return o.transformMatrix ? mtx.copy(o.transformMatrix) : mtx.appendTransform(o.x, o.y, o.scaleX, o.scaleY, o.rotation, o.skewX, o.skewY, o.regX, o.regY); + }; + + /** + * Generates a Matrix2D object representing the combined transform of the display object and all of its + * parent Containers up to the highest level ancestor (usually the {{#crossLink "Stage"}}{{/crossLink}}). This can + * be used to transform positions between coordinate spaces, such as with {{#crossLink "DisplayObject/localToGlobal"}}{{/crossLink}} + * and {{#crossLink "DisplayObject/globalToLocal"}}{{/crossLink}}. + * @method getConcatenatedMatrix + * @param {Matrix2D} [matrix] A {{#crossLink "Matrix2D"}}{{/crossLink}} object to populate with the calculated values. + * If null, a new Matrix2D object is returned. + * @return {Matrix2D} The combined matrix. + **/ + p.getConcatenatedMatrix = function (matrix) { + var o = this, + mtx = this.getMatrix(matrix); + while (o = o.parent) { + mtx.prependMatrix(o.getMatrix(o._props.matrix)); + } + return mtx; + }; + + /** + * Generates a DisplayProps object representing the combined display properties of the object and all of its + * parent Containers up to the highest level ancestor (usually the {{#crossLink "Stage"}}{{/crossLink}}). + * @method getConcatenatedDisplayProps + * @param {DisplayProps} [props] A {{#crossLink "DisplayProps"}}{{/crossLink}} object to populate with the calculated values. + * If null, a new DisplayProps object is returned. + * @return {DisplayProps} The combined display properties. + **/ + p.getConcatenatedDisplayProps = function (props) { + props = props ? props.identity() : new createjs.DisplayProps(); + var o = this, + mtx = o.getMatrix(props.matrix); + do { + props.prepend(o.visible, o.alpha, o.shadow, o.compositeOperation); + + // we do this to avoid problems with the matrix being used for both operations when o._props.matrix is passed in as the props param. + // this could be simplified (ie. just done as part of the prepend above) if we switched to using a pool. + if (o != this) { + mtx.prependMatrix(o.getMatrix(o._props.matrix)); + } + } while (o = o.parent); + return props; + }; + + /** + * Tests whether the display object intersects the specified point in local coordinates (ie. draws a pixel + * with alpha > 0 at the specified position). This ignores the alpha, shadow, hitArea, mask, and compositeOperation + * of the display object. + * + *

Example

+ * + * var myShape = new createjs.Shape(); + * myShape.graphics.beginFill("red").drawRect(100, 100, 20, 50); + * + * console.log(myShape.hitTest(10,10); // false + * console.log(myShape.hitTest(110, 25); // true + * + * Note that to use Stage coordinates (such as {{#crossLink "Stage/mouseX:property"}}{{/crossLink}}), they must + * first be converted to local coordinates: + * + * stage.addEventListener("stagemousedown", handleMouseDown); + * function handleMouseDown(event) { + * var p = myShape.globalToLocal(stage.mouseX, stage.mouseY); + * var hit = myShape.hitTest(p.x, p.y); + * } + * + * Shape-to-shape collision is not currently supported by EaselJS. + * + * @method hitTest + * @param {Number} x The x position to check in the display object's local coordinates. + * @param {Number} y The y position to check in the display object's local coordinates. + * @return {Boolean} A Boolean indicating whether a visible portion of the DisplayObject intersect the specified + * local Point. + */ + p.hitTest = function (x, y) { + var ctx = DisplayObject._hitTestContext; + ctx.setTransform(1, 0, 0, 1, -x, -y); + this.draw(ctx); + + var hit = this._testHit(ctx); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.clearRect(0, 0, 2, 2); + return hit; + }; + + /** + * Provides a chainable shortcut method for setting a number of properties on the instance. + * + *

Example

+ * + * var myGraphics = new createjs.Graphics().beginFill("#ff0000").drawCircle(0, 0, 25); + * var shape = stage.addChild(new createjs.Shape()).set({graphics:myGraphics, x:100, y:100, alpha:0.5}); + * + * @method set + * @param {Object} props A generic object containing properties to copy to the DisplayObject instance. + * @return {DisplayObject} Returns the instance the method is called on (useful for chaining calls.) + * @chainable + */ + p.set = function (props) { + for (var n in props) { + this[n] = props[n]; + } + return this; + }; + + /** + * Returns a rectangle representing this object's bounds in its local coordinate system (ie. with no transformation). + * Objects that have been cached will return the bounds of the cache. + * + * Not all display objects can calculate their own bounds (ex. Shape). For these objects, you can use + * {{#crossLink "DisplayObject/setBounds"}}{{/crossLink}} so that they are included when calculating Container + * bounds. + * + * + * + * + * + * + * + * + * + *
All + * All display objects support setting bounds manually using setBounds(). Likewise, display objects that + * have been cached using cache() will return the bounds of their cache. Manual and cache bounds will override + * the automatic calculations listed below. + *
Bitmap + * Returns the width and height of the sourceRect (if specified) or image, extending from (x=0,y=0). + *
Sprite + * Returns the bounds of the current frame. May have non-zero x/y if a frame registration point was specified + * in the spritesheet data. See also {{#crossLink "SpriteSheet/getFrameBounds"}}{{/crossLink}} + *
Container + * Returns the aggregate (combined) bounds of all children that return a non-null value from getBounds(). + *
Shape + * Does not currently support automatic bounds calculations. Use setBounds() to manually define bounds. + *
Text + * Returns approximate bounds. Horizontal values (x/width) are quite accurate, but vertical values (y/height) are + * not, especially when using textBaseline values other than "top". + *
BitmapText + * Returns approximate bounds. Values will be more accurate if spritesheet frame registration points are close + * to (x=0,y=0). + *
+ * + * Bounds can be expensive to calculate for some objects (ex. text, or containers with many children), and + * are recalculated each time you call getBounds(). You can prevent recalculation on static objects by setting the + * bounds explicitly: + * + * var bounds = obj.getBounds(); + * obj.setBounds(bounds.x, bounds.y, bounds.width, bounds.height); + * // getBounds will now use the set values, instead of recalculating + * + * To reduce memory impact, the returned Rectangle instance may be reused internally; clone the instance or copy its + * values if you need to retain it. + * + * var myBounds = obj.getBounds().clone(); + * // OR: + * myRect.copy(obj.getBounds()); + * + * @method getBounds + * @return {Rectangle} A Rectangle instance representing the bounds, or null if bounds are not available for this + * object. + **/ + p.getBounds = function () { + if (this._bounds) { + return this._rectangle.copy(this._bounds); + } + var cacheCanvas = this.cacheCanvas; + if (cacheCanvas) { + var scale = this._cacheScale; + return this._rectangle.setValues(this._cacheOffsetX, this._cacheOffsetY, cacheCanvas.width / scale, cacheCanvas.height / scale); + } + return null; + }; + + /** + * Returns a rectangle representing this object's bounds in its parent's coordinate system (ie. with transformations applied). + * Objects that have been cached will return the transformed bounds of the cache. + * + * Not all display objects can calculate their own bounds (ex. Shape). For these objects, you can use + * {{#crossLink "DisplayObject/setBounds"}}{{/crossLink}} so that they are included when calculating Container + * bounds. + * + * To reduce memory impact, the returned Rectangle instance may be reused internally; clone the instance or copy its + * values if you need to retain it. + * + * Container instances calculate aggregate bounds for all children that return bounds via getBounds. + * @method getTransformedBounds + * @return {Rectangle} A Rectangle instance representing the bounds, or null if bounds are not available for this object. + **/ + p.getTransformedBounds = function () { + return this._getBounds(); + }; + + /** + * Allows you to manually specify the bounds of an object that either cannot calculate their own bounds (ex. Shape & + * Text) for future reference, or so the object can be included in Container bounds. Manually set bounds will always + * override calculated bounds. + * + * The bounds should be specified in the object's local (untransformed) coordinates. For example, a Shape instance + * with a 25px radius circle centered at 0,0 would have bounds of (-25, -25, 50, 50). + * @method setBounds + * @param {Number} x The x origin of the bounds. Pass null to remove the manual bounds. + * @param {Number} y The y origin of the bounds. + * @param {Number} width The width of the bounds. + * @param {Number} height The height of the bounds. + **/ + p.setBounds = function (x, y, width, height) { + if (x == null) { + this._bounds = x; + } + this._bounds = (this._bounds || new createjs.Rectangle()).setValues(x, y, width, height); + }; + + /** + * Returns a clone of this DisplayObject. Some properties that are specific to this instance's current context are + * reverted to their defaults (for example .parent). Caches are not maintained across clones, and some elements + * are copied by reference (masks, individual filter instances, hit area) + * @method clone + * @return {DisplayObject} A clone of the current DisplayObject instance. + **/ + p.clone = function () { + return this._cloneProps(new DisplayObject()); + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[DisplayObject (name=" + this.name + ")]"; + }; + + // private methods: + // separated so it can be used more easily in subclasses: + /** + * @method _cloneProps + * @param {DisplayObject} o The DisplayObject instance which will have properties from the current DisplayObject + * instance copied into. + * @return {DisplayObject} o + * @protected + **/ + p._cloneProps = function (o) { + o.alpha = this.alpha; + o.mouseEnabled = this.mouseEnabled; + o.tickEnabled = this.tickEnabled; + o.name = this.name; + o.regX = this.regX; + o.regY = this.regY; + o.rotation = this.rotation; + o.scaleX = this.scaleX; + o.scaleY = this.scaleY; + o.shadow = this.shadow; + o.skewX = this.skewX; + o.skewY = this.skewY; + o.visible = this.visible; + o.x = this.x; + o.y = this.y; + o.compositeOperation = this.compositeOperation; + o.snapToPixel = this.snapToPixel; + o.filters = this.filters == null ? null : this.filters.slice(0); + o.mask = this.mask; + o.hitArea = this.hitArea; + o.cursor = this.cursor; + o._bounds = this._bounds; + return o; + }; + + /** + * @method _applyShadow + * @protected + * @param {CanvasRenderingContext2D} ctx + * @param {Shadow} shadow + **/ + p._applyShadow = function (ctx, shadow) { + shadow = shadow || Shadow.identity; + ctx.shadowColor = shadow.color; + ctx.shadowOffsetX = shadow.offsetX; + ctx.shadowOffsetY = shadow.offsetY; + ctx.shadowBlur = shadow.blur; + }; + + /** + * @method _tick + * @param {Object} evtObj An event object that will be dispatched to all tick listeners. This object is reused between dispatchers to reduce construction & GC costs. + * @protected + **/ + p._tick = function (evtObj) { + // because tick can be really performance sensitive, check for listeners before calling dispatchEvent. + var ls = this._listeners; + if (ls && ls["tick"]) { + // reset & reuse the event object to avoid construction / GC costs: + evtObj.target = null; + evtObj.propagationStopped = evtObj.immediatePropagationStopped = false; + this.dispatchEvent(evtObj); + } + }; + + /** + * @method _testHit + * @protected + * @param {CanvasRenderingContext2D} ctx + * @return {Boolean} + **/ + p._testHit = function (ctx) { + try { + var hit = ctx.getImageData(0, 0, 1, 1).data[3] > 1; + } catch (e) { + if (!DisplayObject.suppressCrossDomainErrors) { + throw "An error has occurred. This is most likely due to security restrictions on reading canvas pixel data with local or cross-domain images."; + } + } + return hit; + }; + + /** + * @method _applyFilters + * @protected + **/ + p._applyFilters = function () { + if (!this.filters || this.filters.length == 0 || !this.cacheCanvas) { + return; + } + var l = this.filters.length; + var ctx = this.cacheCanvas.getContext("2d"); + var w = this.cacheCanvas.width; + var h = this.cacheCanvas.height; + for (var i = 0; i < l; i++) { + this.filters[i].applyFilter(ctx, 0, 0, w, h); + } + }; + + /** + * @method _getFilterBounds + * @return {Rectangle} + * @protected + **/ + p._getFilterBounds = function (rect) { + var l, + filters = this.filters, + bounds = this._rectangle.setValues(0, 0, 0, 0); + if (!filters || !(l = filters.length)) { + return bounds; + } + + for (var i = 0; i < l; i++) { + var f = this.filters[i]; + f.getBounds && f.getBounds(bounds); + } + return bounds; + }; + + /** + * @method _getBounds + * @param {Matrix2D} matrix + * @param {Boolean} ignoreTransform If true, does not apply this object's transform. + * @return {Rectangle} + * @protected + **/ + p._getBounds = function (matrix, ignoreTransform) { + return this._transformBounds(this.getBounds(), matrix, ignoreTransform); + }; + + /** + * @method _transformBounds + * @param {Rectangle} bounds + * @param {Matrix2D} matrix + * @param {Boolean} ignoreTransform + * @return {Rectangle} + * @protected + **/ + p._transformBounds = function (bounds, matrix, ignoreTransform) { + if (!bounds) { + return bounds; + } + var x = bounds.x, + y = bounds.y, + width = bounds.width, + height = bounds.height, + mtx = this._props.matrix; + mtx = ignoreTransform ? mtx.identity() : this.getMatrix(mtx); + + if (x || y) { + mtx.appendTransform(0, 0, 1, 1, 0, 0, 0, -x, -y); + } // TODO: simplify this. + if (matrix) { + mtx.prependMatrix(matrix); + } + + var x_a = width * mtx.a, + x_b = width * mtx.b; + var y_c = height * mtx.c, + y_d = height * mtx.d; + var tx = mtx.tx, + ty = mtx.ty; + + var minX = tx, + maxX = tx, + minY = ty, + maxY = ty; + + if ((x = x_a + tx) < minX) { + minX = x; + } else if (x > maxX) { + maxX = x; + } + if ((x = x_a + y_c + tx) < minX) { + minX = x; + } else if (x > maxX) { + maxX = x; + } + if ((x = y_c + tx) < minX) { + minX = x; + } else if (x > maxX) { + maxX = x; + } + + if ((y = x_b + ty) < minY) { + minY = y; + } else if (y > maxY) { + maxY = y; + } + if ((y = x_b + y_d + ty) < minY) { + minY = y; + } else if (y > maxY) { + maxY = y; + } + if ((y = y_d + ty) < minY) { + minY = y; + } else if (y > maxY) { + maxY = y; + } + + return bounds.setValues(minX, minY, maxX - minX, maxY - minY); + }; + + /** + * Indicates whether the display object has any mouse event listeners or a cursor. + * @method _isMouseOpaque + * @return {Boolean} + * @protected + **/ + p._hasMouseEventListener = function () { + var evts = DisplayObject._MOUSE_EVENTS; + for (var i = 0, l = evts.length; i < l; i++) { + if (this.hasEventListener(evts[i])) { + return true; + } + } + return !!this.cursor; + }; + + createjs.DisplayObject = createjs.promote(DisplayObject, "EventDispatcher"); + })(); + + //############################################################################## + // Container.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * A Container is a nestable display list that allows you to work with compound display elements. For example you could + * group arm, leg, torso and head {{#crossLink "Bitmap"}}{{/crossLink}} instances together into a Person Container, and + * transform them as a group, while still being able to move the individual parts relative to each other. Children of + * containers have their transform and alpha properties concatenated with their parent + * Container. + * + * For example, a {{#crossLink "Shape"}}{{/crossLink}} with x=100 and alpha=0.5, placed in a Container with x=50 + * and alpha=0.7 will be rendered to the canvas at x=150 and alpha=0.35. + * Containers have some overhead, so you generally shouldn't create a Container to hold a single child. + * + *

Example

+ * + * var container = new createjs.Container(); + * container.addChild(bitmapInstance, shapeInstance); + * container.x = 100; + * + * @class Container + * @extends DisplayObject + * @constructor + **/ + + function Container() { + this.DisplayObject_constructor(); + + // public properties: + /** + * The array of children in the display list. You should usually use the child management methods such as + * {{#crossLink "Container/addChild"}}{{/crossLink}}, {{#crossLink "Container/removeChild"}}{{/crossLink}}, + * {{#crossLink "Container/swapChildren"}}{{/crossLink}}, etc, rather than accessing this directly, but it is + * included for advanced uses. + * @property children + * @type Array + * @default null + **/ + this.children = []; + + /** + * Indicates whether the children of this container are independently enabled for mouse/pointer interaction. + * If false, the children will be aggregated under the container - for example, a click on a child shape would + * trigger a click event on the container. + * @property mouseChildren + * @type Boolean + * @default true + **/ + this.mouseChildren = true; + + /** + * If false, the tick will not be propagated to children of this Container. This can provide some performance benefits. + * In addition to preventing the "tick" event from being dispatched, it will also prevent tick related updates + * on some display objects (ex. Sprite & MovieClip frame advancing, DOMElement visibility handling). + * @property tickChildren + * @type Boolean + * @default true + **/ + this.tickChildren = true; + } + var p = createjs.extend(Container, createjs.DisplayObject); + + // getter / setters: + /** + * Use the {{#crossLink "Container/numChildren:property"}}{{/crossLink}} property instead. + * @method getNumChildren + * @return {Number} + * @deprecated + **/ + p.getNumChildren = function () { + return this.children.length; + }; + + /** + * Returns the number of children in the container. + * @property numChildren + * @type {Number} + * @readonly + **/ + try { + Object.defineProperties(p, { + numChildren: { get: p.getNumChildren } + }); + } catch (e) {} + + // public methods: + /** + * Constructor alias for backwards compatibility. This method will be removed in future versions. + * Subclasses should be updated to use {{#crossLink "Utility Methods/extends"}}{{/crossLink}}. + * @method initialize + * @deprecated in favour of `createjs.promote()` + **/ + p.initialize = Container; // TODO: deprecated. + + /** + * Returns true or false indicating whether the display object would be visible if drawn to a canvas. + * This does not account for whether it would be visible within the boundaries of the stage. + * + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method isVisible + * @return {Boolean} Boolean indicating whether the display object would be visible if drawn to a canvas + **/ + p.isVisible = function () { + var hasContent = this.cacheCanvas || this.children.length; + return !!(this.visible && this.alpha > 0 && this.scaleX != 0 && this.scaleY != 0 && hasContent); + }; + + /** + * Draws the display object into the specified context ignoring its visible, alpha, shadow, and transform. + * Returns true if the draw was handled (useful for overriding functionality). + * + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method draw + * @param {CanvasRenderingContext2D} ctx The canvas 2D context object to draw into. + * @param {Boolean} [ignoreCache=false] Indicates whether the draw operation should ignore any current cache. + * For example, used for drawing the cache (to prevent it from simply drawing an existing cache back + * into itself). + **/ + p.draw = function (ctx, ignoreCache) { + if (this.DisplayObject_draw(ctx, ignoreCache)) { + return true; + } + + // this ensures we don't have issues with display list changes that occur during a draw: + var list = this.children.slice(); + for (var i = 0, l = list.length; i < l; i++) { + var child = list[i]; + if (!child.isVisible()) { + continue; + } + + // draw the child: + ctx.save(); + child.updateContext(ctx); + child.draw(ctx); + ctx.restore(); + } + return true; + }; + + /** + * Adds a child to the top of the display list. + * + *

Example

+ * + * container.addChild(bitmapInstance); + * + * You can also add multiple children at once: + * + * container.addChild(bitmapInstance, shapeInstance, textInstance); + * + * @method addChild + * @param {DisplayObject} child The display object to add. + * @return {DisplayObject} The child that was added, or the last child if multiple children were added. + **/ + p.addChild = function (child) { + if (child == null) { + return child; + } + var l = arguments.length; + if (l > 1) { + for (var i = 0; i < l; i++) { + this.addChild(arguments[i]); + } + return arguments[l - 1]; + } + if (child.parent) { + child.parent.removeChild(child); + } + child.parent = this; + this.children.push(child); + child.dispatchEvent("added"); + return child; + }; + + /** + * Adds a child to the display list at the specified index, bumping children at equal or greater indexes up one, and + * setting its parent to this Container. + * + *

Example

+ * + * addChildAt(child1, index); + * + * You can also add multiple children, such as: + * + * addChildAt(child1, child2, ..., index); + * + * The index must be between 0 and numChildren. For example, to add myShape under otherShape in the display list, + * you could use: + * + * container.addChildAt(myShape, container.getChildIndex(otherShape)); + * + * This would also bump otherShape's index up by one. Fails silently if the index is out of range. + * + * @method addChildAt + * @param {DisplayObject} child The display object to add. + * @param {Number} index The index to add the child at. + * @return {DisplayObject} Returns the last child that was added, or the last child if multiple children were added. + **/ + p.addChildAt = function (child, index) { + var l = arguments.length; + var indx = arguments[l - 1]; // can't use the same name as the index param or it replaces arguments[1] + if (indx < 0 || indx > this.children.length) { + return arguments[l - 2]; + } + if (l > 2) { + for (var i = 0; i < l - 1; i++) { + this.addChildAt(arguments[i], indx + i); + } + return arguments[l - 2]; + } + if (child.parent) { + child.parent.removeChild(child); + } + child.parent = this; + this.children.splice(index, 0, child); + child.dispatchEvent("added"); + return child; + }; + + /** + * Removes the specified child from the display list. Note that it is faster to use removeChildAt() if the index is + * already known. + * + *

Example

+ * + * container.removeChild(child); + * + * You can also remove multiple children: + * + * removeChild(child1, child2, ...); + * + * Returns true if the child (or children) was removed, or false if it was not in the display list. + * @method removeChild + * @param {DisplayObject} child The child to remove. + * @return {Boolean} true if the child (or children) was removed, or false if it was not in the display list. + **/ + p.removeChild = function (child) { + var l = arguments.length; + if (l > 1) { + var good = true; + for (var i = 0; i < l; i++) { + good = good && this.removeChild(arguments[i]); + } + return good; + } + return this.removeChildAt(createjs.indexOf(this.children, child)); + }; + + /** + * Removes the child at the specified index from the display list, and sets its parent to null. + * + *

Example

+ * + * container.removeChildAt(2); + * + * You can also remove multiple children: + * + * container.removeChild(2, 7, ...) + * + * Returns true if the child (or children) was removed, or false if any index was out of range. + * @method removeChildAt + * @param {Number} index The index of the child to remove. + * @return {Boolean} true if the child (or children) was removed, or false if any index was out of range. + **/ + p.removeChildAt = function (index) { + var l = arguments.length; + if (l > 1) { + var a = []; + for (var i = 0; i < l; i++) { + a[i] = arguments[i]; + } + a.sort(function (a, b) { + return b - a; + }); + var good = true; + for (var i = 0; i < l; i++) { + good = good && this.removeChildAt(a[i]); + } + return good; + } + if (index < 0 || index > this.children.length - 1) { + return false; + } + var child = this.children[index]; + if (child) { + child.parent = null; + } + this.children.splice(index, 1); + child.dispatchEvent("removed"); + return true; + }; + + /** + * Removes all children from the display list. + * + *

Example

+ * + * container.removeAllChildren(); + * + * @method removeAllChildren + **/ + p.removeAllChildren = function () { + var kids = this.children; + while (kids.length) { + this.removeChildAt(0); + } + }; + + /** + * Returns the child at the specified index. + * + *

Example

+ * + * container.getChildAt(2); + * + * @method getChildAt + * @param {Number} index The index of the child to return. + * @return {DisplayObject} The child at the specified index. Returns null if there is no child at the index. + **/ + p.getChildAt = function (index) { + return this.children[index]; + }; + + /** + * Returns the child with the specified name. + * @method getChildByName + * @param {String} name The name of the child to return. + * @return {DisplayObject} The child with the specified name. + **/ + p.getChildByName = function (name) { + var kids = this.children; + for (var i = 0, l = kids.length; i < l; i++) { + if (kids[i].name == name) { + return kids[i]; + } + } + return null; + }; + + /** + * Performs an array sort operation on the child list. + * + *

Example: Display children with a higher y in front.

+ * + * var sortFunction = function(obj1, obj2, options) { + * if (obj1.y > obj2.y) { return 1; } + * if (obj1.y < obj2.y) { return -1; } + * return 0; + * } + * container.sortChildren(sortFunction); + * + * @method sortChildren + * @param {Function} sortFunction the function to use to sort the child list. See JavaScript's Array.sort + * documentation for details. + **/ + p.sortChildren = function (sortFunction) { + this.children.sort(sortFunction); + }; + + /** + * Returns the index of the specified child in the display list, or -1 if it is not in the display list. + * + *

Example

+ * + * var index = container.getChildIndex(child); + * + * @method getChildIndex + * @param {DisplayObject} child The child to return the index of. + * @return {Number} The index of the specified child. -1 if the child is not found. + **/ + p.getChildIndex = function (child) { + return createjs.indexOf(this.children, child); + }; + + /** + * Swaps the children at the specified indexes. Fails silently if either index is out of range. + * @method swapChildrenAt + * @param {Number} index1 + * @param {Number} index2 + **/ + p.swapChildrenAt = function (index1, index2) { + var kids = this.children; + var o1 = kids[index1]; + var o2 = kids[index2]; + if (!o1 || !o2) { + return; + } + kids[index1] = o2; + kids[index2] = o1; + }; + + /** + * Swaps the specified children's depth in the display list. Fails silently if either child is not a child of this + * Container. + * @method swapChildren + * @param {DisplayObject} child1 + * @param {DisplayObject} child2 + **/ + p.swapChildren = function (child1, child2) { + var kids = this.children; + var index1, index2; + for (var i = 0, l = kids.length; i < l; i++) { + if (kids[i] == child1) { + index1 = i; + } + if (kids[i] == child2) { + index2 = i; + } + if (index1 != null && index2 != null) { + break; + } + } + if (i == l) { + return; + } // TODO: throw error? + kids[index1] = child2; + kids[index2] = child1; + }; + + /** + * Changes the depth of the specified child. Fails silently if the child is not a child of this container, or the index is out of range. + * @param {DisplayObject} child + * @param {Number} index + * @method setChildIndex + **/ + p.setChildIndex = function (child, index) { + var kids = this.children, + l = kids.length; + if (child.parent != this || index < 0 || index >= l) { + return; + } + for (var i = 0; i < l; i++) { + if (kids[i] == child) { + break; + } + } + if (i == l || i == index) { + return; + } + kids.splice(i, 1); + kids.splice(index, 0, child); + }; + + /** + * Returns true if the specified display object either is this container or is a descendent (child, grandchild, etc) + * of this container. + * @method contains + * @param {DisplayObject} child The DisplayObject to be checked. + * @return {Boolean} true if the specified display object either is this container or is a descendent. + **/ + p.contains = function (child) { + while (child) { + if (child == this) { + return true; + } + child = child.parent; + } + return false; + }; + + /** + * Tests whether the display object intersects the specified local point (ie. draws a pixel with alpha > 0 at the + * specified position). This ignores the alpha, shadow and compositeOperation of the display object, and all + * transform properties including regX/Y. + * @method hitTest + * @param {Number} x The x position to check in the display object's local coordinates. + * @param {Number} y The y position to check in the display object's local coordinates. + * @return {Boolean} A Boolean indicating whether there is a visible section of a DisplayObject that overlaps the specified + * coordinates. + **/ + p.hitTest = function (x, y) { + // TODO: optimize to use the fast cache check where possible. + return this.getObjectUnderPoint(x, y) != null; + }; + + /** + * Returns an array of all display objects under the specified coordinates that are in this container's display + * list. This routine ignores any display objects with {{#crossLink "DisplayObject/mouseEnabled:property"}}{{/crossLink}} + * set to `false`. The array will be sorted in order of visual depth, with the top-most display object at index 0. + * This uses shape based hit detection, and can be an expensive operation to run, so it is best to use it carefully. + * For example, if testing for objects under the mouse, test on tick (instead of on {{#crossLink "DisplayObject/mousemove:event"}}{{/crossLink}}), + * and only if the mouse's position has changed. + * + *
    + *
  • By default (mode=0) this method evaluates all display objects.
  • + *
  • By setting the `mode` parameter to `1`, the {{#crossLink "DisplayObject/mouseEnabled:property"}}{{/crossLink}} + * and {{#crossLink "mouseChildren:property"}}{{/crossLink}} properties will be respected.
  • + *
  • Setting the `mode` to `2` additionally excludes display objects that do not have active mouse event + * listeners or a {{#crossLink "DisplayObject:cursor:property"}}{{/crossLink}} property. That is, only objects + * that would normally intercept mouse interaction will be included. This can significantly improve performance + * in some cases by reducing the number of display objects that need to be tested.
  • + * + * + * This method accounts for both {{#crossLink "DisplayObject/hitArea:property"}}{{/crossLink}} and {{#crossLink "DisplayObject/mask:property"}}{{/crossLink}}. + * @method getObjectsUnderPoint + * @param {Number} x The x position in the container to test. + * @param {Number} y The y position in the container to test. + * @param {Number} [mode=0] The mode to use to determine which display objects to include. 0-all, 1-respect mouseEnabled/mouseChildren, 2-only mouse opaque objects. + * @return {Array} An Array of DisplayObjects under the specified coordinates. + **/ + p.getObjectsUnderPoint = function (x, y, mode) { + var arr = []; + var pt = this.localToGlobal(x, y); + this._getObjectsUnderPoint(pt.x, pt.y, arr, mode > 0, mode == 1); + return arr; + }; + + /** + * Similar to {{#crossLink "Container/getObjectsUnderPoint"}}{{/crossLink}}, but returns only the top-most display + * object. This runs significantly faster than getObjectsUnderPoint(), but is still potentially an expensive + * operation. See {{#crossLink "Container/getObjectsUnderPoint"}}{{/crossLink}} for more information. + * @method getObjectUnderPoint + * @param {Number} x The x position in the container to test. + * @param {Number} y The y position in the container to test. + * @param {Number} mode The mode to use to determine which display objects to include. 0-all, 1-respect mouseEnabled/mouseChildren, 2-only mouse opaque objects. + * @return {DisplayObject} The top-most display object under the specified coordinates. + **/ + p.getObjectUnderPoint = function (x, y, mode) { + var pt = this.localToGlobal(x, y); + return this._getObjectsUnderPoint(pt.x, pt.y, null, mode > 0, mode == 1); + }; + + /** + * Docced in superclass. + */ + p.getBounds = function () { + return this._getBounds(null, true); + }; + + /** + * Docced in superclass. + */ + p.getTransformedBounds = function () { + return this._getBounds(); + }; + + /** + * Returns a clone of this Container. Some properties that are specific to this instance's current context are + * reverted to their defaults (for example .parent). + * @method clone + * @param {Boolean} [recursive=false] If true, all of the descendants of this container will be cloned recursively. If false, the + * properties of the container will be cloned, but the new instance will not have any children. + * @return {Container} A clone of the current Container instance. + **/ + p.clone = function (recursive) { + var o = this._cloneProps(new Container()); + if (recursive) { + this._cloneChildren(o); + } + return o; + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[Container (name=" + this.name + ")]"; + }; + + // private methods: + /** + * @method _tick + * @param {Object} evtObj An event object that will be dispatched to all tick listeners. This object is reused between dispatchers to reduce construction & GC costs. + * @protected + **/ + p._tick = function (evtObj) { + if (this.tickChildren) { + for (var i = this.children.length - 1; i >= 0; i--) { + var child = this.children[i]; + if (child.tickEnabled && child._tick) { + child._tick(evtObj); + } + } + } + this.DisplayObject__tick(evtObj); + }; + + /** + * Recursively clones all children of this container, and adds them to the target container. + * @method cloneChildren + * @protected + * @param {Container} o The target container. + **/ + p._cloneChildren = function (o) { + if (o.children.length) { + o.removeAllChildren(); + } + var arr = o.children; + for (var i = 0, l = this.children.length; i < l; i++) { + var clone = this.children[i].clone(true); + clone.parent = o; + arr.push(clone); + } + }; + + /** + * @method _getObjectsUnderPoint + * @param {Number} x + * @param {Number} y + * @param {Array} arr + * @param {Boolean} mouse If true, it will respect mouse interaction properties like mouseEnabled, mouseChildren, and active listeners. + * @param {Boolean} activeListener If true, there is an active mouse event listener on a parent object. + * @param {Number} currentDepth Indicates the current depth of the search. + * @return {DisplayObject} + * @protected + **/ + p._getObjectsUnderPoint = function (x, y, arr, mouse, activeListener, currentDepth) { + currentDepth = currentDepth || 0; + if (!currentDepth && !this._testMask(this, x, y)) { + return null; + } + var mtx, + ctx = createjs.DisplayObject._hitTestContext; + activeListener = activeListener || mouse && this._hasMouseEventListener(); + + // draw children one at a time, and check if we get a hit: + var children = this.children, + l = children.length; + for (var i = l - 1; i >= 0; i--) { + var child = children[i]; + var hitArea = child.hitArea; + if (!child.visible || !hitArea && !child.isVisible() || mouse && !child.mouseEnabled) { + continue; + } + if (!hitArea && !this._testMask(child, x, y)) { + continue; + } + + // if a child container has a hitArea then we only need to check its hitArea, so we can treat it as a normal DO: + if (!hitArea && child instanceof Container) { + var result = child._getObjectsUnderPoint(x, y, arr, mouse, activeListener, currentDepth + 1); + if (!arr && result) { + return mouse && !this.mouseChildren ? this : result; + } + } else { + if (mouse && !activeListener && !child._hasMouseEventListener()) { + continue; + } + + // TODO: can we pass displayProps forward, to avoid having to calculate this backwards every time? It's kind of a mixed bag. When we're only hunting for DOs with event listeners, it may not make sense. + var props = child.getConcatenatedDisplayProps(child._props); + mtx = props.matrix; + + if (hitArea) { + mtx.appendMatrix(hitArea.getMatrix(hitArea._props.matrix)); + props.alpha = hitArea.alpha; + } + + ctx.globalAlpha = props.alpha; + ctx.setTransform(mtx.a, mtx.b, mtx.c, mtx.d, mtx.tx - x, mtx.ty - y); + (hitArea || child).draw(ctx); + if (!this._testHit(ctx)) { + continue; + } + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.clearRect(0, 0, 2, 2); + if (arr) { + arr.push(child); + } else { + return mouse && !this.mouseChildren ? this : child; + } + } + } + return null; + }; + + /** + * @method _testMask + * @param {DisplayObject} target + * @param {Number} x + * @param {Number} y + * @return {Boolean} Indicates whether the x/y is within the masked region. + * @protected + **/ + p._testMask = function (target, x, y) { + var mask = target.mask; + if (!mask || !mask.graphics || mask.graphics.isEmpty()) { + return true; + } + + var mtx = this._props.matrix, + parent = target.parent; + mtx = parent ? parent.getConcatenatedMatrix(mtx) : mtx.identity(); + mtx = mask.getMatrix(mask._props.matrix).prependMatrix(mtx); + + var ctx = createjs.DisplayObject._hitTestContext; + ctx.setTransform(mtx.a, mtx.b, mtx.c, mtx.d, mtx.tx - x, mtx.ty - y); + + // draw the mask as a solid fill: + mask.graphics.drawAsPath(ctx); + ctx.fillStyle = "#000"; + ctx.fill(); + + if (!this._testHit(ctx)) { + return false; + } + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.clearRect(0, 0, 2, 2); + + return true; + }; + + /** + * @method _getBounds + * @param {Matrix2D} matrix + * @param {Boolean} ignoreTransform If true, does not apply this object's transform. + * @return {Rectangle} + * @protected + **/ + p._getBounds = function (matrix, ignoreTransform) { + var bounds = this.DisplayObject_getBounds(); + if (bounds) { + return this._transformBounds(bounds, matrix, ignoreTransform); + } + + var mtx = this._props.matrix; + mtx = ignoreTransform ? mtx.identity() : this.getMatrix(mtx); + if (matrix) { + mtx.prependMatrix(matrix); + } + + var l = this.children.length, + rect = null; + for (var i = 0; i < l; i++) { + var child = this.children[i]; + if (!child.visible || !(bounds = child._getBounds(mtx))) { + continue; + } + if (rect) { + rect.extend(bounds.x, bounds.y, bounds.width, bounds.height); + } else { + rect = bounds.clone(); + } + } + return rect; + }; + + createjs.Container = createjs.promote(Container, "DisplayObject"); + })(); + + //############################################################################## + // Stage.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * A stage is the root level {{#crossLink "Container"}}{{/crossLink}} for a display list. Each time its {{#crossLink "Stage/tick"}}{{/crossLink}} + * method is called, it will render its display list to its target canvas. + * + *

    Example

    + * This example creates a stage, adds a child to it, then uses {{#crossLink "Ticker"}}{{/crossLink}} to update the child + * and redraw the stage using {{#crossLink "Stage/update"}}{{/crossLink}}. + * + * var stage = new createjs.Stage("canvasElementId"); + * var image = new createjs.Bitmap("imagePath.png"); + * stage.addChild(image); + * createjs.Ticker.addEventListener("tick", handleTick); + * function handleTick(event) { + * image.x += 10; + * stage.update(); + * } + * + * @class Stage + * @extends Container + * @constructor + * @param {HTMLCanvasElement | String | Object} canvas A canvas object that the Stage will render to, or the string id + * of a canvas object in the current document. + **/ + + function Stage(canvas) { + this.Container_constructor(); + + // public properties: + /** + * Indicates whether the stage should automatically clear the canvas before each render. You can set this to false + * to manually control clearing (for generative art, or when pointing multiple stages at the same canvas for + * example). + * + *

    Example

    + * + * var stage = new createjs.Stage("canvasId"); + * stage.autoClear = false; + * + * @property autoClear + * @type Boolean + * @default true + **/ + this.autoClear = true; + + /** + * The canvas the stage will render to. Multiple stages can share a single canvas, but you must disable autoClear for all but the + * first stage that will be ticked (or they will clear each other's render). + * + * When changing the canvas property you must disable the events on the old canvas, and enable events on the + * new canvas or mouse events will not work as expected. For example: + * + * myStage.enableDOMEvents(false); + * myStage.canvas = anotherCanvas; + * myStage.enableDOMEvents(true); + * + * @property canvas + * @type HTMLCanvasElement | Object + **/ + this.canvas = typeof canvas == "string" ? document.getElementById(canvas) : canvas; + + /** + * The current mouse X position on the canvas. If the mouse leaves the canvas, this will indicate the most recent + * position over the canvas, and mouseInBounds will be set to false. + * @property mouseX + * @type Number + * @readonly + **/ + this.mouseX = 0; + + /** + * The current mouse Y position on the canvas. If the mouse leaves the canvas, this will indicate the most recent + * position over the canvas, and mouseInBounds will be set to false. + * @property mouseY + * @type Number + * @readonly + **/ + this.mouseY = 0; + + /** + * Specifies the area of the stage to affect when calling update. This can be use to selectively + * re-draw specific regions of the canvas. If null, the whole canvas area is drawn. + * @property drawRect + * @type {Rectangle} + */ + this.drawRect = null; + + /** + * Indicates whether display objects should be rendered on whole pixels. You can set the + * {{#crossLink "DisplayObject/snapToPixel"}}{{/crossLink}} property of + * display objects to false to enable/disable this behaviour on a per instance basis. + * @property snapToPixelEnabled + * @type Boolean + * @default false + **/ + this.snapToPixelEnabled = false; + + /** + * Indicates whether the mouse is currently within the bounds of the canvas. + * @property mouseInBounds + * @type Boolean + * @default false + **/ + this.mouseInBounds = false; + + /** + * If true, tick callbacks will be called on all display objects on the stage prior to rendering to the canvas. + * @property tickOnUpdate + * @type Boolean + * @default true + **/ + this.tickOnUpdate = true; + + /** + * If true, mouse move events will continue to be called when the mouse leaves the target canvas. See + * {{#crossLink "Stage/mouseInBounds:property"}}{{/crossLink}}, and {{#crossLink "MouseEvent"}}{{/crossLink}} + * x/y/rawX/rawY. + * @property mouseMoveOutside + * @type Boolean + * @default false + **/ + this.mouseMoveOutside = false; + + /** + * Prevents selection of other elements in the html page if the user clicks and drags, or double clicks on the canvas. + * This works by calling `preventDefault()` on any mousedown events (or touch equivalent) originating on the canvas. + * @property preventSelection + * @type Boolean + * @default true + **/ + this.preventSelection = true; + + /** + * The hitArea property is not supported for Stage. + * @property hitArea + * @type {DisplayObject} + * @default null + */ + + // private properties: + /** + * Holds objects with data for each active pointer id. Each object has the following properties: + * x, y, event, target, overTarget, overX, overY, inBounds, posEvtObj (native event that last updated position) + * @property _pointerData + * @type {Object} + * @private + */ + this._pointerData = {}; + + /** + * Number of active pointers. + * @property _pointerCount + * @type {Object} + * @private + */ + this._pointerCount = 0; + + /** + * The ID of the primary pointer. + * @property _primaryPointerID + * @type {Object} + * @private + */ + this._primaryPointerID = null; + + /** + * @property _mouseOverIntervalID + * @protected + * @type Number + **/ + this._mouseOverIntervalID = null; + + /** + * @property _nextStage + * @protected + * @type Stage + **/ + this._nextStage = null; + + /** + * @property _prevStage + * @protected + * @type Stage + **/ + this._prevStage = null; + + // initialize: + this.enableDOMEvents(true); + } + var p = createjs.extend(Stage, createjs.Container); + + /** + * REMOVED. Removed in favor of using `MySuperClass_constructor`. + * See {{#crossLink "Utility Methods/extend"}}{{/crossLink}} and {{#crossLink "Utility Methods/promote"}}{{/crossLink}} + * for details. + * + * There is an inheritance tutorial distributed with EaselJS in /tutorials/Inheritance. + * + * @method initialize + * @protected + * @deprecated + */ + // p.initialize = function() {}; // searchable for devs wondering where it is. + + + // events: + /** + * Dispatched when the user moves the mouse over the canvas. + * See the {{#crossLink "MouseEvent"}}{{/crossLink}} class for a listing of event properties. + * @event stagemousemove + * @since 0.6.0 + */ + + /** + * Dispatched when the user presses their left mouse button on the canvas. See the {{#crossLink "MouseEvent"}}{{/crossLink}} + * class for a listing of event properties. + * @event stagemousedown + * @since 0.6.0 + */ + + /** + * Dispatched when the user the user presses somewhere on the stage, then releases the mouse button anywhere that the page can detect it (this varies slightly between browsers). + * You can use {{#crossLink "Stage/mouseInBounds:property"}}{{/crossLink}} to check whether the mouse is currently within the stage bounds. + * See the {{#crossLink "MouseEvent"}}{{/crossLink}} class for a listing of event properties. + * @event stagemouseup + * @since 0.6.0 + */ + + /** + * Dispatched when the mouse moves from within the canvas area (mouseInBounds == true) to outside it (mouseInBounds == false). + * This is currently only dispatched for mouse input (not touch). See the {{#crossLink "MouseEvent"}}{{/crossLink}} + * class for a listing of event properties. + * @event mouseleave + * @since 0.7.0 + */ + + /** + * Dispatched when the mouse moves into the canvas area (mouseInBounds == false) from outside it (mouseInBounds == true). + * This is currently only dispatched for mouse input (not touch). See the {{#crossLink "MouseEvent"}}{{/crossLink}} + * class for a listing of event properties. + * @event mouseenter + * @since 0.7.0 + */ + + /** + * Dispatched each update immediately before the tick event is propagated through the display list. + * You can call preventDefault on the event object to cancel propagating the tick event. + * @event tickstart + * @since 0.7.0 + */ + + /** + * Dispatched each update immediately after the tick event is propagated through the display list. Does not fire if + * tickOnUpdate is false. Precedes the "drawstart" event. + * @event tickend + * @since 0.7.0 + */ + + /** + * Dispatched each update immediately before the canvas is cleared and the display list is drawn to it. + * You can call preventDefault on the event object to cancel the draw. + * @event drawstart + * @since 0.7.0 + */ + + /** + * Dispatched each update immediately after the display list is drawn to the canvas and the canvas context is restored. + * @event drawend + * @since 0.7.0 + */ + + // getter / setters: + /** + * Specifies a target stage that will have mouse / touch interactions relayed to it after this stage handles them. + * This can be useful in cases where you have multiple layered canvases and want user interactions + * events to pass through. For example, this would relay mouse events from topStage to bottomStage: + * + * topStage.nextStage = bottomStage; + * + * To disable relaying, set nextStage to null. + * + * MouseOver, MouseOut, RollOver, and RollOut interactions are also passed through using the mouse over settings + * of the top-most stage, but are only processed if the target stage has mouse over interactions enabled. + * Considerations when using roll over in relay targets:
      + *
    1. The top-most (first) stage must have mouse over interactions enabled (via enableMouseOver)
    2. + *
    3. All stages that wish to participate in mouse over interaction must enable them via enableMouseOver
    4. + *
    5. All relay targets will share the frequency value of the top-most stage
    6. + *
    + * To illustrate, in this example the targetStage would process mouse over interactions at 10hz (despite passing + * 30 as it's desired frequency): + * topStage.nextStage = targetStage; + * topStage.enableMouseOver(10); + * targetStage.enableMouseOver(30); + * + * If the target stage's canvas is completely covered by this stage's canvas, you may also want to disable its + * DOM events using: + * + * targetStage.enableDOMEvents(false); + * + * @property nextStage + * @type {Stage} + **/ + p._get_nextStage = function () { + return this._nextStage; + }; + p._set_nextStage = function (value) { + if (this._nextStage) { + this._nextStage._prevStage = null; + } + if (value) { + value._prevStage = this; + } + this._nextStage = value; + }; + + try { + Object.defineProperties(p, { + nextStage: { get: p._get_nextStage, set: p._set_nextStage } + }); + } catch (e) {} // TODO: use Log + + + // public methods: + /** + * Each time the update method is called, the stage will call {{#crossLink "Stage/tick"}}{{/crossLink}} + * unless {{#crossLink "Stage/tickOnUpdate:property"}}{{/crossLink}} is set to false, + * and then render the display list to the canvas. + * + * @method update + * @param {Object} [props] Props object to pass to `tick()`. Should usually be a {{#crossLink "Ticker"}}{{/crossLink}} event object, or similar object with a delta property. + **/ + p.update = function (props) { + if (!this.canvas) { + return; + } + if (this.tickOnUpdate) { + this.tick(props); + } + if (this.dispatchEvent("drawstart", false, true) === false) { + return; + } + createjs.DisplayObject._snapToPixelEnabled = this.snapToPixelEnabled; + var r = this.drawRect, + ctx = this.canvas.getContext("2d"); + ctx.setTransform(1, 0, 0, 1, 0, 0); + if (this.autoClear) { + if (r) { + ctx.clearRect(r.x, r.y, r.width, r.height); + } else { + ctx.clearRect(0, 0, this.canvas.width + 1, this.canvas.height + 1); + } + } + ctx.save(); + if (this.drawRect) { + ctx.beginPath(); + ctx.rect(r.x, r.y, r.width, r.height); + ctx.clip(); + } + this.updateContext(ctx); + this.draw(ctx, false); + ctx.restore(); + this.dispatchEvent("drawend"); + }; + + /** + * Propagates a tick event through the display list. This is automatically called by {{#crossLink "Stage/update"}}{{/crossLink}} + * unless {{#crossLink "Stage/tickOnUpdate:property"}}{{/crossLink}} is set to false. + * + * If a props object is passed to `tick()`, then all of its properties will be copied to the event object that is + * propagated to listeners. + * + * Some time-based features in EaselJS (for example {{#crossLink "Sprite/framerate"}}{{/crossLink}} require that + * a {{#crossLink "Ticker/tick:event"}}{{/crossLink}} event object (or equivalent object with a delta property) be + * passed as the `props` parameter to `tick()`. For example: + * + * Ticker.on("tick", handleTick); + * function handleTick(evtObj) { + * // clone the event object from Ticker, and add some custom data to it: + * var evt = evtObj.clone().set({greeting:"hello", name:"world"}); + * + * // pass it to stage.update(): + * myStage.update(evt); // subsequently calls tick() with the same param + * } + * + * // ... + * myDisplayObject.on("tick", handleDisplayObjectTick); + * function handleDisplayObjectTick(evt) { + * console.log(evt.delta); // the delta property from the Ticker tick event object + * console.log(evt.greeting, evt.name); // custom data: "hello world" + * } + * + * @method tick + * @param {Object} [props] An object with properties that should be copied to the event object. Should usually be a Ticker event object, or similar object with a delta property. + **/ + p.tick = function (props) { + if (!this.tickEnabled || this.dispatchEvent("tickstart", false, true) === false) { + return; + } + var evtObj = new createjs.Event("tick"); + if (props) { + for (var n in props) { + if (props.hasOwnProperty(n)) { + evtObj[n] = props[n]; + } + } + } + this._tick(evtObj); + this.dispatchEvent("tickend"); + }; + + /** + * Default event handler that calls the Stage {{#crossLink "Stage/update"}}{{/crossLink}} method when a {{#crossLink "DisplayObject/tick:event"}}{{/crossLink}} + * event is received. This allows you to register a Stage instance as a event listener on {{#crossLink "Ticker"}}{{/crossLink}} + * directly, using: + * + * Ticker.addEventListener("tick", myStage"); + * + * Note that if you subscribe to ticks using this pattern, then the tick event object will be passed through to + * display object tick handlers, instead of delta and paused parameters. + * @property handleEvent + * @type Function + **/ + p.handleEvent = function (evt) { + if (evt.type == "tick") { + this.update(evt); + } + }; + + /** + * Clears the target canvas. Useful if {{#crossLink "Stage/autoClear:property"}}{{/crossLink}} is set to `false`. + * @method clear + **/ + p.clear = function () { + if (!this.canvas) { + return; + } + var ctx = this.canvas.getContext("2d"); + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.clearRect(0, 0, this.canvas.width + 1, this.canvas.height + 1); + }; + + /** + * Returns a data url that contains a Base64-encoded image of the contents of the stage. The returned data url can + * be specified as the src value of an image element. + * @method toDataURL + * @param {String} [backgroundColor] The background color to be used for the generated image. Any valid CSS color + * value is allowed. The default value is a transparent background. + * @param {String} [mimeType="image/png"] The MIME type of the image format to be create. The default is "image/png". If an unknown MIME type + * is passed in, or if the browser does not support the specified MIME type, the default value will be used. + * @return {String} a Base64 encoded image. + **/ + p.toDataURL = function (backgroundColor, mimeType) { + var data, + ctx = this.canvas.getContext('2d'), + w = this.canvas.width, + h = this.canvas.height; + + if (backgroundColor) { + data = ctx.getImageData(0, 0, w, h); + var compositeOperation = ctx.globalCompositeOperation; + ctx.globalCompositeOperation = "destination-over"; + + ctx.fillStyle = backgroundColor; + ctx.fillRect(0, 0, w, h); + } + + var dataURL = this.canvas.toDataURL(mimeType || "image/png"); + + if (backgroundColor) { + ctx.putImageData(data, 0, 0); + ctx.globalCompositeOperation = compositeOperation; + } + + return dataURL; + }; + + /** + * Enables or disables (by passing a frequency of 0) mouse over ({{#crossLink "DisplayObject/mouseover:event"}}{{/crossLink}} + * and {{#crossLink "DisplayObject/mouseout:event"}}{{/crossLink}}) and roll over events ({{#crossLink "DisplayObject/rollover:event"}}{{/crossLink}} + * and {{#crossLink "DisplayObject/rollout:event"}}{{/crossLink}}) for this stage's display list. These events can + * be expensive to generate, so they are disabled by default. The frequency of the events can be controlled + * independently of mouse move events via the optional `frequency` parameter. + * + *

    Example

    + * + * var stage = new createjs.Stage("canvasId"); + * stage.enableMouseOver(10); // 10 updates per second + * + * @method enableMouseOver + * @param {Number} [frequency=20] Optional param specifying the maximum number of times per second to broadcast + * mouse over/out events. Set to 0 to disable mouse over events completely. Maximum is 50. A lower frequency is less + * responsive, but uses less CPU. + **/ + p.enableMouseOver = function (frequency) { + if (this._mouseOverIntervalID) { + clearInterval(this._mouseOverIntervalID); + this._mouseOverIntervalID = null; + if (frequency == 0) { + this._testMouseOver(true); + } + } + if (frequency == null) { + frequency = 20; + } else if (frequency <= 0) { + return; + } + var o = this; + this._mouseOverIntervalID = setInterval(function () { + o._testMouseOver(); + }, 1000 / Math.min(50, frequency)); + }; + + /** + * Enables or disables the event listeners that stage adds to DOM elements (window, document and canvas). It is good + * practice to disable events when disposing of a Stage instance, otherwise the stage will continue to receive + * events from the page. + * + * When changing the canvas property you must disable the events on the old canvas, and enable events on the + * new canvas or mouse events will not work as expected. For example: + * + * myStage.enableDOMEvents(false); + * myStage.canvas = anotherCanvas; + * myStage.enableDOMEvents(true); + * + * @method enableDOMEvents + * @param {Boolean} [enable=true] Indicates whether to enable or disable the events. Default is true. + **/ + p.enableDOMEvents = function (enable) { + if (enable == null) { + enable = true; + } + var n, + o, + ls = this._eventListeners; + if (!enable && ls) { + for (n in ls) { + o = ls[n]; + o.t.removeEventListener(n, o.f, false); + } + this._eventListeners = null; + } else if (enable && !ls && this.canvas) { + var t = window.addEventListener ? window : document; + var _this = this; + ls = this._eventListeners = {}; + ls["mouseup"] = { t: t, f: function f(e) { + _this._handleMouseUp(e); + } }; + ls["mousemove"] = { t: t, f: function f(e) { + _this._handleMouseMove(e); + } }; + ls["dblclick"] = { t: this.canvas, f: function f(e) { + _this._handleDoubleClick(e); + } }; + ls["mousedown"] = { t: this.canvas, f: function f(e) { + _this._handleMouseDown(e); + } }; + + for (n in ls) { + o = ls[n]; + o.t.addEventListener(n, o.f, false); + } + } + }; + + /** + * Stage instances cannot be cloned. + * @method clone + **/ + p.clone = function () { + throw "Stage cannot be cloned."; + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[Stage (name=" + this.name + ")]"; + }; + + // private methods: + /** + * @method _getElementRect + * @protected + * @param {HTMLElement} e + **/ + p._getElementRect = function (e) { + var bounds; + try { + bounds = e.getBoundingClientRect(); + } // this can fail on disconnected DOM elements in IE9 + catch (err) { + bounds = { top: e.offsetTop, left: e.offsetLeft, width: e.offsetWidth, height: e.offsetHeight }; + } + + var offX = (window.pageXOffset || document.scrollLeft || 0) - (document.clientLeft || document.body.clientLeft || 0); + var offY = (window.pageYOffset || document.scrollTop || 0) - (document.clientTop || document.body.clientTop || 0); + + var styles = window.getComputedStyle ? getComputedStyle(e, null) : e.currentStyle; // IE <9 compatibility. + var padL = parseInt(styles.paddingLeft) + parseInt(styles.borderLeftWidth); + var padT = parseInt(styles.paddingTop) + parseInt(styles.borderTopWidth); + var padR = parseInt(styles.paddingRight) + parseInt(styles.borderRightWidth); + var padB = parseInt(styles.paddingBottom) + parseInt(styles.borderBottomWidth); + + // note: in some browsers bounds properties are read only. + return { + left: bounds.left + offX + padL, + right: bounds.right + offX - padR, + top: bounds.top + offY + padT, + bottom: bounds.bottom + offY - padB + }; + }; + + /** + * @method _getPointerData + * @protected + * @param {Number} id + **/ + p._getPointerData = function (id) { + var data = this._pointerData[id]; + if (!data) { + data = this._pointerData[id] = { x: 0, y: 0 }; + } + return data; + }; + + /** + * @method _handleMouseMove + * @protected + * @param {MouseEvent} e + **/ + p._handleMouseMove = function (e) { + if (!e) { + e = window.event; + } + this._handlePointerMove(-1, e, e.pageX, e.pageY); + }; + + /** + * @method _handlePointerMove + * @protected + * @param {Number} id + * @param {Event} e + * @param {Number} pageX + * @param {Number} pageY + * @param {Stage} owner Indicates that the event has already been captured & handled by the indicated stage. + **/ + p._handlePointerMove = function (id, e, pageX, pageY, owner) { + if (this._prevStage && owner === undefined) { + return; + } // redundant listener. + if (!this.canvas) { + return; + } + var nextStage = this._nextStage, + o = this._getPointerData(id); + + var inBounds = o.inBounds; + this._updatePointerPosition(id, e, pageX, pageY); + if (inBounds || o.inBounds || this.mouseMoveOutside) { + if (id === -1 && o.inBounds == !inBounds) { + this._dispatchMouseEvent(this, inBounds ? "mouseleave" : "mouseenter", false, id, o, e); + } + + this._dispatchMouseEvent(this, "stagemousemove", false, id, o, e); + this._dispatchMouseEvent(o.target, "pressmove", true, id, o, e); + } + + nextStage && nextStage._handlePointerMove(id, e, pageX, pageY, null); + }; + + /** + * @method _updatePointerPosition + * @protected + * @param {Number} id + * @param {Event} e + * @param {Number} pageX + * @param {Number} pageY + **/ + p._updatePointerPosition = function (id, e, pageX, pageY) { + var rect = this._getElementRect(this.canvas); + pageX -= rect.left; + pageY -= rect.top; + + var w = this.canvas.width; + var h = this.canvas.height; + pageX /= (rect.right - rect.left) / w; + pageY /= (rect.bottom - rect.top) / h; + var o = this._getPointerData(id); + if (o.inBounds = pageX >= 0 && pageY >= 0 && pageX <= w - 1 && pageY <= h - 1) { + o.x = pageX; + o.y = pageY; + } else if (this.mouseMoveOutside) { + o.x = pageX < 0 ? 0 : pageX > w - 1 ? w - 1 : pageX; + o.y = pageY < 0 ? 0 : pageY > h - 1 ? h - 1 : pageY; + } + + o.posEvtObj = e; + o.rawX = pageX; + o.rawY = pageY; + + if (id === this._primaryPointerID || id === -1) { + this.mouseX = o.x; + this.mouseY = o.y; + this.mouseInBounds = o.inBounds; + } + }; + + /** + * @method _handleMouseUp + * @protected + * @param {MouseEvent} e + **/ + p._handleMouseUp = function (e) { + this._handlePointerUp(-1, e, false); + }; + + /** + * @method _handlePointerUp + * @protected + * @param {Number} id + * @param {Event} e + * @param {Boolean} clear + * @param {Stage} owner Indicates that the event has already been captured & handled by the indicated stage. + **/ + p._handlePointerUp = function (id, e, clear, owner) { + var nextStage = this._nextStage, + o = this._getPointerData(id); + if (this._prevStage && owner === undefined) { + return; + } // redundant listener. + + var target = null, + oTarget = o.target; + if (!owner && (oTarget || nextStage)) { + target = this._getObjectsUnderPoint(o.x, o.y, null, true); + } + + if (o.down) { + this._dispatchMouseEvent(this, "stagemouseup", false, id, o, e, target);o.down = false; + } + + if (target == oTarget) { + this._dispatchMouseEvent(oTarget, "click", true, id, o, e); + } + this._dispatchMouseEvent(oTarget, "pressup", true, id, o, e); + + if (clear) { + if (id == this._primaryPointerID) { + this._primaryPointerID = null; + } + delete this._pointerData[id]; + } else { + o.target = null; + } + + nextStage && nextStage._handlePointerUp(id, e, clear, owner || target && this); + }; + + /** + * @method _handleMouseDown + * @protected + * @param {MouseEvent} e + **/ + p._handleMouseDown = function (e) { + this._handlePointerDown(-1, e, e.pageX, e.pageY); + }; + + /** + * @method _handlePointerDown + * @protected + * @param {Number} id + * @param {Event} e + * @param {Number} pageX + * @param {Number} pageY + * @param {Stage} owner Indicates that the event has already been captured & handled by the indicated stage. + **/ + p._handlePointerDown = function (id, e, pageX, pageY, owner) { + if (this.preventSelection) { + e.preventDefault(); + } + if (this._primaryPointerID == null || id === -1) { + this._primaryPointerID = id; + } // mouse always takes over. + + if (pageY != null) { + this._updatePointerPosition(id, e, pageX, pageY); + } + var target = null, + nextStage = this._nextStage, + o = this._getPointerData(id); + if (!owner) { + target = o.target = this._getObjectsUnderPoint(o.x, o.y, null, true); + } + + if (o.inBounds) { + this._dispatchMouseEvent(this, "stagemousedown", false, id, o, e, target);o.down = true; + } + this._dispatchMouseEvent(target, "mousedown", true, id, o, e); + + nextStage && nextStage._handlePointerDown(id, e, pageX, pageY, owner || target && this); + }; + + /** + * @method _testMouseOver + * @param {Boolean} clear If true, clears the mouseover / rollover (ie. no target) + * @param {Stage} owner Indicates that the event has already been captured & handled by the indicated stage. + * @param {Stage} eventTarget The stage that the cursor is actively over. + * @protected + **/ + p._testMouseOver = function (clear, owner, eventTarget) { + if (this._prevStage && owner === undefined) { + return; + } // redundant listener. + + var nextStage = this._nextStage; + if (!this._mouseOverIntervalID) { + // not enabled for mouseover, but should still relay the event. + nextStage && nextStage._testMouseOver(clear, owner, eventTarget); + return; + } + var o = this._getPointerData(-1); + // only update if the mouse position has changed. This provides a lot of optimization, but has some trade-offs. + if (!o || !clear && this.mouseX == this._mouseOverX && this.mouseY == this._mouseOverY && this.mouseInBounds) { + return; + } + + var e = o.posEvtObj; + var isEventTarget = eventTarget || e && e.target == this.canvas; + var target = null, + common = -1, + cursor = "", + t, + i, + l; + + if (!owner && (clear || this.mouseInBounds && isEventTarget)) { + target = this._getObjectsUnderPoint(this.mouseX, this.mouseY, null, true); + this._mouseOverX = this.mouseX; + this._mouseOverY = this.mouseY; + } + + var oldList = this._mouseOverTarget || []; + var oldTarget = oldList[oldList.length - 1]; + var list = this._mouseOverTarget = []; + + // generate ancestor list and check for cursor: + t = target; + while (t) { + list.unshift(t); + if (!cursor) { + cursor = t.cursor; + } + t = t.parent; + } + this.canvas.style.cursor = cursor; + if (!owner && eventTarget) { + eventTarget.canvas.style.cursor = cursor; + } + + // find common ancestor: + for (i = 0, l = list.length; i < l; i++) { + if (list[i] != oldList[i]) { + break; + } + common = i; + } + + if (oldTarget != target) { + this._dispatchMouseEvent(oldTarget, "mouseout", true, -1, o, e, target); + } + + for (i = oldList.length - 1; i > common; i--) { + this._dispatchMouseEvent(oldList[i], "rollout", false, -1, o, e, target); + } + + for (i = list.length - 1; i > common; i--) { + this._dispatchMouseEvent(list[i], "rollover", false, -1, o, e, oldTarget); + } + + if (oldTarget != target) { + this._dispatchMouseEvent(target, "mouseover", true, -1, o, e, oldTarget); + } + + nextStage && nextStage._testMouseOver(clear, owner || target && this, eventTarget || isEventTarget && this); + }; + + /** + * @method _handleDoubleClick + * @protected + * @param {MouseEvent} e + * @param {Stage} owner Indicates that the event has already been captured & handled by the indicated stage. + **/ + p._handleDoubleClick = function (e, owner) { + var target = null, + nextStage = this._nextStage, + o = this._getPointerData(-1); + if (!owner) { + target = this._getObjectsUnderPoint(o.x, o.y, null, true); + this._dispatchMouseEvent(target, "dblclick", true, -1, o, e); + } + nextStage && nextStage._handleDoubleClick(e, owner || target && this); + }; + + /** + * @method _dispatchMouseEvent + * @protected + * @param {DisplayObject} target + * @param {String} type + * @param {Boolean} bubbles + * @param {Number} pointerId + * @param {Object} o + * @param {MouseEvent} [nativeEvent] + * @param {DisplayObject} [relatedTarget] + **/ + p._dispatchMouseEvent = function (target, type, bubbles, pointerId, o, nativeEvent, relatedTarget) { + // TODO: might be worth either reusing MouseEvent instances, or adding a willTrigger method to avoid GC. + if (!target || !bubbles && !target.hasEventListener(type)) { + return; + } + /* + // TODO: account for stage transformations? + this._mtx = this.getConcatenatedMatrix(this._mtx).invert(); + var pt = this._mtx.transformPoint(o.x, o.y); + var evt = new createjs.MouseEvent(type, bubbles, false, pt.x, pt.y, nativeEvent, pointerId, pointerId==this._primaryPointerID || pointerId==-1, o.rawX, o.rawY); + */ + var evt = new createjs.MouseEvent(type, bubbles, false, o.x, o.y, nativeEvent, pointerId, pointerId === this._primaryPointerID || pointerId === -1, o.rawX, o.rawY, relatedTarget); + target.dispatchEvent(evt); + }; + + createjs.Stage = createjs.promote(Stage, "Container"); + })(); + + //############################################################################## + // Bitmap.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + + /** + * A Bitmap represents an Image, Canvas, or Video in the display list. A Bitmap can be instantiated using an existing + * HTML element, or a string. + * + *

    Example

    + * + * var bitmap = new createjs.Bitmap("imagePath.jpg"); + * + * Notes: + *
      + *
    1. When a string path or image tag that is not yet loaded is used, the stage may need to be redrawn before it + * will be displayed.
    2. + *
    3. Bitmaps with an SVG source currently will not respect an alpha value other than 0 or 1. To get around this, + * the Bitmap can be cached.
    4. + *
    5. Bitmaps with an SVG source will taint the canvas with cross-origin data, which prevents interactivity. This + * happens in all browsers except recent Firefox builds.
    6. + *
    7. Images loaded cross-origin will throw cross-origin security errors when interacted with using a mouse, using + * methods such as `getObjectUnderPoint`, or using filters, or caching. You can get around this by setting + * `crossOrigin` flags on your images before passing them to EaselJS, eg: `img.crossOrigin="Anonymous";`
    8. + *
    + * + * @class Bitmap + * @extends DisplayObject + * @constructor + * @param {HTMLImageElement | HTMLCanvasElement | HTMLVideoElement | String} imageOrUri The source object or URI to an image to + * display. This can be either an Image, Canvas, or Video object, or a string URI to an image file to load and use. + * If it is a URI, a new Image object will be constructed and assigned to the .image property. + **/ + function Bitmap(imageOrUri) { + this.DisplayObject_constructor(); + + // public properties: + /** + * The image to render. This can be an Image, a Canvas, or a Video. Not all browsers (especially + * mobile browsers) support drawing video to a canvas. + * @property image + * @type HTMLImageElement | HTMLCanvasElement | HTMLVideoElement + **/ + if (typeof imageOrUri == "string") { + this.image = document.createElement("img"); + this.image.src = imageOrUri; + } else { + this.image = imageOrUri; + } + + /** + * Specifies an area of the source image to draw. If omitted, the whole image will be drawn. + * Note that video sources must have a width / height set to work correctly with `sourceRect`. + * @property sourceRect + * @type Rectangle + * @default null + */ + this.sourceRect = null; + } + var p = createjs.extend(Bitmap, createjs.DisplayObject); + + // public methods: + /** + * Constructor alias for backwards compatibility. This method will be removed in future versions. + * Subclasses should be updated to use {{#crossLink "Utility Methods/extends"}}{{/crossLink}}. + * @method initialize + * @deprecated in favour of `createjs.promote()` + **/ + p.initialize = Bitmap; // TODO: deprecated. + + /** + * Returns true or false indicating whether the display object would be visible if drawn to a canvas. + * This does not account for whether it would be visible within the boundaries of the stage. + * + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method isVisible + * @return {Boolean} Boolean indicating whether the display object would be visible if drawn to a canvas + **/ + p.isVisible = function () { + var image = this.image; + var hasContent = this.cacheCanvas || image && (image.naturalWidth || image.getContext || image.readyState >= 2); + return !!(this.visible && this.alpha > 0 && this.scaleX != 0 && this.scaleY != 0 && hasContent); + }; + + /** + * Draws the display object into the specified context ignoring its visible, alpha, shadow, and transform. + * Returns true if the draw was handled (useful for overriding functionality). + * + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method draw + * @param {CanvasRenderingContext2D} ctx The canvas 2D context object to draw into. + * @param {Boolean} [ignoreCache=false] Indicates whether the draw operation should ignore any current cache. + * For example, used for drawing the cache (to prevent it from simply drawing an existing cache back + * into itself). + * @return {Boolean} + **/ + p.draw = function (ctx, ignoreCache) { + if (this.DisplayObject_draw(ctx, ignoreCache) || !this.image) { + return true; + } + var img = this.image, + rect = this.sourceRect; + if (rect) { + // some browsers choke on out of bound values, so we'll fix them: + var x1 = rect.x, + y1 = rect.y, + x2 = x1 + rect.width, + y2 = y1 + rect.height, + x = 0, + y = 0, + w = img.width, + h = img.height; + if (x1 < 0) { + x -= x1;x1 = 0; + } + if (x2 > w) { + x2 = w; + } + if (y1 < 0) { + y -= y1;y1 = 0; + } + if (y2 > h) { + y2 = h; + } + ctx.drawImage(img, x1, y1, x2 - x1, y2 - y1, x, y, x2 - x1, y2 - y1); + } else { + ctx.drawImage(img, 0, 0); + } + return true; + }; + + //Note, the doc sections below document using the specified APIs (from DisplayObject) from + //Bitmap. This is why they have no method implementations. + + /** + * Because the content of a Bitmap is already in a simple format, cache is unnecessary for Bitmap instances. + * You should not cache Bitmap instances as it can degrade performance. + * + * However: If you want to use a filter on a Bitmap, you MUST cache it, or it will not work. + * To see the API for caching, please visit the DisplayObject {{#crossLink "DisplayObject/cache"}}{{/crossLink}} + * method. + * @method cache + **/ + + /** + * Because the content of a Bitmap is already in a simple format, cache is unnecessary for Bitmap instances. + * You should not cache Bitmap instances as it can degrade performance. + * + * However: If you want to use a filter on a Bitmap, you MUST cache it, or it will not work. + * To see the API for caching, please visit the DisplayObject {{#crossLink "DisplayObject/cache"}}{{/crossLink}} + * method. + * @method updateCache + **/ + + /** + * Because the content of a Bitmap is already in a simple format, cache is unnecessary for Bitmap instances. + * You should not cache Bitmap instances as it can degrade performance. + * + * However: If you want to use a filter on a Bitmap, you MUST cache it, or it will not work. + * To see the API for caching, please visit the DisplayObject {{#crossLink "DisplayObject/cache"}}{{/crossLink}} + * method. + * @method uncache + **/ + + /** + * Docced in superclass. + */ + p.getBounds = function () { + var rect = this.DisplayObject_getBounds(); + if (rect) { + return rect; + } + var image = this.image, + o = this.sourceRect || image; + var hasContent = image && (image.naturalWidth || image.getContext || image.readyState >= 2); + return hasContent ? this._rectangle.setValues(0, 0, o.width, o.height) : null; + }; + + /** + * Returns a clone of the Bitmap instance. + * @method clone + * @return {Bitmap} a clone of the Bitmap instance. + **/ + p.clone = function () { + var o = new Bitmap(this.image); + if (this.sourceRect) { + o.sourceRect = this.sourceRect.clone(); + } + this._cloneProps(o); + return o; + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[Bitmap (name=" + this.name + ")]"; + }; + + createjs.Bitmap = createjs.promote(Bitmap, "DisplayObject"); + })(); + + //############################################################################## + // Sprite.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Displays a frame or sequence of frames (ie. an animation) from a SpriteSheet instance. A sprite sheet is a series of + * images (usually animation frames) combined into a single image. For example, an animation consisting of 8 100x100 + * images could be combined into a 400x200 sprite sheet (4 frames across by 2 high). You can display individual frames, + * play frames as an animation, and even sequence animations together. + * + * See the {{#crossLink "SpriteSheet"}}{{/crossLink}} class for more information on setting up frames and animations. + * + *

    Example

    + * + * var instance = new createjs.Sprite(spriteSheet); + * instance.gotoAndStop("frameName"); + * + * Until {{#crossLink "Sprite/gotoAndStop"}}{{/crossLink}} or {{#crossLink "Sprite/gotoAndPlay"}}{{/crossLink}} is called, + * only the first defined frame defined in the sprite sheet will be displayed. + * + * @class Sprite + * @extends DisplayObject + * @constructor + * @param {SpriteSheet} spriteSheet The SpriteSheet instance to play back. This includes the source image(s), frame + * dimensions, and frame data. See {{#crossLink "SpriteSheet"}}{{/crossLink}} for more information. + * @param {String|Number} [frameOrAnimation] The frame number or animation to play initially. + **/ + + function Sprite(spriteSheet, frameOrAnimation) { + this.DisplayObject_constructor(); + + // public properties: + /** + * The frame index that will be drawn when draw is called. Note that with some {{#crossLink "SpriteSheet"}}{{/crossLink}} + * definitions, this will advance non-sequentially. This will always be an integer value. + * @property currentFrame + * @type {Number} + * @default 0 + * @readonly + **/ + this.currentFrame = 0; + + /** + * Returns the name of the currently playing animation. + * @property currentAnimation + * @type {String} + * @final + * @readonly + **/ + this.currentAnimation = null; + + /** + * Prevents the animation from advancing each tick automatically. For example, you could create a sprite + * sheet of icons, set paused to true, and display the appropriate icon by setting currentFrame. + * @property paused + * @type {Boolean} + * @default false + **/ + this.paused = true; + + /** + * The SpriteSheet instance to play back. This includes the source image, frame dimensions, and frame + * data. See {{#crossLink "SpriteSheet"}}{{/crossLink}} for more information. + * @property spriteSheet + * @type {SpriteSheet} + * @readonly + **/ + this.spriteSheet = spriteSheet; + + /** + * Specifies the current frame index within the currently playing animation. When playing normally, this will increase + * from 0 to n-1, where n is the number of frames in the current animation. + * + * This could be a non-integer value if + * using time-based playback (see {{#crossLink "Sprite/framerate"}}{{/crossLink}}, or if the animation's speed is + * not an integer. + * @property currentAnimationFrame + * @type {Number} + * @default 0 + **/ + this.currentAnimationFrame = 0; + + /** + * By default Sprite instances advance one frame per tick. Specifying a framerate for the Sprite (or its related + * SpriteSheet) will cause it to advance based on elapsed time between ticks as appropriate to maintain the target + * framerate. + * + * For example, if a Sprite with a framerate of 10 is placed on a Stage being updated at 40fps, then the Sprite will + * advance roughly one frame every 4 ticks. This will not be exact, because the time between each tick will + * vary slightly between frames. + * + * This feature is dependent on the tick event object (or an object with an appropriate "delta" property) being + * passed into {{#crossLink "Stage/update"}}{{/crossLink}}. + * @property framerate + * @type {Number} + * @default 0 + **/ + this.framerate = 0; + + // private properties: + /** + * Current animation object. + * @property _animation + * @protected + * @type {Object} + * @default null + **/ + this._animation = null; + + /** + * Current frame index. + * @property _currentFrame + * @protected + * @type {Number} + * @default null + **/ + this._currentFrame = null; + + /** + * Skips the next auto advance. Used by gotoAndPlay to avoid immediately jumping to the next frame + * @property _skipAdvance + * @protected + * @type {Boolean} + * @default false + **/ + this._skipAdvance = false; + + if (frameOrAnimation != null) { + this.gotoAndPlay(frameOrAnimation); + } + } + var p = createjs.extend(Sprite, createjs.DisplayObject); + + /** + * Constructor alias for backwards compatibility. This method will be removed in future versions. + * Subclasses should be updated to use {{#crossLink "Utility Methods/extends"}}{{/crossLink}}. + * @method initialize + * @deprecated in favour of `createjs.promote()` + **/ + p.initialize = Sprite; // TODO: Deprecated. This is for backwards support of FlashCC spritesheet export. + + + // events: + /** + * Dispatched when an animation reaches its ends. + * @event animationend + * @param {Object} target The object that dispatched the event. + * @param {String} type The event type. + * @param {String} name The name of the animation that just ended. + * @param {String} next The name of the next animation that will be played, or null. This will be the same as name if the animation is looping. + * @since 0.6.0 + */ + + /** + * Dispatched any time the current frame changes. For example, this could be due to automatic advancement on a tick, + * or calling gotoAndPlay() or gotoAndStop(). + * @event change + * @param {Object} target The object that dispatched the event. + * @param {String} type The event type. + */ + + // public methods: + /** + * Returns true or false indicating whether the display object would be visible if drawn to a canvas. + * This does not account for whether it would be visible within the boundaries of the stage. + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method isVisible + * @return {Boolean} Boolean indicating whether the display object would be visible if drawn to a canvas + **/ + p.isVisible = function () { + var hasContent = this.cacheCanvas || this.spriteSheet.complete; + return !!(this.visible && this.alpha > 0 && this.scaleX != 0 && this.scaleY != 0 && hasContent); + }; + + /** + * Draws the display object into the specified context ignoring its visible, alpha, shadow, and transform. + * Returns true if the draw was handled (useful for overriding functionality). + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method draw + * @param {CanvasRenderingContext2D} ctx The canvas 2D context object to draw into. + * @param {Boolean} ignoreCache Indicates whether the draw operation should ignore any current cache. + * For example, used for drawing the cache (to prevent it from simply drawing an existing cache back + * into itself). + **/ + p.draw = function (ctx, ignoreCache) { + if (this.DisplayObject_draw(ctx, ignoreCache)) { + return true; + } + this._normalizeFrame(); + var o = this.spriteSheet.getFrame(this._currentFrame | 0); + if (!o) { + return false; + } + var rect = o.rect; + if (rect.width && rect.height) { + ctx.drawImage(o.image, rect.x, rect.y, rect.width, rect.height, -o.regX, -o.regY, rect.width, rect.height); + } + return true; + }; + + //Note, the doc sections below document using the specified APIs (from DisplayObject) from + //Bitmap. This is why they have no method implementations. + + /** + * Because the content of a Sprite is already in a raster format, cache is unnecessary for Sprite instances. + * You should not cache Sprite instances as it can degrade performance. + * @method cache + **/ + + /** + * Because the content of a Sprite is already in a raster format, cache is unnecessary for Sprite instances. + * You should not cache Sprite instances as it can degrade performance. + * @method updateCache + **/ + + /** + * Because the content of a Sprite is already in a raster format, cache is unnecessary for Sprite instances. + * You should not cache Sprite instances as it can degrade performance. + * @method uncache + **/ + + /** + * Play (unpause) the current animation. The Sprite will be paused if either {{#crossLink "Sprite/stop"}}{{/crossLink}} + * or {{#crossLink "Sprite/gotoAndStop"}}{{/crossLink}} is called. Single frame animations will remain + * unchanged. + * @method play + **/ + p.play = function () { + this.paused = false; + }; + + /** + * Stop playing a running animation. The Sprite will be playing if {{#crossLink "Sprite/gotoAndPlay"}}{{/crossLink}} + * is called. Note that calling {{#crossLink "Sprite/gotoAndPlay"}}{{/crossLink}} or {{#crossLink "Sprite/play"}}{{/crossLink}} + * will resume playback. + * @method stop + **/ + p.stop = function () { + this.paused = true; + }; + + /** + * Sets paused to false and plays the specified animation name, named frame, or frame number. + * @method gotoAndPlay + * @param {String|Number} frameOrAnimation The frame number or animation name that the playhead should move to + * and begin playing. + **/ + p.gotoAndPlay = function (frameOrAnimation) { + this.paused = false; + this._skipAdvance = true; + this._goto(frameOrAnimation); + }; + + /** + * Sets paused to true and seeks to the specified animation name, named frame, or frame number. + * @method gotoAndStop + * @param {String|Number} frameOrAnimation The frame number or animation name that the playhead should move to + * and stop. + **/ + p.gotoAndStop = function (frameOrAnimation) { + this.paused = true; + this._goto(frameOrAnimation); + }; + + /** + * Advances the playhead. This occurs automatically each tick by default. + * @param [time] {Number} The amount of time in ms to advance by. Only applicable if framerate is set on the Sprite + * or its SpriteSheet. + * @method advance + */ + p.advance = function (time) { + var fps = this.framerate || this.spriteSheet.framerate; + var t = fps && time != null ? time / (1000 / fps) : 1; + this._normalizeFrame(t); + }; + + /** + * Returns a {{#crossLink "Rectangle"}}{{/crossLink}} instance defining the bounds of the current frame relative to + * the origin. For example, a 90 x 70 frame with regX=50 and regY=40 would return a + * rectangle with [x=-50, y=-40, width=90, height=70]. This ignores transformations on the display object. + * + * Also see the SpriteSheet {{#crossLink "SpriteSheet/getFrameBounds"}}{{/crossLink}} method. + * @method getBounds + * @return {Rectangle} A Rectangle instance. Returns null if the frame does not exist, or the image is not fully + * loaded. + **/ + p.getBounds = function () { + // TODO: should this normalizeFrame? + return this.DisplayObject_getBounds() || this.spriteSheet.getFrameBounds(this.currentFrame, this._rectangle); + }; + + /** + * Returns a clone of the Sprite instance. Note that the same SpriteSheet is shared between cloned + * instances. + * @method clone + * @return {Sprite} a clone of the Sprite instance. + **/ + p.clone = function () { + return this._cloneProps(new Sprite(this.spriteSheet)); + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[Sprite (name=" + this.name + ")]"; + }; + + // private methods: + /** + * @method _cloneProps + * @param {Sprite} o + * @return {Sprite} o + * @protected + **/ + p._cloneProps = function (o) { + this.DisplayObject__cloneProps(o); + o.currentFrame = this.currentFrame; + o.currentAnimation = this.currentAnimation; + o.paused = this.paused; + o.currentAnimationFrame = this.currentAnimationFrame; + o.framerate = this.framerate; + + o._animation = this._animation; + o._currentFrame = this._currentFrame; + o._skipAdvance = this._skipAdvance; + return o; + }; + + /** + * Advances the currentFrame if paused is not true. This is called automatically when the {{#crossLink "Stage"}}{{/crossLink}} + * ticks. + * @param {Object} evtObj An event object that will be dispatched to all tick listeners. This object is reused between dispatchers to reduce construction & GC costs. + * @protected + * @method _tick + **/ + p._tick = function (evtObj) { + if (!this.paused) { + if (!this._skipAdvance) { + this.advance(evtObj && evtObj.delta); + } + this._skipAdvance = false; + } + this.DisplayObject__tick(evtObj); + }; + + /** + * Normalizes the current frame, advancing animations and dispatching callbacks as appropriate. + * @protected + * @method _normalizeFrame + **/ + p._normalizeFrame = function (frameDelta) { + frameDelta = frameDelta || 0; + var animation = this._animation; + var paused = this.paused; + var frame = this._currentFrame; + var l; + + if (animation) { + var speed = animation.speed || 1; + var animFrame = this.currentAnimationFrame; + l = animation.frames.length; + if (animFrame + frameDelta * speed >= l) { + var next = animation.next; + if (this._dispatchAnimationEnd(animation, frame, paused, next, l - 1)) { + // something changed in the event stack, so we shouldn't make any more changes here. + return; + } else if (next) { + // sequence. Automatically calls _normalizeFrame again with the remaining frames. + return this._goto(next, frameDelta - (l - animFrame) / speed); + } else { + // end. + this.paused = true; + animFrame = animation.frames.length - 1; + } + } else { + animFrame += frameDelta * speed; + } + this.currentAnimationFrame = animFrame; + this._currentFrame = animation.frames[animFrame | 0]; + } else { + frame = this._currentFrame += frameDelta; + l = this.spriteSheet.getNumFrames(); + if (frame >= l && l > 0) { + if (!this._dispatchAnimationEnd(animation, frame, paused, l - 1)) { + // looped. + if ((this._currentFrame -= l) >= l) { + return this._normalizeFrame(); + } + } + } + } + frame = this._currentFrame | 0; + if (this.currentFrame != frame) { + this.currentFrame = frame; + this.dispatchEvent("change"); + } + }; + + /** + * Dispatches the "animationend" event. Returns true if a handler changed the animation (ex. calling {{#crossLink "Sprite/stop"}}{{/crossLink}}, + * {{#crossLink "Sprite/gotoAndPlay"}}{{/crossLink}}, etc.) + * @property _dispatchAnimationEnd + * @private + * @type {Function} + **/ + p._dispatchAnimationEnd = function (animation, frame, paused, next, end) { + var name = animation ? animation.name : null; + if (this.hasEventListener("animationend")) { + var evt = new createjs.Event("animationend"); + evt.name = name; + evt.next = next; + this.dispatchEvent(evt); + } + // did the animation get changed in the event stack?: + var changed = this._animation != animation || this._currentFrame != frame; + // if the animation hasn't changed, but the sprite was paused, then we want to stick to the last frame: + if (!changed && !paused && this.paused) { + this.currentAnimationFrame = end;changed = true; + } + return changed; + }; + + /** + * Moves the playhead to the specified frame number or animation. + * @method _goto + * @param {String|Number} frameOrAnimation The frame number or animation that the playhead should move to. + * @param {Boolean} [frame] The frame of the animation to go to. Defaults to 0. + * @protected + **/ + p._goto = function (frameOrAnimation, frame) { + this.currentAnimationFrame = 0; + if (isNaN(frameOrAnimation)) { + var data = this.spriteSheet.getAnimation(frameOrAnimation); + if (data) { + this._animation = data; + this.currentAnimation = frameOrAnimation; + this._normalizeFrame(frame); + } + } else { + this.currentAnimation = this._animation = null; + this._currentFrame = frameOrAnimation; + this._normalizeFrame(); + } + }; + + createjs.Sprite = createjs.promote(Sprite, "DisplayObject"); + })(); + + //############################################################################## + // Shape.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * A Shape allows you to display vector art in the display list. It composites a {{#crossLink "Graphics"}}{{/crossLink}} + * instance which exposes all of the vector drawing methods. The Graphics instance can be shared between multiple Shape + * instances to display the same vector graphics with different positions or transforms. + * + * If the vector art will not + * change between draws, you may want to use the {{#crossLink "DisplayObject/cache"}}{{/crossLink}} method to reduce the + * rendering cost. + * + *

    Example

    + * + * var graphics = new createjs.Graphics().beginFill("#ff0000").drawRect(0, 0, 100, 100); + * var shape = new createjs.Shape(graphics); + * + * //Alternatively use can also use the graphics property of the Shape class to renderer the same as above. + * var shape = new createjs.Shape(); + * shape.graphics.beginFill("#ff0000").drawRect(0, 0, 100, 100); + * + * @class Shape + * @extends DisplayObject + * @constructor + * @param {Graphics} graphics Optional. The graphics instance to display. If null, a new Graphics instance will be created. + **/ + + function Shape(graphics) { + this.DisplayObject_constructor(); + + // public properties: + /** + * The graphics instance to display. + * @property graphics + * @type Graphics + **/ + this.graphics = graphics ? graphics : new createjs.Graphics(); + } + var p = createjs.extend(Shape, createjs.DisplayObject); + + // TODO: deprecated + // p.initialize = function() {}; // searchable for devs wondering where it is. REMOVED. See docs for details. + + + // public methods: + /** + * Returns true or false indicating whether the Shape would be visible if drawn to a canvas. + * This does not account for whether it would be visible within the boundaries of the stage. + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method isVisible + * @return {Boolean} Boolean indicating whether the Shape would be visible if drawn to a canvas + **/ + p.isVisible = function () { + var hasContent = this.cacheCanvas || this.graphics && !this.graphics.isEmpty(); + return !!(this.visible && this.alpha > 0 && this.scaleX != 0 && this.scaleY != 0 && hasContent); + }; + + /** + * Draws the Shape into the specified context ignoring its visible, alpha, shadow, and transform. Returns true if + * the draw was handled (useful for overriding functionality). + * + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method draw + * @param {CanvasRenderingContext2D} ctx The canvas 2D context object to draw into. + * @param {Boolean} [ignoreCache=false] Indicates whether the draw operation should ignore any current cache. For example, + * used for drawing the cache (to prevent it from simply drawing an existing cache back into itself). + * @return {Boolean} + **/ + p.draw = function (ctx, ignoreCache) { + if (this.DisplayObject_draw(ctx, ignoreCache)) { + return true; + } + this.graphics.draw(ctx, this); + return true; + }; + + /** + * Returns a clone of this Shape. Some properties that are specific to this instance's current context are reverted to + * their defaults (for example .parent). + * @method clone + * @param {Boolean} recursive If true, this Shape's {{#crossLink "Graphics"}}{{/crossLink}} instance will also be + * cloned. If false, the Graphics instance will be shared with the new Shape. + **/ + p.clone = function (recursive) { + var g = recursive && this.graphics ? this.graphics.clone() : this.graphics; + return this._cloneProps(new Shape(g)); + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[Shape (name=" + this.name + ")]"; + }; + + createjs.Shape = createjs.promote(Shape, "DisplayObject"); + })(); + + //############################################################################## + // Text.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Display one or more lines of dynamic text (not user editable) in the display list. Line wrapping support (using the + * lineWidth) is very basic, wrapping on spaces and tabs only. Note that as an alternative to Text, you can position HTML + * text above or below the canvas relative to items in the display list using the {{#crossLink "DisplayObject/localToGlobal"}}{{/crossLink}} + * method, or using {{#crossLink "DOMElement"}}{{/crossLink}}. + * + * Please note that Text does not support HTML text, and can only display one font style at a time. To use + * multiple font styles, you will need to create multiple text instances, and position them manually. + * + *

    Example

    + * + * var text = new createjs.Text("Hello World", "20px Arial", "#ff7700"); + * text.x = 100; + * text.textBaseline = "alphabetic"; + * + * CreateJS Text supports web fonts (the same rules as Canvas). The font must be loaded and supported by the browser + * before it can be displayed. + * + * Note: Text can be expensive to generate, so cache instances where possible. Be aware that not all + * browsers will render Text exactly the same. + * @class Text + * @extends DisplayObject + * @constructor + * @param {String} [text] The text to display. + * @param {String} [font] The font style to use. Any valid value for the CSS font attribute is acceptable (ex. "bold + * 36px Arial"). + * @param {String} [color] The color to draw the text in. Any valid value for the CSS color attribute is acceptable (ex. + * "#F00", "red", or "#FF0000"). + **/ + + function Text(text, font, color) { + this.DisplayObject_constructor(); + + // public properties: + /** + * The text to display. + * @property text + * @type String + **/ + this.text = text; + + /** + * The font style to use. Any valid value for the CSS font attribute is acceptable (ex. "bold 36px Arial"). + * @property font + * @type String + **/ + this.font = font; + + /** + * The color to draw the text in. Any valid value for the CSS color attribute is acceptable (ex. "#F00"). Default is "#000". + * It will also accept valid canvas fillStyle values. + * @property color + * @type String + **/ + this.color = color; + + /** + * The horizontal text alignment. Any of "start", "end", "left", "right", and "center". For detailed + * information view the + * + * whatwg spec. Default is "left". + * @property textAlign + * @type String + **/ + this.textAlign = "left"; + + /** + * The vertical alignment point on the font. Any of "top", "hanging", "middle", "alphabetic", "ideographic", or + * "bottom". For detailed information view the + * whatwg spec. Default is "top". + * @property textBaseline + * @type String + */ + this.textBaseline = "top"; + + /** + * The maximum width to draw the text. If maxWidth is specified (not null), the text will be condensed or + * shrunk to make it fit in this width. For detailed information view the + * + * whatwg spec. + * @property maxWidth + * @type Number + */ + this.maxWidth = null; + + /** + * If greater than 0, the text will be drawn as a stroke (outline) of the specified width. + * @property outline + * @type Number + **/ + this.outline = 0; + + /** + * Indicates the line height (vertical distance between baselines) for multi-line text. If null or 0, + * the value of getMeasuredLineHeight is used. + * @property lineHeight + * @type Number + **/ + this.lineHeight = 0; + + /** + * Indicates the maximum width for a line of text before it is wrapped to multiple lines. If null, + * the text will not be wrapped. + * @property lineWidth + * @type Number + **/ + this.lineWidth = null; + } + var p = createjs.extend(Text, createjs.DisplayObject); + + // TODO: deprecated + // p.initialize = function() {}; // searchable for devs wondering where it is. REMOVED. See docs for details. + + + // static properties: + /** + * @property _workingContext + * @type CanvasRenderingContext2D + * @private + **/ + var canvas = createjs.createCanvas ? createjs.createCanvas() : document.createElement("canvas"); + if (canvas.getContext) { + Text._workingContext = canvas.getContext("2d");canvas.width = canvas.height = 1; + } + + // constants: + /** + * Lookup table for the ratio to offset bounds x calculations based on the textAlign property. + * @property H_OFFSETS + * @type Object + * @protected + * @static + **/ + Text.H_OFFSETS = { start: 0, left: 0, center: -0.5, end: -1, right: -1 }; + + /** + * Lookup table for the ratio to offset bounds y calculations based on the textBaseline property. + * @property H_OFFSETS + * @type Object + * @protected + * @static + **/ + Text.V_OFFSETS = { top: 0, hanging: -0.01, middle: -0.4, alphabetic: -0.8, ideographic: -0.85, bottom: -1 }; + + // public methods: + /** + * Returns true or false indicating whether the display object would be visible if drawn to a canvas. + * This does not account for whether it would be visible within the boundaries of the stage. + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method isVisible + * @return {Boolean} Whether the display object would be visible if drawn to a canvas + **/ + p.isVisible = function () { + var hasContent = this.cacheCanvas || this.text != null && this.text !== ""; + return !!(this.visible && this.alpha > 0 && this.scaleX != 0 && this.scaleY != 0 && hasContent); + }; + + /** + * Draws the Text into the specified context ignoring its visible, alpha, shadow, and transform. + * Returns true if the draw was handled (useful for overriding functionality). + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method draw + * @param {CanvasRenderingContext2D} ctx The canvas 2D context object to draw into. + * @param {Boolean} ignoreCache Indicates whether the draw operation should ignore any current cache. + * For example, used for drawing the cache (to prevent it from simply drawing an existing cache back + * into itself). + **/ + p.draw = function (ctx, ignoreCache) { + if (this.DisplayObject_draw(ctx, ignoreCache)) { + return true; + } + + var col = this.color || "#000"; + if (this.outline) { + ctx.strokeStyle = col;ctx.lineWidth = this.outline * 1; + } else { + ctx.fillStyle = col; + } + + this._drawText(this._prepContext(ctx)); + return true; + }; + + /** + * Returns the measured, untransformed width of the text without wrapping. Use getBounds for a more robust value. + * @method getMeasuredWidth + * @return {Number} The measured, untransformed width of the text. + **/ + p.getMeasuredWidth = function () { + return this._getMeasuredWidth(this.text); + }; + + /** + * Returns an approximate line height of the text, ignoring the lineHeight property. This is based on the measured + * width of a "M" character multiplied by 1.2, which provides an approximate line height for most fonts. + * @method getMeasuredLineHeight + * @return {Number} an approximate line height of the text, ignoring the lineHeight property. This is + * based on the measured width of a "M" character multiplied by 1.2, which approximates em for most fonts. + **/ + p.getMeasuredLineHeight = function () { + return this._getMeasuredWidth("M") * 1.2; + }; + + /** + * Returns the approximate height of multi-line text by multiplying the number of lines against either the + * lineHeight (if specified) or {{#crossLink "Text/getMeasuredLineHeight"}}{{/crossLink}}. Note that + * this operation requires the text flowing logic to run, which has an associated CPU cost. + * @method getMeasuredHeight + * @return {Number} The approximate height of the untransformed multi-line text. + **/ + p.getMeasuredHeight = function () { + return this._drawText(null, {}).height; + }; + + /** + * Docced in superclass. + */ + p.getBounds = function () { + var rect = this.DisplayObject_getBounds(); + if (rect) { + return rect; + } + if (this.text == null || this.text === "") { + return null; + } + var o = this._drawText(null, {}); + var w = this.maxWidth && this.maxWidth < o.width ? this.maxWidth : o.width; + var x = w * Text.H_OFFSETS[this.textAlign || "left"]; + var lineHeight = this.lineHeight || this.getMeasuredLineHeight(); + var y = lineHeight * Text.V_OFFSETS[this.textBaseline || "top"]; + return this._rectangle.setValues(x, y, w, o.height); + }; + + /** + * Returns an object with width, height, and lines properties. The width and height are the visual width and height + * of the drawn text. The lines property contains an array of strings, one for + * each line of text that will be drawn, accounting for line breaks and wrapping. These strings have trailing + * whitespace removed. + * @method getMetrics + * @return {Object} An object with width, height, and lines properties. + **/ + p.getMetrics = function () { + var o = { lines: [] }; + o.lineHeight = this.lineHeight || this.getMeasuredLineHeight(); + o.vOffset = o.lineHeight * Text.V_OFFSETS[this.textBaseline || "top"]; + return this._drawText(null, o, o.lines); + }; + + /** + * Returns a clone of the Text instance. + * @method clone + * @return {Text} a clone of the Text instance. + **/ + p.clone = function () { + return this._cloneProps(new Text(this.text, this.font, this.color)); + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[Text (text=" + (this.text.length > 20 ? this.text.substr(0, 17) + "..." : this.text) + ")]"; + }; + + // private methods: + /** + * @method _cloneProps + * @param {Text} o + * @protected + * @return {Text} o + **/ + p._cloneProps = function (o) { + this.DisplayObject__cloneProps(o); + o.textAlign = this.textAlign; + o.textBaseline = this.textBaseline; + o.maxWidth = this.maxWidth; + o.outline = this.outline; + o.lineHeight = this.lineHeight; + o.lineWidth = this.lineWidth; + return o; + }; + + /** + * @method _getWorkingContext + * @param {CanvasRenderingContext2D} ctx + * @return {CanvasRenderingContext2D} + * @protected + **/ + p._prepContext = function (ctx) { + ctx.font = this.font || "10px sans-serif"; + ctx.textAlign = this.textAlign || "left"; + ctx.textBaseline = this.textBaseline || "top"; + return ctx; + }; + + /** + * Draws multiline text. + * @method _drawText + * @param {CanvasRenderingContext2D} ctx + * @param {Object} o + * @param {Array} lines + * @return {Object} + * @protected + **/ + p._drawText = function (ctx, o, lines) { + var paint = !!ctx; + if (!paint) { + ctx = Text._workingContext; + ctx.save(); + this._prepContext(ctx); + } + var lineHeight = this.lineHeight || this.getMeasuredLineHeight(); + + var maxW = 0, + count = 0; + var hardLines = String(this.text).split(/(?:\r\n|\r|\n)/); + for (var i = 0, l = hardLines.length; i < l; i++) { + var str = hardLines[i]; + var w = null; + + if (this.lineWidth != null && (w = ctx.measureText(str).width) > this.lineWidth) { + // text wrapping: + var words = str.split(/(\s)/); + str = words[0]; + w = ctx.measureText(str).width; + + for (var j = 1, jl = words.length; j < jl; j += 2) { + // Line needs to wrap: + var wordW = ctx.measureText(words[j] + words[j + 1]).width; + if (w + wordW > this.lineWidth) { + if (paint) { + this._drawTextLine(ctx, str, count * lineHeight); + } + if (lines) { + lines.push(str); + } + if (w > maxW) { + maxW = w; + } + str = words[j + 1]; + w = ctx.measureText(str).width; + count++; + } else { + str += words[j] + words[j + 1]; + w += wordW; + } + } + } + + if (paint) { + this._drawTextLine(ctx, str, count * lineHeight); + } + if (lines) { + lines.push(str); + } + if (o && w == null) { + w = ctx.measureText(str).width; + } + if (w > maxW) { + maxW = w; + } + count++; + } + + if (o) { + o.width = maxW; + o.height = count * lineHeight; + } + if (!paint) { + ctx.restore(); + } + return o; + }; + + /** + * @method _drawTextLine + * @param {CanvasRenderingContext2D} ctx + * @param {String} text + * @param {Number} y + * @protected + **/ + p._drawTextLine = function (ctx, text, y) { + // Chrome 17 will fail to draw the text if the last param is included but null, so we feed it a large value instead: + if (this.outline) { + ctx.strokeText(text, 0, y, this.maxWidth || 0xFFFF); + } else { + ctx.fillText(text, 0, y, this.maxWidth || 0xFFFF); + } + }; + + /** + * @method _getMeasuredWidth + * @param {String} text + * @protected + **/ + p._getMeasuredWidth = function (text) { + var ctx = Text._workingContext; + ctx.save(); + var w = this._prepContext(ctx).measureText(text).width; + ctx.restore(); + return w; + }; + + createjs.Text = createjs.promote(Text, "DisplayObject"); + })(); + + //############################################################################## + // BitmapText.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Displays text using bitmap glyphs defined in a sprite sheet. Multi-line text is supported + * using new line characters, but automatic wrapping is not supported. See the + * {{#crossLink "BitmapText/spriteSheet:property"}}{{/crossLink}} + * property for more information on defining glyphs. + * + * Important: BitmapText extends Container, but is not designed to be used as one. + * As such, methods like addChild and removeChild are disabled. + * @class BitmapText + * @extends DisplayObject + * @param {String} [text=""] The text to display. + * @param {SpriteSheet} [spriteSheet=null] The spritesheet that defines the character glyphs. + * @constructor + **/ + + function BitmapText(text, spriteSheet) { + this.Container_constructor(); + + // public properties: + /** + * The text to display. + * @property text + * @type String + * @default "" + **/ + this.text = text || ""; + + /** + * A SpriteSheet instance that defines the glyphs for this bitmap text. Each glyph/character + * should have a single frame animation defined in the sprite sheet named the same as + * corresponding character. For example, the following animation definition: + * + * "A": {frames: [0]} + * + * would indicate that the frame at index 0 of the spritesheet should be drawn for the "A" character. The short form + * is also acceptable: + * + * "A": 0 + * + * Note that if a character in the text is not found in the sprite sheet, it will also + * try to use the alternate case (upper or lower). + * + * See SpriteSheet for more information on defining sprite sheet data. + * @property spriteSheet + * @type SpriteSheet + * @default null + **/ + this.spriteSheet = spriteSheet; + + /** + * The height of each line of text. If 0, then it will use a line height calculated + * by checking for the height of the "1", "T", or "L" character (in that order). If + * those characters are not defined, it will use the height of the first frame of the + * sprite sheet. + * @property lineHeight + * @type Number + * @default 0 + **/ + this.lineHeight = 0; + + /** + * This spacing (in pixels) will be added after each character in the output. + * @property letterSpacing + * @type Number + * @default 0 + **/ + this.letterSpacing = 0; + + /** + * If a space character is not defined in the sprite sheet, then empty pixels equal to + * spaceWidth will be inserted instead. If 0, then it will use a value calculated + * by checking for the width of the "1", "l", "E", or "A" character (in that order). If + * those characters are not defined, it will use the width of the first frame of the + * sprite sheet. + * @property spaceWidth + * @type Number + * @default 0 + **/ + this.spaceWidth = 0; + + // private properties: + /** + * @property _oldProps + * @type Object + * @protected + **/ + this._oldProps = { text: 0, spriteSheet: 0, lineHeight: 0, letterSpacing: 0, spaceWidth: 0 }; + } + var p = createjs.extend(BitmapText, createjs.Container); + + /** + * REMOVED. Removed in favor of using `MySuperClass_constructor`. + * See {{#crossLink "Utility Methods/extend"}}{{/crossLink}} and {{#crossLink "Utility Methods/promote"}}{{/crossLink}} + * for details. + * + * There is an inheritance tutorial distributed with EaselJS in /tutorials/Inheritance. + * + * @method initialize + * @protected + * @deprecated + */ + // p.initialize = function() {}; // searchable for devs wondering where it is. + + // static properties: + /** + * BitmapText uses Sprite instances to draw text. To reduce the creation and destruction of instances (and thus garbage collection), it maintains + * an internal object pool of sprite instances to reuse. Increasing this value can cause more sprites to be + * retained, slightly increasing memory use, but reducing instantiation. + * @property maxPoolSize + * @type Number + * @static + * @default 100 + **/ + BitmapText.maxPoolSize = 100; + + /** + * Sprite object pool. + * @type {Array} + * @static + * @private + */ + BitmapText._spritePool = []; + + // public methods: + /** + * Docced in superclass. + **/ + p.draw = function (ctx, ignoreCache) { + if (this.DisplayObject_draw(ctx, ignoreCache)) { + return; + } + this._updateText(); + this.Container_draw(ctx, ignoreCache); + }; + + /** + * Docced in superclass. + **/ + p.getBounds = function () { + this._updateText(); + return this.Container_getBounds(); + }; + + /** + * Returns true or false indicating whether the display object would be visible if drawn to a canvas. + * This does not account for whether it would be visible within the boundaries of the stage. + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method isVisible + * @return {Boolean} Boolean indicating whether the display object would be visible if drawn to a canvas + **/ + p.isVisible = function () { + var hasContent = this.cacheCanvas || this.spriteSheet && this.spriteSheet.complete && this.text; + return !!(this.visible && this.alpha > 0 && this.scaleX !== 0 && this.scaleY !== 0 && hasContent); + }; + + p.clone = function () { + return this._cloneProps(new BitmapText(this.text, this.spriteSheet)); + }; + + /** + * Disabled in BitmapText. + * @method addChild + **/ + /** + * Disabled in BitmapText. + * @method addChildAt + **/ + /** + * Disabled in BitmapText. + * @method removeChild + **/ + /** + * Disabled in BitmapText. + * @method removeChildAt + **/ + /** + * Disabled in BitmapText. + * @method removeAllChildren + **/ + p.addChild = p.addChildAt = p.removeChild = p.removeChildAt = p.removeAllChildren = function () {}; + + // private methods: + /** + * @method _cloneProps + * @param {BitmapText} o + * @return {BitmapText} o + * @protected + **/ + p._cloneProps = function (o) { + this.Container__cloneProps(o); + o.lineHeight = this.lineHeight; + o.letterSpacing = this.letterSpacing; + o.spaceWidth = this.spaceWidth; + return o; + }; + + /** + * @method _getFrameIndex + * @param {String} character + * @param {SpriteSheet} spriteSheet + * @return {Number} + * @protected + **/ + p._getFrameIndex = function (character, spriteSheet) { + var c, + o = spriteSheet.getAnimation(character); + if (!o) { + character != (c = character.toUpperCase()) || character != (c = character.toLowerCase()) || (c = null); + if (c) { + o = spriteSheet.getAnimation(c); + } + } + return o && o.frames[0]; + }; + + /** + * @method _getFrame + * @param {String} character + * @param {SpriteSheet} spriteSheet + * @return {Object} + * @protected + **/ + p._getFrame = function (character, spriteSheet) { + var index = this._getFrameIndex(character, spriteSheet); + return index == null ? index : spriteSheet.getFrame(index); + }; + + /** + * @method _getLineHeight + * @param {SpriteSheet} ss + * @return {Number} + * @protected + **/ + p._getLineHeight = function (ss) { + var frame = this._getFrame("1", ss) || this._getFrame("T", ss) || this._getFrame("L", ss) || ss.getFrame(0); + return frame ? frame.rect.height : 1; + }; + /** + * @method _getSpaceWidth + * @param {SpriteSheet} ss + * @return {Number} + * @protected + **/ + p._getSpaceWidth = function (ss) { + var frame = this._getFrame("1", ss) || this._getFrame("l", ss) || this._getFrame("e", ss) || this._getFrame("a", ss) || ss.getFrame(0); + return frame ? frame.rect.width : 1; + }; + + /** + * @method _drawText + * @protected + **/ + p._updateText = function () { + var x = 0, + y = 0, + o = this._oldProps, + change = false, + spaceW = this.spaceWidth, + lineH = this.lineHeight, + ss = this.spriteSheet; + var pool = BitmapText._spritePool, + kids = this.children, + childIndex = 0, + numKids = kids.length, + sprite; + + for (var n in o) { + if (o[n] != this[n]) { + o[n] = this[n]; + change = true; + } + } + if (!change) { + return; + } + + var hasSpace = !!this._getFrame(" ", ss); + if (!hasSpace && !spaceW) { + spaceW = this._getSpaceWidth(ss); + } + if (!lineH) { + lineH = this._getLineHeight(ss); + } + + for (var i = 0, l = this.text.length; i < l; i++) { + var character = this.text.charAt(i); + if (character == " " && !hasSpace) { + x += spaceW; + continue; + } else if (character == "\n" || character == "\r") { + if (character == "\r" && this.text.charAt(i + 1) == "\n") { + i++; + } // crlf + x = 0; + y += lineH; + continue; + } + + var index = this._getFrameIndex(character, ss); + if (index == null) { + continue; + } + + if (childIndex < numKids) { + sprite = kids[childIndex]; + } else { + kids.push(sprite = pool.length ? pool.pop() : new createjs.Sprite()); + sprite.parent = this; + numKids++; + } + sprite.spriteSheet = ss; + sprite.gotoAndStop(index); + sprite.x = x; + sprite.y = y; + childIndex++; + + x += sprite.getBounds().width + this.letterSpacing; + } + while (numKids > childIndex) { + // faster than removeChild. + pool.push(sprite = kids.pop()); + sprite.parent = null; + numKids--; + } + if (pool.length > BitmapText.maxPoolSize) { + pool.length = BitmapText.maxPoolSize; + } + }; + + createjs.BitmapText = createjs.promote(BitmapText, "Container"); + })(); + + //############################################################################## + // MovieClip.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * The MovieClip class associates a TweenJS Timeline with an EaselJS {{#crossLink "Container"}}{{/crossLink}}. It allows + * you to create objects which encapsulate timeline animations, state changes, and synched actions. Due to the + * complexities inherent in correctly setting up a MovieClip, it is largely intended for tool output and is not included + * in the main EaselJS library. + * + * Currently MovieClip only works properly if it is tick based (as opposed to time based) though some concessions have + * been made to support time-based timelines in the future. + * + *

    Example

    + * This example animates two shapes back and forth. The grey shape starts on the left, but we jump to a mid-point in + * the animation using {{#crossLink "MovieClip/gotoAndPlay"}}{{/crossLink}}. + * + * var stage = new createjs.Stage("canvas"); + * createjs.Ticker.addEventListener("tick", stage); + * + * var mc = new createjs.MovieClip(null, 0, true, {start:20}); + * stage.addChild(mc); + * + * var child1 = new createjs.Shape( + * new createjs.Graphics().beginFill("#999999") + * .drawCircle(30,30,30)); + * var child2 = new createjs.Shape( + * new createjs.Graphics().beginFill("#5a9cfb") + * .drawCircle(30,30,30)); + * + * mc.timeline.addTween( + * createjs.Tween.get(child1) + * .to({x:0}).to({x:60}, 50).to({x:0}, 50)); + * mc.timeline.addTween( + * createjs.Tween.get(child2) + * .to({x:60}).to({x:0}, 50).to({x:60}, 50)); + * + * mc.gotoAndPlay("start"); + * + * It is recommended to use tween.to() to animate and set properties (use no duration to have it set + * immediately), and the tween.wait() method to create delays between animations. Note that using the + * tween.set() method to affect properties will likely not provide the desired result. + * + * @class MovieClip + * @main MovieClip + * @extends Container + * @constructor + * @param {String} [mode=independent] Initial value for the mode property. One of {{#crossLink "MovieClip/INDEPENDENT:property"}}{{/crossLink}}, + * {{#crossLink "MovieClip/SINGLE_FRAME:property"}}{{/crossLink}}, or {{#crossLink "MovieClip/SYNCHED:property"}}{{/crossLink}}. + * The default is {{#crossLink "MovieClip/INDEPENDENT:property"}}{{/crossLink}}. + * @param {Number} [startPosition=0] Initial value for the {{#crossLink "MovieClip/startPosition:property"}}{{/crossLink}} + * property. + * @param {Boolean} [loop=true] Initial value for the {{#crossLink "MovieClip/loop:property"}}{{/crossLink}} + * property. The default is `true`. + * @param {Object} [labels=null] A hash of labels to pass to the {{#crossLink "MovieClip/timeline:property"}}{{/crossLink}} + * instance associated with this MovieClip. Labels only need to be passed if they need to be used. + **/ + + function MovieClip(mode, startPosition, loop, labels) { + this.Container_constructor(); + !MovieClip.inited && MovieClip.init(); // static init + + + // public properties: + /** + * Controls how this MovieClip advances its time. Must be one of 0 (INDEPENDENT), 1 (SINGLE_FRAME), or 2 (SYNCHED). + * See each constant for a description of the behaviour. + * @property mode + * @type String + * @default null + **/ + this.mode = mode || MovieClip.INDEPENDENT; + + /** + * Specifies what the first frame to play in this movieclip, or the only frame to display if mode is SINGLE_FRAME. + * @property startPosition + * @type Number + * @default 0 + */ + this.startPosition = startPosition || 0; + + /** + * Indicates whether this MovieClip should loop when it reaches the end of its timeline. + * @property loop + * @type Boolean + * @default true + */ + this.loop = loop; + + /** + * The current frame of the movieclip. + * @property currentFrame + * @type Number + * @default 0 + * @readonly + */ + this.currentFrame = 0; + + /** + * The TweenJS Timeline that is associated with this MovieClip. This is created automatically when the MovieClip + * instance is initialized. Animations are created by adding TweenJS Tween + * instances to the timeline. + * + *

    Example

    + * + * var tween = createjs.Tween.get(target).to({x:0}).to({x:100}, 30); + * var mc = new createjs.MovieClip(); + * mc.timeline.addTween(tween); + * + * Elements can be added and removed from the timeline by toggling an "_off" property + * using the tweenInstance.to() method. Note that using Tween.set is not recommended to + * create MovieClip animations. The following example will toggle the target off on frame 0, and then back on for + * frame 1. You can use the "visible" property to achieve the same effect. + * + * var tween = createjs.Tween.get(target).to({_off:false}) + * .wait(1).to({_off:true}) + * .wait(1).to({_off:false}); + * + * @property timeline + * @type Timeline + * @default null + */ + this.timeline = new createjs.Timeline(null, labels, { paused: true, position: startPosition, useTicks: true }); + + /** + * If true, the MovieClip's position will not advance when ticked. + * @property paused + * @type Boolean + * @default false + */ + this.paused = false; + + /** + * If true, actions in this MovieClip's tweens will be run when the playhead advances. + * @property actionsEnabled + * @type Boolean + * @default true + */ + this.actionsEnabled = true; + + /** + * If true, the MovieClip will automatically be reset to its first frame whenever the timeline adds + * it back onto the display list. This only applies to MovieClip instances with mode=INDEPENDENT. + *

    + * For example, if you had a character animation with a "body" child MovieClip instance + * with different costumes on each frame, you could set body.autoReset = false, so that + * you can manually change the frame it is on, without worrying that it will be reset + * automatically. + * @property autoReset + * @type Boolean + * @default true + */ + this.autoReset = true; + + /** + * An array of bounds for each frame in the MovieClip. This is mainly intended for tool output. + * @property frameBounds + * @type Array + * @default null + */ + this.frameBounds = this.frameBounds || null; // TODO: Deprecated. This is for backwards support of FlashCC + + /** + * By default MovieClip instances advance one frame per tick. Specifying a framerate for the MovieClip + * will cause it to advance based on elapsed time between ticks as appropriate to maintain the target + * framerate. + * + * For example, if a MovieClip with a framerate of 10 is placed on a Stage being updated at 40fps, then the MovieClip will + * advance roughly one frame every 4 ticks. This will not be exact, because the time between each tick will + * vary slightly between frames. + * + * This feature is dependent on the tick event object (or an object with an appropriate "delta" property) being + * passed into {{#crossLink "Stage/update"}}{{/crossLink}}. + * @property framerate + * @type {Number} + * @default null + **/ + this.framerate = null; + + // private properties: + /** + * @property _synchOffset + * @type Number + * @default 0 + * @private + */ + this._synchOffset = 0; + + /** + * @property _prevPos + * @type Number + * @default -1 + * @private + */ + this._prevPos = -1; // TODO: evaluate using a ._reset Boolean prop instead of -1. + + /** + * @property _prevPosition + * @type Number + * @default 0 + * @private + */ + this._prevPosition = 0; + + /** + * The time remaining from the previous tick, only applicable when .framerate is set. + * @property _t + * @type Number + * @private + */ + this._t = 0; + + /** + * List of display objects that are actively being managed by the MovieClip. + * @property _managed + * @type Object + * @private + */ + this._managed = {}; + } + var p = createjs.extend(MovieClip, createjs.Container); + + // constants: + /** + * The MovieClip will advance independently of its parent, even if its parent is paused. + * This is the default mode. + * @property INDEPENDENT + * @static + * @type String + * @default "independent" + * @readonly + **/ + MovieClip.INDEPENDENT = "independent"; + + /** + * The MovieClip will only display a single frame (as determined by the startPosition property). + * @property SINGLE_FRAME + * @static + * @type String + * @default "single" + * @readonly + **/ + MovieClip.SINGLE_FRAME = "single"; + + /** + * The MovieClip will be advanced only when its parent advances and will be synched to the position of + * the parent MovieClip. + * @property SYNCHED + * @static + * @type String + * @default "synched" + * @readonly + **/ + MovieClip.SYNCHED = "synched"; + + // static properties: + MovieClip.inited = false; + + // static methods: + MovieClip.init = function () { + if (MovieClip.inited) { + return; + } + // plugins introduce some overhead to Tween, so we only install this if an MC is instantiated. + MovieClipPlugin.install(); + MovieClip.inited = true; + }; + + // getter / setters: + /** + * Use the {{#crossLink "MovieClip/labels:property"}}{{/crossLink}} property instead. + * @method getLabels + * @return {Array} + * @deprecated + **/ + p.getLabels = function () { + return this.timeline.getLabels(); + }; + + /** + * Use the {{#crossLink "MovieClip/currentLabel:property"}}{{/crossLink}} property instead. + * @method getCurrentLabel + * @return {String} + * @deprecated + **/ + p.getCurrentLabel = function () { + this._updateTimeline(); + return this.timeline.getCurrentLabel(); + }; + + /** + * Use the {{#crossLink "MovieClip/duration:property"}}{{/crossLink}} property instead. + * @method getDuration + * @return {Number} + * @protected + **/ + p.getDuration = function () { + return this.timeline.duration; + }; + + /** + * Returns an array of objects with label and position (aka frame) properties, sorted by position. + * Shortcut to TweenJS: Timeline.getLabels(); + * @property labels + * @type {Array} + * @readonly + **/ + + /** + * Returns the name of the label on or immediately before the current frame. See TweenJS: Timeline.getCurrentLabel() + * for more information. + * @property currentLabel + * @type {String} + * @readonly + **/ + + /** + * Returns the duration of this MovieClip in seconds or ticks. Identical to {{#crossLink "MovieClip/duration:property"}}{{/crossLink}} + * and provided for Flash API compatibility. + * @property totalFrames + * @type {Number} + * @readonly + **/ + + /** + * Returns the duration of this MovieClip in seconds or ticks. + * @property duration + * @type {Number} + * @readonly + **/ + try { + Object.defineProperties(p, { + labels: { get: p.getLabels }, + currentLabel: { get: p.getCurrentLabel }, + totalFrames: { get: p.getDuration }, + duration: { get: p.getDuration } + }); + } catch (e) {} + + // public methods: + /** + * Constructor alias for backwards compatibility. This method will be removed in future versions. + * Subclasses should be updated to use {{#crossLink "Utility Methods/extends"}}{{/crossLink}}. + * @method initialize + * @deprecated in favour of `createjs.promote()` + **/ + p.initialize = MovieClip; // TODO: Deprecated. This is for backwards support of FlashCC + + /** + * Returns true or false indicating whether the display object would be visible if drawn to a canvas. + * This does not account for whether it would be visible within the boundaries of the stage. + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method isVisible + * @return {Boolean} Boolean indicating whether the display object would be visible if drawn to a canvas + **/ + p.isVisible = function () { + // children are placed in draw, so we can't determine if we have content. + return !!(this.visible && this.alpha > 0 && this.scaleX != 0 && this.scaleY != 0); + }; + + /** + * Draws the display object into the specified context ignoring its visible, alpha, shadow, and transform. + * Returns true if the draw was handled (useful for overriding functionality). + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method draw + * @param {CanvasRenderingContext2D} ctx The canvas 2D context object to draw into. + * @param {Boolean} ignoreCache Indicates whether the draw operation should ignore any current cache. + * For example, used for drawing the cache (to prevent it from simply drawing an existing cache back + * into itself). + **/ + p.draw = function (ctx, ignoreCache) { + // draw to cache first: + if (this.DisplayObject_draw(ctx, ignoreCache)) { + return true; + } + this._updateTimeline(); + this.Container_draw(ctx, ignoreCache); + return true; + }; + + /** + * Sets paused to false. + * @method play + **/ + p.play = function () { + this.paused = false; + }; + + /** + * Sets paused to true. + * @method stop + **/ + p.stop = function () { + this.paused = true; + }; + + /** + * Advances this movie clip to the specified position or label and sets paused to false. + * @method gotoAndPlay + * @param {String|Number} positionOrLabel The animation name or frame number to go to. + **/ + p.gotoAndPlay = function (positionOrLabel) { + this.paused = false; + this._goto(positionOrLabel); + }; + + /** + * Advances this movie clip to the specified position or label and sets paused to true. + * @method gotoAndStop + * @param {String|Number} positionOrLabel The animation or frame name to go to. + **/ + p.gotoAndStop = function (positionOrLabel) { + this.paused = true; + this._goto(positionOrLabel); + }; + + /** + * Advances the playhead. This occurs automatically each tick by default. + * @param [time] {Number} The amount of time in ms to advance by. Only applicable if framerate is set. + * @method advance + */ + p.advance = function (time) { + // TODO: should we worry at all about clips who change their own modes via frame scripts? + var independent = MovieClip.INDEPENDENT; + if (this.mode != independent) { + return; + } + + var o = this, + fps = o.framerate; + while ((o = o.parent) && fps == null) { + if (o.mode == independent) { + fps = o._framerate; + } + } + this._framerate = fps; + + var t = fps != null && fps != -1 && time != null ? time / (1000 / fps) + this._t : 1; + var frames = t | 0; + this._t = t - frames; // leftover time + + while (!this.paused && frames--) { + this._prevPosition = this._prevPos < 0 ? 0 : this._prevPosition + 1; + this._updateTimeline(); + } + }; + + /** + * MovieClip instances cannot be cloned. + * @method clone + **/ + p.clone = function () { + // TODO: add support for this? Need to clone the Timeline & retarget tweens - pretty complex. + throw "MovieClip cannot be cloned."; + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[MovieClip (name=" + this.name + ")]"; + }; + + // private methods: + /** + * @method _tick + * @param {Object} evtObj An event object that will be dispatched to all tick listeners. This object is reused between dispatchers to reduce construction & GC costs. + * function. + * @protected + **/ + p._tick = function (evtObj) { + this.advance(evtObj && evtObj.delta); + this.Container__tick(evtObj); + }; + + /** + * @method _goto + * @param {String|Number} positionOrLabel The animation name or frame number to go to. + * @protected + **/ + p._goto = function (positionOrLabel) { + var pos = this.timeline.resolve(positionOrLabel); + if (pos == null) { + return; + } + // prevent _updateTimeline from overwriting the new position because of a reset: + if (this._prevPos == -1) { + this._prevPos = NaN; + } + this._prevPosition = pos; + this._t = 0; + this._updateTimeline(); + }; + + /** + * @method _reset + * @private + **/ + p._reset = function () { + this._prevPos = -1; + this._t = this.currentFrame = 0; + this.paused = false; + }; + + /** + * @method _updateTimeline + * @protected + **/ + p._updateTimeline = function () { + var tl = this.timeline; + var synched = this.mode != MovieClip.INDEPENDENT; + tl.loop = this.loop == null ? true : this.loop; + + var pos = synched ? this.startPosition + (this.mode == MovieClip.SINGLE_FRAME ? 0 : this._synchOffset) : this._prevPos < 0 ? 0 : this._prevPosition; + var mode = synched || !this.actionsEnabled ? createjs.Tween.NONE : null; + + // pre-assign currentFrame so it is available to frame scripts: + this.currentFrame = tl._calcPosition(pos); + + // update timeline position, ignoring actions if this is a graphic. + tl.setPosition(pos, mode); + + this._prevPosition = tl._prevPosition; + if (this._prevPos == tl._prevPos) { + return; + } + this.currentFrame = this._prevPos = tl._prevPos; + + for (var n in this._managed) { + this._managed[n] = 1; + } + + var tweens = tl._tweens; + for (var i = 0, l = tweens.length; i < l; i++) { + var tween = tweens[i]; + var target = tween._target; + if (target == this || tween.passive) { + continue; + } // TODO: this assumes actions tween has this as the target. Valid? + var offset = tween._stepPosition; + + if (target instanceof createjs.DisplayObject) { + // motion tween. + this._addManagedChild(target, offset); + } else { + // state tween. + this._setState(target.state, offset); + } + } + + var kids = this.children; + for (i = kids.length - 1; i >= 0; i--) { + var id = kids[i].id; + if (this._managed[id] == 1) { + this.removeChildAt(i); + delete this._managed[id]; + } + } + }; + + /** + * @method _setState + * @param {Array} state + * @param {Number} offset + * @protected + **/ + p._setState = function (state, offset) { + if (!state) { + return; + } + for (var i = state.length - 1; i >= 0; i--) { + var o = state[i]; + var target = o.t; + var props = o.p; + for (var n in props) { + target[n] = props[n]; + } + this._addManagedChild(target, offset); + } + }; + + /** + * Adds a child to the timeline, and sets it up as a managed child. + * @method _addManagedChild + * @param {MovieClip} child The child MovieClip to manage + * @param {Number} offset + * @private + **/ + p._addManagedChild = function (child, offset) { + if (child._off) { + return; + } + this.addChildAt(child, 0); + + if (child instanceof MovieClip) { + child._synchOffset = offset; + // TODO: this does not precisely match Flash. Flash loses track of the clip if it is renamed or removed from the timeline, which causes it to reset. + if (child.mode == MovieClip.INDEPENDENT && child.autoReset && !this._managed[child.id]) { + child._reset(); + } + } + this._managed[child.id] = 2; + }; + + /** + * @method _getBounds + * @param {Matrix2D} matrix + * @param {Boolean} ignoreTransform + * @return {Rectangle} + * @protected + **/ + p._getBounds = function (matrix, ignoreTransform) { + var bounds = this.DisplayObject_getBounds(); + if (!bounds) { + this._updateTimeline(); + if (this.frameBounds) { + bounds = this._rectangle.copy(this.frameBounds[this.currentFrame]); + } + } + if (bounds) { + return this._transformBounds(bounds, matrix, ignoreTransform); + } + return this.Container__getBounds(matrix, ignoreTransform); + }; + + createjs.MovieClip = createjs.promote(MovieClip, "Container"); + + // MovieClipPlugin for TweenJS: + /** + * This plugin works with TweenJS to prevent the startPosition + * property from tweening. + * @private + * @class MovieClipPlugin + * @constructor + **/ + function MovieClipPlugin() { + throw "MovieClipPlugin cannot be instantiated."; + } + + /** + * @method priority + * @private + **/ + MovieClipPlugin.priority = 100; // very high priority, should run first + + /** + * @method install + * @private + **/ + MovieClipPlugin.install = function () { + createjs.Tween.installPlugin(MovieClipPlugin, ["startPosition"]); + }; + + /** + * @method init + * @param {Tween} tween + * @param {String} prop + * @param {String|Number|Boolean} value + * @private + **/ + MovieClipPlugin.init = function (tween, prop, value) { + return value; + }; + + /** + * @method step + * @private + **/ + MovieClipPlugin.step = function () { + // unused. + }; + + /** + * @method tween + * @param {Tween} tween + * @param {String} prop + * @param {String | Number | Boolean} value + * @param {Array} startValues + * @param {Array} endValues + * @param {Number} ratio + * @param {Object} wait + * @param {Object} end + * @return {*} + */ + MovieClipPlugin.tween = function (tween, prop, value, startValues, endValues, ratio, wait, end) { + if (!(tween.target instanceof MovieClip)) { + return value; + } + return ratio == 1 ? endValues[prop] : startValues[prop]; + }; + })(); + + //############################################################################## + // SpriteSheetUtils.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * The SpriteSheetUtils class is a collection of static methods for working with {{#crossLink "SpriteSheet"}}{{/crossLink}}s. + * A sprite sheet is a series of images (usually animation frames) combined into a single image on a regular grid. For + * example, an animation consisting of 8 100x100 images could be combined into a 400x200 sprite sheet (4 frames across + * by 2 high). The SpriteSheetUtils class uses a static interface and should not be instantiated. + * @class SpriteSheetUtils + * @static + **/ + + function SpriteSheetUtils() { + throw "SpriteSheetUtils cannot be instantiated"; + } + + // private static properties: + /** + * @property _workingCanvas + * @static + * @type HTMLCanvasElement | Object + * @protected + */ + /** + * @property _workingContext + * @static + * @type CanvasRenderingContext2D + * @protected + */ + var canvas = createjs.createCanvas ? createjs.createCanvas() : document.createElement("canvas"); + if (canvas.getContext) { + SpriteSheetUtils._workingCanvas = canvas; + SpriteSheetUtils._workingContext = canvas.getContext("2d"); + canvas.width = canvas.height = 1; + } + + // public static methods: + /** + * This is an experimental method, and may be buggy. Please report issues.

    + * Extends the existing sprite sheet by flipping the original frames horizontally, vertically, or both, + * and adding appropriate animation & frame data. The flipped animations will have a suffix added to their names + * (_h, _v, _hv as appropriate). Make sure the sprite sheet images are fully loaded before using this method. + *

    + * For example:
    + * SpriteSheetUtils.addFlippedFrames(mySpriteSheet, true, true); + * The above would add frames that are flipped horizontally AND frames that are flipped vertically. + *

    + * Note that you can also flip any display object by setting its scaleX or scaleY to a negative value. On some + * browsers (especially those without hardware accelerated canvas) this can result in slightly degraded performance, + * which is why addFlippedFrames is available. + * @method addFlippedFrames + * @static + * @param {SpriteSheet} spriteSheet + * @param {Boolean} horizontal If true, horizontally flipped frames will be added. + * @param {Boolean} vertical If true, vertically flipped frames will be added. + * @param {Boolean} both If true, frames that are flipped both horizontally and vertically will be added. + * @deprecated Modern browsers perform better when flipping via a transform (ex. scaleX=-1) rendering this obsolete. + **/ + SpriteSheetUtils.addFlippedFrames = function (spriteSheet, horizontal, vertical, both) { + if (!horizontal && !vertical && !both) { + return; + } + + var count = 0; + if (horizontal) { + SpriteSheetUtils._flip(spriteSheet, ++count, true, false); + } + if (vertical) { + SpriteSheetUtils._flip(spriteSheet, ++count, false, true); + } + if (both) { + SpriteSheetUtils._flip(spriteSheet, ++count, true, true); + } + }; + + /** + * Returns a single frame of the specified sprite sheet as a new PNG image. An example of when this may be useful is + * to use a spritesheet frame as the source for a bitmap fill. + * + * WARNING: In almost all cases it is better to display a single frame using a {{#crossLink "Sprite"}}{{/crossLink}} + * with a {{#crossLink "Sprite/gotoAndStop"}}{{/crossLink}} call than it is to slice out a frame using this + * method and display it with a Bitmap instance. You can also crop an image using the {{#crossLink "Bitmap/sourceRect"}}{{/crossLink}} + * property of {{#crossLink "Bitmap"}}{{/crossLink}}. + * + * The extractFrame method may cause cross-domain warnings since it accesses pixels directly on the canvas. + * @method extractFrame + * @static + * @param {SpriteSheet} spriteSheet The SpriteSheet instance to extract a frame from. + * @param {Number|String} frameOrAnimation The frame number or animation name to extract. If an animation + * name is specified, only the first frame of the animation will be extracted. + * @return {HTMLImageElement} a single frame of the specified sprite sheet as a new PNG image. + */ + SpriteSheetUtils.extractFrame = function (spriteSheet, frameOrAnimation) { + if (isNaN(frameOrAnimation)) { + frameOrAnimation = spriteSheet.getAnimation(frameOrAnimation).frames[0]; + } + var data = spriteSheet.getFrame(frameOrAnimation); + if (!data) { + return null; + } + var r = data.rect; + var canvas = SpriteSheetUtils._workingCanvas; + canvas.width = r.width; + canvas.height = r.height; + SpriteSheetUtils._workingContext.drawImage(data.image, r.x, r.y, r.width, r.height, 0, 0, r.width, r.height); + var img = document.createElement("img"); + img.src = canvas.toDataURL("image/png"); + return img; + }; + + /** + * Merges the rgb channels of one image with the alpha channel of another. This can be used to combine a compressed + * JPEG image containing color data with a PNG32 monochromatic image containing alpha data. With certain types of + * images (those with detail that lend itself to JPEG compression) this can provide significant file size savings + * versus a single RGBA PNG32. This method is very fast (generally on the order of 1-2 ms to run). + * @method mergeAlpha + * @static + * @param {HTMLImageElement} rbgImage The image (or canvas) containing the RGB channels to use. + * @param {HTMLImageElement} alphaImage The image (or canvas) containing the alpha channel to use. + * @param {HTMLCanvasElement} canvas Optional. If specified, this canvas will be used and returned. If not, a new canvas will be created. + * @return {HTMLCanvasElement} A canvas with the combined image data. This can be used as a source for Bitmap or SpriteSheet. + * @deprecated Tools such as ImageAlpha generally provide better results. This will be moved to sandbox in the future. + */ + SpriteSheetUtils.mergeAlpha = function (rgbImage, alphaImage, canvas) { + if (!canvas) { + canvas = createjs.createCanvas ? createjs.createCanvas() : document.createElement("canvas"); + } + canvas.width = Math.max(alphaImage.width, rgbImage.width); + canvas.height = Math.max(alphaImage.height, rgbImage.height); + var ctx = canvas.getContext("2d"); + ctx.save(); + ctx.drawImage(rgbImage, 0, 0); + ctx.globalCompositeOperation = "destination-in"; + ctx.drawImage(alphaImage, 0, 0); + ctx.restore(); + return canvas; + }; + + // private static methods: + SpriteSheetUtils._flip = function (spriteSheet, count, h, v) { + var imgs = spriteSheet._images; + var canvas = SpriteSheetUtils._workingCanvas; + var ctx = SpriteSheetUtils._workingContext; + var il = imgs.length / count; + for (var i = 0; i < il; i++) { + var src = imgs[i]; + src.__tmp = i; // a bit hacky, but faster than doing indexOf below. + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.clearRect(0, 0, canvas.width + 1, canvas.height + 1); + canvas.width = src.width; + canvas.height = src.height; + ctx.setTransform(h ? -1 : 1, 0, 0, v ? -1 : 1, h ? src.width : 0, v ? src.height : 0); + ctx.drawImage(src, 0, 0); + var img = document.createElement("img"); + img.src = canvas.toDataURL("image/png"); + // work around a strange bug in Safari: + img.width = src.width; + img.height = src.height; + imgs.push(img); + } + + var frames = spriteSheet._frames; + var fl = frames.length / count; + for (i = 0; i < fl; i++) { + src = frames[i]; + var rect = src.rect.clone(); + img = imgs[src.image.__tmp + il * count]; + + var frame = { image: img, rect: rect, regX: src.regX, regY: src.regY }; + if (h) { + rect.x = img.width - rect.x - rect.width; // update rect + frame.regX = rect.width - src.regX; // update registration point + } + if (v) { + rect.y = img.height - rect.y - rect.height; // update rect + frame.regY = rect.height - src.regY; // update registration point + } + frames.push(frame); + } + + var sfx = "_" + (h ? "h" : "") + (v ? "v" : ""); + var names = spriteSheet._animations; + var data = spriteSheet._data; + var al = names.length / count; + for (i = 0; i < al; i++) { + var name = names[i]; + src = data[name]; + var anim = { name: name + sfx, speed: src.speed, next: src.next, frames: [] }; + if (src.next) { + anim.next += sfx; + } + frames = src.frames; + for (var j = 0, l = frames.length; j < l; j++) { + anim.frames.push(frames[j] + fl * count); + } + data[anim.name] = anim; + names.push(anim.name); + } + }; + + createjs.SpriteSheetUtils = SpriteSheetUtils; + })(); + + //############################################################################## + // SpriteSheetBuilder.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * The SpriteSheetBuilder allows you to generate {{#crossLink "SpriteSheet"}}{{/crossLink}} instances at run time + * from any display object. This can allow you to maintain your assets as vector graphics (for low file size), and + * render them at run time as SpriteSheets for better performance. + * + * SpriteSheets can be built either synchronously, or asynchronously, so that large SpriteSheets can be generated + * without locking the UI. + * + * Note that the "images" used in the generated SpriteSheet are actually canvas elements, and that they will be + * sized to the nearest power of 2 up to the value of {{#crossLink "SpriteSheetBuilder/maxWidth:property"}}{{/crossLink}} + * or {{#crossLink "SpriteSheetBuilder/maxHeight:property"}}{{/crossLink}}. + * @class SpriteSheetBuilder + * @param {Number} [framerate=0] The {{#crossLink "SpriteSheet/framerate:property"}}{{/crossLink}} of + * {{#crossLink "SpriteSheet"}}{{/crossLink}} instances that are created. + * @extends EventDispatcher + * @constructor + **/ + + function SpriteSheetBuilder(framerate) { + this.EventDispatcher_constructor(); + + // public properties: + /** + * The maximum width for the images (not individual frames) in the generated SpriteSheet. It is recommended to + * use a power of 2 for this value (ex. 1024, 2048, 4096). If the frames cannot all fit within the max + * dimensions, then additional images will be created as needed. + * @property maxWidth + * @type Number + * @default 2048 + */ + this.maxWidth = 2048; + + /** + * The maximum height for the images (not individual frames) in the generated SpriteSheet. It is recommended to + * use a power of 2 for this value (ex. 1024, 2048, 4096). If the frames cannot all fit within the max + * dimensions, then additional images will be created as needed. + * @property maxHeight + * @type Number + * @default 2048 + **/ + this.maxHeight = 2048; + + /** + * The SpriteSheet that was generated. This will be null before a build is completed successfully. + * @property spriteSheet + * @type SpriteSheet + **/ + this.spriteSheet = null; + + /** + * The scale to apply when drawing all frames to the SpriteSheet. This is multiplied against any scale specified + * in the addFrame call. This can be used, for example, to generate a SpriteSheet at run time that is tailored + * to the a specific device resolution (ex. tablet vs mobile). + * @property scale + * @type Number + * @default 1 + **/ + this.scale = 1; + + /** + * The padding to use between frames. This is helpful to preserve antialiasing on drawn vector content. + * @property padding + * @type Number + * @default 1 + **/ + this.padding = 1; + + /** + * A number from 0.01 to 0.99 that indicates what percentage of time the builder can use. This can be + * thought of as the number of seconds per second the builder will use. For example, with a timeSlice value of 0.3, + * the builder will run 20 times per second, using approximately 15ms per build (30% of available time, or 0.3s per second). + * Defaults to 0.3. + * @property timeSlice + * @type Number + * @default 0.3 + **/ + this.timeSlice = 0.3; + + /** + * A value between 0 and 1 that indicates the progress of a build, or -1 if a build has not + * been initiated. + * @property progress + * @type Number + * @default -1 + * @readonly + */ + this.progress = -1; + + /** + * A {{#crossLink "SpriteSheet/framerate:property"}}{{/crossLink}} value that will be passed to new {{#crossLink "SpriteSheet"}}{{/crossLink}} instances that are + * created. If no framerate is specified (or it is 0), then SpriteSheets will use the {{#crossLink "Ticker"}}{{/crossLink}} + * framerate. + * @property framerate + * @type Number + * @default 0 + */ + this.framerate = framerate || 0; + + // private properties: + /** + * @property _frames + * @protected + * @type Array + **/ + this._frames = []; + + /** + * @property _animations + * @protected + * @type Array + **/ + this._animations = {}; + + /** + * @property _data + * @protected + * @type Array + **/ + this._data = null; + + /** + * @property _nextFrameIndex + * @protected + * @type Number + **/ + this._nextFrameIndex = 0; + + /** + * @property _index + * @protected + * @type Number + **/ + this._index = 0; + + /** + * @property _timerID + * @protected + * @type Number + **/ + this._timerID = null; + + /** + * @property _scale + * @protected + * @type Number + **/ + this._scale = 1; + } + var p = createjs.extend(SpriteSheetBuilder, createjs.EventDispatcher); + + /** + * REMOVED. Removed in favor of using `MySuperClass_constructor`. + * See {{#crossLink "Utility Methods/extend"}}{{/crossLink}} and {{#crossLink "Utility Methods/promote"}}{{/crossLink}} + * for details. + * + * There is an inheritance tutorial distributed with EaselJS in /tutorials/Inheritance. + * + * @method initialize + * @protected + * @deprecated + */ + // p.initialize = function() {}; // searchable for devs wondering where it is. + + + // constants: + SpriteSheetBuilder.ERR_DIMENSIONS = "frame dimensions exceed max spritesheet dimensions"; + SpriteSheetBuilder.ERR_RUNNING = "a build is already running"; + + // events: + /** + * Dispatched when a build completes. + * @event complete + * @param {Object} target The object that dispatched the event. + * @param {String} type The event type. + * @since 0.6.0 + */ + + /** + * Dispatched when an asynchronous build has progress. + * @event progress + * @param {Object} target The object that dispatched the event. + * @param {String} type The event type. + * @param {Number} progress The current progress value (0-1). + * @since 0.6.0 + */ + + // public methods: + /** + * Adds a frame to the {{#crossLink "SpriteSheet"}}{{/crossLink}}. Note that the frame will not be drawn until you + * call {{#crossLink "SpriteSheetBuilder/build"}}{{/crossLink}} method. The optional setup params allow you to have + * a function run immediately before the draw occurs. For example, this allows you to add a single source multiple + * times, but manipulate it or its children to change it to generate different frames. + * + * Note that the source's transformations (x, y, scale, rotate, alpha) will be ignored, except for regX/Y. To apply + * transforms to a source object and have them captured in the SpriteSheet, simply place it into a {{#crossLink "Container"}}{{/crossLink}} + * and pass in the Container as the source. + * @method addFrame + * @param {DisplayObject} source The source {{#crossLink "DisplayObject"}}{{/crossLink}} to draw as the frame. + * @param {Rectangle} [sourceRect] A {{#crossLink "Rectangle"}}{{/crossLink}} defining the portion of the + * source to draw to the frame. If not specified, it will look for a `getBounds` method, bounds property, or + * `nominalBounds` property on the source to use. If one is not found, the frame will be skipped. + * @param {Number} [scale=1] Optional. The scale to draw this frame at. Default is 1. + * @param {Function} [setupFunction] A function to call immediately before drawing this frame. It will be called with two parameters: the source, and setupData. + * @param {Object} [setupData] Arbitrary setup data to pass to setupFunction as the second parameter. + * @return {Number} The index of the frame that was just added, or null if a sourceRect could not be determined. + **/ + p.addFrame = function (source, sourceRect, scale, setupFunction, setupData) { + if (this._data) { + throw SpriteSheetBuilder.ERR_RUNNING; + } + var rect = sourceRect || source.bounds || source.nominalBounds; + if (!rect && source.getBounds) { + rect = source.getBounds(); + } + if (!rect) { + return null; + } + scale = scale || 1; + return this._frames.push({ source: source, sourceRect: rect, scale: scale, funct: setupFunction, data: setupData, index: this._frames.length, height: rect.height * scale }) - 1; + }; + + /** + * Adds an animation that will be included in the created {{#crossLink "SpriteSheet"}}{{/crossLink}}. + * @method addAnimation + * @param {String} name The name for the animation. + * @param {Array} frames An array of frame indexes that comprise the animation. Ex. [3,6,5] would describe an animation + * that played frame indexes 3, 6, and 5 in that order. + * @param {String} [next] Specifies the name of the animation to continue to after this animation ends. You can + * also pass false to have the animation stop when it ends. By default it will loop to the start of the same animation. + * @param {Number} [speed] Specifies a frame advance speed for this animation. For example, a value of 0.5 would + * cause the animation to advance every second tick. Note that earlier versions used `frequency` instead, which had + * the opposite effect. + **/ + p.addAnimation = function (name, frames, next, speed) { + if (this._data) { + throw SpriteSheetBuilder.ERR_RUNNING; + } + this._animations[name] = { frames: frames, next: next, speed: speed }; + }; + + /** + * This will take a {{#crossLink "MovieClip"}}{{/crossLink}} instance, and add its frames and labels to this + * builder. Labels will be added as an animation running from the label index to the next label. For example, if + * there is a label named "foo" at frame 0 and a label named "bar" at frame 10, in a MovieClip with 15 frames, it + * will add an animation named "foo" that runs from frame index 0 to 9, and an animation named "bar" that runs from + * frame index 10 to 14. + * + * Note that this will iterate through the full MovieClip with {{#crossLink "MovieClip/actionsEnabled:property"}}{{/crossLink}} + * set to `false`, ending on the last frame. + * @method addMovieClip + * @param {MovieClip} source The source MovieClip instance to add to the SpriteSheet. + * @param {Rectangle} [sourceRect] A {{#crossLink "Rectangle"}}{{/crossLink}} defining the portion of the source to + * draw to the frame. If not specified, it will look for a {{#crossLink "DisplayObject/getBounds"}}{{/crossLink}} + * method, `frameBounds` Array, `bounds` property, or `nominalBounds` property on the source to use. If one is not + * found, the MovieClip will be skipped. + * @param {Number} [scale=1] The scale to draw the movie clip at. + * @param {Function} [setupFunction] A function to call immediately before drawing each frame. It will be called + * with three parameters: the source, setupData, and the frame index. + * @param {Object} [setupData] Arbitrary setup data to pass to setupFunction as the second parameter. + * @param {Function} [labelFunction] This method will be called for each MovieClip label that is added with four + * parameters: the label name, the source MovieClip instance, the starting frame index (in the movieclip timeline) + * and the end index. It must return a new name for the label/animation, or `false` to exclude the label. + **/ + p.addMovieClip = function (source, sourceRect, scale, setupFunction, setupData, labelFunction) { + if (this._data) { + throw SpriteSheetBuilder.ERR_RUNNING; + } + var rects = source.frameBounds; + var rect = sourceRect || source.bounds || source.nominalBounds; + if (!rect && source.getBounds) { + rect = source.getBounds(); + } + if (!rect && !rects) { + return; + } + + var i, + l, + baseFrameIndex = this._frames.length; + var duration = source.timeline.duration; + for (i = 0; i < duration; i++) { + var r = rects && rects[i] ? rects[i] : rect; + this.addFrame(source, r, scale, this._setupMovieClipFrame, { i: i, f: setupFunction, d: setupData }); + } + var labels = source.timeline._labels; + var lbls = []; + for (var n in labels) { + lbls.push({ index: labels[n], label: n }); + } + if (lbls.length) { + lbls.sort(function (a, b) { + return a.index - b.index; + }); + for (i = 0, l = lbls.length; i < l; i++) { + var label = lbls[i].label; + var start = baseFrameIndex + lbls[i].index; + var end = baseFrameIndex + (i == l - 1 ? duration : lbls[i + 1].index); + var frames = []; + for (var j = start; j < end; j++) { + frames.push(j); + } + if (labelFunction) { + label = labelFunction(label, source, start, end); + if (!label) { + continue; + } + } + this.addAnimation(label, frames, true); // for now, this loops all animations. + } + } + }; + + /** + * Builds a {{#crossLink "SpriteSheet"}}{{/crossLink}} instance based on the current frames. + * @method build + * @return {SpriteSheet} The created SpriteSheet instance, or null if a build is already running or an error + * occurred. + **/ + p.build = function () { + if (this._data) { + throw SpriteSheetBuilder.ERR_RUNNING; + } + this._startBuild(); + while (this._drawNext()) {} + this._endBuild(); + return this.spriteSheet; + }; + + /** + * Asynchronously builds a {{#crossLink "SpriteSheet"}}{{/crossLink}} instance based on the current frames. It will + * run 20 times per second, using an amount of time defined by `timeSlice`. When it is complete it will call the + * specified callback. + * @method buildAsync + * @param {Number} [timeSlice] Sets the timeSlice property on this instance. + **/ + p.buildAsync = function (timeSlice) { + if (this._data) { + throw SpriteSheetBuilder.ERR_RUNNING; + } + this.timeSlice = timeSlice; + this._startBuild(); + var _this = this; + this._timerID = setTimeout(function () { + _this._run(); + }, 50 - Math.max(0.01, Math.min(0.99, this.timeSlice || 0.3)) * 50); + }; + + /** + * Stops the current asynchronous build. + * @method stopAsync + **/ + p.stopAsync = function () { + clearTimeout(this._timerID); + this._data = null; + }; + + /** + * SpriteSheetBuilder instances cannot be cloned. + * @method clone + **/ + p.clone = function () { + throw "SpriteSheetBuilder cannot be cloned."; + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[SpriteSheetBuilder]"; + }; + + // private methods: + /** + * @method _startBuild + * @protected + **/ + p._startBuild = function () { + var pad = this.padding || 0; + this.progress = 0; + this.spriteSheet = null; + this._index = 0; + this._scale = this.scale; + var dataFrames = []; + this._data = { + images: [], + frames: dataFrames, + framerate: this.framerate, + animations: this._animations // TODO: should we "clone" _animations in case someone adds more animations after a build? + }; + + var frames = this._frames.slice(); + frames.sort(function (a, b) { + return a.height <= b.height ? -1 : 1; + }); + + if (frames[frames.length - 1].height + pad * 2 > this.maxHeight) { + throw SpriteSheetBuilder.ERR_DIMENSIONS; + } + var y = 0, + x = 0; + var img = 0; + while (frames.length) { + var o = this._fillRow(frames, y, img, dataFrames, pad); + if (o.w > x) { + x = o.w; + } + y += o.h; + if (!o.h || !frames.length) { + var canvas = createjs.createCanvas ? createjs.createCanvas() : document.createElement("canvas"); + canvas.width = this._getSize(x, this.maxWidth); + canvas.height = this._getSize(y, this.maxHeight); + this._data.images[img] = canvas; + if (!o.h) { + x = y = 0; + img++; + } + } + } + }; + + /** + * @method _setupMovieClipFrame + * @protected + * @return {Number} The width & height of the row. + **/ + p._setupMovieClipFrame = function (source, data) { + var ae = source.actionsEnabled; + source.actionsEnabled = false; + source.gotoAndStop(data.i); + source.actionsEnabled = ae; + data.f && data.f(source, data.d, data.i); + }; + + /** + * @method _getSize + * @protected + * @return {Number} The width & height of the row. + **/ + p._getSize = function (size, max) { + var pow = 4; + while (Math.pow(2, ++pow) < size) {} + return Math.min(max, Math.pow(2, pow)); + }; + + /** + * @method _fillRow + * @param {Array} frames + * @param {Number} y + * @param {HTMLImageElement} img + * @param {Object} dataFrames + * @param {Number} pad + * @protected + * @return {Number} The width & height of the row. + **/ + p._fillRow = function (frames, y, img, dataFrames, pad) { + var w = this.maxWidth; + var maxH = this.maxHeight; + y += pad; + var h = maxH - y; + var x = pad; + var height = 0; + for (var i = frames.length - 1; i >= 0; i--) { + var frame = frames[i]; + var sc = this._scale * frame.scale; + var rect = frame.sourceRect; + var source = frame.source; + var rx = Math.floor(sc * rect.x - pad); + var ry = Math.floor(sc * rect.y - pad); + var rh = Math.ceil(sc * rect.height + pad * 2); + var rw = Math.ceil(sc * rect.width + pad * 2); + if (rw > w) { + throw SpriteSheetBuilder.ERR_DIMENSIONS; + } + if (rh > h || x + rw > w) { + continue; + } + frame.img = img; + frame.rect = new createjs.Rectangle(x, y, rw, rh); + height = height || rh; + frames.splice(i, 1); + dataFrames[frame.index] = [x, y, rw, rh, img, Math.round(-rx + sc * source.regX - pad), Math.round(-ry + sc * source.regY - pad)]; + x += rw; + } + return { w: x, h: height }; + }; + + /** + * @method _endBuild + * @protected + **/ + p._endBuild = function () { + this.spriteSheet = new createjs.SpriteSheet(this._data); + this._data = null; + this.progress = 1; + this.dispatchEvent("complete"); + }; + + /** + * @method _run + * @protected + **/ + p._run = function () { + var ts = Math.max(0.01, Math.min(0.99, this.timeSlice || 0.3)) * 50; + var t = new Date().getTime() + ts; + var complete = false; + while (t > new Date().getTime()) { + if (!this._drawNext()) { + complete = true;break; + } + } + if (complete) { + this._endBuild(); + } else { + var _this = this; + this._timerID = setTimeout(function () { + _this._run(); + }, 50 - ts); + } + var p = this.progress = this._index / this._frames.length; + if (this.hasEventListener("progress")) { + var evt = new createjs.Event("progress"); + evt.progress = p; + this.dispatchEvent(evt); + } + }; + + /** + * @method _drawNext + * @protected + * @return Boolean Returns false if this is the last draw. + **/ + p._drawNext = function () { + var frame = this._frames[this._index]; + var sc = frame.scale * this._scale; + var rect = frame.rect; + var sourceRect = frame.sourceRect; + var canvas = this._data.images[frame.img]; + var ctx = canvas.getContext("2d"); + frame.funct && frame.funct(frame.source, frame.data); + ctx.save(); + ctx.beginPath(); + ctx.rect(rect.x, rect.y, rect.width, rect.height); + ctx.clip(); + ctx.translate(Math.ceil(rect.x - sourceRect.x * sc), Math.ceil(rect.y - sourceRect.y * sc)); + ctx.scale(sc, sc); + frame.source.draw(ctx); // display object will draw itself. + ctx.restore(); + return ++this._index < this._frames.length; + }; + + createjs.SpriteSheetBuilder = createjs.promote(SpriteSheetBuilder, "EventDispatcher"); + })(); + + //############################################################################## + // DOMElement.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * This class is still experimental, and more advanced use is likely to be buggy. Please report bugs. + * + * A DOMElement allows you to associate a HTMLElement with the display list. It will be transformed + * within the DOM as though it is child of the {{#crossLink "Container"}}{{/crossLink}} it is added to. However, it is + * not rendered to canvas, and as such will retain whatever z-index it has relative to the canvas (ie. it will be + * drawn in front of or behind the canvas). + * + * The position of a DOMElement is relative to their parent node in the DOM. It is recommended that + * the DOM Object be added to a div that also contains the canvas so that they share the same position + * on the page. + * + * DOMElement is useful for positioning HTML elements over top of canvas content, and for elements + * that you want to display outside the bounds of the canvas. For example, a tooltip with rich HTML + * content. + * + *

    Mouse Interaction

    + * + * DOMElement instances are not full EaselJS display objects, and do not participate in EaselJS mouse + * events or support methods like hitTest. To get mouse events from a DOMElement, you must instead add handlers to + * the htmlElement (note, this does not support EventDispatcher) + * + * var domElement = new createjs.DOMElement(htmlElement); + * domElement.htmlElement.onclick = function() { + * console.log("clicked"); + * } + * + * @class DOMElement + * @extends DisplayObject + * @constructor + * @param {HTMLElement} htmlElement A reference or id for the DOM element to manage. + */ + + function DOMElement(htmlElement) { + this.DisplayObject_constructor(); + + if (typeof htmlElement == "string") { + htmlElement = document.getElementById(htmlElement); + } + this.mouseEnabled = false; + + var style = htmlElement.style; + style.position = "absolute"; + style.transformOrigin = style.WebkitTransformOrigin = style.msTransformOrigin = style.MozTransformOrigin = style.OTransformOrigin = "0% 0%"; + + // public properties: + /** + * The DOM object to manage. + * @property htmlElement + * @type HTMLElement + */ + this.htmlElement = htmlElement; + + // private properties: + /** + * @property _oldMtx + * @type Matrix2D + * @protected + */ + this._oldProps = null; + } + var p = createjs.extend(DOMElement, createjs.DisplayObject); + + // TODO: deprecated + // p.initialize = function() {}; // searchable for devs wondering where it is. REMOVED. See docs for details. + + + // public methods: + /** + * Returns true or false indicating whether the display object would be visible if drawn to a canvas. + * This does not account for whether it would be visible within the boundaries of the stage. + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method isVisible + * @return {Boolean} Boolean indicating whether the display object would be visible if drawn to a canvas + */ + p.isVisible = function () { + return this.htmlElement != null; + }; + + /** + * Draws the display object into the specified context ignoring its visible, alpha, shadow, and transform. + * Returns true if the draw was handled (useful for overriding functionality). + * NOTE: This method is mainly for internal use, though it may be useful for advanced uses. + * @method draw + * @param {CanvasRenderingContext2D} ctx The canvas 2D context object to draw into. + * @param {Boolean} ignoreCache Indicates whether the draw operation should ignore any current cache. + * For example, used for drawing the cache (to prevent it from simply drawing an existing cache back + * into itself). + * @return {Boolean} + */ + p.draw = function (ctx, ignoreCache) { + // this relies on the _tick method because draw isn't called if the parent is not visible. + // the actual update happens in _handleDrawEnd + return true; + }; + + /** + * Not applicable to DOMElement. + * @method cache + */ + p.cache = function () {}; + + /** + * Not applicable to DOMElement. + * @method uncache + */ + p.uncache = function () {}; + + /** + * Not applicable to DOMElement. + * @method updateCache + */ + p.updateCache = function () {}; + + /** + * Not applicable to DOMElement. + * @method hitTest + */ + p.hitTest = function () {}; + + /** + * Not applicable to DOMElement. + * @method localToGlobal + */ + p.localToGlobal = function () {}; + + /** + * Not applicable to DOMElement. + * @method globalToLocal + */ + p.globalToLocal = function () {}; + + /** + * Not applicable to DOMElement. + * @method localToLocal + */ + p.localToLocal = function () {}; + + /** + * DOMElement cannot be cloned. Throws an error. + * @method clone + */ + p.clone = function () { + throw "DOMElement cannot be cloned."; + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + */ + p.toString = function () { + return "[DOMElement (name=" + this.name + ")]"; + }; + + /** + * Interaction events should be added to `htmlElement`, and not the DOMElement instance, since DOMElement instances + * are not full EaselJS display objects and do not participate in EaselJS mouse events. + * @event click + */ + + /** + * Interaction events should be added to `htmlElement`, and not the DOMElement instance, since DOMElement instances + * are not full EaselJS display objects and do not participate in EaselJS mouse events. + * @event dblClick + */ + + /** + * Interaction events should be added to `htmlElement`, and not the DOMElement instance, since DOMElement instances + * are not full EaselJS display objects and do not participate in EaselJS mouse events. + * @event mousedown + */ + + /** + * The HTMLElement can listen for the mouseover event, not the DOMElement instance. + * Since DOMElement instances are not full EaselJS display objects and do not participate in EaselJS mouse events. + * @event mouseover + */ + + /** + * Not applicable to DOMElement. + * @event tick + */ + + // private methods: + /** + * @method _tick + * @param {Object} evtObj An event object that will be dispatched to all tick listeners. This object is reused between dispatchers to reduce construction & GC costs. + * function. + * @protected + */ + p._tick = function (evtObj) { + var stage = this.getStage(); + stage && stage.on("drawend", this._handleDrawEnd, this, true); + this.DisplayObject__tick(evtObj); + }; + + /** + * @method _handleDrawEnd + * @param {Event} evt + * @protected + */ + p._handleDrawEnd = function (evt) { + var o = this.htmlElement; + if (!o) { + return; + } + var style = o.style; + + var props = this.getConcatenatedDisplayProps(this._props), + mtx = props.matrix; + + var visibility = props.visible ? "visible" : "hidden"; + if (visibility != style.visibility) { + style.visibility = visibility; + } + if (!props.visible) { + return; + } + + var oldProps = this._oldProps, + oldMtx = oldProps && oldProps.matrix; + var n = 10000; // precision + + if (!oldMtx || !oldMtx.equals(mtx)) { + var str = "matrix(" + (mtx.a * n | 0) / n + "," + (mtx.b * n | 0) / n + "," + (mtx.c * n | 0) / n + "," + (mtx.d * n | 0) / n + "," + (mtx.tx + 0.5 | 0); + style.transform = style.WebkitTransform = style.OTransform = style.msTransform = str + "," + (mtx.ty + 0.5 | 0) + ")"; + style.MozTransform = str + "px," + (mtx.ty + 0.5 | 0) + "px)"; + if (!oldProps) { + oldProps = this._oldProps = new createjs.DisplayProps(true, NaN); + } + oldProps.matrix.copy(mtx); + } + + if (oldProps.alpha != props.alpha) { + style.opacity = "" + (props.alpha * n | 0) / n; + oldProps.alpha = props.alpha; + } + }; + + createjs.DOMElement = createjs.promote(DOMElement, "DisplayObject"); + })(); + + //############################################################################## + // Filter.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Base class that all filters should inherit from. Filters need to be applied to objects that have been cached using + * the {{#crossLink "DisplayObject/cache"}}{{/crossLink}} method. If an object changes, please cache it again, or use + * {{#crossLink "DisplayObject/updateCache"}}{{/crossLink}}. Note that the filters must be applied before caching. + * + *

    Example

    + * + * myInstance.filters = [ + * new createjs.ColorFilter(0, 0, 0, 1, 255, 0, 0), + * new createjs.BlurFilter(5, 5, 10) + * ]; + * myInstance.cache(0,0, 100, 100); + * + * Note that each filter can implement a {{#crossLink "Filter/getBounds"}}{{/crossLink}} method, which returns the + * margins that need to be applied in order to fully display the filter. For example, the {{#crossLink "BlurFilter"}}{{/crossLink}} + * will cause an object to feather outwards, resulting in a margin around the shape. + * + *

    EaselJS Filters

    + * EaselJS comes with a number of pre-built filters: + *
    • {{#crossLink "AlphaMapFilter"}}{{/crossLink}} : Map a greyscale image to the alpha channel of a display object
    • + *
    • {{#crossLink "AlphaMaskFilter"}}{{/crossLink}}: Map an image's alpha channel to the alpha channel of a display object
    • + *
    • {{#crossLink "BlurFilter"}}{{/crossLink}}: Apply vertical and horizontal blur to a display object
    • + *
    • {{#crossLink "ColorFilter"}}{{/crossLink}}: Color transform a display object
    • + *
    • {{#crossLink "ColorMatrixFilter"}}{{/crossLink}}: Transform an image using a {{#crossLink "ColorMatrix"}}{{/crossLink}}
    • + *
    + * + * @class Filter + * @constructor + **/ + + function Filter() {} + var p = Filter.prototype; + + /** + * REMOVED. Removed in favor of using `MySuperClass_constructor`. + * See {{#crossLink "Utility Methods/extend"}}{{/crossLink}} and {{#crossLink "Utility Methods/promote"}}{{/crossLink}} + * for details. + * + * There is an inheritance tutorial distributed with EaselJS in /tutorials/Inheritance. + * + * @method initialize + * @protected + * @deprecated + */ + // p.initialize = function() {}; // searchable for devs wondering where it is. + + + // public methods: + /** + * Provides padding values for this filter. That is, how much the filter will extend the visual bounds of an object it is applied to. + * @method getBounds + * @param {Rectangle} [rect] If specified, the provided Rectangle instance will be expanded by the padding amounts and returned. + * @return {Rectangle} If a `rect` param was provided, it is returned. If not, either a new rectangle with the padding values, or null if no padding is required for this filter. + **/ + p.getBounds = function (rect) { + return rect; + }; + + /** + * Applies the filter to the specified context. + * @method applyFilter + * @param {CanvasRenderingContext2D} ctx The 2D context to use as the source. + * @param {Number} x The x position to use for the source rect. + * @param {Number} y The y position to use for the source rect. + * @param {Number} width The width to use for the source rect. + * @param {Number} height The height to use for the source rect. + * @param {CanvasRenderingContext2D} [targetCtx] The 2D context to draw the result to. Defaults to the context passed to ctx. + * @param {Number} [targetX] The x position to draw the result to. Defaults to the value passed to x. + * @param {Number} [targetY] The y position to draw the result to. Defaults to the value passed to y. + * @return {Boolean} If the filter was applied successfully. + **/ + p.applyFilter = function (ctx, x, y, width, height, targetCtx, targetX, targetY) { + // this is the default behaviour because most filters access pixel data. It is overridden when not needed. + targetCtx = targetCtx || ctx; + if (targetX == null) { + targetX = x; + } + if (targetY == null) { + targetY = y; + } + try { + var imageData = ctx.getImageData(x, y, width, height); + } catch (e) { + return false; + } + if (this._applyFilter(imageData)) { + targetCtx.putImageData(imageData, targetX, targetY); + return true; + } + return false; + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[Filter]"; + }; + + /** + * Returns a clone of this Filter instance. + * @method clone + * @return {Filter} A clone of the current Filter instance. + **/ + p.clone = function () { + return new Filter(); + }; + + // private methods: + /** + * @method _applyFilter + * @param {ImageData} imageData Target ImageData instance. + * @return {Boolean} + **/ + p._applyFilter = function (imageData) { + return true; + }; + + createjs.Filter = Filter; + })(); + + //############################################################################## + // BlurFilter.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Applies a box blur to DisplayObjects. Note that this filter is fairly CPU intensive, particularly if the quality is + * set higher than 1. + * + *

    Example

    + * This example creates a red circle, and then applies a 5 pixel blur to it. It uses the {{#crossLink "Filter/getBounds"}}{{/crossLink}} + * method to account for the spread that the blur causes. + * + * var shape = new createjs.Shape().set({x:100,y:100}); + * shape.graphics.beginFill("#ff0000").drawCircle(0,0,50); + * + * var blurFilter = new createjs.BlurFilter(5, 5, 1); + * shape.filters = [blurFilter]; + * var bounds = blurFilter.getBounds(); + * + * shape.cache(-50+bounds.x, -50+bounds.y, 100+bounds.width, 100+bounds.height); + * + * See {{#crossLink "Filter"}}{{/crossLink}} for an more information on applying filters. + * @class BlurFilter + * @extends Filter + * @constructor + * @param {Number} [blurX=0] The horizontal blur radius in pixels. + * @param {Number} [blurY=0] The vertical blur radius in pixels. + * @param {Number} [quality=1] The number of blur iterations. + **/ + + function BlurFilter(blurX, blurY, quality) { + if (isNaN(blurX) || blurX < 0) blurX = 0; + if (isNaN(blurY) || blurY < 0) blurY = 0; + if (isNaN(quality) || quality < 1) quality = 1; + + // public properties: + /** + * Horizontal blur radius in pixels + * @property blurX + * @default 0 + * @type Number + **/ + this.blurX = blurX | 0; + + /** + * Vertical blur radius in pixels + * @property blurY + * @default 0 + * @type Number + **/ + this.blurY = blurY | 0; + + /** + * Number of blur iterations. For example, a value of 1 will produce a rough blur. A value of 2 will produce a + * smoother blur, but take twice as long to run. + * @property quality + * @default 1 + * @type Number + **/ + this.quality = quality | 0; + } + var p = createjs.extend(BlurFilter, createjs.Filter); + + // TODO: deprecated + // p.initialize = function() {}; // searchable for devs wondering where it is. REMOVED. See docs for details. + + + // constants: + /** + * Array of multiply values for blur calculations. + * @property MUL_TABLE + * @type Array + * @protected + * @static + **/ + BlurFilter.MUL_TABLE = [1, 171, 205, 293, 57, 373, 79, 137, 241, 27, 391, 357, 41, 19, 283, 265, 497, 469, 443, 421, 25, 191, 365, 349, 335, 161, 155, 149, 9, 278, 269, 261, 505, 245, 475, 231, 449, 437, 213, 415, 405, 395, 193, 377, 369, 361, 353, 345, 169, 331, 325, 319, 313, 307, 301, 37, 145, 285, 281, 69, 271, 267, 263, 259, 509, 501, 493, 243, 479, 118, 465, 459, 113, 446, 55, 435, 429, 423, 209, 413, 51, 403, 199, 393, 97, 3, 379, 375, 371, 367, 363, 359, 355, 351, 347, 43, 85, 337, 333, 165, 327, 323, 5, 317, 157, 311, 77, 305, 303, 75, 297, 294, 73, 289, 287, 71, 141, 279, 277, 275, 68, 135, 67, 133, 33, 262, 260, 129, 511, 507, 503, 499, 495, 491, 61, 121, 481, 477, 237, 235, 467, 232, 115, 457, 227, 451, 7, 445, 221, 439, 218, 433, 215, 427, 425, 211, 419, 417, 207, 411, 409, 203, 202, 401, 399, 396, 197, 49, 389, 387, 385, 383, 95, 189, 47, 187, 93, 185, 23, 183, 91, 181, 45, 179, 89, 177, 11, 175, 87, 173, 345, 343, 341, 339, 337, 21, 167, 83, 331, 329, 327, 163, 81, 323, 321, 319, 159, 79, 315, 313, 39, 155, 309, 307, 153, 305, 303, 151, 75, 299, 149, 37, 295, 147, 73, 291, 145, 289, 287, 143, 285, 71, 141, 281, 35, 279, 139, 69, 275, 137, 273, 17, 271, 135, 269, 267, 133, 265, 33, 263, 131, 261, 130, 259, 129, 257, 1]; + + /** + * Array of shift values for blur calculations. + * @property SHG_TABLE + * @type Array + * @protected + * @static + **/ + BlurFilter.SHG_TABLE = [0, 9, 10, 11, 9, 12, 10, 11, 12, 9, 13, 13, 10, 9, 13, 13, 14, 14, 14, 14, 10, 13, 14, 14, 14, 13, 13, 13, 9, 14, 14, 14, 15, 14, 15, 14, 15, 15, 14, 15, 15, 15, 14, 15, 15, 15, 15, 15, 14, 15, 15, 15, 15, 15, 15, 12, 14, 15, 15, 13, 15, 15, 15, 15, 16, 16, 16, 15, 16, 14, 16, 16, 14, 16, 13, 16, 16, 16, 15, 16, 13, 16, 15, 16, 14, 9, 16, 16, 16, 16, 16, 16, 16, 16, 16, 13, 14, 16, 16, 15, 16, 16, 10, 16, 15, 16, 14, 16, 16, 14, 16, 16, 14, 16, 16, 14, 15, 16, 16, 16, 14, 15, 14, 15, 13, 16, 16, 15, 17, 17, 17, 17, 17, 17, 14, 15, 17, 17, 16, 16, 17, 16, 15, 17, 16, 17, 11, 17, 16, 17, 16, 17, 16, 17, 17, 16, 17, 17, 16, 17, 17, 16, 16, 17, 17, 17, 16, 14, 17, 17, 17, 17, 15, 16, 14, 16, 15, 16, 13, 16, 15, 16, 14, 16, 15, 16, 12, 16, 15, 16, 17, 17, 17, 17, 17, 13, 16, 15, 17, 17, 17, 16, 15, 17, 17, 17, 16, 15, 17, 17, 14, 16, 17, 17, 16, 17, 17, 16, 15, 17, 16, 14, 17, 16, 15, 17, 16, 17, 17, 16, 17, 15, 16, 17, 14, 17, 16, 15, 17, 16, 17, 13, 17, 16, 17, 17, 16, 17, 14, 17, 16, 17, 16, 17, 16, 17, 9]; + + // public methods: + /** docced in super class **/ + p.getBounds = function (rect) { + var x = this.blurX | 0, + y = this.blurY | 0; + if (x <= 0 && y <= 0) { + return rect; + } + var q = Math.pow(this.quality, 0.2); + return (rect || new createjs.Rectangle()).pad(x * q + 1, y * q + 1, x * q + 1, y * q + 1); + }; + + /** docced in super class **/ + p.clone = function () { + return new BlurFilter(this.blurX, this.blurY, this.quality); + }; + + /** docced in super class **/ + p.toString = function () { + return "[BlurFilter]"; + }; + + // private methods: + + /** docced in super class **/ + p._applyFilter = function (imageData) { + + var radiusX = this.blurX >> 1; + if (isNaN(radiusX) || radiusX < 0) return false; + var radiusY = this.blurY >> 1; + if (isNaN(radiusY) || radiusY < 0) return false; + if (radiusX == 0 && radiusY == 0) return false; + + var iterations = this.quality; + if (isNaN(iterations) || iterations < 1) iterations = 1; + iterations |= 0; + if (iterations > 3) iterations = 3; + if (iterations < 1) iterations = 1; + + var px = imageData.data; + var x = 0, + y = 0, + i = 0, + p = 0, + yp = 0, + yi = 0, + yw = 0, + r = 0, + g = 0, + b = 0, + a = 0, + pr = 0, + pg = 0, + pb = 0, + pa = 0; + + var divx = radiusX + radiusX + 1 | 0; + var divy = radiusY + radiusY + 1 | 0; + var w = imageData.width | 0; + var h = imageData.height | 0; + + var w1 = w - 1 | 0; + var h1 = h - 1 | 0; + var rxp1 = radiusX + 1 | 0; + var ryp1 = radiusY + 1 | 0; + + var ssx = { r: 0, b: 0, g: 0, a: 0 }; + var sx = ssx; + for (i = 1; i < divx; i++) { + sx = sx.n = { r: 0, b: 0, g: 0, a: 0 }; + } + sx.n = ssx; + + var ssy = { r: 0, b: 0, g: 0, a: 0 }; + var sy = ssy; + for (i = 1; i < divy; i++) { + sy = sy.n = { r: 0, b: 0, g: 0, a: 0 }; + } + sy.n = ssy; + + var si = null; + + var mtx = BlurFilter.MUL_TABLE[radiusX] | 0; + var stx = BlurFilter.SHG_TABLE[radiusX] | 0; + var mty = BlurFilter.MUL_TABLE[radiusY] | 0; + var sty = BlurFilter.SHG_TABLE[radiusY] | 0; + + while (iterations-- > 0) { + + yw = yi = 0; + var ms = mtx; + var ss = stx; + for (y = h; --y > -1;) { + r = rxp1 * (pr = px[yi | 0]); + g = rxp1 * (pg = px[yi + 1 | 0]); + b = rxp1 * (pb = px[yi + 2 | 0]); + a = rxp1 * (pa = px[yi + 3 | 0]); + + sx = ssx; + + for (i = rxp1; --i > -1;) { + sx.r = pr; + sx.g = pg; + sx.b = pb; + sx.a = pa; + sx = sx.n; + } + + for (i = 1; i < rxp1; i++) { + p = yi + ((w1 < i ? w1 : i) << 2) | 0; + r += sx.r = px[p]; + g += sx.g = px[p + 1]; + b += sx.b = px[p + 2]; + a += sx.a = px[p + 3]; + + sx = sx.n; + } + + si = ssx; + for (x = 0; x < w; x++) { + px[yi++] = r * ms >>> ss; + px[yi++] = g * ms >>> ss; + px[yi++] = b * ms >>> ss; + px[yi++] = a * ms >>> ss; + + p = yw + ((p = x + radiusX + 1) < w1 ? p : w1) << 2; + + r -= si.r - (si.r = px[p]); + g -= si.g - (si.g = px[p + 1]); + b -= si.b - (si.b = px[p + 2]); + a -= si.a - (si.a = px[p + 3]); + + si = si.n; + } + yw += w; + } + + ms = mty; + ss = sty; + for (x = 0; x < w; x++) { + yi = x << 2 | 0; + + r = ryp1 * (pr = px[yi]) | 0; + g = ryp1 * (pg = px[yi + 1 | 0]) | 0; + b = ryp1 * (pb = px[yi + 2 | 0]) | 0; + a = ryp1 * (pa = px[yi + 3 | 0]) | 0; + + sy = ssy; + for (i = 0; i < ryp1; i++) { + sy.r = pr; + sy.g = pg; + sy.b = pb; + sy.a = pa; + sy = sy.n; + } + + yp = w; + + for (i = 1; i <= radiusY; i++) { + yi = yp + x << 2; + + r += sy.r = px[yi]; + g += sy.g = px[yi + 1]; + b += sy.b = px[yi + 2]; + a += sy.a = px[yi + 3]; + + sy = sy.n; + + if (i < h1) { + yp += w; + } + } + + yi = x; + si = ssy; + if (iterations > 0) { + for (y = 0; y < h; y++) { + p = yi << 2; + px[p + 3] = pa = a * ms >>> ss; + if (pa > 0) { + px[p] = r * ms >>> ss; + px[p + 1] = g * ms >>> ss; + px[p + 2] = b * ms >>> ss; + } else { + px[p] = px[p + 1] = px[p + 2] = 0; + } + + p = x + ((p = y + ryp1) < h1 ? p : h1) * w << 2; + + r -= si.r - (si.r = px[p]); + g -= si.g - (si.g = px[p + 1]); + b -= si.b - (si.b = px[p + 2]); + a -= si.a - (si.a = px[p + 3]); + + si = si.n; + + yi += w; + } + } else { + for (y = 0; y < h; y++) { + p = yi << 2; + px[p + 3] = pa = a * ms >>> ss; + if (pa > 0) { + pa = 255 / pa; + px[p] = (r * ms >>> ss) * pa; + px[p + 1] = (g * ms >>> ss) * pa; + px[p + 2] = (b * ms >>> ss) * pa; + } else { + px[p] = px[p + 1] = px[p + 2] = 0; + } + + p = x + ((p = y + ryp1) < h1 ? p : h1) * w << 2; + + r -= si.r - (si.r = px[p]); + g -= si.g - (si.g = px[p + 1]); + b -= si.b - (si.b = px[p + 2]); + a -= si.a - (si.a = px[p + 3]); + + si = si.n; + + yi += w; + } + } + } + } + return true; + }; + + createjs.BlurFilter = createjs.promote(BlurFilter, "Filter"); + })(); + + //############################################################################## + // AlphaMapFilter.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Applies a greyscale alpha map image (or canvas) to the target, such that the alpha channel of the result will + * be copied from the red channel of the map, and the RGB channels will be copied from the target. + * + * Generally, it is recommended that you use {{#crossLink "AlphaMaskFilter"}}{{/crossLink}}, because it has much + * better performance. + * + *

    Example

    + * This example draws a red->blue box, caches it, and then uses the cache canvas as an alpha map on a 100x100 image. + * + * var box = new createjs.Shape(); + * box.graphics.beginLinearGradientFill(["#ff0000", "#0000ff"], [0, 1], 0, 0, 0, 100) + * box.graphics.drawRect(0, 0, 100, 100); + * box.cache(0, 0, 100, 100); + * + * var bmp = new createjs.Bitmap("path/to/image.jpg"); + * bmp.filters = [ + * new createjs.AlphaMapFilter(box.cacheCanvas) + * ]; + * bmp.cache(0, 0, 100, 100); + * stage.addChild(bmp); + * + * See {{#crossLink "Filter"}}{{/crossLink}} for more information on applying filters. + * @class AlphaMapFilter + * @extends Filter + * @constructor + * @param {HTMLImageElement|HTMLCanvasElement} alphaMap The greyscale image (or canvas) to use as the alpha value for the + * result. This should be exactly the same dimensions as the target. + **/ + + function AlphaMapFilter(alphaMap) { + + // public properties: + /** + * The greyscale image (or canvas) to use as the alpha value for the result. This should be exactly the same + * dimensions as the target. + * @property alphaMap + * @type HTMLImageElement|HTMLCanvasElement + **/ + this.alphaMap = alphaMap; + + // private properties: + /** + * @property _alphaMap + * @protected + * @type HTMLImageElement|HTMLCanvasElement + **/ + this._alphaMap = null; + + /** + * @property _mapData + * @protected + * @type Uint8ClampedArray + **/ + this._mapData = null; + } + var p = createjs.extend(AlphaMapFilter, createjs.Filter); + + // TODO: deprecated + // p.initialize = function() {}; // searchable for devs wondering where it is. REMOVED. See docs for details. + + + // public methods: + /** docced in super class **/ + p.clone = function () { + var o = new AlphaMapFilter(this.alphaMap); + o._alphaMap = this._alphaMap; + o._mapData = this._mapData; + return o; + }; + + /** docced in super class **/ + p.toString = function () { + return "[AlphaMapFilter]"; + }; + + // private methods: + /** docced in super class **/ + p._applyFilter = function (imageData) { + if (!this.alphaMap) { + return true; + } + if (!this._prepAlphaMap()) { + return false; + } + + // TODO: update to support scenarios where the target has different dimensions. + var data = imageData.data; + var map = this._mapData; + for (var i = 0, l = data.length; i < l; i += 4) { + data[i + 3] = map[i] || 0; + } + + return true; + }; + + /** + * @method _prepAlphaMap + * @protected + **/ + p._prepAlphaMap = function () { + if (!this.alphaMap) { + return false; + } + if (this.alphaMap == this._alphaMap && this._mapData) { + return true; + } + + this._mapData = null; + var map = this._alphaMap = this.alphaMap; + var canvas = map; + var ctx; + if (map instanceof HTMLCanvasElement) { + ctx = canvas.getContext("2d"); + } else { + canvas = createjs.createCanvas ? createjs.createCanvas() : document.createElement("canvas"); + canvas.width = map.width; + canvas.height = map.height; + ctx = canvas.getContext("2d"); + ctx.drawImage(map, 0, 0); + } + + try { + var imgData = ctx.getImageData(0, 0, map.width, map.height); + } catch (e) { + //if (!this.suppressCrossDomainErrors) throw new Error("unable to access local image data: " + e); + return false; + } + + this._mapData = imgData.data; + return true; + }; + + createjs.AlphaMapFilter = createjs.promote(AlphaMapFilter, "Filter"); + })(); + + //############################################################################## + // AlphaMaskFilter.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Applies the alpha from the mask image (or canvas) to the target, such that the alpha channel of the result will + * be derived from the mask, and the RGB channels will be copied from the target. This can be used, for example, to + * apply an alpha mask to a display object. This can also be used to combine a JPG compressed RGB image with a PNG32 + * alpha mask, which can result in a much smaller file size than a single PNG32 containing ARGB. + * + * IMPORTANT NOTE: This filter currently does not support the targetCtx, or targetX/Y parameters correctly. + * + *

    Example

    + * This example draws a gradient box, then caches it and uses the "cacheCanvas" as the alpha mask on a 100x100 image. + * + * var box = new createjs.Shape(); + * box.graphics.beginLinearGradientFill(["#000000", "rgba(0, 0, 0, 0)"], [0, 1], 0, 0, 100, 100) + * box.graphics.drawRect(0, 0, 100, 100); + * box.cache(0, 0, 100, 100); + * + * var bmp = new createjs.Bitmap("path/to/image.jpg"); + * bmp.filters = [ + * new createjs.AlphaMaskFilter(box.cacheCanvas) + * ]; + * bmp.cache(0, 0, 100, 100); + * + * See {{#crossLink "Filter"}}{{/crossLink}} for more information on applying filters. + * @class AlphaMaskFilter + * @extends Filter + * @constructor + * @param {HTMLImageElement|HTMLCanvasElement} mask + **/ + + function AlphaMaskFilter(mask) { + + // public properties: + /** + * The image (or canvas) to use as the mask. + * @property mask + * @type HTMLImageElement|HTMLCanvasElement + **/ + this.mask = mask; + } + var p = createjs.extend(AlphaMaskFilter, createjs.Filter); + + // TODO: deprecated + // p.initialize = function() {}; // searchable for devs wondering where it is. REMOVED. See docs for details. + + + // public methods: + /** + * Applies the filter to the specified context. + * + * IMPORTANT NOTE: This filter currently does not support the targetCtx, or targetX/Y parameters + * correctly. + * @method applyFilter + * @param {CanvasRenderingContext2D} ctx The 2D context to use as the source. + * @param {Number} x The x position to use for the source rect. + * @param {Number} y The y position to use for the source rect. + * @param {Number} width The width to use for the source rect. + * @param {Number} height The height to use for the source rect. + * @param {CanvasRenderingContext2D} [targetCtx] NOT SUPPORTED IN THIS FILTER. The 2D context to draw the result to. Defaults to the context passed to ctx. + * @param {Number} [targetX] NOT SUPPORTED IN THIS FILTER. The x position to draw the result to. Defaults to the value passed to x. + * @param {Number} [targetY] NOT SUPPORTED IN THIS FILTER. The y position to draw the result to. Defaults to the value passed to y. + * @return {Boolean} If the filter was applied successfully. + **/ + p.applyFilter = function (ctx, x, y, width, height, targetCtx, targetX, targetY) { + if (!this.mask) { + return true; + } + targetCtx = targetCtx || ctx; + if (targetX == null) { + targetX = x; + } + if (targetY == null) { + targetY = y; + } + + targetCtx.save(); + if (ctx != targetCtx) { + // TODO: support targetCtx and targetX/Y + // clearRect, then draw the ctx in? + return false; + } + + targetCtx.globalCompositeOperation = "destination-in"; + targetCtx.drawImage(this.mask, targetX, targetY); + targetCtx.restore(); + return true; + }; + + /** docced in super class **/ + p.clone = function () { + return new AlphaMaskFilter(this.mask); + }; + + /** docced in super class **/ + p.toString = function () { + return "[AlphaMaskFilter]"; + }; + + createjs.AlphaMaskFilter = createjs.promote(AlphaMaskFilter, "Filter"); + })(); + + //############################################################################## + // ColorFilter.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Applies a color transform to DisplayObjects. + * + *

    Example

    + * This example draws a red circle, and then transforms it to Blue. This is accomplished by multiplying all the channels + * to 0 (except alpha, which is set to 1), and then adding 255 to the blue channel. + * + * var shape = new createjs.Shape().set({x:100,y:100}); + * shape.graphics.beginFill("#ff0000").drawCircle(0,0,50); + * + * shape.filters = [ + * new createjs.ColorFilter(0,0,0,1, 0,0,255,0) + * ]; + * shape.cache(-50, -50, 100, 100); + * + * See {{#crossLink "Filter"}}{{/crossLink}} for an more information on applying filters. + * @class ColorFilter + * @param {Number} [redMultiplier=1] The amount to multiply against the red channel. This is a range between 0 and 1. + * @param {Number} [greenMultiplier=1] The amount to multiply against the green channel. This is a range between 0 and 1. + * @param {Number} [blueMultiplier=1] The amount to multiply against the blue channel. This is a range between 0 and 1. + * @param {Number} [alphaMultiplier=1] The amount to multiply against the alpha channel. This is a range between 0 and 1. + * @param {Number} [redOffset=0] The amount to add to the red channel after it has been multiplied. This is a range + * between -255 and 255. + * @param {Number} [greenOffset=0] The amount to add to the green channel after it has been multiplied. This is a range + * between -255 and 255. + * @param {Number} [blueOffset=0] The amount to add to the blue channel after it has been multiplied. This is a range + * between -255 and 255. + * @param {Number} [alphaOffset=0] The amount to add to the alpha channel after it has been multiplied. This is a range + * between -255 and 255. + * @constructor + * @extends Filter + **/ + + function ColorFilter(redMultiplier, greenMultiplier, blueMultiplier, alphaMultiplier, redOffset, greenOffset, blueOffset, alphaOffset) { + + // public properties: + /** + * Red channel multiplier. + * @property redMultiplier + * @type Number + **/ + this.redMultiplier = redMultiplier != null ? redMultiplier : 1; + + /** + * Green channel multiplier. + * @property greenMultiplier + * @type Number + **/ + this.greenMultiplier = greenMultiplier != null ? greenMultiplier : 1; + + /** + * Blue channel multiplier. + * @property blueMultiplier + * @type Number + **/ + this.blueMultiplier = blueMultiplier != null ? blueMultiplier : 1; + + /** + * Alpha channel multiplier. + * @property alphaMultiplier + * @type Number + **/ + this.alphaMultiplier = alphaMultiplier != null ? alphaMultiplier : 1; + + /** + * Red channel offset (added to value). + * @property redOffset + * @type Number + **/ + this.redOffset = redOffset || 0; + + /** + * Green channel offset (added to value). + * @property greenOffset + * @type Number + **/ + this.greenOffset = greenOffset || 0; + + /** + * Blue channel offset (added to value). + * @property blueOffset + * @type Number + **/ + this.blueOffset = blueOffset || 0; + + /** + * Alpha channel offset (added to value). + * @property alphaOffset + * @type Number + **/ + this.alphaOffset = alphaOffset || 0; + } + var p = createjs.extend(ColorFilter, createjs.Filter); + + // TODO: deprecated + // p.initialize = function() {}; // searchable for devs wondering where it is. REMOVED. See docs for details. + + + // public methods: + /** docced in super class **/ + p.toString = function () { + return "[ColorFilter]"; + }; + + /** docced in super class **/ + p.clone = function () { + return new ColorFilter(this.redMultiplier, this.greenMultiplier, this.blueMultiplier, this.alphaMultiplier, this.redOffset, this.greenOffset, this.blueOffset, this.alphaOffset); + }; + + // private methods: + /** docced in super class **/ + p._applyFilter = function (imageData) { + var data = imageData.data; + var l = data.length; + for (var i = 0; i < l; i += 4) { + data[i] = data[i] * this.redMultiplier + this.redOffset; + data[i + 1] = data[i + 1] * this.greenMultiplier + this.greenOffset; + data[i + 2] = data[i + 2] * this.blueMultiplier + this.blueOffset; + data[i + 3] = data[i + 3] * this.alphaMultiplier + this.alphaOffset; + } + return true; + }; + + createjs.ColorFilter = createjs.promote(ColorFilter, "Filter"); + })(); + + //############################################################################## + // ColorMatrix.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Provides helper functions for assembling a matrix for use with the {{#crossLink "ColorMatrixFilter"}}{{/crossLink}}. + * Most methods return the instance to facilitate chained calls. + * + *

    Example

    + * + * myColorMatrix.adjustHue(20).adjustBrightness(50); + * + * See {{#crossLink "Filter"}}{{/crossLink}} for an example of how to apply filters, or {{#crossLink "ColorMatrixFilter"}}{{/crossLink}} + * for an example of how to use ColorMatrix to change a DisplayObject's color. + * @class ColorMatrix + * @param {Number} brightness + * @param {Number} contrast + * @param {Number} saturation + * @param {Number} hue + * @constructor + **/ + + function ColorMatrix(brightness, contrast, saturation, hue) { + this.setColor(brightness, contrast, saturation, hue); + } + var p = ColorMatrix.prototype; + + /** + * REMOVED. Removed in favor of using `MySuperClass_constructor`. + * See {{#crossLink "Utility Methods/extend"}}{{/crossLink}} and {{#crossLink "Utility Methods/promote"}}{{/crossLink}} + * for details. + * + * There is an inheritance tutorial distributed with EaselJS in /tutorials/Inheritance. + * + * @method initialize + * @protected + * @deprecated + */ + // p.initialize = function() {}; // searchable for devs wondering where it is. + + + // constants: + /** + * Array of delta values for contrast calculations. + * @property DELTA_INDEX + * @type Array + * @protected + * @static + **/ + ColorMatrix.DELTA_INDEX = [0, 0.01, 0.02, 0.04, 0.05, 0.06, 0.07, 0.08, 0.1, 0.11, 0.12, 0.14, 0.15, 0.16, 0.17, 0.18, 0.20, 0.21, 0.22, 0.24, 0.25, 0.27, 0.28, 0.30, 0.32, 0.34, 0.36, 0.38, 0.40, 0.42, 0.44, 0.46, 0.48, 0.5, 0.53, 0.56, 0.59, 0.62, 0.65, 0.68, 0.71, 0.74, 0.77, 0.80, 0.83, 0.86, 0.89, 0.92, 0.95, 0.98, 1.0, 1.06, 1.12, 1.18, 1.24, 1.30, 1.36, 1.42, 1.48, 1.54, 1.60, 1.66, 1.72, 1.78, 1.84, 1.90, 1.96, 2.0, 2.12, 2.25, 2.37, 2.50, 2.62, 2.75, 2.87, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.3, 4.7, 4.9, 5.0, 5.5, 6.0, 6.5, 6.8, 7.0, 7.3, 7.5, 7.8, 8.0, 8.4, 8.7, 9.0, 9.4, 9.6, 9.8, 10.0]; + + /** + * Identity matrix values. + * @property IDENTITY_MATRIX + * @type Array + * @protected + * @static + **/ + ColorMatrix.IDENTITY_MATRIX = [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1]; + + /** + * The constant length of a color matrix. + * @property LENGTH + * @type Number + * @protected + * @static + **/ + ColorMatrix.LENGTH = ColorMatrix.IDENTITY_MATRIX.length; + + // public methods: + /** + * Resets the instance with the specified values. + * @method setColor + * @param {Number} brightness + * @param {Number} contrast + * @param {Number} saturation + * @param {Number} hue + * @return {ColorMatrix} The ColorMatrix instance the method is called on (useful for chaining calls.) + * @chainable + */ + p.setColor = function (brightness, contrast, saturation, hue) { + return this.reset().adjustColor(brightness, contrast, saturation, hue); + }; + + /** + * Resets the matrix to identity values. + * @method reset + * @return {ColorMatrix} The ColorMatrix instance the method is called on (useful for chaining calls.) + * @chainable + */ + p.reset = function () { + return this.copy(ColorMatrix.IDENTITY_MATRIX); + }; + + /** + * Shortcut method to adjust brightness, contrast, saturation and hue. + * Equivalent to calling adjustHue(hue), adjustContrast(contrast), + * adjustBrightness(brightness), adjustSaturation(saturation), in that order. + * @method adjustColor + * @param {Number} brightness + * @param {Number} contrast + * @param {Number} saturation + * @param {Number} hue + * @return {ColorMatrix} The ColorMatrix instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.adjustColor = function (brightness, contrast, saturation, hue) { + this.adjustHue(hue); + this.adjustContrast(contrast); + this.adjustBrightness(brightness); + return this.adjustSaturation(saturation); + }; + + /** + * Adjusts the brightness of pixel color by adding the specified value to the red, green and blue channels. + * Positive values will make the image brighter, negative values will make it darker. + * @method adjustBrightness + * @param {Number} value A value between -255 & 255 that will be added to the RGB channels. + * @return {ColorMatrix} The ColorMatrix instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.adjustBrightness = function (value) { + if (value == 0 || isNaN(value)) { + return this; + } + value = this._cleanValue(value, 255); + this._multiplyMatrix([1, 0, 0, 0, value, 0, 1, 0, 0, value, 0, 0, 1, 0, value, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1]); + return this; + }; + + /** + * Adjusts the contrast of pixel color. + * Positive values will increase contrast, negative values will decrease contrast. + * @method adjustContrast + * @param {Number} value A value between -100 & 100. + * @return {ColorMatrix} The ColorMatrix instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.adjustContrast = function (value) { + if (value == 0 || isNaN(value)) { + return this; + } + value = this._cleanValue(value, 100); + var x; + if (value < 0) { + x = 127 + value / 100 * 127; + } else { + x = value % 1; + if (x == 0) { + x = ColorMatrix.DELTA_INDEX[value]; + } else { + x = ColorMatrix.DELTA_INDEX[value << 0] * (1 - x) + ColorMatrix.DELTA_INDEX[(value << 0) + 1] * x; // use linear interpolation for more granularity. + } + x = x * 127 + 127; + } + this._multiplyMatrix([x / 127, 0, 0, 0, 0.5 * (127 - x), 0, x / 127, 0, 0, 0.5 * (127 - x), 0, 0, x / 127, 0, 0.5 * (127 - x), 0, 0, 0, 1, 0, 0, 0, 0, 0, 1]); + return this; + }; + + /** + * Adjusts the color saturation of the pixel. + * Positive values will increase saturation, negative values will decrease saturation (trend towards greyscale). + * @method adjustSaturation + * @param {Number} value A value between -100 & 100. + * @return {ColorMatrix} The ColorMatrix instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.adjustSaturation = function (value) { + if (value == 0 || isNaN(value)) { + return this; + } + value = this._cleanValue(value, 100); + var x = 1 + (value > 0 ? 3 * value / 100 : value / 100); + var lumR = 0.3086; + var lumG = 0.6094; + var lumB = 0.0820; + this._multiplyMatrix([lumR * (1 - x) + x, lumG * (1 - x), lumB * (1 - x), 0, 0, lumR * (1 - x), lumG * (1 - x) + x, lumB * (1 - x), 0, 0, lumR * (1 - x), lumG * (1 - x), lumB * (1 - x) + x, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1]); + return this; + }; + + /** + * Adjusts the hue of the pixel color. + * @method adjustHue + * @param {Number} value A value between -180 & 180. + * @return {ColorMatrix} The ColorMatrix instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.adjustHue = function (value) { + if (value == 0 || isNaN(value)) { + return this; + } + value = this._cleanValue(value, 180) / 180 * Math.PI; + var cosVal = Math.cos(value); + var sinVal = Math.sin(value); + var lumR = 0.213; + var lumG = 0.715; + var lumB = 0.072; + this._multiplyMatrix([lumR + cosVal * (1 - lumR) + sinVal * -lumR, lumG + cosVal * -lumG + sinVal * -lumG, lumB + cosVal * -lumB + sinVal * (1 - lumB), 0, 0, lumR + cosVal * -lumR + sinVal * 0.143, lumG + cosVal * (1 - lumG) + sinVal * 0.140, lumB + cosVal * -lumB + sinVal * -0.283, 0, 0, lumR + cosVal * -lumR + sinVal * -(1 - lumR), lumG + cosVal * -lumG + sinVal * lumG, lumB + cosVal * (1 - lumB) + sinVal * lumB, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1]); + return this; + }; + + /** + * Concatenates (multiplies) the specified matrix with this one. + * @method concat + * @param {Array} matrix An array or ColorMatrix instance. + * @return {ColorMatrix} The ColorMatrix instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.concat = function (matrix) { + matrix = this._fixMatrix(matrix); + if (matrix.length != ColorMatrix.LENGTH) { + return this; + } + this._multiplyMatrix(matrix); + return this; + }; + + /** + * Returns a clone of this ColorMatrix. + * @method clone + * @return {ColorMatrix} A clone of this ColorMatrix. + **/ + p.clone = function () { + return new ColorMatrix().copy(this); + }; + + /** + * Return a length 25 (5x5) array instance containing this matrix's values. + * @method toArray + * @return {Array} An array holding this matrix's values. + **/ + p.toArray = function () { + var arr = []; + for (var i = 0, l = ColorMatrix.LENGTH; i < l; i++) { + arr[i] = this[i]; + } + return arr; + }; + + /** + * Copy the specified matrix's values to this matrix. + * @method copy + * @param {Array} matrix An array or ColorMatrix instance. + * @return {ColorMatrix} The ColorMatrix instance the method is called on (useful for chaining calls.) + * @chainable + **/ + p.copy = function (matrix) { + var l = ColorMatrix.LENGTH; + for (var i = 0; i < l; i++) { + this[i] = matrix[i]; + } + return this; + }; + + /** + * Returns a string representation of this object. + * @method toString + * @return {String} a string representation of the instance. + **/ + p.toString = function () { + return "[ColorMatrix]"; + }; + + // private methods: + /** + * @method _multiplyMatrix + * @param {Array} matrix + * @protected + **/ + p._multiplyMatrix = function (matrix) { + var i, + j, + k, + col = []; + + for (i = 0; i < 5; i++) { + for (j = 0; j < 5; j++) { + col[j] = this[j + i * 5]; + } + for (j = 0; j < 5; j++) { + var val = 0; + for (k = 0; k < 5; k++) { + val += matrix[j + k * 5] * col[k]; + } + this[j + i * 5] = val; + } + } + }; + + /** + * Make sure values are within the specified range, hue has a limit of 180, brightness is 255, others are 100. + * @method _cleanValue + * @param {Number} value The raw number + * @param {Number} limit The maximum that the number can be. The minimum is the limit * -1. + * @protected + **/ + p._cleanValue = function (value, limit) { + return Math.min(limit, Math.max(-limit, value)); + }; + + /** + * Makes sure matrixes are 5x5 (25 long). + * @method _fixMatrix + * @param {Array} matrix + * @protected + **/ + p._fixMatrix = function (matrix) { + if (matrix instanceof ColorMatrix) { + matrix = matrix.toArray(); + } + if (matrix.length < ColorMatrix.LENGTH) { + matrix = matrix.slice(0, matrix.length).concat(ColorMatrix.IDENTITY_MATRIX.slice(matrix.length, ColorMatrix.LENGTH)); + } else if (matrix.length > ColorMatrix.LENGTH) { + matrix = matrix.slice(0, ColorMatrix.LENGTH); + } + return matrix; + }; + + createjs.ColorMatrix = ColorMatrix; + })(); + + //############################################################################## + // ColorMatrixFilter.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Allows you to carry out complex color operations such as modifying saturation, brightness, or inverting. See the + * {{#crossLink "ColorMatrix"}}{{/crossLink}} for more information on changing colors. For an easier color transform, + * consider the {{#crossLink "ColorFilter"}}{{/crossLink}}. + * + *

    Example

    + * This example creates a red circle, inverts its hue, and then saturates it to brighten it up. + * + * var shape = new createjs.Shape().set({x:100,y:100}); + * shape.graphics.beginFill("#ff0000").drawCircle(0,0,50); + * + * var matrix = new createjs.ColorMatrix().adjustHue(180).adjustSaturation(100); + * shape.filters = [ + * new createjs.ColorMatrixFilter(matrix) + * ]; + * + * shape.cache(-50, -50, 100, 100); + * + * See {{#crossLink "Filter"}}{{/crossLink}} for an more information on applying filters. + * @class ColorMatrixFilter + * @constructor + * @extends Filter + * @param {Array | ColorMatrix} matrix A 4x5 matrix describing the color operation to perform. See also the {{#crossLink "ColorMatrix"}}{{/crossLink}} + * class. + **/ + + function ColorMatrixFilter(matrix) { + + // public properties: + /** + * A 4x5 matrix describing the color operation to perform. See also the {{#crossLink "ColorMatrix"}}{{/crossLink}} + * @property matrix + * @type Array | ColorMatrix + **/ + this.matrix = matrix; + } + var p = createjs.extend(ColorMatrixFilter, createjs.Filter); + + // TODO: deprecated + // p.initialize = function() {}; // searchable for devs wondering where it is. REMOVED. See docs for details. + + + // public methods: + /** docced in super class **/ + p.toString = function () { + return "[ColorMatrixFilter]"; + }; + + /** docced in super class **/ + p.clone = function () { + return new ColorMatrixFilter(this.matrix); + }; + + // private methods: + /** docced in super class **/ + p._applyFilter = function (imageData) { + var data = imageData.data; + var l = data.length; + var r, g, b, a; + var mtx = this.matrix; + var m0 = mtx[0], + m1 = mtx[1], + m2 = mtx[2], + m3 = mtx[3], + m4 = mtx[4]; + var m5 = mtx[5], + m6 = mtx[6], + m7 = mtx[7], + m8 = mtx[8], + m9 = mtx[9]; + var m10 = mtx[10], + m11 = mtx[11], + m12 = mtx[12], + m13 = mtx[13], + m14 = mtx[14]; + var m15 = mtx[15], + m16 = mtx[16], + m17 = mtx[17], + m18 = mtx[18], + m19 = mtx[19]; + + for (var i = 0; i < l; i += 4) { + r = data[i]; + g = data[i + 1]; + b = data[i + 2]; + a = data[i + 3]; + data[i] = r * m0 + g * m1 + b * m2 + a * m3 + m4; // red + data[i + 1] = r * m5 + g * m6 + b * m7 + a * m8 + m9; // green + data[i + 2] = r * m10 + g * m11 + b * m12 + a * m13 + m14; // blue + data[i + 3] = r * m15 + g * m16 + b * m17 + a * m18 + m19; // alpha + } + return true; + }; + + createjs.ColorMatrixFilter = createjs.promote(ColorMatrixFilter, "Filter"); + })(); + + //############################################################################## + // Touch.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + // constructor: + /** + * Global utility for working with multi-touch enabled devices in EaselJS. Currently supports W3C Touch API (iOS and + * modern Android browser) and the Pointer API (IE), including ms-prefixed events in IE10, and unprefixed in IE11. + * + * Ensure that you {{#crossLink "Touch/disable"}}{{/crossLink}} touch when cleaning up your application. You do not have + * to check if touch is supported to enable it, as it will fail gracefully if it is not supported. + * + *

    Example

    + * + * var stage = new createjs.Stage("canvasId"); + * createjs.Touch.enable(stage); + * + * Note: It is important to disable Touch on a stage that you are no longer using: + * + * createjs.Touch.disable(stage); + * + * @class Touch + * @static + **/ + + function Touch() { + throw "Touch cannot be instantiated"; + } + + // public static methods: + /** + * Returns `true` if touch is supported in the current browser. + * @method isSupported + * @return {Boolean} Indicates whether touch is supported in the current browser. + * @static + **/ + Touch.isSupported = function () { + return !!('ontouchstart' in window || // iOS & Android + window.navigator['msPointerEnabled'] && window.navigator['msMaxTouchPoints'] > 0 // IE10 + || window.navigator['pointerEnabled'] && window.navigator['maxTouchPoints'] > 0); // IE11+ + }; + + /** + * Enables touch interaction for the specified EaselJS {{#crossLink "Stage"}}{{/crossLink}}. Currently supports iOS + * (and compatible browsers, such as modern Android browsers), and IE10/11. Supports both single touch and + * multi-touch modes. Extends the EaselJS {{#crossLink "MouseEvent"}}{{/crossLink}} model, but without support for + * double click or over/out events. See the MouseEvent {{#crossLink "MouseEvent/pointerId:property"}}{{/crossLink}} + * for more information. + * @method enable + * @param {Stage} stage The {{#crossLink "Stage"}}{{/crossLink}} to enable touch on. + * @param {Boolean} [singleTouch=false] If `true`, only a single touch will be active at a time. + * @param {Boolean} [allowDefault=false] If `true`, then default gesture actions (ex. scrolling, zooming) will be + * allowed when the user is interacting with the target canvas. + * @return {Boolean} Returns `true` if touch was successfully enabled on the target stage. + * @static + **/ + Touch.enable = function (stage, singleTouch, allowDefault) { + if (!stage || !stage.canvas || !Touch.isSupported()) { + return false; + } + if (stage.__touch) { + return true; + } + + // inject required properties on stage: + stage.__touch = { pointers: {}, multitouch: !singleTouch, preventDefault: !allowDefault, count: 0 }; + + // note that in the future we may need to disable the standard mouse event model before adding + // these to prevent duplicate calls. It doesn't seem to be an issue with iOS devices though. + if ('ontouchstart' in window) { + Touch._IOS_enable(stage); + } else if (window.navigator['msPointerEnabled'] || window.navigator["pointerEnabled"]) { + Touch._IE_enable(stage); + } + return true; + }; + + /** + * Removes all listeners that were set up when calling `Touch.enable()` on a stage. + * @method disable + * @param {Stage} stage The {{#crossLink "Stage"}}{{/crossLink}} to disable touch on. + * @static + **/ + Touch.disable = function (stage) { + if (!stage) { + return; + } + if ('ontouchstart' in window) { + Touch._IOS_disable(stage); + } else if (window.navigator['msPointerEnabled'] || window.navigator["pointerEnabled"]) { + Touch._IE_disable(stage); + } + + delete stage.__touch; + }; + + // Private static methods: + /** + * @method _IOS_enable + * @protected + * @param {Stage} stage + * @static + **/ + Touch._IOS_enable = function (stage) { + var canvas = stage.canvas; + var f = stage.__touch.f = function (e) { + Touch._IOS_handleEvent(stage, e); + }; + canvas.addEventListener("touchstart", f, false); + canvas.addEventListener("touchmove", f, false); + canvas.addEventListener("touchend", f, false); + canvas.addEventListener("touchcancel", f, false); + }; + + /** + * @method _IOS_disable + * @protected + * @param {Stage} stage + * @static + **/ + Touch._IOS_disable = function (stage) { + var canvas = stage.canvas; + if (!canvas) { + return; + } + var f = stage.__touch.f; + canvas.removeEventListener("touchstart", f, false); + canvas.removeEventListener("touchmove", f, false); + canvas.removeEventListener("touchend", f, false); + canvas.removeEventListener("touchcancel", f, false); + }; + + /** + * @method _IOS_handleEvent + * @param {Stage} stage + * @param {Object} e The event to handle + * @protected + * @static + **/ + Touch._IOS_handleEvent = function (stage, e) { + if (!stage) { + return; + } + if (stage.__touch.preventDefault) { + e.preventDefault && e.preventDefault(); + } + var touches = e.changedTouches; + var type = e.type; + for (var i = 0, l = touches.length; i < l; i++) { + var touch = touches[i]; + var id = touch.identifier; + if (touch.target != stage.canvas) { + continue; + } + + if (type == "touchstart") { + this._handleStart(stage, id, e, touch.pageX, touch.pageY); + } else if (type == "touchmove") { + this._handleMove(stage, id, e, touch.pageX, touch.pageY); + } else if (type == "touchend" || type == "touchcancel") { + this._handleEnd(stage, id, e); + } + } + }; + + /** + * @method _IE_enable + * @protected + * @param {Stage} stage + * @static + **/ + Touch._IE_enable = function (stage) { + var canvas = stage.canvas; + var f = stage.__touch.f = function (e) { + Touch._IE_handleEvent(stage, e); + }; + + if (window.navigator["pointerEnabled"] === undefined) { + canvas.addEventListener("MSPointerDown", f, false); + window.addEventListener("MSPointerMove", f, false); + window.addEventListener("MSPointerUp", f, false); + window.addEventListener("MSPointerCancel", f, false); + if (stage.__touch.preventDefault) { + canvas.style.msTouchAction = "none"; + } + } else { + canvas.addEventListener("pointerdown", f, false); + window.addEventListener("pointermove", f, false); + window.addEventListener("pointerup", f, false); + window.addEventListener("pointercancel", f, false); + if (stage.__touch.preventDefault) { + canvas.style.touchAction = "none"; + } + } + stage.__touch.activeIDs = {}; + }; + + /** + * @method _IE_disable + * @protected + * @param {Stage} stage + * @static + **/ + Touch._IE_disable = function (stage) { + var f = stage.__touch.f; + + if (window.navigator["pointerEnabled"] === undefined) { + window.removeEventListener("MSPointerMove", f, false); + window.removeEventListener("MSPointerUp", f, false); + window.removeEventListener("MSPointerCancel", f, false); + if (stage.canvas) { + stage.canvas.removeEventListener("MSPointerDown", f, false); + } + } else { + window.removeEventListener("pointermove", f, false); + window.removeEventListener("pointerup", f, false); + window.removeEventListener("pointercancel", f, false); + if (stage.canvas) { + stage.canvas.removeEventListener("pointerdown", f, false); + } + } + }; + + /** + * @method _IE_handleEvent + * @param {Stage} stage + * @param {Object} e The event to handle. + * @protected + * @static + **/ + Touch._IE_handleEvent = function (stage, e) { + if (!stage) { + return; + } + if (stage.__touch.preventDefault) { + e.preventDefault && e.preventDefault(); + } + var type = e.type; + var id = e.pointerId; + var ids = stage.__touch.activeIDs; + + if (type == "MSPointerDown" || type == "pointerdown") { + if (e.srcElement != stage.canvas) { + return; + } + ids[id] = true; + this._handleStart(stage, id, e, e.pageX, e.pageY); + } else if (ids[id]) { + // it's an id we're watching + if (type == "MSPointerMove" || type == "pointermove") { + this._handleMove(stage, id, e, e.pageX, e.pageY); + } else if (type == "MSPointerUp" || type == "MSPointerCancel" || type == "pointerup" || type == "pointercancel") { + delete ids[id]; + this._handleEnd(stage, id, e); + } + } + }; + + /** + * @method _handleStart + * @param {Stage} stage + * @param {String|Number} id + * @param {Object} e + * @param {Number} x + * @param {Number} y + * @protected + **/ + Touch._handleStart = function (stage, id, e, x, y) { + var props = stage.__touch; + if (!props.multitouch && props.count) { + return; + } + var ids = props.pointers; + if (ids[id]) { + return; + } + ids[id] = true; + props.count++; + stage._handlePointerDown(id, e, x, y); + }; + + /** + * @method _handleMove + * @param {Stage} stage + * @param {String|Number} id + * @param {Object} e + * @param {Number} x + * @param {Number} y + * @protected + **/ + Touch._handleMove = function (stage, id, e, x, y) { + if (!stage.__touch.pointers[id]) { + return; + } + stage._handlePointerMove(id, e, x, y); + }; + + /** + * @method _handleEnd + * @param {Stage} stage + * @param {String|Number} id + * @param {Object} e + * @protected + **/ + Touch._handleEnd = function (stage, id, e) { + // TODO: cancel should be handled differently for proper UI (ex. an up would trigger a click, a cancel would more closely resemble an out). + var props = stage.__touch; + var ids = props.pointers; + if (!ids[id]) { + return; + } + props.count--; + stage._handlePointerUp(id, e, true); + delete ids[id]; + }; + + createjs.Touch = Touch; + })(); + + //############################################################################## + // version.js + //############################################################################## + + this.createjs = this.createjs || {}; + + (function () { + "use strict"; + + /** + * Static class holding library specific information such as the version and buildDate of + * the library. + * @class EaselJS + **/ + + var s = createjs.EaselJS = createjs.EaselJS || {}; + + /** + * The version string for this release. + * @property version + * @type String + * @static + **/ + s.version = /*=version*/"0.8.2"; // injected by build process + + /** + * The build date for this release in UTC format. + * @property buildDate + * @type String + * @static + **/ + s.buildDate = /*=date*/"Thu, 26 Nov 2015 20:44:34 GMT"; // injected by build process + })(); + + /*** EXPORTS FROM exports-loader ***/ + module.exports = window.createjs; + }).call(window); + +/***/ }), +/* 3 */ +/***/ (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( "