diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..8c815de10 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitignore +.idea +**/*.pyc +**/*.swp +black-env +debian \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..b0b9cf043 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Installation method** +How did you install OpenWebRX? (Raspberry Pi SD card image, Debian / Ubuntu packages, Docker image, manually?) + +**Versions** +What version of OpenWebRX are you running? (Check on startup, or see `owrx/version.py`. If a `-dev` version is used, ideally state the commit the issue is appearing on) + +**Log messages** +Are there any relevant messages relating to the bug in the output / log of OpenWebRX? (On most installations, the log should be available using the command `sudo journalctl -u openwebrx`) + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..ebb33baa8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: General support request or other project-relasted question + url: https://groups.io/g/openwebrx + about: Request help on the community mailing list diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..c33cd0fdd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: feature +assignees: '' + +--- + +Before posting a new feature request, please check if a similar idea has already been listed +* on the issue tracker +* on the [OpenWebRX github project](https://github.com/users/jketterl/projects/1). + +In the latter case, please only proceed if you have additional information about the feature, and please let us know that there's already a card there. + +**Feature description** +Please describe in plain words what functionality you'd like to see in OpenWebRX, and why you think it's useful. + +**Target audience** +Please let us know if you think that this feature is of particular interest for a particular group of users (e.g. hams, SWLs, DXers, ...) diff --git a/.gitignore b/.gitignore index 6a211b7e9..8d33574fd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ -*.pyc -*.swp +**/*.pyc +**/*.swp tags +.idea +packages diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..a583fd0f2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,231 @@ +**unreleased** +- SDR device log messages are now available in the web configuration to simplify troubleshooting +- Added support for the MSK144 digimode +- Added support for decoding ADS-B with dump1090 +- Added support for decoding HFDL and VDL2 aircraft communications +- Added decoding of ISM band transmissions using rtl_433 +- Added support for decoding RDS data on WFM broadcasts using redsea decoder +- Added decoding for DAB broadcast stations using csdr-eti and dablin +- Added IPv6 support +- Added MQTT support +- New devices supported: + - Afedri SDR-Net + +**1.2.2** +- Fixed an over-the-air code injection vulnerability + +**1.2.1** +- FifiSDR support fixed (pipeline formats now line up correctly) +- Added "Device" input for FifiSDR devices for sound card selection + +**1.2.0** +- Major rewrite of all demodulation components to make use of the new csdr/pycsdr and digiham/pydigiham demodulator + modules +- Preliminary display of M17 callsign information +- New devices supported: + - Blade RF + +**1.1.0** +- Reworked most graphical elements as SVGs for faster loadtimes and crispier display on hi-dpi displays +- Updated pipelines to match changes in digiham +- Changed D-Star and NXDN integrations to use new decoders from digiham +- Added D-Star and NXDN metadata display + +**1.0.0** +- Introduced `squelch_auto_margin` config option that allows configuring the auto squelch level +- Removed `port` configuration option; `rtltcp_compat` takes the port number with the new connectors +- Added support for new WSJT-X modes FST4, FST4W (only available with WSJT-X 2.3) and Q65 (only avilable with + WSJT-X 2.4) +- Added support for demodulating M17 digital voice signals using m17-cxx-demod +- New reporting infrastructure, allowing WSPR and FST4W spots to be sent to wsprnet.org +- Add some basic filtering capabilities to the map +- New arguments to the `openwebrx` command-line to facilitate the administration of users (try `openwebrx admin`) +- Default bandwidth changes: + - "WFM" changed to 150kHz + - "Packet" (APRS) changed to 12.5kHz +- Configuration rework: + - New: fully web-based configuration interface + - System configuration parameters have been moved to a new, separate `openwebrx.conf` file + - Remaining parameters are now editable in the web configuration + - Existing `config_webrx.py` files will still be read, but changes made in the web configuration will be written to + a new storage system + - Added upload of avatar and panorama image via web configuration +- New devices supported: + - HPSDR devices (Hermes Lite 2) thanks to @jancona + - BBRF103 / RX666 / RX888 devices supported by libsddc + - R&S devices using the EB200 or Ammos protocols + +**0.20.3** +- Fix a compatibility issue with python versions <= 3.6 + +**0.20.2** +- Fix a security problem that allowed arbitrary commands to be executed on the receiver + ([See github issue #215](https://github.com/jketterl/openwebrx/issues/215)) + +**0.20.1** +- Remove broken OSM map fallback + +**0.20.0** +- Added the ability to sign multiple keys in a single request, thus enabling multiple users to claim a single receiver + on receiverbook.de +- Fixed file descriptor leaks to prevent "too many open files" errors +- Add new demodulator chain for FreeDV +- Added new HD audio streaming mode along with a new WFM demodulator +- Reworked AGC code for better results in AM, SSB and digital modes +- Added support for demodulation of "Digital Radio Mondiale" (DRM) broadcast using the "dream" decoder. +- New default waterfall color scheme +- Prototype of a continuous automatic waterfall calibration mode +- New devices supported: + - FunCube Dongle Pro+ (`"type": "fcdpp"`) + - Support for connections to rtl_tcp (`"type": "rtl_tcp"`) + +**0.19.1** +- Added ability to authenticate receivers with listing sites using "receiver id" tokens + +**0.19.0** +- Fix direwolf connection setup by implementing a retry loop +- Pass direct sampling mode changes for rtl_sdr_soapy to owrx_connector +- OSM maps instead of Google when google_maps_api_key is not set (thanks @jquagga) +- Improved logic to pass parameters to soapy devices. + - `rtl_sdr_soapy`: added support for `bias_tee` + - `sdrplay`: added support for `bias_tee`, `rf_notch` and `dab_notch` + - `airspy`: added support for `bitpack` +- Added support for Perseus-SDR devices, (thanks @amontefusco) +- Property System has been rewritten so that defaults on sdr behave as expected +- Waterfall range auto-adjustment now only takes the center 80% of the spectrum into account, which should work better + with SDRs that oversample or have rather flat filter curves towards the spectrum edges +- Bugfix for negative network usage +- FiFi SDR: prevent arecord from shutting down after 2GB of data has been sent +- Added support for bias tee control on rtl_sdr devices +- All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC +- `rtl_sdr` type now also supports the `direct_sampling` option +- Added decoding implementation for for digimode "JS8Call" + (requires an installation of [js8call](http://js8call.com/) and + [the js8py library](https://github.com/jketterl/js8py)) +- Reorganization of the frontend demodulator code +- Improve receiver load time by concatenating javascript assets +- Docker images migrated to Debian slim images; This was necessary to allow the use of function multiversioning in + csdr and owrx_connector to allow the images to run on a wider range of CPUs +- Docker containers have been updated to include the SDRplay driver version 3 +- HackRF support is now based on SoapyHackRF +- Removed sdr.hu server listing support since the site has been shut down +- Added support for Radioberry 2 Rasbperry Pi SDR Cape + +**0.18.0** +- Support for SoapyRemote + +**2020-02-08** +- Compression, resampling and filtering in the frontend have been rewritten in javascript, sdr.js has been removed +- Decoding of Pocsag modulation is now possible +- Removed the 3D waterfall since it had no real application and required ~1MB of javascript code to be downloaded +- Improved the frontend handling of the "too many users" scenario +- PSK63 digimode is now available (same decoding pipeline as PSK31, but with adopted parameters) +- The frequency can now be manipulated with the mousewheel, which should allow the user to tune more precise. The tuning + step size is determined by the digit the mouse cursor is hovering over. +- Clicking on the frequency now opens an input for direct frequency selection +- URL hashes have been fixed and improved: They are now updated automatically, so a shared URL will include frequency + and demodulator, which allows for improved sharing and linking. +- New daylight scheduler for background decoding, allows profiles to be selected by local sunrise / sunset times +- New devices supported: + - LimeSDR (`"type": "lime_sdr"`) + - PlutoSDR (`"type": "pluto_sdr"`) + - RTL_SDR via Soapy (`"type": "rtl_sdr_soapy"`) on special request to allow use of the direct sampling mode + +**2020-01-04** +- The [owrx_connector](https://github.com/jketterl/owrx_connector) is now the default way of communicating with sdr + devices. The old sdr types have been replaced, all `_connector` suffixes on the type must be removed! +- The sources have been refactored, making it a lot easier to add support for other devices +- SDR device failure handling has been improved, including user feedback +- New devices supported: + - FiFiSDR (`"type": "fifi_sdr"`) + +**2019-12-15** +- wsjt-x updated to 2.1.2 +- The rtl_tcp compatibility mode of the owrx_connector is now configurable using the `rtltcp_compat` flag + +**2019-12-10** +- added support for airspyhf devices (Airspy HF+ / Discovery) + +**2019-12-05** +- explicit device filter for soapy devices for multi-device setups + +**2019-12-03** +- compatibility fixes for safari browsers (ios and mac) + +**2019-11-24** +- There is now a new way to interface with SDR hardware, . + They talk directly to the hardware (no rtl_sdr / rx_sdr necessary) and offer I/Q data on a socket, just like nmux + did before. They additionally offer a control socket that allows openwebrx to control the SDR parameters directly, + without the need for repeated restarts. This allows for quicker profile changes, and also reduces the risk of your + SDR hardware from failing during the switchover. See `config_webrx.py` for further information and instructions. +- Offset tuning using the `lfo_offset` has been reworked in a way that `center_freq` has to be set to the frequency you + actually want to listen to. If you're using an `lfo_offset` already, you will probably need to change its sign. +- `initial_squelch_level` can now be set on each profile. +- As usual, plenty of fixes and improvements. + +**2019-10-27** +- Part of the frontend code has been reworked + - Audio buffer minimums have been completely stripped. As a result, you should get better latency. Unfortunately, + this also means there will be some skipping when audio starts. + - Now also supports AudioWorklets (for those browser that have it). The Raspberry Pi image has been updated to include + https due to the SecureContext requirement. + - Mousewheel controls for the receiver sliders +- Error handling for failed SDR devices + +**2019-09-29** +- One of the most-requested features is finally coming to OpenWebRX: Bookmarks (sometimes also referred to as labels). + There's two kinds of bookmarks available: + - Serverside bookmarks that are set up by the receiver administrator. Check the file `bookmarks.json` for examples! + - Clientside bookmarks which every user can store for themselves. They are stored in the browser's localStorage. +- Some more bugs in the websocket handling have been fixed. + +**2019-09-25** +- Automatic reporting of spots to [pskreporter](https://pskreporter.info/) is now possible. Please have a look at the + configuration on how to set it up. +- Websocket communication has been overhauled in large parts. It should now be more reliable, and failing connections + should now have no impact on other users. +- Profile scheduling allows to set up band-hopping if you are running background services. +- APRS now has the ability to show symbols on the map, if a corresponding symbol set has been installed. Check the + config! +- Debug logging has been disabled in a handful of modules, expect vastly reduced output on the shell. + +**2019-09-13** +- New set of APRS-related features + - Decode Packet transmissions using [direwolf](https://github.com/wb2osz/direwolf) (1k2 only for now) + - APRS packets are mostly decoded and shown both in a new panel and on the map + - APRS is also available as a background service + - direwolfs I-gate functionality can be enabled, which allows your receiver to work as a receive-only I-gate for the + APRS network in the background +- Demodulation for background services has been optimized to use less total bandwidth, saving CPU +- More metrics have been added; they can be used together with collectd and its curl_json plugin for now, with some + limitations. + +**2019-07-21** +- Latest Features: + - More WSJT-X modes have been added, including the new FT4 mode + - I started adding a bandplan feature, the first thing visible is the "dial" indicator that brings you right to the + dial frequency for digital modes + - fixed some bugs in the websocket communication which broke the map + +**2019-07-13** +- Latest Features: + - FT8 Integration (using wsjt-x demodulators) + - New Map Feature that shows both decoded grid squares from FT8 and Locations decoded from YSF digital voice + - New Feature report that will show what functionality is available +- There's a new Raspbian SD Card image available (see below) + +**2019-06-30** +- I have done some major rework on the openwebrx core, and I am planning to continue adding more features in the near + future. Please check this place for updates. +- My work has not been accepted into the upstream repository, so you will need to chose between my fork and the official + version. +- I have enabled the issue tracker on this project, so feel free to file bugs or suggest enhancements there! +- This version sports the following new and amazing features: + - Support of multiple SDR devices simultaneously + - Support for multiple profiles per SDR that allow the user to listen to different frequencies + - Support for digital voice decoding + - Feature detection that will disable functionality when dependencies are not available (if you're missing the digital + buttons, this is probably why) +- Raspbian SD Card Images and Docker builds available (see below) +- I am currently working on the feature set for a stable release, but you are more than welcome to test development + versions! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 79c1a91c4..000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,15 +0,0 @@ -First of all, thank you for taking the time to contribute to this project! - -Before I can accept your contributions, I need a signed copy of the Individual Contributor License Agreement (ICLA) from you, which is available here. - -The ICLA is needed because it will allow me to dual license the OpenWebRX project under AGPL and a commercial license. -I will also apply dual licensing to csdr, but only those parts that are original work (e.g. without the parts enabled by `-DUSE_IMA_ADPCM`; code taken from other projects is clearly separable). - -However, even if there is commercial interest in the projects, I promise to keep them as open as possible, keeping my original intention to provide an open-source web-based SDR receiver software to the amateur radio operators and SDR enthusiasts. - -This contributor agreement is based on the one of Apache Software Foundation, with some modifications. (You can review differences here). -When you contribute for the first time, I will send you the ICLA. Replying with only the information requested and the text "I Agree" is sufficient. - -Thanks, - -Andras, HA7ILM diff --git a/CONTRIBUTORS b/CONTRIBUTORS deleted file mode 100644 index 22cca21f2..000000000 --- a/CONTRIBUTORS +++ /dev/null @@ -1,5 +0,0 @@ -This is a list of the great people who contributed code to the OpenWebRX repository. (Names are sorted alphabetically.) - -Gnoxter -John Seamons, ZL/KF6VO - diff --git a/ICLA.txt b/ICLA.txt deleted file mode 100644 index d24e4a54d..000000000 --- a/ICLA.txt +++ /dev/null @@ -1,128 +0,0 @@ - Individual Contributor License Agreement ("Agreement") - -In order to clarify the intellectual property license granted -with Contributions from any person or entity, Retzler András -(hereinafter referred to as "Project Owner") must have a -Contributor License Agreement ("CLA") on file that has -been signed by each Contributor, indicating agreement to the license -terms below. This license is for your protection as a Contributor as -well as the protection of the Project Owner; it does not change your -rights to use your own Contributions for any other purpose. -Please read this document carefully before signing and keep a copy -for your records. - - Full name: ______________________________________________________ - - (optional) Public name: _________________________________________ - - Mailing Address: ________________________________________________ - - ________________________________________________ - - Country: ______________________________________________________ - - (optional) Telephone: ___________________________________________ - - E-Mail: ______________________________________________________ - -You accept and agree to the following terms and conditions for Your -present and future Contributions submitted to the Project Owner. - -Except for the license granted herein to the Project Owner and recipients -of software distributed by the Project Owner, You reserve all right, title, -and interest in and to Your Contributions. - -1. Definitions. - - "You" (or "Your") shall mean the copyright owner or legal entity - authorized by the copyright owner that is making this Agreement - with the Project Owner. For legal entities, the entity making a - Contribution and all other entities that control, are controlled - by, or are under common control with that entity are considered to - be a single Contributor. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "Contribution" shall mean any original work of authorship, - including any modifications or additions to an existing work, that - is intentionally submitted by You to the Project Owner for inclusion - in, or documentation of, any of the products owned or managed by - the Project Owner (the "Work"). For the purposes of this definition, - "submitted" means any form of electronic, verbal, or written - communication sent to the Project Owner or its representatives, - including but not limited to communication on electronic mailing - lists, source code control systems, and issue tracking systems that - are managed by, or on behalf of, the Project Owner for the purpose of - discussing and improving the Work, but excluding communication that - is conspicuously marked or otherwise designated in writing by You - as "Not a Contribution." - -2. Grant of Copyright License. Subject to the terms and conditions of - this Agreement, You hereby grant to the Project Owner and to - recipients of software distributed by the Project Owner a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare derivative works of, - publicly display, publicly perform, sublicense, and distribute Your - Contributions and such derivative works. - -3. Grant of Patent License. Subject to the terms and conditions of - this Agreement, You hereby grant to the Project Owner and to - recipients of software distributed by the Project Owner a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have - made, use, offer to sell, sell, import, and otherwise transfer the - Work, where such license applies only to those patent claims - licensable by You that are necessarily infringed by Your - Contribution(s) alone or by combination of Your Contribution(s) - with the Work to which such Contribution(s) was submitted. If any - entity institutes patent litigation against You or any other entity - (including a cross-claim or counterclaim in a lawsuit) alleging - that your Contribution, or the Work to which you have contributed, - constitutes direct or contributory patent infringement, then any - patent licenses granted to that entity under this Agreement for - that Contribution or Work shall terminate as of the date such - litigation is filed. - -4. You represent that you are legally entitled to grant the above - license. If your employer(s) has rights to intellectual property - that you create that includes your Contributions, you represent - that you have received permission to make Contributions on behalf - of that employer, that your employer has waived such rights for - your Contributions to the Project Owner, or that your employer has - executed a separate Corporate CLA with the Project Owner. - -5. You represent that each of Your Contributions is Your original - creation (see section 7 for submissions on behalf of others). You - represent that Your Contribution submissions include complete - details of any third-party license or other restriction (including, - but not limited to, related patents and trademarks) of which you - are personally aware and which are associated with any part of Your - Contributions. - -6. You are not expected to provide support for Your Contributions, - except to the extent You desire to provide support. You may provide - support for free, for a fee, or not at all. Unless required by - applicable law or agreed to in writing, You provide Your - Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS - OF ANY KIND, either express or implied, including, without - limitation, any warranties or conditions of TITLE, NON- - INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. - -7. Should You wish to submit work that is not Your original creation, - You may submit it to the Project Owner separately from any - Contribution, identifying the complete details of its source and of - any license or other restriction (including, but not limited to, - related patents, trademarks, and license agreements) of which you - are personally aware, and conspicuously marking the work as - "Submitted on behalf of a third-party: [named here]". - -8. You agree to notify the Project Owner of any facts or circumstances of - which you become aware that would make these representations - inaccurate in any respect. - -Please sign: __________________________________ Date: ________________ - -Text derived from the Apache Individual Contributor License Agreement -("Agreement") V2.0, available at http://apache.org/licenses/icla.txt diff --git a/README.md b/README.md index d308c36f7..f8ed6b87f 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,47 @@ OpenWebRX ========= -[:floppy_disk: Setup guide for Ubuntu](http://blog.sdr.hu/2015/06/30/quick-setup-openwebrx.html) | [:blue_book: Knowledge base on the Wiki](https://github.com/simonyiszk/openwebrx/wiki/) | [:earth_americas: Receivers on SDR.hu](http://sdr.hu/) - OpenWebRX is a multi-user SDR receiver software with a web interface. -![OpenWebRX](http://blog.sdr.hu/images/openwebrx/screenshot.png) +![OpenWebRX](https://www.openwebrx.de/gfx/openwebrx-screenshot.png) It has the following features: -- csdr based demodulators (AM/FM/SSB/CW/BPSK31), -- filter passband can be set from GUI, -- waterfall display can be shifted back in time, -- it extensively uses HTML5 features like WebSocket, Web Audio API, and <canvas>, -- it works in Google Chrome, Chromium (above version 37) and Mozilla Firefox (above version 28), -- currently supports RTL-SDR, HackRF, SDRplay, AirSpy and many other devices, see the OpenWebRX Wiki, -- it has a 3D waterfall display: - -![OpenWebRX 3D waterfall](http://blog.sdr.hu/images/openwebrx/screenshot-3d.gif) - -**News (2015-08-18)** -- My BSc. thesis written on OpenWebRX is available here. -- Several bugs were fixed to improve reliability and stability. -- OpenWebRX now supports compression of audio and waterfall stream, so the required network uplink bandwidth has been decreased from 2 Mbit/s to about 200 kbit/s per client! (Measured with the default settings. It is also dependent on `fft_size`.) -- OpenWebRX now uses sdr.js (*libcsdr* compiled to JavaScript) for some client-side DSP tasks. -- Receivers can now be listed on SDR.hu. -- License for OpenWebRX is now Affero GPL v3. - -**News (2016-02-14)** -- The DDC in *csdr* has been manually optimized for ARM NEON, so it runs around 3 times faster on the Raspberry Pi 2 than before. -- Also we use *ncat* instead of *rtl_mus*, and it is 3 times faster in some cases. -- OpenWebRX now supports URLs like: `http://localhost:8073/#freq=145555000,mod=usb` -- UI improvements were made, thanks to John Seamons and Gnoxter. - -**News (2017-04-04)** -- *ncat* has been replaced with a custom implementation called *nmux* due to a bug that caused regular crashes on some machines. The *nmux* tool is part of the *csdr* package. -- Most consumer SDR devices are supported via rx_tools, see the OpenWebRX Wiki on that. - -**News (2017-07-12)** -- OpenWebRX now has a BPSK31 demodulator and a 3D waterfall display. - -> When upgrading OpenWebRX, please make sure that you also upgrade *csdr*! - -## OpenWebRX servers on SDR.hu - -[SDR.hu](http://sdr.hu) is a site which lists the active, public OpenWebRX servers. Your receiver [can also be part of it](http://sdr.hu/openwebrx), if you want. - -![sdr.hu](http://blog.sdr.hu/images/openwebrx/screenshot-sdrhu.png) +- [csdr](https://github.com/jketterl/csdr) based demodulators (AM/FM/SSB/CW/BPSK31/BPSK63) +- filter passband can be set from GUI +- it extensively uses HTML5 features like WebSocket, Web Audio API, and Canvas +- it works in Google Chrome, Chromium and Mozilla Firefox +- supports a wide range of [SDR hardware](https://github.com/jketterl/openwebrx/wiki/Supported-Hardware#sdr-devices) +- Multiple SDR devices can be used simultaneously +- [digiham](https://github.com/jketterl/digiham) based demodularors (DMR, YSF, Pocsag, D-Star, NXDN) +- [wsjt-x](https://wsjt.sourceforge.io/) based demodulators (FT8, FT4, WSPR, JT65, JT9, FST4, + FST4W) +- [direwolf](https://github.com/wb2osz/direwolf) based demodulation of APRS packets +- [JS8Call](http://js8call.com/) support +- [DRM](https://github.com/jketterl/openwebrx/wiki/DRM-demodulator-notes) support +- [FreeDV](https://github.com/jketterl/openwebrx/wiki/FreeDV-demodulator-notes) support +- M17 support based on [m17-cxx-demod](https://github.com/mobilinkd/m17-cxx-demod) ## Setup -OpenWebRX currently requires Linux and python 2.7 to run. - -First you will need to install the dependencies: - -- libcsdr -- rtl-sdr - -After cloning this repository and connecting an RTL-SDR dongle to your computer, you can run the server: - - python openwebrx.py +The following methods of setting up a receiver are currently available: -You can now open the GUI at http://localhost:8073. +- Raspberry Pi SD card images +- Debian repository +- Docker images +- Manual installation -Please note that the server is also listening on the following ports (on localhost only): +Please checkout the [setup guide on the wiki](https://github.com/jketterl/openwebrx/wiki/Setup-Guide) for more details +on the respective methods. -- port 4951 for the multi-user I/Q server. +## Community -Now the next step is to customize the parameters of your server in `config_webrx.py`. +If you have trouble setting up or configuring your receiver, you have some great idea you want to see implemented, or +you just generally want to have some OpenWebRX-related chat, come visit us over on +[our groups.io group](https://groups.io/g/openwebrx). -Actually, if you do something cool with OpenWebRX, please drop me a mail: -*Andras Retzler, HA7ILM <randras@sdr.hu>* +If you want to hang out, chat, or get in touch directly with the developers, receiver operators or users, feel free to +drop by in [our Discord server](https://discord.gg/gnE9hPz). ## Usage tips @@ -80,16 +51,10 @@ The filter envelope can be dragged at its ends and moved around to set the passb However, if you hold down the shift key, you can drag the center line (BFO) or the whole passband (PBS). -## Setup tips - -If you have any problems installing OpenWebRX, you should check out the Wiki about it, which has a page on the common problems and their solutions. - -Sometimes the actual error message is not at the end of the terminal output, you may have to look at the whole output to find it. - -If you want to run OpenWebRX on a remote server instead of *localhost*, do not forget to set *server_hostname* in `config_webrx.py`. - ## Licensing -OpenWebRX is available under Affero GPL v3 license (summary). +OpenWebRX is available under Affero GPL v3 license +([summary](https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0))). -OpenWebRX is also available under a commercial license on request. Please contact me at the address *<randras@sdr.hu>* for licensing options. +OpenWebRX is also available under a commercial license on request. Please contact me at the address +*<randras@sdr.hu>* for licensing options. diff --git a/bands.json b/bands.json new file mode 100644 index 000000000..b2b8c4d04 --- /dev/null +++ b/bands.json @@ -0,0 +1,391 @@ +[ + { + "name": "2190m", + "lower_bound": 135700, + "upper_bound": 137800, + "frequencies": { + "fst4": 136000, + "fst4w": 136000, + "wspr": 136000 + }, + "tags": ["hamradio"] + }, + { + "name": "630m", + "lower_bound": 472000, + "upper_bound": 479000, + "frequencies": { + "fst4": 474200, + "fst4w": 474200, + "wspr": 474200 + }, + "tags": ["hamradio"] + }, + { + "name": "160m", + "lower_bound": 1810000, + "upper_bound": 2000000, + "frequencies": { + "bpsk31": 1838000, + "ft8": 1840000, + "wspr": 1836600, + "jt65": 1838000, + "jt9": 1839000, + "js8": 1842000, + "fst4": 1839000, + "fst4w": 1836800 + }, + "tags": ["hamradio"] + }, + { + "name": "80m", + "lower_bound": 3500000, + "upper_bound": 3800000, + "frequencies": { + "bpsk31": 3580000, + "ft8": 3573000, + "wspr": 3568600, + "jt65": 3570000, + "jt9": 3572000, + "ft4": [3568000, 3575000], + "js8": 3578000 + }, + "tags": ["hamradio"] + }, + { + "name": "60m", + "lower_bound": 5351500, + "upper_bound": 5366500, + "frequencies": { + "ft8": 5357000, + "wspr": [5287200, 5364700] + }, + "tags": ["hamradio"] + }, + { + "name": "40m", + "lower_bound": 7000000, + "upper_bound": 7200000, + "frequencies": { + "bpsk31": 7040000, + "ft8": 7074000, + "wspr": 7038600, + "jt65": 7076000, + "jt9": 7078000, + "ft4": 7047500, + "js8": 7078000 + }, + "tags": ["hamradio"] + }, + { + "name": "30m", + "lower_bound": 10100000, + "upper_bound": 10150000, + "frequencies": { + "bpsk31": 10141000, + "ft8": 10136000, + "wspr": 10138700, + "jt65": 10138000, + "jt9": 10140000, + "ft4": 10140000, + "js8": 10130000 + }, + "tags": ["hamradio"] + }, + { + "name": "20m", + "lower_bound": 14000000, + "upper_bound": 14350000, + "frequencies": { + "bpsk31": 14070000, + "ft8": 14074000, + "wspr": 14095600, + "jt65": 14076000, + "jt9": 14078000, + "ft4": 14080000, + "js8": 14078000 + }, + "tags": ["hamradio"] + }, + { + "name": "17m", + "lower_bound": 18068000, + "upper_bound": 18168000, + "frequencies": { + "bpsk31": 18098000, + "ft8": 18100000, + "wspr": 18104600, + "jt65": 18102000, + "jt9": 18104000, + "ft4": 18104000, + "js8": 18104000 + }, + "tags": ["hamradio"] + }, + { + "name": "15m", + "lower_bound": 21000000, + "upper_bound": 21450000, + "frequencies": { + "bpsk31": 21070000, + "ft8": 21074000, + "wspr": 21094600, + "jt65": 21076000, + "jt9": 21078000, + "ft4": 21140000, + "js8": 21078000 + }, + "tags": ["hamradio"] + }, + { + "name": "12m", + "lower_bound": 24890000, + "upper_bound": 24990000, + "frequencies": { + "bpsk31": 24920000, + "ft8": 24915000, + "wspr": 24924600, + "jt65": 24917000, + "jt9": 24919000, + "ft4": 24919000, + "js8": 24922000 + }, + "tags": ["hamradio"] + }, + { + "name": "10m", + "lower_bound": 28000000, + "upper_bound": 29700000, + "frequencies": { + "bpsk31": [28070000, 28120000], + "ft8": 28074000, + "wspr": 28124600, + "jt65": 28076000, + "jt9": 28078000, + "ft4": 28180000, + "js8": 28078000 + }, + "tags": ["hamradio"] + }, + { + "name": "6m", + "lower_bound": 50030000, + "upper_bound": 51000000, + "frequencies": { + "bpsk31": 50305000, + "ft8": 50313000, + "wspr": 50293000, + "jt65": 50310000, + "jt9": 50312000, + "ft4": 50318000, + "js8": 50318000, + "q65": [50211000, 50275000], + "msk144": 50260000 + }, + "tags": ["hamradio"] + }, + { + "name": "4m", + "lower_bound": 70150000, + "upper_bound": 70200000, + "frequencies": { + "wspr": 70091000, + "msk144": 70230000 + }, + "tags": ["hamradio"] + }, + { + "name": "2m", + "lower_bound": 144000000, + "upper_bound": 146000000, + "frequencies": { + "wspr": 144489000, + "ft8": 144174000, + "ft4": 144170000, + "jt65": 144120000, + "packet": 144800000, + "q65": 144116000, + "msk144": 144360000 + }, + "tags": ["hamradio"] + }, + { + "name": "70cm", + "lower_bound": 430000000, + "upper_bound": 440000000, + "frequencies": { + "pocsag": 439987500, + "q65": 432065000, + "msk144": 432360000 + }, + "tags": ["hamradio"] + }, + { + "name": "23cm", + "lower_bound": 1240000000, + "upper_bound": 1300000000, + "frequencies": { + "q65": 1296065000 + }, + "tags": ["hamradio"] + }, + { + "name": "13cm", + "lower_bound": 2320000000, + "upper_bound": 2450000000, + "frequencies": { + "q65": [2301065000, 2304065000, 2320065000] + }, + "tags": ["hamradio"] + }, + { + "name": "9cm", + "lower_bound": 3400000000, + "upper_bound": 3475000000, + "frequencies": { + "q65": 3400065000 + }, + "tags": ["hamradio"] + }, + { + "name": "6cm", + "lower_bound": 5650000000, + "upper_bound": 5850000000, + "frequencies": { + "q65": 5760200000 + }, + "tags": ["hamradio"] + }, + { + "name": "3cm", + "lower_bound": 10000000000, + "upper_bound": 10500000000, + "frequencies": { + "q65": 10368200000 + }, + "tags": ["hamradio"] + }, + { + "name": "120m Broadcast", + "lower_bound": 2300000, + "upper_bound": 2495000, + "tags": ["broadcast"] + }, + { + "name": "90m Broadcast", + "lower_bound": 3200000, + "upper_bound": 3400000, + "tags": ["broadcast"] + }, + { + "name": "75m Broadcast", + "lower_bound": 3900000, + "upper_bound": 4000000, + "tags": ["broadcast"] + }, + { + "name": "60m Broadcast", + "lower_bound": 4750000, + "upper_bound": 4995000, + "tags": ["broadcast"] + }, + { + "name": "49m Broadcast", + "lower_bound": 5900000, + "upper_bound": 6200000, + "tags": ["broadcast"] + }, + { + "name": "41m Broadcast", + "lower_bound": 7200000, + "upper_bound": 7450000, + "tags": ["broadcast"] + }, + { + "name": "31m Broadcast", + "lower_bound": 9400000, + "upper_bound": 9900000, + "tags": ["broadcast"] + }, + { + "name": "25m Broadcast", + "lower_bound": 11600000, + "upper_bound": 12100000, + "tags": ["broadcast"] + }, + { + "name": "22m Broadcast", + "lower_bound": 13570000, + "upper_bound": 13870000, + "tags": ["broadcast"] + }, + { + "name": "19m Broadcast", + "lower_bound": 15100000, + "upper_bound": 15830000, + "tags": ["broadcast"] + }, + { + "name": "16m Broadcast", + "lower_bound": 17480000, + "upper_bound": 17900000, + "tags": ["broadcast"] + }, + { + "name": "15m Broadcast", + "lower_bound": 18900000, + "upper_bound": 19020000, + "tags": ["broadcast"] + }, + { + "name": "13m Broadcast", + "lower_bound": 21450000, + "upper_bound": 21850000, + "tags": ["broadcast"] + }, + { + "name": "11m Broadcast", + "lower_bound": 25670000, + "upper_bound": 26100000, + "tags": ["broadcast"] + }, + { + "name": "FM Broadcast", + "lower_bound": 87500000, + "upper_bound": 108000000, + "tags": ["broadcast"] + }, + { + "name": "11m CB", + "lower_bound": 26965000, + "upper_bound": 27405000, + "frequencies": { + "js8": 27245000 + }, + "tags": ["public"] + }, + { + "name": "PMR446", + "lower_bound": 446000000, + "upper_bound": 446200000, + "tags": ["public"] + }, + { + "name": "Aeronautical Radionavigation", + "lower_bound": 960000000, + "upper_bound": 1215000000, + "tags": [], + "frequencies": { + "adsb": 1090000000 + } + }, + { + "name": "ISM-433", + "lower_bound": 433050000, + "upper_bound": 434790000, + "tags": [], + "frequencies": { + "ism": 433920000 + } + } +] diff --git a/config_webrx.py b/config_webrx.py deleted file mode 100644 index 34e480c6b..000000000 --- a/config_webrx.py +++ /dev/null @@ -1,216 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -config_webrx: configuration options for OpenWebRX - - This file is part of OpenWebRX, - an open-source SDR receiver software with a web UI. - Copyright (c) 2013-2015 by Andras Retzler - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation, either version 3 of the - License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - - ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - - In addition, as a special exception, the copyright holders - state that config_rtl.py and config_webrx.py are not part of the - Corresponding Source defined in GNU AGPL version 3 section 1. - - (It means that you do not have to redistribute config_rtl.py and - config_webrx.py if you make any changes to these two configuration files, - and use them for running your web service with OpenWebRX.) -""" - -# NOTE: you can find additional information about configuring OpenWebRX in the Wiki: -# https://github.com/simonyiszk/openwebrx/wiki - -# ==== Server settings ==== -web_port=8073 -server_hostname="localhost" # If this contains an incorrect value, the web UI may freeze on load (it can't open websocket) -max_clients=20 - -# ==== Web GUI configuration ==== -receiver_name="[Callsign]" -receiver_location="Budapest, Hungary" -receiver_qra="JN97ML" -receiver_asl=200 -receiver_ant="Longwire" -receiver_device="RTL-SDR" -receiver_admin="example@example.com" -receiver_gps=(47.000000,19.000000) -photo_height=350 -photo_title="Panorama of Budapest from Schönherz Zoltán Dormitory" -photo_desc=""" -You can add your own background photo and receiver information.
-Receiver is operated by: %[RX_ADMIN]
-Device: %[RX_DEVICE]
-Antenna: %[RX_ANT]
-Website: http://localhost -""" - -# ==== sdr.hu listing ==== -# If you want your ham receiver to be listed publicly on sdr.hu, then take the following steps: -# 1. Register at: http://sdr.hu/register -# 2. You will get an unique key by email. Copy it and paste here: -sdrhu_key = "" -# 3. Set this setting to True to enable listing: -sdrhu_public_listing = False - -# ==== DSP/RX settings ==== -fft_fps=9 -fft_size=4096 #Should be power of 2 -fft_voverlap_factor=0.3 #If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the diagram. - -# samp_rate = 250000 -samp_rate = 2400000 -center_freq = 144250000 -rf_gain = 5 #in dB. For an RTL-SDR, rf_gain=0 will set the tuner to auto gain mode, else it will be in manual gain mode. -ppm = 0 - -audio_compression="adpcm" #valid values: "adpcm", "none" -fft_compression="adpcm" #valid values: "adpcm", "none" - -digimodes_enable=True #Decoding digimodes come with higher CPU usage. -digimodes_fft_size=1024 - -start_rtl_thread=True - -""" -Note: if you experience audio underruns while CPU usage is 100%, you can: -- decrease `samp_rate`, -- set `fft_voverlap_factor` to 0, -- decrease `fft_fps` and `fft_size`, -- limit the number of users by decreasing `max_clients`. -""" - -# ==== I/Q sources ==== -# (Uncomment the appropriate by removing # characters at the beginning of the corresponding lines.) - -################################################################################################# -# Is my SDR hardware supported? # -# Check here: https://github.com/simonyiszk/openwebrx/wiki#guides-for-receiver-hardware-support # -################################################################################################# - -# You can use other SDR hardware as well, by giving your own command that outputs the I/Q samples... Some examples of configuration are available here (default is RTL-SDR): - -# >> RTL-SDR via rtl_sdr -start_rtl_command="rtl_sdr -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) -format_conversion="csdr convert_u8_f" - -#lna_gain=8 -#rf_amp=1 -#start_rtl_command="hackrf_transfer -s {samp_rate} -f {center_freq} -g {rf_gain} -l{lna_gain} -a{rf_amp} -r-".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm, rf_amp=rf_amp, lna_gain=lna_gain) -#format_conversion="csdr convert_s8_f" -""" -To use a HackRF, compile the HackRF host tools from its "stdout" branch: - git clone https://github.com/mossmann/hackrf/ - cd hackrf - git fetch - git checkout origin/stdout - cd host - mkdir build - cd build - cmake .. -DINSTALL_UDEV_RULES=ON - make - sudo make install -""" - -# >> Sound card SDR (needs ALSA) -# I did not have the chance to properly test it. -#samp_rate = 96000 -#start_rtl_command="arecord -f S16_LE -r {samp_rate} -c2 -".format(samp_rate=samp_rate) -#format_conversion="csdr convert_s16_f | csdr gain_ff 30" - -# >> /dev/urandom test signal source -# samp_rate = 2400000 -# start_rtl_command="cat /dev/urandom | (pv -qL `python -c 'print int({samp_rate} * 2.2)'` 2>&1)".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate) -# format_conversion="csdr convert_u8_f" - -# >> Pre-recorded raw I/Q file as signal source -# You will have to correctly specify: samp_rate, center_freq, format_conversion in order to correctly play an I/Q file. -#start_rtl_command="(while true; do cat my_iq_file.raw; done) | csdr flowcontrol {sr} 20 ".format(sr=samp_rate*2*1.05) -#format_conversion="csdr convert_u8_f" - -#>> The rx_sdr command works with a variety of SDR harware: RTL-SDR, HackRF, SDRplay, UHD, Airspy, Red Pitaya, audio devices, etc. -# It will auto-detect your SDR hardware if the following tools are installed: -# * the vendor provided driver and library, -# * the vendor-specific SoapySDR wrapper library, -# * and SoapySDR itself. -# Check out this article on the OpenWebRX Wiki: https://github.com/simonyiszk/openwebrx/wiki/Using-rx_tools-with-OpenWebRX/ -#start_rtl_command="rx_sdr -F CF32 -s {samp_rate} -f {center_freq} -p {ppm} -g {rf_gain} -".format(rf_gain=rf_gain, center_freq=center_freq, samp_rate=samp_rate, ppm=ppm) -#format_conversion="" - -# >> gr-osmosdr signal source using GNU Radio (follow this guide: https://github.com/simonyiszk/openwebrx/wiki/Using-GrOsmoSDR-as-signal-source) -#start_rtl_command="cat /tmp/osmocom_fifo" -#format_conversion="" - -# ==== Misc settings ==== - -shown_center_freq = center_freq #you can change this if you use an upconverter - -client_audio_buffer_size = 5 -#increasing client_audio_buffer_size will: -# - also increase the latency -# - decrease the chance of audio underruns - -start_freq = center_freq -start_mod = "nfm" #nfm, am, lsb, usb, cw - -iq_server_port = 4951 #TCP port for ncat to listen on. It will send I/Q data over its connections, for internal use in OpenWebRX. It is only accessible from the localhost by default. - -#access_log = "~/openwebrx_access.log" - -# ==== Color themes ==== - -#A guide is available to help you set these values: https://github.com/simonyiszk/openwebrx/wiki/Calibrating-waterfall-display-levels - -### default theme by teejez: -waterfall_colors = "[0x000000ff,0x0000ffff,0x00ffffff,0x00ff00ff,0xffff00ff,0xff0000ff,0xff00ffff,0xffffffff]" -waterfall_min_level = -88 #in dB -waterfall_max_level = -20 -waterfall_auto_level_margin = (5, 40) -### old theme by HA7ILM: -#waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" -#waterfall_min_level = -115 #in dB -#waterfall_max_level = 0 -#waterfall_auto_level_margin = (20, 30) -##For the old colors, you might also want to set [fft_voverlap_factor] to 0. - -#Note: When the auto waterfall level button is clicked, the following happens: -# [waterfall_min_level] = [current_min_power_level] - [waterfall_auto_level_margin[0]] -# [waterfall_max_level] = [current_max_power_level] + [waterfall_auto_level_margin[1]] -# -# ___|____________________________________|____________________________________|____________________________________|___> signal power -# \_waterfall_auto_level_margin[0]_/ |__ current_min_power_level | \_waterfall_auto_level_margin[1]_/ -# current_max_power_level __| - -# 3D view settings -mathbox_waterfall_frequency_resolution = 128 #bins -mathbox_waterfall_history_length = 10 #seconds -mathbox_waterfall_colors = "[0x000000ff,0x2e6893ff, 0x69a5d0ff, 0x214b69ff, 0x9dc4e0ff, 0xfff775ff, 0xff8a8aff, 0xb20000ff]" - -# === Experimental settings === -#Warning! The settings below are very experimental. -csdr_dynamic_bufsize = False # This allows you to change the buffering mode of csdr. -csdr_print_bufsizes = False # This prints the buffer sizes used for csdr processes. -csdr_through = False # Setting this True will print out how much data is going into the DSP chains. - -nmux_memory = 50 #in megabytes. This sets the approximate size of the circular buffer used by nmux. - -#Look up external IP address automatically from icanhazip.com, and use it as [server_hostname] -""" -print "[openwebrx-config] Detecting external IP address..." -import urllib2 -server_hostname=urllib2.urlopen("http://icanhazip.com").read()[:-1] -print "[openwebrx-config] External IP address detected:", server_hostname -""" diff --git a/csdr.py b/csdr.py deleted file mode 100755 index a2fb4902b..000000000 --- a/csdr.py +++ /dev/null @@ -1,424 +0,0 @@ -""" -OpenWebRX csdr plugin: do the signal processing with csdr - - This file is part of OpenWebRX, - an open-source SDR receiver software with a web UI. - Copyright (c) 2013-2015 by Andras Retzler - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation, either version 3 of the - License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -""" - -import subprocess -import time -import os -import code -import signal -import fcntl - -class dsp: - - def __init__(self): - self.samp_rate = 250000 - self.output_rate = 11025 #this is default, and cannot be set at the moment - self.fft_size = 1024 - self.fft_fps = 5 - self.offset_freq = 0 - self.low_cut = -4000 - self.high_cut = 4000 - self.bpf_transition_bw = 320 #Hz, and this is a constant - self.ddc_transition_bw_rate = 0.15 # of the IF sample rate - self.running = False - self.secondary_processes_running = False - self.audio_compression = "none" - self.fft_compression = "none" - self.demodulator = "nfm" - self.name = "csdr" - self.format_conversion = "csdr convert_u8_f" - self.base_bufsize = 512 - self.nc_port = 4951 - self.csdr_dynamic_bufsize = False - self.csdr_print_bufsizes = False - self.csdr_through = False - self.squelch_level = 0 - self.fft_averages = 50 - self.iqtee = False - self.iqtee2 = False - self.secondary_demodulator = None - self.secondary_fft_size = 1024 - self.secondary_process_fft = None - self.secondary_process_demod = None - self.pipe_names=["bpf_pipe", "shift_pipe", "squelch_pipe", "smeter_pipe", "iqtee_pipe", "iqtee2_pipe"] - self.secondary_pipe_names=["secondary_shift_pipe"] - self.secondary_offset_freq = 1000 - - def chain(self,which): - any_chain_base="nc -v 127.0.0.1 {nc_port} | " - if self.csdr_dynamic_bufsize: any_chain_base+="csdr setbuf {start_bufsize} | " - if self.csdr_through: any_chain_base+="csdr through | " - any_chain_base+=self.format_conversion+(" | " if self.format_conversion!="" else "") ##"csdr flowcontrol {flowcontrol} auto 1.5 10 | " - if which == "fft": - fft_chain_base = any_chain_base+"csdr fft_cc {fft_size} {fft_block_size} | " + \ - ("csdr logpower_cf -70 | " if self.fft_averages == 0 else "csdr logaveragepower_cf -70 {fft_size} {fft_averages} | ") + \ - "csdr fft_exchange_sides_ff {fft_size}" - if self.fft_compression=="adpcm": - return fft_chain_base+" | csdr compress_fft_adpcm_f_u8 {fft_size}" - else: - return fft_chain_base - chain_begin=any_chain_base+"csdr shift_addition_cc --fifo {shift_pipe} | csdr fir_decimate_cc {decimation} {ddc_transition_bw} HAMMING | csdr bandpass_fir_fft_cc --fifo {bpf_pipe} {bpf_transition_bw} HAMMING | csdr squelch_and_smeter_cc --fifo {squelch_pipe} --outfifo {smeter_pipe} 5 1 | " - if self.secondary_demodulator: - chain_begin+="csdr tee {iqtee_pipe} | " - chain_begin+="csdr tee {iqtee2_pipe} | " - chain_end = "" - if self.audio_compression=="adpcm": - chain_end = " | csdr encode_ima_adpcm_i16_u8" - if which == "nfm": return chain_begin + "csdr fmdemod_quadri_cf | csdr limit_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr deemphasis_nfm_ff 11025 | csdr fastagc_ff 1024 | csdr convert_f_s16"+chain_end - elif which == "am": return chain_begin + "csdr amdemod_cf | csdr fastdcblock_ff | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end - elif which == "ssb": return chain_begin + "csdr realpart_cf | csdr old_fractional_decimator_ff {last_decimation} | csdr agc_ff | csdr limit_ff | csdr convert_f_s16"+chain_end - - def secondary_chain(self, which): - secondary_chain_base="cat {input_pipe} | " - if which == "fft": - return secondary_chain_base+"csdr realpart_cf | csdr fft_fc {secondary_fft_input_size} {secondary_fft_block_size} | csdr logpower_cf -70 " + (" | csdr compress_fft_adpcm_f_u8 {secondary_fft_size}" if self.fft_compression=="adpcm" else "") - elif which == "bpsk31": - return secondary_chain_base + "csdr shift_addition_cc --fifo {secondary_shift_pipe} | " + \ - "csdr bandpass_fir_fft_cc $(csdr '=-(31.25)/{if_samp_rate}') $(csdr '=(31.25)/{if_samp_rate}') $(csdr '=31.25/{if_samp_rate}') | " + \ - "csdr simple_agc_cc 0.001 0.5 | " + \ - "csdr timing_recovery_cc GARDNER {secondary_samples_per_bits} 0.5 2 --add_q | " + \ - "CSDR_FIXED_BUFSIZE=1 csdr dbpsk_decoder_c_u8 | " + \ - "CSDR_FIXED_BUFSIZE=1 csdr psk31_varicode_decoder_u8_u8" - - def set_secondary_demodulator(self, what): - self.secondary_demodulator = what - - def secondary_fft_block_size(self): - return (self.samp_rate/self.decimation)/(self.fft_fps*2) #*2 is there because we do FFT on real signal here - - def secondary_decimation(self): - return 1 #currently unused - - def secondary_bpf_cutoff(self): - if self.secondary_demodulator == "bpsk31": - return (31.25/2) / self.if_samp_rate() - return 0 - - def secondary_bpf_transition_bw(self): - if self.secondary_demodulator == "bpsk31": - return (31.25/2) / self.if_samp_rate() - return 0 - - def secondary_samples_per_bits(self): - if self.secondary_demodulator == "bpsk31": - return int(round(self.if_samp_rate()/31.25))&~3 - return 0 - - def secondary_bw(self): - if self.secondary_demodulator == "bpsk31": - return 31.25 - - def start_secondary_demodulator(self): - if(not self.secondary_demodulator): return - print "[openwebrx] starting secondary demodulator from IF input sampled at %d"%self.if_samp_rate() - secondary_command_fft=self.secondary_chain("fft") - secondary_command_demod=self.secondary_chain(self.secondary_demodulator) - self.try_create_pipes(self.secondary_pipe_names, secondary_command_demod + secondary_command_fft) - - secondary_command_fft=secondary_command_fft.format( \ - input_pipe=self.iqtee_pipe, \ - secondary_fft_input_size=self.secondary_fft_size, \ - secondary_fft_size=self.secondary_fft_size, \ - secondary_fft_block_size=self.secondary_fft_block_size(), \ - ) - secondary_command_demod=secondary_command_demod.format( \ - input_pipe=self.iqtee2_pipe, \ - secondary_shift_pipe=self.secondary_shift_pipe, \ - secondary_decimation=self.secondary_decimation(), \ - secondary_samples_per_bits=self.secondary_samples_per_bits(), \ - secondary_bpf_cutoff=self.secondary_bpf_cutoff(), \ - secondary_bpf_transition_bw=self.secondary_bpf_transition_bw(), \ - if_samp_rate=self.if_samp_rate() - ) - - print "[openwebrx-dsp-plugin:csdr] secondary command (fft) =", secondary_command_fft - print "[openwebrx-dsp-plugin:csdr] secondary command (demod) =", secondary_command_demod - #code.interact(local=locals()) - my_env=os.environ.copy() - #if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; - if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; - self.secondary_process_fft = subprocess.Popen(secondary_command_fft, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) - print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (fft)" - self.secondary_process_demod = subprocess.Popen(secondary_command_demod, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) #TODO digimodes - print "[openwebrx-dsp-plugin:csdr] Popen on secondary command (demod)" #TODO digimodes - self.secondary_processes_running = True - - #open control pipes for csdr and send initialization data - # print "==========> 1" - if self.secondary_shift_pipe != None: #TODO digimodes - # print "==========> 2", self.secondary_shift_pipe - self.secondary_shift_pipe_file=open(self.secondary_shift_pipe,"w") #TODO digimodes - # print "==========> 3" - self.set_secondary_offset_freq(self.secondary_offset_freq) #TODO digimodes - # print "==========> 4" - - self.set_pipe_nonblocking(self.secondary_process_demod.stdout) - self.set_pipe_nonblocking(self.secondary_process_fft.stdout) - - def set_secondary_offset_freq(self, value): - self.secondary_offset_freq=value - if self.secondary_processes_running: - self.secondary_shift_pipe_file.write("%g\n"%(-float(self.secondary_offset_freq)/self.if_samp_rate())) - self.secondary_shift_pipe_file.flush() - - def stop_secondary_demodulator(self): - if self.secondary_processes_running == False: return - self.try_delete_pipes(self.secondary_pipe_names) - if self.secondary_process_fft: os.killpg(os.getpgid(self.secondary_process_fft.pid), signal.SIGTERM) - if self.secondary_process_demod: os.killpg(os.getpgid(self.secondary_process_demod.pid), signal.SIGTERM) - self.secondary_processes_running = False - - def read_secondary_demod(self, size): - return self.secondary_process_demod.stdout.read(size) - - def read_secondary_fft(self, size): - return self.secondary_process_fft.stdout.read(size) - - def get_secondary_demodulator(self): - return self.secondary_demodulator - - def set_secondary_fft_size(self,secondary_fft_size): - #to change this, restart is required - self.secondary_fft_size=secondary_fft_size - - def set_audio_compression(self,what): - self.audio_compression = what - - def set_fft_compression(self,what): - self.fft_compression = what - - def get_fft_bytes_to_read(self): - if self.fft_compression=="none": return self.fft_size*4 - if self.fft_compression=="adpcm": return (self.fft_size/2)+(10/2) - - def get_secondary_fft_bytes_to_read(self): - if self.fft_compression=="none": return self.secondary_fft_size*4 - if self.fft_compression=="adpcm": return (self.secondary_fft_size/2)+(10/2) - - def set_samp_rate(self,samp_rate): - #to change this, restart is required - self.samp_rate=samp_rate - self.decimation=1 - while self.samp_rate/(self.decimation+1)>self.output_rate: - self.decimation+=1 - self.last_decimation=float(self.if_samp_rate())/self.output_rate - - def if_samp_rate(self): - return self.samp_rate/self.decimation - - def get_name(self): - return self.name - - def get_output_rate(self): - return self.output_rate - - def set_output_rate(self,output_rate): - self.output_rate=output_rate - self.set_samp_rate(self.samp_rate) #as it depends on output_rate - - def set_demodulator(self,demodulator): - #to change this, restart is required - self.demodulator=demodulator - - def get_demodulator(self): - return self.demodulator - - def set_fft_size(self,fft_size): - #to change this, restart is required - self.fft_size=fft_size - - def set_fft_fps(self,fft_fps): - #to change this, restart is required - self.fft_fps=fft_fps - - def set_fft_averages(self,fft_averages): - #to change this, restart is required - self.fft_averages=fft_averages - - def fft_block_size(self): - if self.fft_averages == 0: return self.samp_rate/self.fft_fps - else: return self.samp_rate/self.fft_fps/self.fft_averages - - def set_format_conversion(self,format_conversion): - self.format_conversion=format_conversion - - def set_offset_freq(self,offset_freq): - self.offset_freq=offset_freq - if self.running: - self.shift_pipe_file.write("%g\n"%(-float(self.offset_freq)/self.samp_rate)) - self.shift_pipe_file.flush() - - def set_bpf(self,low_cut,high_cut): - self.low_cut=low_cut - self.high_cut=high_cut - if self.running: - self.bpf_pipe_file.write( "%g %g\n"%(float(self.low_cut)/self.if_samp_rate(), float(self.high_cut)/self.if_samp_rate()) ) - self.bpf_pipe_file.flush() - - def get_bpf(self): - return [self.low_cut, self.high_cut] - - def set_squelch_level(self, squelch_level): - self.squelch_level=squelch_level - if self.running: - self.squelch_pipe_file.write( "%g\n"%(float(self.squelch_level)) ) - self.squelch_pipe_file.flush() - - def get_smeter_level(self): - if self.running: - line=self.smeter_pipe_file.readline() - return float(line[:-1]) - - def mkfifo(self,path): - try: - os.unlink(path) - except: - pass - os.mkfifo(path) - - def ddc_transition_bw(self): - return self.ddc_transition_bw_rate*(self.if_samp_rate()/float(self.samp_rate)) - - def try_create_pipes(self, pipe_names, command_base): - # print "try_create_pipes" - for pipe_name in pipe_names: - # print "\t"+pipe_name - if "{"+pipe_name+"}" in command_base: - setattr(self, pipe_name, self.pipe_base_path+pipe_name) - self.mkfifo(getattr(self, pipe_name)) - else: - setattr(self, pipe_name, None) - - def try_delete_pipes(self, pipe_names): - for pipe_name in pipe_names: - pipe_path = getattr(self,pipe_name,None) - if pipe_path: - try: os.unlink(pipe_path) - except Exception as e: print "[openwebrx-dsp-plugin:csdr] try_delete_pipes() ::", e - - def set_pipe_nonblocking(self, pipe): - flags = fcntl.fcntl(pipe, fcntl.F_GETFL) - fcntl.fcntl(pipe, fcntl.F_SETFL, flags | os.O_NONBLOCK) - - def start(self): - command_base=self.chain(self.demodulator) - - #create control pipes for csdr - self.pipe_base_path="/tmp/openwebrx_pipe_{myid}_".format(myid=id(self)) - # self.bpf_pipe = self.shift_pipe = self.squelch_pipe = self.smeter_pipe = None - - self.try_create_pipes(self.pipe_names, command_base) - - # if "{bpf_pipe}" in command_base: - # self.bpf_pipe=pipe_base_path+"bpf" - # self.mkfifo(self.bpf_pipe) - # if "{shift_pipe}" in command_base: - # self.shift_pipe=pipe_base_path+"shift" - # self.mkfifo(self.shift_pipe) - # if "{squelch_pipe}" in command_base: - # self.squelch_pipe=pipe_base_path+"squelch" - # self.mkfifo(self.squelch_pipe) - # if "{smeter_pipe}" in command_base: - # self.smeter_pipe=pipe_base_path+"smeter" - # self.mkfifo(self.smeter_pipe) - # if "{iqtee_pipe}" in command_base: - # self.iqtee_pipe=pipe_base_path+"iqtee" - # self.mkfifo(self.iqtee_pipe) - # if "{iqtee2_pipe}" in command_base: - # self.iqtee2_pipe=pipe_base_path+"iqtee2" - # self.mkfifo(self.iqtee2_pipe) - - #run the command - command=command_base.format( bpf_pipe=self.bpf_pipe, shift_pipe=self.shift_pipe, decimation=self.decimation, \ - last_decimation=self.last_decimation, fft_size=self.fft_size, fft_block_size=self.fft_block_size(), fft_averages=self.fft_averages, \ - bpf_transition_bw=float(self.bpf_transition_bw)/self.if_samp_rate(), ddc_transition_bw=self.ddc_transition_bw(), \ - flowcontrol=int(self.samp_rate*2), start_bufsize=self.base_bufsize*self.decimation, nc_port=self.nc_port, \ - squelch_pipe=self.squelch_pipe, smeter_pipe=self.smeter_pipe, iqtee_pipe=self.iqtee_pipe, iqtee2_pipe=self.iqtee2_pipe ) - - print "[openwebrx-dsp-plugin:csdr] Command =",command - #code.interact(local=locals()) - my_env=os.environ.copy() - if self.csdr_dynamic_bufsize: my_env["CSDR_DYNAMIC_BUFSIZE_ON"]="1"; - if self.csdr_print_bufsizes: my_env["CSDR_PRINT_BUFSIZES"]="1"; - self.process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True, preexec_fn=os.setpgrp, env=my_env) - self.running = True - - #open control pipes for csdr and send initialization data - if self.bpf_pipe != None: - self.bpf_pipe_file=open(self.bpf_pipe,"w") - self.set_bpf(self.low_cut,self.high_cut) - if self.shift_pipe != None: - self.shift_pipe_file=open(self.shift_pipe,"w") - self.set_offset_freq(self.offset_freq) - if self.squelch_pipe != None: - self.squelch_pipe_file=open(self.squelch_pipe,"w") - self.set_squelch_level(self.squelch_level) - if self.smeter_pipe != None: - self.smeter_pipe_file=open(self.smeter_pipe,"r") - self.set_pipe_nonblocking(self.smeter_pipe_file) - - self.start_secondary_demodulator() - - def read(self,size): - return self.process.stdout.read(size) - - def stop(self): - os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) - self.stop_secondary_demodulator() - #if(self.process.poll()!=None):return # returns None while subprocess is running - #while(self.process.poll()==None): - # #self.process.kill() - # print "killproc",os.getpgid(self.process.pid),self.process.pid - # os.killpg(self.process.pid, signal.SIGTERM) - # - # time.sleep(0.1) - - self.try_delete_pipes(self.pipe_names) - - # if self.bpf_pipe: - # try: os.unlink(self.bpf_pipe) - # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.bpf_pipe - # if self.shift_pipe: - # try: os.unlink(self.shift_pipe) - # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.shift_pipe - # if self.squelch_pipe: - # try: os.unlink(self.squelch_pipe) - # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.squelch_pipe - # if self.smeter_pipe: - # try: os.unlink(self.smeter_pipe) - # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.smeter_pipe - # if self.iqtee_pipe: - # try: os.unlink(self.iqtee_pipe) - # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee_pipe - # if self.iqtee2_pipe: - # try: os.unlink(self.iqtee2_pipe) - # except: print "[openwebrx-dsp-plugin:csdr] stop() :: unlink failed: " + self.iqtee2_pipe - - self.running = False - - def restart(self): - self.stop() - self.start() - - def __del__(self): - self.stop() - del(self.process) diff --git a/csdr/__init__.py b/csdr/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/csdr/chain/__init__.py b/csdr/chain/__init__.py new file mode 100644 index 000000000..bebe121de --- /dev/null +++ b/csdr/chain/__init__.py @@ -0,0 +1,147 @@ +from csdr.module import Module +from pycsdr.modules import Buffer +from pycsdr.types import Format +from typing import Union, Callable, Optional + + +class Chain(Module): + def __init__(self, workers): + super().__init__() + self.workers = workers + for i in range(1, len(self.workers)): + self._connect(self.workers[i - 1], self.workers[i]) + + def empty(self): + return not self.workers + + def _connect(self, w1, w2, buffer: Optional[Buffer] = None) -> None: + if buffer is None: + buffer = Buffer(w1.getOutputFormat()) + w1.setWriter(buffer) + w2.setReader(buffer.getReader()) + + def setReader(self, reader): + if self.reader is reader: + return + super().setReader(reader) + if self.workers: + self.workers[0].setReader(reader) + + def setWriter(self, writer): + if self.writer is writer: + return + super().setWriter(writer) + if self.workers: + self.workers[-1].setWriter(writer) + + def indexOf(self, search: Union[Callable, object]) -> int: + def searchFn(x): + if callable(search): + return search(x) + else: + return x is search + + try: + return next(i for i, v in enumerate(self.workers) if searchFn(v)) + except StopIteration: + return -1 + + def replace(self, index, newWorker): + if index >= len(self.workers): + raise IndexError("Index {} does not exist".format(index)) + + self.workers[index].stop() + self.workers[index] = newWorker + + error = None + + if index == 0: + if self.reader is not None: + newWorker.setReader(self.reader) + else: + try: + previousWorker = self.workers[index - 1] + self._connect(previousWorker, newWorker) + except ValueError as e: + # store error for later raising, but still attempt the second connection + error = e + + if index == len(self.workers) - 1: + if self.writer is not None: + newWorker.setWriter(self.writer) + else: + try: + nextWorker = self.workers[index + 1] + self._connect(newWorker, nextWorker) + except ValueError as e: + error = e + + if error is not None: + raise error + + def append(self, newWorker): + previousWorker = None + if self.workers: + previousWorker = self.workers[-1] + + self.workers.append(newWorker) + + if previousWorker: + self._connect(previousWorker, newWorker) + elif self.reader is not None: + newWorker.setReader(self.reader) + + if self.writer is not None: + newWorker.setWriter(self.writer) + + def insert(self, index, newWorker): + nextWorker = None + previousWorker = None + if index < len(self.workers): + nextWorker = self.workers[index] + if index > 0: + previousWorker = self.workers[index - 1] + + self.workers.insert(index, newWorker) + + if nextWorker: + self._connect(newWorker, nextWorker) + elif self.writer is not None: + newWorker.setWriter(self.writer) + + if previousWorker: + self._connect(previousWorker, newWorker) + elif self.reader is not None: + newWorker.setReader(self.reader) + + def remove(self, index): + removedWorker = self.workers[index] + self.workers.remove(removedWorker) + removedWorker.stop() + + if index == 0: + if self.reader is not None and len(self.workers): + self.workers[0].setReader(self.reader) + elif index == len(self.workers): + if self.writer is not None: + self.workers[-1].setWriter(self.writer) + else: + previousWorker = self.workers[index - 1] + nextWorker = self.workers[index] + self._connect(previousWorker, nextWorker) + + def stop(self): + for w in self.workers: + w.stop() + + def getInputFormat(self) -> Format: + if self.workers: + return self.workers[0].getInputFormat() + else: + raise BufferError("getInputFormat on empty chain") + + def getOutputFormat(self) -> Format: + if self.workers: + return self.workers[-1].getOutputFormat() + else: + raise BufferError("getOutputFormat on empty chain") diff --git a/csdr/chain/analog.py b/csdr/chain/analog.py new file mode 100644 index 000000000..6124665a3 --- /dev/null +++ b/csdr/chain/analog.py @@ -0,0 +1,127 @@ +from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, HdAudio, DeemphasisTauChain, \ + MetaProvider, RdsChain +from pycsdr.modules import AmDemod, DcBlock, FmDemod, Limit, NfmDeemphasis, Agc, WfmDeemphasis, FractionalDecimator, \ + RealPart, Writer, Buffer +from pycsdr.types import Format, AgcProfile +from csdr.chain.redsea import Redsea +from typing import Optional +from owrx.feature import FeatureDetector + + +class Am(BaseDemodulatorChain): + def __init__(self): + agc = Agc(Format.FLOAT) + agc.setProfile(AgcProfile.SLOW) + agc.setInitialGain(200) + workers = [ + AmDemod(), + DcBlock(), + agc, + ] + + super().__init__(workers) + + +class NFm(BaseDemodulatorChain): + def __init__(self, sampleRate: int): + self.sampleRate = sampleRate + agc = Agc(Format.FLOAT) + agc.setProfile(AgcProfile.SLOW) + agc.setMaxGain(3) + workers = [ + FmDemod(), + Limit(), + NfmDeemphasis(sampleRate), + agc, + ] + super().__init__(workers) + + def setSampleRate(self, sampleRate: int) -> None: + if sampleRate == self.sampleRate: + return + self.sampleRate = sampleRate + self.replace(2, NfmDeemphasis(sampleRate)) + + +class WFm(BaseDemodulatorChain, FixedIfSampleRateChain, DeemphasisTauChain, HdAudio, MetaProvider, RdsChain): + def __init__(self, sampleRate: int, tau: float, rdsRbds: bool): + self.sampleRate = sampleRate + self.tau = tau + self.rdsRbds = rdsRbds + self.limit = Limit() + # this buffer is used to tap into the raw audio stream for redsea RDS decoding + self.metaTapBuffer = Buffer(Format.FLOAT) + workers = [ + FmDemod(), + self.limit, + FractionalDecimator(Format.FLOAT, 200000.0 / self.sampleRate, prefilter=True), + WfmDeemphasis(self.sampleRate, self.tau), + ] + self.metaChain = None + self.metaWriter = None + super().__init__(workers) + + def _connect(self, w1, w2, buffer: Optional[Buffer] = None) -> None: + if w1 is self.limit: + buffer = self.metaTapBuffer + super()._connect(w1, w2, buffer) + + def getFixedIfSampleRate(self): + return 200000 + + def setDeemphasisTau(self, tau: float) -> None: + if tau == self.tau: + return + self.tau = tau + self.replace(3, WfmDeemphasis(self.sampleRate, self.tau)) + + def setSampleRate(self, sampleRate: int) -> None: + if sampleRate == self.sampleRate: + return + self.sampleRate = sampleRate + self.replace(2, FractionalDecimator(Format.FLOAT, 200000.0 / self.sampleRate, prefilter=True)) + self.replace(3, WfmDeemphasis(self.sampleRate, self.tau)) + + def setMetaWriter(self, writer: Writer) -> None: + if not FeatureDetector().is_available("redsea"): + return + if self.metaChain is None: + self.metaChain = Redsea(self.getFixedIfSampleRate(), self.rdsRbds) + self.metaChain.setReader(self.metaTapBuffer.getReader()) + self.metaWriter = writer + self.metaChain.setWriter(self.metaWriter) + + def stop(self): + super().stop() + if self.metaChain is not None: + self.metaChain.stop() + self.metaChain = None + self.metaWriter = None + + def setRdsRbds(self, rdsRbds: bool) -> None: + self.rdsRbds = rdsRbds + if self.metaChain is not None: + self.metaChain.stop() + self.metaChain = Redsea(self.getFixedIfSampleRate(), self.rdsRbds) + self.metaChain.setReader(self.metaTapBuffer.getReader()) + self.metaChain.setWriter(self.metaWriter) + + +class Ssb(BaseDemodulatorChain): + def __init__(self): + workers = [ + RealPart(), + Agc(Format.FLOAT), + ] + super().__init__(workers) + + +class Empty(BaseDemodulatorChain): + def __init__(self): + super().__init__([]) + + def getOutputFormat(self) -> Format: + return Format.FLOAT + + def setWriter(self, writer): + pass diff --git a/csdr/chain/clientaudio.py b/csdr/chain/clientaudio.py new file mode 100644 index 000000000..9fd748b89 --- /dev/null +++ b/csdr/chain/clientaudio.py @@ -0,0 +1,72 @@ +from csdr.chain import Chain +from pycsdr.modules import AudioResampler, Convert, AdpcmEncoder, Limit +from pycsdr.types import Format + + +class Converter(Chain): + def __init__(self, format: Format, inputRate: int, clientRate: int): + workers = [] + if inputRate != clientRate: + # we only have an audio resampler for float ATM so if we need to resample, we need to convert + if format != Format.FLOAT: + workers += [Convert(format, Format.FLOAT)] + workers += [AudioResampler(inputRate, clientRate), Limit(), Convert(Format.FLOAT, Format.SHORT)] + elif format != Format.SHORT: + workers += [Convert(format, Format.SHORT)] + super().__init__(workers) + + +class ClientAudioChain(Chain): + def __init__(self, format: Format, inputRate: int, clientRate: int, compression: str): + self.format = format + self.inputRate = inputRate + self.clientRate = clientRate + workers = [] + converter = self._buildConverter() + if not converter.empty(): + workers += [converter] + if compression == "adpcm": + workers += [AdpcmEncoder(sync=True)] + super().__init__(workers) + + def _buildConverter(self): + return Converter(self.format, self.inputRate, self.clientRate) + + def _updateConverter(self): + converter = self._buildConverter() + index = self.indexOf(lambda x: isinstance(x, Converter)) + if converter.empty(): + if index >= 0: + self.remove(index) + else: + if index >= 0: + self.replace(index, converter) + else: + self.insert(0, converter) + + def setFormat(self, format: Format) -> None: + if format == self.format: + return + self.format = format + self._updateConverter() + + def setInputRate(self, inputRate: int) -> None: + if inputRate == self.inputRate: + return + self.inputRate = inputRate + self._updateConverter() + + def setClientRate(self, clientRate: int) -> None: + if clientRate == self.clientRate: + return + self.clientRate = clientRate + self._updateConverter() + + def setAudioCompression(self, compression: str) -> None: + index = self.indexOf(lambda x: isinstance(x, AdpcmEncoder)) + if compression == "adpcm": + if index < 0: + self.append(AdpcmEncoder(sync=True)) + else: + if index >= 0: + self.remove(index) diff --git a/csdr/chain/dablin.py b/csdr/chain/dablin.py new file mode 100644 index 000000000..c5a321167 --- /dev/null +++ b/csdr/chain/dablin.py @@ -0,0 +1,107 @@ +from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, \ + MetaProvider, DabServiceSelector, DialFrequencyReceiver +from csdr.module import PickleModule +from csdreti.modules import EtiDecoder +from owrx.dab.dablin import DablinModule +from pycsdr.modules import Downmix, Buffer, Shift, Writer +from pycsdr.types import Format +from typing import Optional +from random import random + +import logging + +logger = logging.getLogger(__name__) + + +class MetaProcessor(PickleModule): + def __init__(self, shifter: Shift): + self.shifter = shifter + self.shift = 0.0 + self.coarse_increment = -32 / 2048000 + self.fine_increment = - (1/3) / 2048000 + # carrier spacing is 1kHz, don't drift further than that. + self.max_shift = 1000 / 2048000 + super().__init__() + + def process(self, data): + result = {} + for key, value in data.items(): + if key == "coarse_frequency_shift": + if value > 0: + self._nudgeShift(random() * self.coarse_increment) + else: + self._nudgeShift(random() * -self.coarse_increment) + elif key == "fine_frequency_shift": + if abs(value) > 10: + self._nudgeShift(self.fine_increment * value) + else: + # pass through everything else + result[key] = value + # don't send out data if there was nothing interesting for the client + if not result: + return + result["mode"] = "DAB" + return result + + def _nudgeShift(self, amount): + self.shift += amount + if self.shift > self.max_shift: + self.shift = self.max_shift + elif self.shift < -self.max_shift: + self.shift = -self.max_shift + logger.debug("new shift: %f", self.shift) + self.shifter.setRate(self.shift) + + def resetShift(self): + logger.debug("resetting shift") + self.shift = 0 + self.shifter.setRate(0) + + +class Dablin(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, MetaProvider, DabServiceSelector, DialFrequencyReceiver): + def __init__(self): + shift = Shift(0) + self.decoder = EtiDecoder() + + metaBuffer = Buffer(Format.CHAR) + self.decoder.setMetaWriter(metaBuffer) + self.processor = MetaProcessor(shift) + self.processor.setReader(metaBuffer.getReader()) + # use a dummy to start with. it won't run without. + # will be replaced by setMetaWriter(). + self.processor.setWriter(Buffer(Format.CHAR)) + + self.dablin = DablinModule() + + workers = [ + shift, + self.decoder, + self.dablin, + Downmix(Format.FLOAT), + ] + super().__init__(workers) + + def _connect(self, w1, w2, buffer: Optional[Buffer] = None) -> None: + if isinstance(w2, EtiDecoder): + # eti decoder needs big chunks of data + buffer = Buffer(w1.getOutputFormat(), size=2097152) + super()._connect(w1, w2, buffer) + + def getFixedIfSampleRate(self) -> int: + return 2048000 + + def getFixedAudioRate(self) -> int: + return 48000 + + def stop(self): + self.processor.stop() + + def setMetaWriter(self, writer: Writer) -> None: + self.processor.setWriter(writer) + + def setDabServiceId(self, serviceId: int) -> None: + self.decoder.setServiceIdFilter([serviceId]) + self.dablin.setDabServiceId(serviceId) + + def setDialFrequency(self, frequency: int) -> None: + self.processor.resetShift() diff --git a/csdr/chain/demodulator.py b/csdr/chain/demodulator.py new file mode 100644 index 000000000..7b4506ccd --- /dev/null +++ b/csdr/chain/demodulator.py @@ -0,0 +1,88 @@ +from csdr.chain import Chain +from abc import ABC, ABCMeta, abstractmethod +from pycsdr.modules import Writer + + +class FixedAudioRateChain(ABC): + @abstractmethod + def getFixedAudioRate(self) -> int: + pass + + +class FixedIfSampleRateChain(ABC): + @abstractmethod + def getFixedIfSampleRate(self) -> int: + pass + + +class DialFrequencyReceiver(ABC): + @abstractmethod + def setDialFrequency(self, frequency: int) -> None: + pass + + +# marker interface +class HdAudio: + pass + + +class MetaProvider(ABC): + @abstractmethod + def setMetaWriter(self, writer: Writer) -> None: + pass + + +class SlotFilterChain(ABC): + @abstractmethod + def setSlotFilter(self, filter: int) -> None: + pass + + +class SecondarySelectorChain(ABC): + def getBandwidth(self) -> float: + pass + + +class DeemphasisTauChain(ABC): + @abstractmethod + def setDeemphasisTau(self, tau: float) -> None: + pass + + +class RdsChain(ABC): + @abstractmethod + def setRdsRbds(self, rdsRbds: bool) -> None: + pass + + +class DabServiceSelector(ABC): + @abstractmethod + def setDabServiceId(self, serviceId: int) -> None: + pass + + +class BaseDemodulatorChain(Chain): + def supportsSquelch(self) -> bool: + return True + + def setSampleRate(self, sampleRate: int) -> None: + pass + + +class SecondaryDemodulator(Chain): + def supportsSquelch(self) -> bool: + return True + + def setSampleRate(self, sampleRate: int) -> None: + pass + + def isSecondaryFftShown(self): + return True + + +class ServiceDemodulator(SecondaryDemodulator, FixedAudioRateChain, metaclass=ABCMeta): + pass + + +class DemodulatorError(Exception): + pass diff --git a/csdr/chain/digiham.py b/csdr/chain/digiham.py new file mode 100644 index 000000000..ebd282f0b --- /dev/null +++ b/csdr/chain/digiham.py @@ -0,0 +1,143 @@ +from csdr.chain.demodulator import BaseDemodulatorChain, FixedAudioRateChain, FixedIfSampleRateChain, DialFrequencyReceiver, MetaProvider, SlotFilterChain, DemodulatorError, ServiceDemodulator +from pycsdr.modules import FmDemod, Agc, Writer, Buffer, DcBlock, Lowpass +from pycsdr.types import Format +from digiham.modules import DstarDecoder, FskDemodulator, GfskDemodulator, DigitalVoiceFilter, MbeSynthesizer, NarrowRrcFilter, NxdnDecoder, DmrDecoder, WideRrcFilter, YsfDecoder, PocsagDecoder +from digiham.ambe import Modes, ServerError +from owrx.meta import MetaParser +from owrx.pocsag import PocsagParser + +import logging + +logger = logging.getLogger(__name__) + + +class DigihamChain(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, DialFrequencyReceiver, MetaProvider): + def __init__(self, fskDemodulator, decoder, mbeMode, filter=None, codecserver: str = ""): + self.decoder = decoder + if codecserver is None: + codecserver = "" + agc = Agc(Format.SHORT) + agc.setMaxGain(30) + agc.setInitialGain(3) + workers = [FmDemod(), DcBlock()] + if filter is not None: + workers += [filter] + try: + mbeSynthesizer = MbeSynthesizer(mbeMode, codecserver) + except ConnectionError as ce: + raise DemodulatorError("Connection to codecserver failed: {}".format(ce)) + except ServerError as se: + raise DemodulatorError("Codecserver error: {}".format(se)) + except RuntimeError as re: + logger.exception("Codecserver error while instantiating MbeSynthesizer:") + raise DemodulatorError("Fatal codecserver error. Please check receiver logs.") + workers += [ + fskDemodulator, + decoder, + mbeSynthesizer, + DigitalVoiceFilter(), + agc + ] + self.metaParser = None + self.dialFrequency = None + super().__init__(workers) + + def getFixedIfSampleRate(self): + return 48000 + + def getFixedAudioRate(self): + return 8000 + + def setMetaWriter(self, writer: Writer) -> None: + if self.metaParser is None: + self.metaParser = MetaParser() + buffer = Buffer(Format.CHAR) + self.decoder.setMetaWriter(buffer) + self.metaParser.setReader(buffer.getReader()) + if self.dialFrequency is not None: + self.metaParser.setDialFrequency(self.dialFrequency) + self.metaParser.setWriter(writer) + + def supportsSquelch(self): + return False + + def setDialFrequency(self, frequency: int) -> None: + self.dialFrequency = frequency + if self.metaParser is None: + return + self.metaParser.setDialFrequency(frequency) + + def stop(self): + if self.metaParser is not None: + self.metaParser.stop() + super().stop() + + +class Dstar(DigihamChain): + def __init__(self, codecserver: str = ""): + super().__init__( + fskDemodulator=FskDemodulator(samplesPerSymbol=10), + decoder=DstarDecoder(), + mbeMode=Modes.DStarMode, + filter=WideRrcFilter(), + codecserver=codecserver + ) + + +class Nxdn(DigihamChain): + def __init__(self, codecserver: str = ""): + super().__init__( + fskDemodulator=GfskDemodulator(samplesPerSymbol=20), + decoder=NxdnDecoder(), + mbeMode=Modes.NxdnMode, + filter=NarrowRrcFilter(), + codecserver=codecserver + ) + + +class Dmr(DigihamChain, SlotFilterChain): + def __init__(self, codecserver: str = ""): + super().__init__( + fskDemodulator=GfskDemodulator(samplesPerSymbol=10), + decoder=DmrDecoder(), + mbeMode=Modes.DmrMode, + filter=WideRrcFilter(), + codecserver=codecserver, + ) + + def setSlotFilter(self, slotFilter: int) -> None: + self.decoder.setSlotFilter(slotFilter) + + +class Ysf(DigihamChain): + def __init__(self, codecserver: str = ""): + super().__init__( + fskDemodulator=GfskDemodulator(samplesPerSymbol=10), + decoder=YsfDecoder(), + mbeMode=Modes.YsfMode, + filter=WideRrcFilter(), + codecserver=codecserver + ) + + +class PocsagDemodulator(ServiceDemodulator, DialFrequencyReceiver): + def __init__(self): + self.parser = PocsagParser() + workers = [ + FmDemod(), + DcBlock(), + Lowpass(Format.FLOAT, 1200 / self.getFixedAudioRate()), + FskDemodulator(samplesPerSymbol=40, invert=True), + PocsagDecoder(), + self.parser, + ] + super().__init__(workers) + + def supportsSquelch(self) -> bool: + return False + + def getFixedAudioRate(self) -> int: + return 48000 + + def setDialFrequency(self, frequency: int) -> None: + self.parser.setDialFrequency(frequency) diff --git a/csdr/chain/digimodes.py b/csdr/chain/digimodes.py new file mode 100644 index 000000000..a84206093 --- /dev/null +++ b/csdr/chain/digimodes.py @@ -0,0 +1,120 @@ +from csdr.chain.demodulator import ServiceDemodulator, SecondaryDemodulator, DialFrequencyReceiver, SecondarySelectorChain +from csdr.module.msk144 import Msk144Module, ParserAdapter +from owrx.audio.chopper import AudioChopper, AudioChopperParser +from owrx.aprs.kiss import KissDeframer +from owrx.aprs import Ax25Parser, AprsParser +from pycsdr.modules import Convert, FmDemod, Agc, TimingRecovery, DBPskDecoder, VaricodeDecoder, RttyDecoder, BaudotDecoder, Lowpass +from pycsdr.types import Format +from owrx.aprs.direwolf import DirewolfModule + + +class AudioChopperDemodulator(ServiceDemodulator, DialFrequencyReceiver): + def __init__(self, mode: str, parser: AudioChopperParser): + self.chopper = AudioChopper(mode, parser) + workers = [Convert(Format.FLOAT, Format.SHORT), self.chopper] + super().__init__(workers) + + def getFixedAudioRate(self): + return 12000 + + def setDialFrequency(self, frequency: int) -> None: + self.chopper.setDialFrequency(frequency) + + +class Msk144Demodulator(ServiceDemodulator, DialFrequencyReceiver): + def __init__(self): + self.parser = ParserAdapter() + workers = [ + Convert(Format.FLOAT, Format.SHORT), + Msk144Module(), + self.parser, + ] + super().__init__(workers) + + def getFixedAudioRate(self) -> int: + return 12000 + + def setDialFrequency(self, frequency: int) -> None: + self.parser.setDialFrequency(frequency) + + +class PacketDemodulator(ServiceDemodulator, DialFrequencyReceiver): + def __init__(self, service: bool = False): + self.parser = AprsParser() + workers = [ + FmDemod(), + Convert(Format.FLOAT, Format.SHORT), + DirewolfModule(service=service), + KissDeframer(), + Ax25Parser(), + self.parser, + ] + super().__init__(workers) + + def supportsSquelch(self) -> bool: + return False + + def getFixedAudioRate(self) -> int: + return 48000 + + def setDialFrequency(self, frequency: int) -> None: + self.parser.setDialFrequency(frequency) + + +class PskDemodulator(SecondaryDemodulator, SecondarySelectorChain): + def __init__(self, baudRate: float): + self.baudRate = baudRate + # this is an assumption, we will adjust in setSampleRate + self.sampleRate = 12000 + secondary_samples_per_bits = int(round(self.sampleRate / self.baudRate)) & ~3 + workers = [ + Agc(Format.COMPLEX_FLOAT), + TimingRecovery(Format.COMPLEX_FLOAT, secondary_samples_per_bits, 0.5, 2), + DBPskDecoder(), + VaricodeDecoder(), + ] + super().__init__(workers) + + def getBandwidth(self): + return self.baudRate + + def setSampleRate(self, sampleRate: int) -> None: + if sampleRate == self.sampleRate: + return + self.sampleRate = sampleRate + secondary_samples_per_bits = int(round(self.sampleRate / self.baudRate)) & ~3 + self.replace(1, TimingRecovery(Format.COMPLEX_FLOAT, secondary_samples_per_bits, 0.5, 2)) + + +class RttyDemodulator(SecondaryDemodulator, SecondarySelectorChain): + def __init__(self, baudRate, bandWidth, invert=False): + self.baudRate = baudRate + self.bandWidth = bandWidth + self.invert = invert + # this is an assumption, we will adjust in setSampleRate + self.sampleRate = 12000 + secondary_samples_per_bit = int(round(self.sampleRate / self.baudRate)) + cutoff = self.baudRate / self.sampleRate + loop_gain = self.sampleRate / self.getBandwidth() / 5 + workers = [ + Agc(Format.COMPLEX_FLOAT), + FmDemod(), + Lowpass(Format.FLOAT, cutoff), + TimingRecovery(Format.FLOAT, secondary_samples_per_bit, loop_gain, 10), + RttyDecoder(invert), + BaudotDecoder(), + ] + super().__init__(workers) + + def getBandwidth(self) -> float: + return self.bandWidth + + def setSampleRate(self, sampleRate: int) -> None: + if sampleRate == self.sampleRate: + return + self.sampleRate = sampleRate + secondary_samples_per_bit = int(round(self.sampleRate / self.baudRate)) + cutoff = self.baudRate / self.sampleRate + loop_gain = self.sampleRate / self.getBandwidth() / 5 + self.replace(2, Lowpass(Format.FLOAT, cutoff)) + self.replace(3, TimingRecovery(Format.FLOAT, secondary_samples_per_bit, loop_gain, 10)) diff --git a/csdr/chain/drm.py b/csdr/chain/drm.py new file mode 100644 index 000000000..78b90f0df --- /dev/null +++ b/csdr/chain/drm.py @@ -0,0 +1,23 @@ +from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain +from pycsdr.modules import Convert, Downmix +from pycsdr.types import Format +from csdr.module.drm import DrmModule + + +class Drm(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain): + def __init__(self): + workers = [ + Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT), + DrmModule(), + Downmix(Format.SHORT), + ] + super().__init__(workers) + + def supportsSquelch(self) -> bool: + return False + + def getFixedIfSampleRate(self) -> int: + return 48000 + + def getFixedAudioRate(self) -> int: + return 48000 diff --git a/csdr/chain/dummy.py b/csdr/chain/dummy.py new file mode 100644 index 000000000..b4e0220b9 --- /dev/null +++ b/csdr/chain/dummy.py @@ -0,0 +1,14 @@ +from pycsdr.types import Format +from csdr.chain import Module + + +class DummyDemodulator(Module): + def __init__(self, outputFormat: Format): + self.outputFormat = outputFormat + super().__init__() + + def getInputFormat(self) -> Format: + return Format.COMPLEX_FLOAT + + def getOutputFormat(self) -> Format: + return self.outputFormat diff --git a/csdr/chain/dump1090.py b/csdr/chain/dump1090.py new file mode 100644 index 000000000..04a294211 --- /dev/null +++ b/csdr/chain/dump1090.py @@ -0,0 +1,27 @@ +from pycsdr.modules import Convert +from pycsdr.types import Format +from csdr.chain.demodulator import ServiceDemodulator +from owrx.adsb.dump1090 import Dump1090Module, RawDeframer +from owrx.adsb.modes import ModeSParser + + +class Dump1090(ServiceDemodulator): + def __init__(self): + workers = [ + Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT), + Dump1090Module(), + RawDeframer(), + ModeSParser(), + ] + + super().__init__(workers) + pass + + def getFixedAudioRate(self) -> int: + return 2400000 + + def isSecondaryFftShown(self): + return False + + def supportsSquelch(self) -> bool: + return False diff --git a/csdr/chain/dumphfdl.py b/csdr/chain/dumphfdl.py new file mode 100644 index 000000000..413b61500 --- /dev/null +++ b/csdr/chain/dumphfdl.py @@ -0,0 +1,16 @@ +from csdr.chain.demodulator import ServiceDemodulator +from owrx.hfdl.dumphfdl import DumpHFDLModule, HFDLMessageParser + + +class DumpHFDL(ServiceDemodulator): + def __init__(self): + super().__init__([ + DumpHFDLModule(), + HFDLMessageParser(), + ]) + + def getFixedAudioRate(self) -> int: + return 12000 + + def supportsSquelch(self) -> bool: + return False diff --git a/csdr/chain/dumpvdl2.py b/csdr/chain/dumpvdl2.py new file mode 100644 index 000000000..cae271f51 --- /dev/null +++ b/csdr/chain/dumpvdl2.py @@ -0,0 +1,19 @@ +from csdr.chain.demodulator import ServiceDemodulator +from owrx.vdl2.dumpvdl2 import DumpVDL2Module, VDL2MessageParser +from pycsdr.modules import Convert +from pycsdr.types import Format + + +class DumpVDL2(ServiceDemodulator): + def __init__(self): + super().__init__([ + Convert(Format.COMPLEX_FLOAT, Format.COMPLEX_SHORT), + DumpVDL2Module(), + VDL2MessageParser(), + ]) + + def getFixedAudioRate(self) -> int: + return 105000 + + def supportsSquelch(self) -> bool: + return False diff --git a/csdr/chain/fft.py b/csdr/chain/fft.py new file mode 100644 index 000000000..c782ca0d8 --- /dev/null +++ b/csdr/chain/fft.py @@ -0,0 +1,96 @@ +from csdr.chain import Chain +from pycsdr.modules import Fft, LogPower, LogAveragePower, FftSwap, FftAdpcm + + +class FftAverager(Chain): + def __init__(self, fft_size, fft_averages): + self.fftSize = fft_size + self.fftAverages = fft_averages + workers = [self._getWorker()] + super().__init__(workers) + + def setFftAverages(self, fft_averages): + if self.fftAverages == fft_averages: + return + self.fftAverages = fft_averages + self.replace(0, self._getWorker()) + + def _getWorker(self): + if self.fftAverages == 0: + return LogPower(add_db=-70) + else: + return LogAveragePower(add_db=-70, fft_size=self.fftSize, avg_number=self.fftAverages) + + +class FftChain(Chain): + def __init__(self, samp_rate, fft_size, fft_v_overlap_factor, fft_fps, fft_compression): + self.sampleRate = samp_rate + self.vOverlapFactor = fft_v_overlap_factor + self.fps = fft_fps + self.size = fft_size + + self.blockSize = 0 + + self.fft = Fft(size=self.size, every_n_samples=self.blockSize) + self.averager = FftAverager(fft_size=self.size, fft_averages=10) + self.fftExchangeSides = FftSwap(fft_size=self.size) + workers = [ + self.fft, + self.averager, + self.fftExchangeSides, + ] + self.compressFftAdpcm = None + if fft_compression == "adpcm": + self.compressFftAdpcm = FftAdpcm(fft_size=self.size) + workers += [self.compressFftAdpcm] + + self._updateParameters() + + super().__init__(workers) + + def _setBlockSize(self, fft_block_size): + if self.blockSize == int(fft_block_size): + return + self.blockSize = int(fft_block_size) + self.fft.setEveryNSamples(self.blockSize) + + def setVOverlapFactor(self, fft_v_overlap_factor): + if self.vOverlapFactor == fft_v_overlap_factor: + return + self.vOverlapFactor = fft_v_overlap_factor + self._updateParameters() + + def setFps(self, fft_fps): + if self.fps == fft_fps: + return + self.fps = fft_fps + self._updateParameters() + + def setSampleRate(self, samp_rate): + if self.sampleRate == samp_rate: + return + self.sampleRate = samp_rate + self._updateParameters() + + def _updateParameters(self): + fftAverages = 0 + + if self.vOverlapFactor > 0: + fftAverages = int(round(1.0 * self.sampleRate / self.size / self.fps / (1.0 - self.vOverlapFactor))) + self.averager.setFftAverages(fftAverages) + + if fftAverages == 0: + self._setBlockSize(self.sampleRate / self.fps) + else: + self._setBlockSize(self.sampleRate / self.fps / fftAverages) + + def setCompression(self, compression: str) -> None: + if compression == "adpcm" and not self.compressFftAdpcm: + self.compressFftAdpcm = FftAdpcm(self.size) + # should always be at the end + self.append(self.compressFftAdpcm) + elif compression == "none" and self.compressFftAdpcm: + self.compressFftAdpcm.stop() + self.compressFftAdpcm = None + # should always be at that position (right?) + self.remove(3) diff --git a/csdr/chain/freedv.py b/csdr/chain/freedv.py new file mode 100644 index 000000000..97e2061cd --- /dev/null +++ b/csdr/chain/freedv.py @@ -0,0 +1,28 @@ +from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain +from csdr.module.freedv import FreeDVModule +from pycsdr.modules import RealPart, Agc, Convert +from pycsdr.types import Format + + +class FreeDV(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain): + def __init__(self): + agc = Agc(Format.SHORT) + agc.setMaxGain(30) + agc.setInitialGain(3) + workers = [ + RealPart(), + Agc(Format.FLOAT), + Convert(Format.FLOAT, Format.SHORT), + FreeDVModule(), + agc, + ] + super().__init__(workers) + + def getFixedIfSampleRate(self) -> int: + return 8000 + + def getFixedAudioRate(self) -> int: + return 8000 + + def supportsSquelch(self) -> bool: + return False diff --git a/csdr/chain/m17.py b/csdr/chain/m17.py new file mode 100644 index 000000000..937103c3e --- /dev/null +++ b/csdr/chain/m17.py @@ -0,0 +1,29 @@ +from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, MetaProvider +from csdr.module.m17 import M17Module +from pycsdr.modules import FmDemod, Limit, Convert, Writer, DcBlock +from pycsdr.types import Format + + +class M17(BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, MetaProvider): + def __init__(self): + self.module = M17Module() + workers = [ + FmDemod(), + DcBlock(), + Limit(), + Convert(Format.FLOAT, Format.SHORT), + self.module, + ] + super().__init__(workers) + + def getFixedIfSampleRate(self) -> int: + return 48000 + + def getFixedAudioRate(self) -> int: + return 8000 + + def supportsSquelch(self) -> bool: + return False + + def setMetaWriter(self, writer: Writer) -> None: + self.module.setMetaWriter(writer) diff --git a/csdr/chain/redsea.py b/csdr/chain/redsea.py new file mode 100644 index 000000000..a03da6866 --- /dev/null +++ b/csdr/chain/redsea.py @@ -0,0 +1,14 @@ +from csdr.chain import Chain +from pycsdr.modules import Convert +from pycsdr.types import Format +from owrx.rds.redsea import RedseaModule +from csdr.module import JsonParser + + +class Redsea(Chain): + def __init__(self, sampleRate: int, rbds: bool): + super().__init__([ + Convert(Format.FLOAT, Format.SHORT), + RedseaModule(sampleRate, rbds), + JsonParser("WFM"), + ]) diff --git a/csdr/chain/rtl433.py b/csdr/chain/rtl433.py new file mode 100644 index 000000000..34d4842ae --- /dev/null +++ b/csdr/chain/rtl433.py @@ -0,0 +1,18 @@ +from owrx.ism.rtl433 import Rtl433Module, IsmParser +from csdr.chain.demodulator import ServiceDemodulator + + +class Rtl433(ServiceDemodulator): + def getFixedAudioRate(self) -> int: + return 1200000 + + def __init__(self): + super().__init__( + [ + Rtl433Module(), + IsmParser(), + ] + ) + + def supportsSquelch(self) -> bool: + return False diff --git a/csdr/chain/selector.py b/csdr/chain/selector.py new file mode 100644 index 000000000..74a86361b --- /dev/null +++ b/csdr/chain/selector.py @@ -0,0 +1,187 @@ +from csdr.chain import Chain +from pycsdr.modules import Shift, FirDecimate, Bandpass, Squelch, FractionalDecimator, Writer +from pycsdr.types import Format +from typing import Union +import math + + +class Decimator(Chain): + def __init__(self, inputRate: int, outputRate: int): + if outputRate > inputRate: + raise ValueError("impossible decimation: cannot upsample {} to {}".format(inputRate, outputRate)) + self.inputRate = inputRate + self.outputRate = outputRate + + decimation, fraction = self._getDecimation(outputRate) + transition = 0.15 * (outputRate / float(self.inputRate)) + # set the cutoff on the fist decimation stage lower so that the resulting output + # is already prepared for the second (fractional) decimation stage. + # this spares us a second filter. + cutoff = 0.5 * decimation / (self.inputRate / outputRate) + + workers = [ + FirDecimate(decimation, transition, cutoff), + ] + + if fraction != 1.0: + workers += [FractionalDecimator(Format.COMPLEX_FLOAT, fraction)] + + super().__init__(workers) + + def _getDecimation(self, outputRate: int) -> (int, float): + if outputRate > self.inputRate: + raise SelectorError( + "cannot provide selected output rate {} since it is bigger than input rate {}".format( + outputRate, + self.inputRate + ) + ) + d = self.inputRate / outputRate + dInt = int(d) + dFloat = float(self.inputRate / dInt) / outputRate + return dInt, dFloat + + def _reconfigure(self): + decimation, fraction = self._getDecimation(self.outputRate) + transition = 0.15 * (self.outputRate / float(self.inputRate)) + cutoff = 0.5 * decimation / (self.inputRate / self.outputRate) + self.replace(0, FirDecimate(decimation, transition, cutoff)) + index = self.indexOf(lambda x: isinstance(x, FractionalDecimator)) + if fraction != 1.0: + decimator = FractionalDecimator(Format.COMPLEX_FLOAT, fraction) + if index >= 0: + self.replace(index, decimator) + else: + self.append(decimator) + elif index >= 0: + self.remove(index) + + def setOutputRate(self, outputRate: int) -> None: + if outputRate == self.outputRate: + return + self.outputRate = outputRate + self._reconfigure() + + def setInputRate(self, inputRate: int) -> None: + if inputRate == self.inputRate: + return + self.inputRate = inputRate + self._reconfigure() + + +class Selector(Chain): + def __init__(self, inputRate: int, outputRate: int, withSquelch: bool = True): + self.inputRate = inputRate + self.outputRate = outputRate + self.frequencyOffset = 0 + + self.shift = Shift(0.0) + + self.decimation = Decimator(inputRate, outputRate) + + self.bandpass = self._buildBandpass() + self.bandpassCutoffs = [None, None] + + workers = [self.shift, self.decimation] + + if withSquelch: + self.readings_per_second = 4 + # s-meter readings are available every 1024 samples + # the reporting interval is measured in those 1024-sample blocks + self.squelch = Squelch(5, int(outputRate / (self.readings_per_second * 1024))) + workers += [self.squelch] + + super().__init__(workers) + + def _buildBandpass(self) -> Bandpass: + bp_transition = 320.0 / self.outputRate + return Bandpass(transition=bp_transition, use_fft=True) + + def setFrequencyOffset(self, offset: int) -> None: + if offset == self.frequencyOffset: + return + self.frequencyOffset = offset + self._updateShift() + + def _updateShift(self): + shift = -self.frequencyOffset / self.inputRate + self.shift.setRate(shift) + + def _convertToLinear(self, db: float) -> float: + return float(math.pow(10, db / 10)) + + def setSquelchLevel(self, level: float) -> None: + self.squelch.setSquelchLevel(self._convertToLinear(level)) + + def _enableBandpass(self): + index = self.indexOf(lambda x: isinstance(x, Bandpass)) + if index < 0: + self.insert(2, self.bandpass) + + def _disableBandpass(self): + index = self.indexOf(lambda x: isinstance(x, Bandpass)) + if index >= 0: + self.remove(index) + + def setBandpass(self, lowCut: float, highCut: float) -> None: + self.bandpassCutoffs = [lowCut, highCut] + if None in self.bandpassCutoffs: + self._disableBandpass() + else: + self._enableBandpass() + scaled = [x / self.outputRate for x in self.bandpassCutoffs] + self.bandpass.setBandpass(*scaled) + + def setLowCut(self, lowCut: Union[float, None]) -> None: + self.bandpassCutoffs[0] = lowCut + self.setBandpass(*self.bandpassCutoffs) + + def setHighCut(self, highCut: Union[float, None]) -> None: + self.bandpassCutoffs[1] = highCut + self.setBandpass(*self.bandpassCutoffs) + + def setPowerWriter(self, writer: Writer) -> None: + self.squelch.setPowerWriter(writer) + + def setOutputRate(self, outputRate: int) -> None: + if outputRate == self.outputRate: + return + self.outputRate = outputRate + + self.decimation.setOutputRate(outputRate) + self.squelch.setReportInterval(int(outputRate / (self.readings_per_second * 1024))) + index = self.indexOf(lambda x: isinstance(x, Bandpass)) + self.bandpass = self._buildBandpass() + self.setBandpass(*self.bandpassCutoffs) + if index >= 0: + self.replace(index, self.bandpass) + + def setInputRate(self, inputRate: int) -> None: + if inputRate == self.inputRate: + return + self.inputRate = inputRate + self.decimation.setInputRate(inputRate) + self._updateShift() + + +class SecondarySelector(Chain): + def __init__(self, sampleRate: int, bandwidth: float): + self.sampleRate = sampleRate + self.frequencyOffset = 0 + self.shift = Shift(0.0) + cutoffRate = bandwidth / sampleRate + self.bandpass = Bandpass(-cutoffRate, cutoffRate, cutoffRate, use_fft=True) + workers = [self.shift, self.bandpass] + super().__init__(workers) + + def setFrequencyOffset(self, offset: int) -> None: + if offset == self.frequencyOffset: + return + self.frequencyOffset = offset + if self.frequencyOffset is None: + return + self.shift.setRate(-offset / self.sampleRate) + + +class SelectorError(Exception): + pass diff --git a/csdr/module/__init__.py b/csdr/module/__init__.py new file mode 100644 index 000000000..c1fdd88b2 --- /dev/null +++ b/csdr/module/__init__.py @@ -0,0 +1,231 @@ +from pycsdr.modules import Module as BaseModule +from pycsdr.modules import Reader, Writer, Buffer +from pycsdr.types import Format +from abc import ABCMeta, abstractmethod +from threading import Thread +from io import BytesIO +from subprocess import Popen, PIPE, TimeoutExpired +from functools import partial +import pickle +import logging +import json + +logger = logging.getLogger(__name__) + + +class Module(BaseModule, metaclass=ABCMeta): + def __init__(self): + self.reader = None + self.writer = None + super().__init__() + + def setReader(self, reader: Reader) -> None: + self.reader = reader + + def setWriter(self, writer: Writer) -> None: + self.writer = writer + + @abstractmethod + def getInputFormat(self) -> Format: + pass + + @abstractmethod + def getOutputFormat(self) -> Format: + pass + + def pump(self, read, write): + def copy(): + while True: + data = None + try: + data = read() + except ValueError: + pass + except BrokenPipeError: + break + if data is None or isinstance(data, bytes) and len(data) == 0: + break + try: + write(data) + except BrokenPipeError: + break + + return copy + + +class AutoStartModule(Module, metaclass=ABCMeta): + def _checkStart(self) -> None: + if self.reader is not None and self.writer is not None: + self.start() + + def setReader(self, reader: Reader) -> None: + super().setReader(reader) + self._checkStart() + + def setWriter(self, writer: Writer) -> None: + super().setWriter(writer) + self._checkStart() + + @abstractmethod + def start(self): + pass + + +class ThreadModule(AutoStartModule, Thread, metaclass=ABCMeta): + def __init__(self): + self.doRun = True + super().__init__() + Thread.__init__(self) + + @abstractmethod + def run(self): + pass + + def stop(self): + self.doRun = False + self.reader.stop() + + def start(self): + # don't start twice. + if self.is_alive(): + return + Thread.start(self) + + +class PickleModule(ThreadModule): + def getInputFormat(self) -> Format: + return Format.CHAR + + def getOutputFormat(self) -> Format: + return Format.CHAR + + def run(self): + while self.doRun: + data = self.reader.read() + if data is None: + self.doRun = False + break + io = BytesIO(data.tobytes()) + try: + while True: + output = self.process(pickle.load(io)) + if output is not None: + self.writer.write(pickle.dumps(output)) + except EOFError: + pass + + @abstractmethod + def process(self, input): + pass + + +class LineBasedModule(ThreadModule, metaclass=ABCMeta): + def __init__(self): + self.retained = bytes() + super().__init__() + + def getInputFormat(self) -> Format: + return Format.CHAR + + def getOutputFormat(self) -> Format: + return Format.CHAR + + def run(self): + while self.doRun: + data = self.reader.read() + if data is None: + self.doRun = False + else: + self.retained += data + lines = self.retained.split(b"\n") + + # keep the last line + # this should either be empty if the last char was \n + # or an incomplete line if the read returned early + self.retained = lines[-1] + + # log all completed lines + for line in lines[0:-1]: + parsed = self.process(line) + if parsed is not None: + self.writer.write(pickle.dumps(parsed)) + + @abstractmethod + def process(self, line: bytes) -> any: + pass + + +class JsonParser(LineBasedModule): + def __init__(self, mode: str): + self.mode = mode + super().__init__() + + def process(self, line): + try: + msg = json.loads(line) + msg["mode"] = self.mode + logger.debug(msg) + return msg + except json.JSONDecodeError: + logger.exception("error parsing decoder json") + + +class PopenModule(AutoStartModule, metaclass=ABCMeta): + def __init__(self): + self.process = None + super().__init__() + + @abstractmethod + def getCommand(self): + pass + + def _getProcess(self): + return Popen(self.getCommand(), stdin=PIPE, stdout=PIPE) + + def start(self): + self.process = self._getProcess() + # resume in case the reader has been stop()ed before + self.reader.resume() + Thread(target=self.pump(self.reader.read, self.process.stdin.write)).start() + Thread(target=self.pump(partial(self.process.stdout.read1, 1024), self.writer.write)).start() + + def stop(self): + if self.process is not None: + # Try terminating normally, kill if failed to terminate + try: + self.process.terminate() + self.process.wait(3) + except TimeoutExpired: + self.process.kill() + self.process = None + self.reader.stop() + + +class LogReader(Thread): + def __init__(self, prefix: str, buffer: Buffer): + self.reader = buffer.getReader() + self.logger = logging.getLogger(prefix) + self.retained = bytes() + super().__init__() + self.start() + + def run(self) -> None: + while True: + data = self.reader.read() + if data is None: + return + + self.retained += data + lines = self.retained.split(b"\n") + + # keep the last line + # this should either be empty if the last char was \n + # or an incomplete line if the read returned early + self.retained = lines[-1] + + # log all completed lines + for line in lines[0:-1]: + self.logger.info("{}: {}".format("STDOUT", line.decode(errors="replace"))) + + def stop(self): + self.reader.stop() diff --git a/csdr/module/drm.py b/csdr/module/drm.py new file mode 100644 index 000000000..a7515bd8d --- /dev/null +++ b/csdr/module/drm.py @@ -0,0 +1,11 @@ +from pycsdr.modules import ExecModule +from pycsdr.types import Format + + +class DrmModule(ExecModule): + def __init__(self): + super().__init__( + Format.COMPLEX_SHORT, + Format.SHORT, + ["dream", "-c", "6", "--sigsrate", "48000", "--audsrate", "48000", "-I", "-", "-O", "-"] + ) diff --git a/csdr/module/freedv.py b/csdr/module/freedv.py new file mode 100644 index 000000000..90bc38a70 --- /dev/null +++ b/csdr/module/freedv.py @@ -0,0 +1,11 @@ +from pycsdr.types import Format +from pycsdr.modules import ExecModule + + +class FreeDVModule(ExecModule): + def __init__(self): + super().__init__( + Format.SHORT, + Format.SHORT, + ["freedv_rx", "1600", "-", "-"] + ) diff --git a/csdr/module/m17.py b/csdr/module/m17.py new file mode 100644 index 000000000..5bcf5449f --- /dev/null +++ b/csdr/module/m17.py @@ -0,0 +1,58 @@ +from csdr.module import PopenModule +from pycsdr.types import Format +from pycsdr.modules import Writer +from subprocess import Popen, PIPE +from threading import Thread + +import re +import pickle + + +class M17Module(PopenModule): + lsfRegex = re.compile("SRC: ([a-zA-Z0-9]+), DEST: ([a-zA-Z0-9]+)") + + def __init__(self): + super().__init__() + self.metawriter = None + + def getInputFormat(self) -> Format: + return Format.SHORT + + def getOutputFormat(self) -> Format: + return Format.SHORT + + def getCommand(self): + return ["m17-demod", "-l"] + + def _getProcess(self): + return Popen(self.getCommand(), stdin=PIPE, stdout=PIPE, stderr=PIPE) + + def start(self): + super().start() + Thread(target=self._readOutput).start() + + def _readOutput(self): + while True: + line = self.process.stderr.readline() + if not line: + break + self.parseOutput(line.decode()) + + def parseOutput(self, line): + if self.metawriter is None: + return + matches = self.lsfRegex.match(line) + msg = {"protocol": "M17"} + if matches: + # fake sync + msg["sync"] = "voice" + msg["source"] = matches.group(1) + msg["destination"] = matches.group(2) + elif line.startswith("EOS"): + pass + else: + return + self.metawriter.write(pickle.dumps(msg)) + + def setMetaWriter(self, writer: Writer) -> None: + self.metawriter = writer diff --git a/csdr/module/msk144.py b/csdr/module/msk144.py new file mode 100644 index 000000000..532ed67fa --- /dev/null +++ b/csdr/module/msk144.py @@ -0,0 +1,33 @@ +from pycsdr.types import Format +from pycsdr.modules import ExecModule +from csdr.module import LineBasedModule +from owrx.wsjt import WsjtParser, Msk144Profile +import pickle + +import logging +logger = logging.getLogger(__name__) + + +class Msk144Module(ExecModule): + def __init__(self): + super().__init__( + Format.SHORT, + Format.CHAR, + ["msk144decoder"] + ) + + +class ParserAdapter(LineBasedModule): + def __init__(self): + self.parser = WsjtParser() + self.dialFrequency = 0 + self.profile = Msk144Profile() + super().__init__() + + def process(self, line: bytes): + # actual messages from msk144decoder should start with "*** " + if line[0:4] == b"*** ": + return self.parser.parse(self.profile, self.dialFrequency, line[4:]) + + def setDialFrequency(self, frequency: int) -> None: + self.dialFrequency = frequency diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 000000000..aa95d2f47 --- /dev/null +++ b/debian/changelog @@ -0,0 +1,256 @@ +openwebrx (1.3.0) UNRELEASED; urgency=low + * SDR device log messages are now available in the web configuration to + simplify troubleshooting + * Added support for the MSK144 digimode + * Added support for decoding ADS-B with dump1090 + * Added support for decoding HFDL and VDL2 aircraft communications + * Added decoding of ISM band transmissions using rtl_433 + * Added support for decoding RDS data on WFM broadcasts using redsea decoder + * Added decoding for DAB broadcast stations using csdr-eti and dablin + * Added IPv6 support + * Added MQTT support + * New devices supported: + - Afedri SDR-Net + + -- Jakob Ketterl Fri, 30 Sep 2022 16:47:00 +0000 + +openwebrx (1.2.2) bullseye jammy; urgency=high + + * - Fixed an over-the-air code injection vulnerability + + -- Jakob Ketterl Sun, 08 Oct 2023 21:29:00 +0000 + +openwebrx (1.2.1) bullseye jammy; urgency=low + + * FifiSDR support fixed (pipeline formats now line up correctly) + * Added "Device" input for FifiSDR devices for sound card selection + + -- Jakob Ketterl Tue, 20 Sep 2022 16:01:00 +0000 + +openwebrx (1.2.0) bullseye jammy; urgency=low + + * Major rewrite of all demodulation components to make use of the new + csdr/pycsdr and digiham/pydigiham demodulator modules + * Preliminary display of M17 callsign information + * New devices supported: + - Blade RF + + -- Jakob Ketterl Wed, 15 Jun 2022 16:20:00 +0000 + +openwebrx (1.1.0) buster hirsute; urgency=low + + * Reworked most graphical elements as SVGs for faster loadtimes and crispier + display on hi-dpi displays + * Updated pipelines to match changes in digiham + * Changed D-Star and NXDN integrations to use new decoder from digiham + * Added D-Star and NXDN metadata display + + -- Jakob Ketterl Mon, 02 Aug 2021 16:24:00 +0000 + +openwebrx (1.0.0) buster hirsute; urgency=low + * Introduced `squelch_auto_margin` config option that allows configuring the + auto squelch level + * Removed `port` configuration option; `rtltcp_compat` takes the port number + with the new connectors + * Added support for new WSJT-X modes FST4, FST4W (only available with WSJT-X + 2.3) and Q65 (only available with WSJT-X 2.4) + * Added support for demodulating M17 digital voice signals using + m17-cxx-demod + * New reporting infrastructure, allowing WSPR and FST4W spots to be sent to + wsprnet.org + * Add some basic filtering capabilities to the map + * New arguments to the `openwebrx` command-line to facilitate the + administration of users (try `openwebrx admin`) + * New command-line tool `openwebrx-admin` that facilitates the + administration of users + * Default bandwidth changes: + - "WFM" changed to 150kHz + - "Packet" (APRS) changed to 12.5kHz + * Configuration rework: + - New: fully web-based configuration interface + - System configuration parameters have been moved to a new, separate + `openwebrx.conf` file + - Remaining parameters are now editable in the web configuration + - Existing `config_webrx.py` files will still be read, but changes made in + the web configuration will be written to a new storage system + - Added upload of avatar and panorama image via web configuration + * New devices supported: + - HPSDR devices (Hermes Lite 2) thanks to @jancona + - BBRF103 / RX666 / RX888 devices supported by libsddc + - R&S devices using the EB200 or Ammos protocols + + -- Jakob Ketterl Thu, 06 May 2021 17:22:00 +0000 + +openwebrx (0.20.3) buster focal; urgency=low + + * Fix a compatibility issue with python versions <= 3.6 + + -- Jakob Ketterl Tue, 26 Jan 2021 15:28:00 +0000 + +openwebrx (0.20.2) buster focal; urgency=high + + * Fix a security problem that allowed arbitrary commands to be executed on + the receiver (See github issue #215: + https://github.com/jketterl/openwebrx/issues/215) + + -- Jakob Ketterl Sun, 24 Jan 2021 22:50:00 +0000 + +openwebrx (0.20.1) buster focal; urgency=low + + * Remove broken OSM map fallback + + -- Jakob Ketterl Mon, 30 Nov 2020 17:29:00 +0000 + +openwebrx (0.20.0) buster focal; urgency=low + + * Added the ability to sign multiple keys in a single request, thus enabling + multiple users to claim a single receiver on receiverbook.de + * Fixed file descriptor leaks to prevent "too many open files" errors + * Add new demodulator chain for FreeDV + * Added new HD audio streaming mode along with a new WFM demodulator + * Reworked AGC code for better results in AM, SSB and digital modes + * Added support for demodulation of "Digital Radio Mondiale" (DRM) broadcast + using the "dream" decoder. + * New default waterfall color scheme + * Prototype of a continuous automatic waterfall calibration mode + * New devices supported: + - FunCube Dongle Pro+ (`"type": "fcdpp"`) + - Support for connections to rtl_tcp (`"type": "rtl_tcp"`) + + -- Jakob Ketterl Sun, 11 Oct 2020 13:02:00 +0000 + +openwebrx (0.19.1) buster focal; urgency=low + + * Added ability to authenticate receivers with listing sites using + "receiver id" tokens + + -- Jakob Ketterl Sat, 13 Jun 2020 16:46:00 +0000 + +openwebrx (0.19.0) buster focal; urgency=low + * Fix direwolf connection setup by implementing a retry loop + * Pass direct sampling mode changes for rtl_sdr_soapy to owrx_connector + * OSM maps instead of Google when google_maps_api_key is not set (thanks + @jquagga) + * Improved logic to pass parameters to soapy devices. + - `rtl_sdr_soapy`: added support for `bias_tee` + - `sdrplay`: added support for `bias_tee`, `rf_notch` and `dab_notch` + - `airspy`: added support for `bitpack` + * Added support for Perseus-SDR devices, (thanks @amontefusco) + * Property System has been rewritten so that defaults on sdr behave as + expected + * Waterfall range auto-adjustment now only takes the center 80% of the + spectrum into account, which should work better with SDRs that oversample + or have rather flat filter curves towards the spectrum edges + * Bugfix for negative network usage + * FiFi SDR: prevent arecord from shutting down after 2GB of data has been + sent + * Added support for bias tee control on rtl_sdr devices + * All connector driven SDRs now support `"rf_gain": "auto"` to enable AGC + * `rtl_sdr` type now also supports the `direct_sampling` option + * Added decoding implementation for for digimode "JS8Call" (requires an + installation of js8call and the js8py library) + * Reorganization of the frontend demodulator code + * Improve receiver load time by concatenating javascript assets + * HackRF support is now based on SoapyHackRF + * Removed sdr.hu server listing support since the site has been shut down + * Added support for Radioberry 2 Rasbperry Pi SDR Cape + + -- Jakob Ketterl Mon, 01 Jun 2020 17:02:00 +0000 + +openwebrx (0.18.0) buster; urgency=low + + * Compression, resampling and filtering in the frontend have been rewritten + in javascript, sdr.js has been removed + * Decoding of Pocsag modulation is now possible + * Removed the 3D waterfall since it had no real application and required ~1MB + of javascript code to be downloaded + * Improved the frontend handling of the "too many users" scenario + * PSK63 digimode is now available (same decoding pipeline as PSK31, but with + adopted parameters) + * The frequency can now be manipulated with the mousewheel, which should + allow the user to tune more precise. The tuning step size is determined by + the digit the mouse cursor is hovering over. + * Clicking on the frequency now opens an input for direct frequency selection + * URL hashes have been fixed and improved: They are now updated + automatically, so a shared URL will include frequency and demodulator, + which allows for improved sharing and linking. + * New daylight scheduler for background decoding, allows profiles to be + selected by local sunrise / sunset times + * The owrx_connector is now the default way of communicating with sdr + devices. The old sdr types have been replaced, all `_connector` suffixes on + the type must be removed! + * The sources have been refactored, making it a lot easier to add support for + other devices + * SDR device failure handling has been improved, including user feedback + * New devices supported: + * wsjt-x updated to 2.1.2 + * The rtl_tcp compatibility mode of the owrx_connector is now configurable + using the `rtltcp_compat` flag + * explicit device filter for soapy devices for multi-device setups + * compatibility fixes for safari browsers (ios and mac) + * Offset tuning using the `lfo_offset` has been reworked in a way that + `center_freq` has to be set to the frequency you actually want to listen + to. If you're using an `lfo_offset` already, you will probably need to + change its sign. + * `initial_squelch_level` can now be set on each profile. + * Part of the frontend code has been reworked + - Audio buffer minimums have been completely stripped. As a result, you + should get better latency. Unfortunately, this also means there will be + some skipping when audio starts. + - Now also supports AudioWorklets (for those browser that have it). + - Mousewheel controls for the receiver sliders + * Error handling for failed SDR devices + * One of the most-requested features is finally coming to OpenWebRX: + Bookmarks (sometimes also referred to as labels). + There's two kinds of bookmarks available: + - Serverside bookmarks that are set up by the receiver administrator. + Check the file `bookmarks.json` for examples! + - Clientside bookmarks which every user can store for themselves. They are + stored in the browser's localStorage. + * Automatic reporting of spots to [pskreporter](https://pskreporter.info/) is + now possible. Please have a look at the configuration on how to set it up. + * Websocket communication has been overhauled in large parts. It should now + be more reliable, and failing connections should now have no impact on + other users. + * Profile scheduling allows to set up band-hopping if you are running + background services. + * APRS now has the ability to show symbols on the map, if a corresponding + symbol set has been installed. Check the config! + * Debug logging has been disabled in a handful of modules, expect vastly + reduced output on the shell. + * New set of APRS-related features + - Decode Packet transmissions using direwolf (1k2 only for now) + - APRS packets are mostly decoded and shown both in a new panel and on the + map + - APRS is also available as a background service + - direwolfs I-gate functionality can be enabled, which allows your receiver + to work as a receive-only I-gate for the APRS network in the background + * Demodulation for background services has been optimized to use less total + bandwidth, saving CPU + * More metrics have been added; they can be used together with collectd and + its curl_json plugin for now, with some limitations. + * New bandplan feature, the first thing visible is the "dial" indicator that + brings you right to the dial frequency for digital modes + * fixed some bugs in the websocket communication which broke the map + * WSJT-X integration (FT8, FT4, WSPR, JT65, JT9 using wsjt-x demodulators) + * New Map Feature that shows both decoded grid squares from FT8 and Locations + decoded from YSF digital voice + * New Feature report that will show what functionality is available + * major rework on the openwebrx core + * Support of multiple SDR devices simultaneously + * Support for multiple profiles per SDR that allow the user to listen to + different frequencies + * Support for digital voice decoding + * Feature detection that will disable functionality when dependencies are not + available (if you're missing the digital + buttons, this is probably why) + * Support added for the following SDR sources: + - LimeSDR (`"type": "lime_sdr"`) + - PlutoSDR (`"type": "pluto_sdr"`) + - RTL_SDR via Soapy (`"type": "rtl_sdr_soapy"`) on special request to allow + use of the direct sampling mode + - SoapyRemote (`"type": "soapy_remote"`) + - FiFiSDR (`"type": "fifi_sdr"`) + - airspyhf devices (Airspy HF+ / Discovery) (`"type": "airspyhf"`) + + -- Jakob Ketterl Tue, 18 Feb 2020 20:09:00 +0000 diff --git a/debian/compat b/debian/compat new file mode 100644 index 000000000..f599e28b8 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +10 diff --git a/debian/control b/debian/control new file mode 100644 index 000000000..ae30de550 --- /dev/null +++ b/debian/control @@ -0,0 +1,50 @@ +Source: openwebrx +Maintainer: Jakob Ketterl +Section: hamradio +Priority: optional +Rules-Requires-Root: no +Standards-Version: 4.2.0 +Build-Depends: debhelper (>= 11), + dh-python, + python3-all (>= 3.5), + python3-setuptools +Homepage: https://www.openwebrx.de/ +Vcs-Browser: https://github.com/jketterl/openwebrx +Vcs-Git: https://github.com/jketterl/openwebrx.git + +Package: openwebrx +Architecture: all +Depends: adduser, + python3 (>= 3.5), + python3-setuptools, + owrx-connector (>= 0.7), + python3-csdr (>= 0.19), + ${python3:Depends}, + ${misc:Depends} +Recommends: python3-digiham (>= 0.6), + direwolf (>= 1.4), + wsjtx, + js8call, + runds-connector (>= 0.2), + hpsdrconnector, + aprs-symbols, + m17-demod, + js8call, + python3-js8py (>= 0.2), + nmux (>= 0.18), + codecserver (>= 0.1), + msk144decoder, + dump1090-fa-minimal, + dumphfdl, + dumpvdl2, + rtl-433, + extra-sdr-drivers, + perseus-tools, + dream-headless, + codec2, + redsea, + python3-csdr-eti, + dablin, + python3-paho-mqtt +Description: multi-user web sdr + Open source, multi-user SDR receiver with a web interface diff --git a/debian/openwebrx.config b/debian/openwebrx.config new file mode 100755 index 000000000..2f17fb9a4 --- /dev/null +++ b/debian/openwebrx.config @@ -0,0 +1,9 @@ +#!/bin/sh -e +. /usr/share/debconf/confmodule + +db_get openwebrx/admin_user_configured +if [ "${1:-}" = "reconfigure" ] || [ "${RET}" != true ]; then + db_settitle openwebrx/title + db_input high openwebrx/admin_user_password || true + db_go +fi diff --git a/debian/openwebrx.desktop b/debian/openwebrx.desktop new file mode 100644 index 000000000..e28674cf2 --- /dev/null +++ b/debian/openwebrx.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Version=1.0 +Name=OpenWebRX +Type=Application +Comment=Web-based software defined radio receiver +Icon=openwebrx +Exec=xdg-open http://localhost:8073/ +Categories=Network;HamRadio diff --git a/debian/openwebrx.dirs b/debian/openwebrx.dirs new file mode 100644 index 000000000..c87b1b22d --- /dev/null +++ b/debian/openwebrx.dirs @@ -0,0 +1 @@ +/etc/openwebrx/openwebrx.conf.d \ No newline at end of file diff --git a/debian/openwebrx.install b/debian/openwebrx.install new file mode 100644 index 000000000..9b89f9f26 --- /dev/null +++ b/debian/openwebrx.install @@ -0,0 +1,5 @@ +bands.json etc/openwebrx/ +openwebrx.conf etc/openwebrx/ +systemd/openwebrx.service lib/systemd/system/ +debian/openwebrx.svg usr/share/icons/hicolor/scalable/apps +debian/openwebrx.desktop usr/share/applications \ No newline at end of file diff --git a/debian/openwebrx.postinst b/debian/openwebrx.postinst new file mode 100755 index 000000000..a7e0a5138 --- /dev/null +++ b/debian/openwebrx.postinst @@ -0,0 +1,66 @@ +#!/bin/bash +. /usr/share/debconf/confmodule + +set -euo pipefail + +OWRX_USER="openwebrx" +OWRX_DATADIR="/var/lib/openwebrx" +OWRX_USERS_FILE="${OWRX_DATADIR}/users.json" +OWRX_SETTINGS_FILE="${OWRX_DATADIR}/settings.json" +OWRX_BOOKMARKS_FILE="${OWRX_DATADIR}/bookmarks.json" + +case "$1" in + configure|reconfigure) + adduser --system --group --no-create-home --home /nonexistent --quiet "${OWRX_USER}" + usermod -aG plugdev "${OWRX_USER}" + + # ensure group exists first (dependency is optional) + # addgroup will error out if the group exists, but is not a system group. it doesn't matter for the intended purpose, but we need extra protection for this case. + if [ ! $(getent group perseususb) ]; then + addgroup --system --quiet perseususb + fi + usermod -aG perseususb "${OWRX_USER}" + + # create OpenWebRX data directory and set the correct permissions + if [ ! -d "${OWRX_DATADIR}" ] && [ ! -L "${OWRX_DATADIR}" ]; then mkdir "${OWRX_DATADIR}"; fi + chown "${OWRX_USER}": ${OWRX_DATADIR} + + # create empty config files now to avoid permission problems later + if [ ! -e "${OWRX_USERS_FILE}" ]; then + echo "[]" > "${OWRX_USERS_FILE}" + chown "${OWRX_USER}": "${OWRX_USERS_FILE}" + chmod 0600 "${OWRX_USERS_FILE}" + fi + + if [ ! -e "${OWRX_SETTINGS_FILE}" ]; then + echo "{}" > "${OWRX_SETTINGS_FILE}" + chown "${OWRX_USER}": "${OWRX_SETTINGS_FILE}" + fi + + if [ ! -e "${OWRX_BOOKMARKS_FILE}" ]; then + touch "${OWRX_BOOKMARKS_FILE}" + chown "${OWRX_USER}": "${OWRX_BOOKMARKS_FILE}" + fi + + db_get openwebrx/admin_user_password + if [ ! -z "${RET}" ]; then + if ! openwebrx admin --silent hasuser admin; then + # create initial openwebrx user + OWRX_PASSWORD="${RET}" openwebrx admin --noninteractive adduser admin + else + # change existing user's password + OWRX_PASSWORD="${RET}" openwebrx admin --noninteractive resetpassword admin + fi + fi + # remove password from debconf database + db_unregister openwebrx/admin_user_password + # set a marker that admin is configured to avoid future questions + db_set openwebrx/admin_user_configured true + ;; + *) + echo "postinst called with unknown argument '$1'" 1>&2 + exit 1 + ;; +esac + +#DEBHELPER# diff --git a/debian/openwebrx.postrm b/debian/openwebrx.postrm new file mode 100755 index 000000000..9260b8ec4 --- /dev/null +++ b/debian/openwebrx.postrm @@ -0,0 +1,8 @@ +#!/bin/sh -e + +if [ "$1" = purge ] && [ -e /usr/share/debconf/confmodule ]; then + . /usr/share/debconf/confmodule + db_purge +fi + +#DEBHELPER# diff --git a/debian/openwebrx.svg b/debian/openwebrx.svg new file mode 100644 index 000000000..50f4acdde --- /dev/null +++ b/debian/openwebrx.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/debian/openwebrx.templates b/debian/openwebrx.templates new file mode 100644 index 000000000..6819fbadc --- /dev/null +++ b/debian/openwebrx.templates @@ -0,0 +1,27 @@ +Template: openwebrx/admin_user_password +Type: password +Description: OpenWebRX "admin" user password: + The system can create a user for the OpenWebRX web configuration interface for + you. Using this user, you will be able to log into the "settings" area of + OpenWebRX to configure your receiver conveniently through your browser. + . + The name of the created user will be "admin". + . + If you do not wish to create a web admin user right now, you can leave this + empty for now. You can return to this prompt at a later time by running the + command "sudo dpkg-reconfigure openwebrx". + . + You can also use the "openwebrx admin" command to create, delete or manage + existing users. More information is available in by running the command + "openwebrx admin --help". + +Template: openwebrx/admin_user_configured +Type: boolean +Default: false +Description: OpenWebRX "admin" user previously configured? + Marker used internally by the config scripts to remember if an admin user has + been created. + +Template: openwebrx/title +Type: title +Description: Configuring OpenWebRX \ No newline at end of file diff --git a/debian/rules b/debian/rules new file mode 100755 index 000000000..3b7418e3e --- /dev/null +++ b/debian/rules @@ -0,0 +1,8 @@ +#!/usr/bin/make -f +export PYBUILD_NAME=openwebrx + +%: + dh $@ --with python3 --buildsystem=pybuild --with systemd + +override_dh_strip_nondeterminism: + dh_strip_nondeterminism -X.png diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 000000000..9f6742789 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) \ No newline at end of file diff --git a/docker.sh b/docker.sh new file mode 100755 index 000000000..b3657af65 --- /dev/null +++ b/docker.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +set -euo pipefail + +ARCH=$(uname -m) +IMAGES="openwebrx-rtlsdr openwebrx-sdrplay openwebrx-hackrf openwebrx-airspy openwebrx-afedri openwebrx-rtlsdr-soapy openwebrx-plutosdr openwebrx-limesdr openwebrx-soapyremote openwebrx-perseus openwebrx-fcdpp openwebrx-radioberry openwebrx-uhd openwebrx-rtltcp openwebrx-runds openwebrx-hpsdr openwebrx-bladerf openwebrx-full openwebrx" +ALL_ARCHS="x86_64 armv7l aarch64" +TAG=${TAG:-"latest"} +ARCHTAG="${TAG}-${ARCH}" + +usage () { + echo "Usage: ${0} [command]" + echo "Available commands:" + echo " help Show this usage information" + echo " build Build all docker images" + echo " push Push built docker images to the docker hub" + echo " manifest Compile the docker hub manifest (combines arm and x86 tags into one)" + echo " tag Tag a release" +} + +build () { + # build the base images + docker build --pull -t openwebrx-base:${ARCHTAG} -f docker/Dockerfiles/Dockerfile-base . + docker build --build-arg ARCHTAG=${ARCHTAG} -t openwebrx-soapysdr-base:${ARCHTAG} -f docker/Dockerfiles/Dockerfile-soapysdr . + + for image in ${IMAGES}; do + i=${image:10} + # "openwebrx" is a special image that gets tag-aliased later on + if [[ ! -z "${i}" ]] ; then + docker build --build-arg ARCHTAG=$ARCHTAG -t jketterl/${image}:${ARCHTAG} -f docker/Dockerfiles/Dockerfile-${i} . + fi + done + + # tag openwebrx alias image + docker tag jketterl/openwebrx-full:${ARCHTAG} jketterl/openwebrx:${ARCHTAG} +} + +push () { + for image in ${IMAGES}; do + docker push jketterl/${image}:${ARCHTAG} + done +} + +manifest () { + for image in ${IMAGES}; do + # there's no docker manifest rm command, and the create --amend does not work, so we have to clean up manually + rm -rf "${HOME}/.docker/manifests/docker.io_jketterl_${image}-${TAG}" + IMAGE_LIST="" + for a in ${ALL_ARCHS}; do + IMAGE_LIST="${IMAGE_LIST} jketterl/${image}:${TAG}-${a}" + done + docker manifest create jketterl/${image}:${TAG} ${IMAGE_LIST} + docker manifest push --purge jketterl/${image}:${TAG} + done +} + +tag () { + if [[ -x ${1:-} || -z ${2:-} ]] ; then + echo "Usage: ${0} tag [SRC_TAG] [TARGET_TAG]" + return + fi + + local SRC_TAG=${1} + local TARGET_TAG=${2} + + for image in ${IMAGES}; do + # there's no docker manifest rm command, and the create --amend does not work, so we have to clean up manually + rm -rf "${HOME}/.docker/manifests/docker.io_jketterl_${image}-${TARGET_TAG}" + IMAGE_LIST="" + for a in ${ALL_ARCHS}; do + docker pull jketterl/${image}:${SRC_TAG}-${a} + docker tag jketterl/${image}:${SRC_TAG}-${a} jketterl/${image}:${TARGET_TAG}-${a} + docker push jketterl/${image}:${TARGET_TAG}-${a} + IMAGE_LIST="${IMAGE_LIST} jketterl/${image}:${TARGET_TAG}-${a}" + done + docker manifest create jketterl/${image}:${TARGET_TAG} ${IMAGE_LIST} + docker manifest push --purge jketterl/${image}:${TARGET_TAG} + docker pull jketterl/${image}:${TARGET_TAG} + done +} + +case ${1:-} in + build) + build + ;; + push) + push + ;; + manifest) + manifest + ;; + tag) + tag ${@:2} + ;; + *) + usage + ;; +esac \ No newline at end of file diff --git a/docker/Dockerfiles/Dockerfile-afedri b/docker/Dockerfiles/Dockerfile-afedri new file mode 100644 index 000000000..ad7c88e9c --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-afedri @@ -0,0 +1,8 @@ +ARG ARCHTAG +FROM openwebrx-soapysdr-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-afedri.sh / +RUN /install-dependencies-afedri.sh &&\ + rm /install-dependencies-afedri.sh + +ADD . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-airspy b/docker/Dockerfiles/Dockerfile-airspy new file mode 100644 index 000000000..94b348bd9 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-airspy @@ -0,0 +1,8 @@ +ARG ARCHTAG +FROM openwebrx-soapysdr-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-airspy.sh / +RUN /install-dependencies-airspy.sh &&\ + rm /install-dependencies-airspy.sh + +ADD . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-base b/docker/Dockerfiles/Dockerfile-base new file mode 100644 index 000000000..e50f03a61 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-base @@ -0,0 +1,28 @@ +FROM debian:bookworm-slim + +COPY docker/files/js8call/js8call-hamlib.patch \ + docker/files/wsjtx/wsjtx.patch \ + docker/files/wsjtx/wsjtx-hamlib.patch \ + docker/files/dream/dream.patch \ + docker/files/direwolf/direwolf-hamlib.patch \ + docker/scripts/install-dependencies.sh / +RUN /install-dependencies.sh && \ + rm /install-dependencies.sh && \ + rm /*.patch +COPY docker/scripts/install-owrx-tools.sh / +RUN /install-owrx-tools.sh && \ + rm /install-owrx-tools.sh + +COPY docker/files/services/codecserver /etc/services.d/codecserver + +ENTRYPOINT ["/init"] + +WORKDIR /opt/openwebrx + +VOLUME /etc/openwebrx +VOLUME /var/lib/openwebrx + +ENV S6_CMD_ARG0="/opt/openwebrx/docker/scripts/run.sh" +CMD [""] + +EXPOSE 8073 diff --git a/docker/Dockerfiles/Dockerfile-bladerf b/docker/Dockerfiles/Dockerfile-bladerf new file mode 100644 index 000000000..badcf77ae --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-bladerf @@ -0,0 +1,8 @@ +ARG ARCHTAG +FROM openwebrx-soapysdr-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-bladerf.sh / +RUN /install-dependencies-bladerf.sh &&\ + rm /install-dependencies-bladerf.sh + +COPY . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-fcdpp b/docker/Dockerfiles/Dockerfile-fcdpp new file mode 100644 index 000000000..3e28ac7fc --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-fcdpp @@ -0,0 +1,8 @@ +ARG ARCHTAG +FROM openwebrx-soapysdr-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-fcdpp.sh / +RUN /install-dependencies-fcdpp.sh &&\ + rm /install-dependencies-fcdpp.sh + +COPY . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-full b/docker/Dockerfiles/Dockerfile-full new file mode 100644 index 000000000..6d68e8a0d --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-full @@ -0,0 +1,32 @@ +ARG ARCHTAG +FROM openwebrx-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-*.sh \ + docker/files/sdrplay/install-lib.*.patch \ + docker/scripts/install-connectors.sh / + +RUN /install-dependencies-rtlsdr.sh &&\ + /install-dependencies-soapysdr.sh &&\ + /install-dependencies-hackrf.sh &&\ + /install-dependencies-sdrplay.sh &&\ + /install-dependencies-airspy.sh &&\ + /install-dependencies-afedri.sh &&\ + /install-dependencies-rtlsdr-soapy.sh &&\ + /install-dependencies-plutosdr.sh &&\ + /install-dependencies-limesdr.sh &&\ + /install-dependencies-soapyremote.sh &&\ + /install-dependencies-perseus.sh &&\ + /install-dependencies-fcdpp.sh &&\ + /install-dependencies-radioberry.sh &&\ + /install-dependencies-uhd.sh &&\ + /install-dependencies-hpsdr.sh &&\ + /install-dependencies-bladerf.sh &&\ + /install-connectors.sh &&\ + /install-dependencies-runds.sh &&\ + rm /install-dependencies-*.sh &&\ + rm /install-lib.*.patch && \ + rm /install-connectors.sh + +COPY docker/files/services/sdrplay /etc/services.d/sdrplay + +ADD . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-hackrf b/docker/Dockerfiles/Dockerfile-hackrf new file mode 100644 index 000000000..6dab0f156 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-hackrf @@ -0,0 +1,8 @@ +ARG ARCHTAG +FROM openwebrx-soapysdr-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-hackrf.sh / +RUN /install-dependencies-hackrf.sh &&\ + rm /install-dependencies-hackrf.sh + +COPY . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-hpsdr b/docker/Dockerfiles/Dockerfile-hpsdr new file mode 100644 index 000000000..96d58b915 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-hpsdr @@ -0,0 +1,9 @@ +ARG ARCHTAG +FROM openwebrx-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-hpsdr.sh / + +RUN /install-dependencies-hpsdr.sh &&\ + rm /install-dependencies-hpsdr.sh + +COPY . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-limesdr b/docker/Dockerfiles/Dockerfile-limesdr new file mode 100644 index 000000000..9603c601f --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-limesdr @@ -0,0 +1,8 @@ +ARG ARCHTAG +FROM openwebrx-soapysdr-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-limesdr.sh / +RUN /install-dependencies-limesdr.sh &&\ + rm /install-dependencies-limesdr.sh + +COPY . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-perseus b/docker/Dockerfiles/Dockerfile-perseus new file mode 100644 index 000000000..bc16583cc --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-perseus @@ -0,0 +1,8 @@ +ARG ARCHTAG +FROM openwebrx-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-perseus.sh / +RUN /install-dependencies-perseus.sh &&\ + rm /install-dependencies-perseus.sh + +COPY . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-plutosdr b/docker/Dockerfiles/Dockerfile-plutosdr new file mode 100644 index 000000000..4a263e856 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-plutosdr @@ -0,0 +1,8 @@ +ARG ARCHTAG +FROM openwebrx-soapysdr-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-plutosdr.sh / +RUN /install-dependencies-plutosdr.sh &&\ + rm /install-dependencies-plutosdr.sh + +COPY . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-radioberry b/docker/Dockerfiles/Dockerfile-radioberry new file mode 100644 index 000000000..3cbe978a2 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-radioberry @@ -0,0 +1,8 @@ +ARG ARCHTAG +FROM openwebrx-soapysdr-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-radioberry.sh / +RUN /install-dependencies-radioberry.sh &&\ + rm /install-dependencies-radioberry.sh + +COPY . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-rtlsdr b/docker/Dockerfiles/Dockerfile-rtlsdr new file mode 100644 index 000000000..614464180 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-rtlsdr @@ -0,0 +1,12 @@ +ARG ARCHTAG +FROM openwebrx-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-rtlsdr.sh \ + docker/scripts/install-connectors.sh / + +RUN /install-dependencies-rtlsdr.sh &&\ + rm /install-dependencies-rtlsdr.sh &&\ + /install-connectors.sh &&\ + rm /install-connectors.sh + +COPY . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-rtlsdr-soapy b/docker/Dockerfiles/Dockerfile-rtlsdr-soapy new file mode 100644 index 000000000..5dce90fda --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-rtlsdr-soapy @@ -0,0 +1,8 @@ +ARG ARCHTAG +FROM openwebrx-soapysdr-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-rtlsdr-soapy.sh / +RUN /install-dependencies-rtlsdr-soapy.sh &&\ + rm /install-dependencies-rtlsdr-soapy.sh + +COPY . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-rtltcp b/docker/Dockerfiles/Dockerfile-rtltcp new file mode 100644 index 000000000..240799dca --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-rtltcp @@ -0,0 +1,9 @@ +ARG ARCHTAG +FROM openwebrx-base:$ARCHTAG + +COPY docker/scripts/install-connectors.sh / + +RUN /install-connectors.sh &&\ + rm /install-connectors.sh + +COPY . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-runds b/docker/Dockerfiles/Dockerfile-runds new file mode 100644 index 000000000..2a087e1ff --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-runds @@ -0,0 +1,12 @@ +ARG ARCHTAG +FROM openwebrx-base:$ARCHTAG + +COPY docker/scripts/install-connectors.sh \ + docker/scripts/install-dependencies-runds.sh / + +RUN /install-connectors.sh &&\ + rm /install-connectors.sh && \ + /install-dependencies-runds.sh && \ + rm /install-dependencies-runds.sh + +COPY . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-sdrplay b/docker/Dockerfiles/Dockerfile-sdrplay new file mode 100644 index 000000000..bb53d7ead --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-sdrplay @@ -0,0 +1,12 @@ +ARG ARCHTAG +FROM openwebrx-soapysdr-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-sdrplay.sh \ + docker/files/sdrplay/install-lib.*.patch / +RUN /install-dependencies-sdrplay.sh &&\ + rm /install-dependencies-sdrplay.sh &&\ + rm /install-lib.*.patch + +COPY docker/files/services/sdrplay /etc/services.d/sdrplay + +COPY . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-soapyremote b/docker/Dockerfiles/Dockerfile-soapyremote new file mode 100644 index 000000000..e5c207c84 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-soapyremote @@ -0,0 +1,8 @@ +ARG ARCHTAG +FROM openwebrx-soapysdr-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-soapyremote.sh / +RUN /install-dependencies-soapyremote.sh &&\ + rm /install-dependencies-soapyremote.sh + +COPY . /opt/openwebrx diff --git a/docker/Dockerfiles/Dockerfile-soapysdr b/docker/Dockerfiles/Dockerfile-soapysdr new file mode 100644 index 000000000..45ac693b6 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-soapysdr @@ -0,0 +1,9 @@ +ARG ARCHTAG +FROM openwebrx-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-soapysdr.sh \ + docker/scripts/install-connectors.sh / +RUN /install-dependencies-soapysdr.sh &&\ + rm /install-dependencies-soapysdr.sh &&\ + /install-connectors.sh &&\ + rm /install-connectors.sh diff --git a/docker/Dockerfiles/Dockerfile-uhd b/docker/Dockerfiles/Dockerfile-uhd new file mode 100644 index 000000000..ae1e758a0 --- /dev/null +++ b/docker/Dockerfiles/Dockerfile-uhd @@ -0,0 +1,8 @@ +ARG ARCHTAG +FROM openwebrx-soapysdr-base:$ARCHTAG + +COPY docker/scripts/install-dependencies-uhd.sh / +RUN /install-dependencies-uhd.sh &&\ + rm /install-dependencies-uhd.sh + +COPY . /opt/openwebrx diff --git a/docker/files/direwolf/direwolf-hamlib.patch b/docker/files/direwolf/direwolf-hamlib.patch new file mode 100644 index 000000000..2347c24f9 --- /dev/null +++ b/docker/files/direwolf/direwolf-hamlib.patch @@ -0,0 +1,20 @@ +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 9e710f5..da90b43 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -257,13 +257,8 @@ else() + set(GPSD_LIBRARIES "") + endif() + +-find_package(hamlib) +-if(HAMLIB_FOUND) +- set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -DUSE_HAMLIB") +-else() +- set(HAMLIB_INCLUDE_DIRS "") +- set(HAMLIB_LIBRARIES "") +-endif() ++set(HAMLIB_INCLUDE_DIRS "") ++set(HAMLIB_LIBRARIES "") + + if(LINUX) + find_package(ALSA REQUIRED) diff --git a/docker/files/dream/dream.patch b/docker/files/dream/dream.patch new file mode 100644 index 000000000..58de6c4d3 --- /dev/null +++ b/docker/files/dream/dream.patch @@ -0,0 +1,96 @@ +--- dream.pro.org 2020-09-04 22:51:51.579926191 +0200 ++++ dream.pro 2020-09-04 22:52:57.609434707 +0200 +@@ -70,9 +70,6 @@ + exists(/opt/local/include/speex/speex_preprocess.h) { + CONFIG += speexdsp + } +- exists(/opt/local/include/hamlib/rig.h) { +- CONFIG += hamlib +- } + contains(QT_VERSION, ^4\\.7.*) { + QT += phonon opengl svg + DEFINES -= QWT_NO_SVG +@@ -138,12 +135,6 @@ + packagesExist(sndfile) { + CONFIG += sndfile + } +- packagesExist(hamlib) { +- CONFIG += hamlib +- } +- packagesExist(gpsd) { +- CONFIG += gps +- } + packagesExist(pcap) { + CONFIG += pcap + } +@@ -159,14 +150,6 @@ + exists(/usr/local/include/sndfile.h) { + CONFIG += sndfile + } +- exists(/usr/include/hamlib/rig.h) | \ +- exists(/usr/local/include/hamlib/rig.h) { +- CONFIG += hamlib +- } +- exists(/usr/include/gps.h) | \ +- exists(/usr/local/include/gps.h) { +- CONFIG += gps +- } + exists(/usr/include/pcap.h) | \ + exists(/usr/local/include/pcap.h) { + CONFIG += pcap +@@ -194,9 +177,6 @@ + exists($$OUT_PWD/include/speex/speex_preprocess.h) { + CONFIG += speexdsp + } +- exists($$OUT_PWD/include/hamlib/rig.h) { +- CONFIG += hamlib +- } + exists($$OUT_PWD/include/pcap.h) { + CONFIG += pcap + } +@@ -225,7 +205,7 @@ + LIBS += -lz + } + } +-exists($$OUT_PWD/include/neaacdec.h) { ++exists(/usr/include/neaacdec.h) { + DEFINES += HAVE_LIBFAAD \ + USE_FAAD2_LIBRARY + LIBS += -lfaad_drm +@@ -257,11 +237,6 @@ + win32:LIBS += libspeexdsp.lib + message("with libspeexdsp") + } +-gps { +- DEFINES += HAVE_LIBGPS +- unix:LIBS += -lgps +- message("with gps") +-} + pcap { + DEFINES += HAVE_LIBPCAP + unix:LIBS += -lpcap +@@ -269,24 +244,6 @@ + win32-g++:LIBS += -lwpcap -lpacket + message("with pcap") + } +-hamlib { +- DEFINES += HAVE_LIBHAMLIB +- macx:LIBS += -framework IOKit +- unix:LIBS += -lhamlib +- win32:LIBS += libhamlib-2.lib +- HEADERS += src/util/Hamlib.h +- SOURCES += src/util/Hamlib.cpp +- qt { +- HEADERS += src/util-QT/Rig.h +- SOURCES += src/util-QT/Rig.cpp +- } +- gui { +- HEADERS += src/GUI-QT/RigDlg.h +- SOURCES += src/GUI-QT/RigDlg.cpp +- FORMS += RigDlg.ui +- } +- message("with hamlib") +-} + qwt { + DEFINES += QWT_NO_SVG + macx { diff --git a/docker/files/js8call/js8call-hamlib.patch b/docker/files/js8call/js8call-hamlib.patch new file mode 100644 index 000000000..899f83e31 --- /dev/null +++ b/docker/files/js8call/js8call-hamlib.patch @@ -0,0 +1,151 @@ +diff -ur js8call-orig/CMake/Modules/Findhamlib.cmake js8call/CMake/Modules/Findhamlib.cmake +--- js8call-orig/CMake/Modules/Findhamlib.cmake 2020-07-22 18:14:18.014499840 +0200 ++++ js8call/CMake/Modules/Findhamlib.cmake 2020-07-22 18:16:07.200375473 +0200 +@@ -78,4 +78,4 @@ + # Handle the QUIETLY and REQUIRED arguments and set HAMLIB_FOUND to + # TRUE if all listed variables are TRUE + include (FindPackageHandleStandardArgs) +-find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES hamlib_LIBRARY_DIRS) ++find_package_handle_standard_args (hamlib DEFAULT_MSG hamlib_INCLUDE_DIRS hamlib_LIBRARIES) +diff -ur js8call-orig/CMakeLists.txt js8call/CMakeLists.txt +--- js8call-orig/CMakeLists.txt 2020-07-22 18:14:18.014499840 +0200 ++++ js8call/CMakeLists.txt 2020-07-22 18:17:55.629633825 +0200 +@@ -558,7 +558,7 @@ + # + # libhamlib setup + # +-set (hamlib_STATIC 1) ++set (hamlib_STATIC 0) + find_package (hamlib 3 REQUIRED) + find_program (RIGCTL_EXE rigctl) + find_program (RIGCTLD_EXE rigctld) +@@ -911,56 +911,6 @@ + target_link_libraries (js8 wsjt_fort wsjt_cxx Qt5::Core) + endif (${OPENMP_FOUND} OR APPLE) + +-# build the main application +-add_executable (js8call MACOSX_BUNDLE +- ${sqlite3_CSRCS} +- ${wsjtx_CXXSRCS} +- ${wsjtx_GENUISRCS} +- wsjtx.rc +- ${WSJTX_ICON_FILE} +- ${wsjtx_RESOURCES_RCC} +- images.qrc +- ) +- +-if (WSJT_CREATE_WINMAIN) +- set_target_properties (js8call PROPERTIES WIN32_EXECUTABLE ON) +-endif (WSJT_CREATE_WINMAIN) +- +-set_target_properties (js8call PROPERTIES +- MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Darwin/Info.plist.in" +- MACOSX_BUNDLE_INFO_STRING "${WSJTX_DESCRIPTION_SUMMARY}" +- MACOSX_BUNDLE_ICON_FILE "${WSJTX_ICON_FILE}" +- MACOSX_BUNDLE_BUNDLE_VERSION ${wsjtx_VERSION} +- MACOSX_BUNDLE_SHORT_VERSION_STRING "v${wsjtx_VERSION}" +- MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${wsjtx_VERSION}" +- MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_NAME}" +- MACOSX_BUNDLE_BUNDLE_EXECUTABLE_NAME "${PROJECT_NAME}" +- MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}" +- MACOSX_BUNDLE_GUI_IDENTIFIER "org.kn4crd.js8call" +- ) +- +-target_include_directories (js8call PRIVATE ${FFTW3_INCLUDE_DIRS}) +-if (APPLE) +- target_link_libraries (js8call wsjt_fort wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES}) +-else () +- target_link_libraries (js8call wsjt_fort_omp wsjt_cxx wsjt_qt wsjt_qtmm ${hamlib_LIBRARIES} ${FFTW3_LIBRARIES}) +- if (OpenMP_C_FLAGS) +- set_target_properties (js8call PROPERTIES +- COMPILE_FLAGS "${OpenMP_C_FLAGS}" +- LINK_FLAGS "${OpenMP_C_FLAGS}" +- ) +- endif () +- set_target_properties (js8call PROPERTIES +- Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/fortran_modules_omp +- ) +- if (WIN32) +- set_target_properties (js8call PROPERTIES +- LINK_FLAGS -Wl,--stack,16777216 +- ) +- endif () +-endif () +-qt5_use_modules (js8call SerialPort) # not sure why the interface link library syntax above doesn't work +- + # if (UNIX) + # if (NOT WSJT_SKIP_MANPAGES) + # add_subdirectory (manpages) +@@ -976,38 +926,10 @@ + # + # installation + # +-install (TARGETS js8call +- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime +- BUNDLE DESTINATION . COMPONENT runtime +- ) +- + install (TARGETS js8 RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime + BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime + ) + +-install (PROGRAMS +- ${RIGCTL_EXE} +- DESTINATION ${CMAKE_INSTALL_BINDIR} +- #COMPONENT runtime +- RENAME rigctl-local${CMAKE_EXECUTABLE_SUFFIX} +- ) +- +-install (PROGRAMS +- ${RIGCTLD_EXE} +- DESTINATION ${CMAKE_INSTALL_BINDIR} +- #COMPONENT runtime +- RENAME rigctld-local${CMAKE_EXECUTABLE_SUFFIX} +- ) +- +-install (FILES +- README +- COPYING +- INSTALL +- INSTALL-WSJTX +- DESTINATION ${CMAKE_INSTALL_DOCDIR} +- #COMPONENT runtime +- ) +- + install (FILES + contrib/Ephemeris/JPLEPH + DESTINATION ${CMAKE_INSTALL_DATADIR}/${CMAKE_PROJECT_NAME} +@@ -1061,32 +983,6 @@ + "${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h" + ) + +- +-if (NOT WIN32 AND NOT APPLE) +- # install a desktop file so js8call appears in the application start +- # menu with an icon +- install ( +- FILES js8call.desktop +- DESTINATION /usr/share/applications +- #COMPONENT runtime +- ) +- install ( +- FILES icons/Unix/js8call_icon.png +- DESTINATION /usr/share/pixmaps +- #COMPONENT runtime +- ) +- +- IF("${CMAKE_INSTALL_PREFIX}" STREQUAL "/opt/js8call") +- execute_process(COMMAND ln -s /opt/js8call/bin/js8call ljs8call) +- +- install(FILES +- ${CMAKE_BINARY_DIR}/ljs8call DESTINATION /usr/bin/ RENAME js8call +- #COMPONENT runtime +- ) +- endif() +-endif (NOT WIN32 AND NOT APPLE) +- +- + # + # bundle fixup only done in Release or MinSizeRel configurations + # +Only in js8call/: .idea diff --git a/docker/files/sdrplay/install-lib.aarch64.patch b/docker/files/sdrplay/install-lib.aarch64.patch new file mode 120000 index 000000000..2ec83d025 --- /dev/null +++ b/docker/files/sdrplay/install-lib.aarch64.patch @@ -0,0 +1 @@ +install-lib.x86_64.patch \ No newline at end of file diff --git a/docker/files/sdrplay/install-lib.armv7l.patch b/docker/files/sdrplay/install-lib.armv7l.patch new file mode 100644 index 000000000..22a78f6cd --- /dev/null +++ b/docker/files/sdrplay/install-lib.armv7l.patch @@ -0,0 +1,40 @@ +diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh +--- sdrplay-orig/install_lib.sh 2020-05-24 14:13:04.561271707 +0000 ++++ sdrplay/install_lib.sh 2020-05-24 14:16:20.068329040 +0000 +@@ -4,19 +4,6 @@ + MAJVERS="3" + + echo "Installing SDRplay RSP API library ${VERS}..." +-read -p "Press RETURN to view the license agreement" ret +- +-more sdrplay_license.txt +- +-while true; do +- echo "Press y and RETURN to accept the license agreement and continue with" +- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn +- case $yn in +- [Yy]* ) break;; +- [Nn]* ) exit;; +- * ) echo "Please answer y or n";; +- esac +-done + + ARCH=`uname -m` + +@@ -141,16 +128,6 @@ + echo "SDRplay API ${VERS} Installation Finished" + echo " " + +-while true; do +- echo "Would you like to add SDRplay USB IDs to the local database for easier +-" +- read -p "identification in applications such as lsusb? [y/n] " yn +- case $yn in +- [Yy]* ) break;; +- [Nn]* ) exit;; +- * ) echo "Please answer y or n";; +- esac +-done + sudo cp scripts/sdrplay_usbids.sh ${INSTALLBINDIR}/. + sudo chmod 755 ${INSTALLBINDIR}/sdrplay_usbids.sh + sudo cp scripts/sdrplay_ids.txt ${INSTALLBINDIR}/. diff --git a/docker/files/sdrplay/install-lib.x86_64.patch b/docker/files/sdrplay/install-lib.x86_64.patch new file mode 100644 index 000000000..62402f805 --- /dev/null +++ b/docker/files/sdrplay/install-lib.x86_64.patch @@ -0,0 +1,148 @@ +diff -ur sdrplay-orig/install_lib.sh sdrplay/install_lib.sh +--- sdrplay-orig/install_lib.sh 2024-02-20 00:57:57.438264040 +0100 ++++ sdrplay/install_lib.sh 2024-02-20 01:01:14.293463093 +0100 +@@ -17,26 +17,7 @@ + echo "the system files." + echo " " + +-read -p "Press RETURN to view the license agreement" ret +-more -d sdrplay_license.txt +-while true; do +- echo "Press y and RETURN to accept the license agreement and continue with" +- read -p "the installation, or press n and RETURN to exit the installer [y/n] " yn +- case $yn in +- [Yy]* ) break;; +- [Nn]* ) exit;; +- * ) echo "Please answer y or n";; +- esac +-done +- +-echo " " +-echo "A copy of the license agreement can be found here: ${HOME}/sdrplay_license.txt" +-cp sdrplay_license.txt ${HOME}/. +-chmod 644 ${HOME}/sdrplay_license.txt +-echo " " +- + ARCH=$(uname -m|sed -e 's/x86_64/64/' -e 's/aarch64/64/' -e 's/arm64/64/' -e 's/i.86/32/') +-INIT=$(file -L /sbin/init|sed -e 's/^.* \(32\|64\)-bit.*$/\1/') + COMPILER=$(getconf LONG_BIT) + ARCHM=$(uname -m) + INSTALLARCH=$(uname -m) +@@ -47,12 +28,11 @@ + + echo " " + echo "Architecture reported as being $ARCH bit" +-echo "System reports $INIT bit files found" + echo "System is also setup to produce $COMPILER bit files" + echo "Architecture reports machine as being $ARCHM compliant" + echo " " + +-if [ "${ARCH}" != "64" ] || [ "${INIT}" != "64" ] || [ "${COMPILER}" != "64" ]; then ++if [ "${ARCH}" != "64" ] || [ "${COMPILER}" != "64" ]; then + echo "This installer only supports 64 bit architectures." + echo "One of the above indicates that something is not set for" + echo "64 bit operation. Please either fix the relevant OS issue or" +@@ -194,11 +174,6 @@ + sudo chmod 644 /etc/udev/hwdb.d/20-sdrplay.hwdb + sudo systemd-hwdb update + sudo udevadm trigger +- if [ "${SRVTYPE}" != "initd" ]; then +- sudo systemctl restart udev +- else +- sudo service udev restart +- fi + echo "Done" + fi + else +@@ -234,7 +209,7 @@ + fi + + echo " " +-locservice="/opt/sdrplay_api" ++locservice="/usr/local/bin" + locheader="/usr/local/include" + loclib="/usr/local/lib" + locscripts="/etc/systemd/system" +@@ -254,45 +229,6 @@ + echo "Daemon start system : ${DAEMON_SYS}" + echo " " + +-# 0--------1---------2---------3---------4---------5---------6---------7---------8 +-while true; do +- echo "To continue the installation with these defaults press y and RETURN" +- read -p "or press n and RETURN to change them [y/n] " yn +- case $yn in +- [Yy]* ) change="n";break;; +- [Nn]* ) change="y";break;; +- * ) echo "Please answer y or n";; +- esac +-done +- +-if [ "${change}" == "y" ]; then +- echo "Changing default locations..." +- read -p "API service location [${locservice}]: " newloc +- if [ "${newloc}" != "" ]; then +- locservice=${newloc} +- fi +- read -p "API header files location [${locheader}]: " newloc +- if [ "${newloc}" != "" ]; then +- locheader=${newloc} +- fi +- read -p "API shared library location [${loclib}]: " newloc +- if [ "${newloc}" != "" ]; then +- loclib=${newloc} +- fi +- +- echo "API service : ${locservice}" +- echo "API header files : ${locheader}" +- echo "API shared library : ${loclib}" +- while true; do +- read -p "Please confirm these are correct [y/n] " yn +- case $yn in +- [Yy]* ) break;; +- [Nn]* ) echo "paths not confirmed. Exiting...";exit 1;; +- * ) echo "Please answer y or n";; +- esac +- done +-fi +- + sudo mkdir -p -m 755 ${locservice} >> /dev/null 2>&1 + sudo mkdir -p -m 755 ${locheader} >> /dev/null 2>&1 + sudo mkdir -p -m 755 ${loclib} >> /dev/null 2>&1 +@@ -324,10 +260,6 @@ + echo -n "Installing Service scripts and starting daemon..." + if [ -d "/etc/systemd/system" ]; then + SRVTYPE="systemd" +- if [ -f "/etc/systemd/system/sdrplay.service" ]; then +- sudo systemctl stop sdrplay +- sudo systemctl disable sdrplay +- fi + sudo bash -c 'cat > /etc/systemd/system/sdrplay.service' << EOF + [Unit] + Description=SDRplay API Service +@@ -346,8 +278,6 @@ + EOF + + sudo chmod 644 /etc/systemd/system/sdrplay.service +- sudo systemctl enable sdrplay +- sudo systemctl start sdrplay + else + SRVTYPE="initd" + if [ -f "/etc/init.d/sdrplayService" ]; then +@@ -450,16 +380,6 @@ + echo "finished, please reboot this device." + + echo " " +-echo "To start and stop the API service, use the following commands..." +-echo " " +-if [ "${SRVTYPE}" != "systemd" ]; then +- echo "sudo service sdrplayService start" +- echo "sudo service sdrplayService stop" +-else +- echo "sudo systemctl start sdrplay" +- echo "sudo systemctl stop sdrplay" +-fi +-echo " " + echo "If supported on your system, lsusb will now show the RSP name" + echo " " + echo "SDRplay API ${VERS} Installation Finished" diff --git a/docker/files/services/codecserver/run b/docker/files/services/codecserver/run new file mode 100755 index 000000000..13b7872ea --- /dev/null +++ b/docker/files/services/codecserver/run @@ -0,0 +1,2 @@ +#!/command/execlineb -P +/usr/local/bin/codecserver \ No newline at end of file diff --git a/docker/files/services/sdrplay/run b/docker/files/services/sdrplay/run new file mode 100755 index 000000000..cc8b6a7d4 --- /dev/null +++ b/docker/files/services/sdrplay/run @@ -0,0 +1,2 @@ +#!/command/execlineb -P +/usr/local/bin/sdrplay_apiService \ No newline at end of file diff --git a/docker/files/wsjtx/wsjtx-hamlib.patch b/docker/files/wsjtx/wsjtx-hamlib.patch new file mode 100644 index 000000000..a4e277d10 --- /dev/null +++ b/docker/files/wsjtx/wsjtx-hamlib.patch @@ -0,0 +1,50 @@ +--- CMakeLists.txt.orig 2021-09-28 14:33:14.329598412 +0200 ++++ CMakeLists.txt 2021-09-28 14:34:23.052345270 +0200 +@@ -106,24 +106,6 @@ + + + # +-# build and install hamlib locally so it can be referenced by the +-# WSJT-X build +-# +-ExternalProject_Add (hamlib +- GIT_REPOSITORY ${hamlib_repo} +- GIT_TAG ${hamlib_TAG} +- GIT_SHALLOW False +- URL ${CMAKE_CURRENT_SOURCE_DIR}/src/${__hamlib_upstream}.tar.gz +- URL_HASH MD5=${hamlib_md5sum} +- #UPDATE_COMMAND ${CMAKE_COMMAND} -E env "[ -f ./bootstrap ] && ./bootstrap" +- PATCH_COMMAND ${PATCH_EXECUTABLE} -p1 -N < ${CMAKE_CURRENT_SOURCE_DIR}/hamlib.patch +- CONFIGURE_COMMAND /configure --prefix= --disable-shared --enable-static --without-cxx-binding ${EXTRA_FLAGS} # LIBUSB_LIBS=${USB_LIBRARY} +- BUILD_COMMAND $(MAKE) all V=1 # $(MAKE) is ExternalProject_Add() magic to do recursive make +- INSTALL_COMMAND $(MAKE) install-strip V=1 DESTDIR="" +- STEP_TARGETS update install +- ) +- +-# + # custom target to make a hamlib source tarball + # + add_custom_target (hamlib_sources +@@ -161,7 +143,6 @@ + # build and optionally install WSJT-X using the hamlib package built + # above + # +-ExternalProject_Get_Property (hamlib INSTALL_DIR) + ExternalProject_Add (wsjtx + GIT_REPOSITORY ${wsjtx_repo} + GIT_TAG ${WSJTX_TAG} +@@ -186,14 +167,8 @@ + DEPENDEES build + ) + +-set_target_properties (hamlib PROPERTIES EXCLUDE_FROM_ALL 1) + set_target_properties (wsjtx PROPERTIES EXCLUDE_FROM_ALL 1) + +-add_dependencies (wsjtx-configure hamlib-install) +-add_dependencies (wsjtx-build hamlib-install) +-add_dependencies (wsjtx-install hamlib-install) +-add_dependencies (wsjtx-package hamlib-install) +- + # export traditional targets + add_custom_target (build ALL DEPENDS wsjtx-build) + add_custom_target (install DEPENDS wsjtx-install) diff --git a/docker/files/wsjtx/wsjtx.patch b/docker/files/wsjtx/wsjtx.patch new file mode 100644 index 000000000..75ce3a6c4 --- /dev/null +++ b/docker/files/wsjtx/wsjtx.patch @@ -0,0 +1,341 @@ +diff -ur wsjtx-orig/CMakeLists.txt wsjtx/CMakeLists.txt +--- wsjtx-orig/CMakeLists.txt 2023-01-28 17:43:05.586124507 +0100 ++++ wsjtx/CMakeLists.txt 2023-01-28 17:56:07.108634912 +0100 +@@ -122,7 +122,7 @@ + option (WSJT_QDEBUG_TO_FILE "Redirect Qt debuging messages to a trace file.") + option (WSJT_SOFT_KEYING "Apply a ramp to CW keying envelope to reduce transients." ON) + option (WSJT_SKIP_MANPAGES "Skip *nix manpage generation.") +-option (WSJT_GENERATE_DOCS "Generate documentation files." ON) ++option (WSJT_GENERATE_DOCS "Generate documentation files.") + option (WSJT_RIG_NONE_CAN_SPLIT "Allow split operation with \"None\" as rig.") + option (WSJT_TRACE_UDP "Debugging option that turns on UDP message protocol diagnostics.") + option (WSJT_BUILD_UTILS "Build simulators and code demonstrators." ON) +@@ -170,77 +170,7 @@ + ) + + set (wsjt_qt_CXXSRCS +- helper_functions.cpp +- qt_helpers.cpp +- widgets/MessageBox.cpp +- MetaDataRegistry.cpp +- Network/NetworkServerLookup.cpp + revision_utils.cpp +- L10nLoader.cpp +- WFPalette.cpp +- Radio.cpp +- RadioMetaType.cpp +- NonInheritingProcess.cpp +- models/IARURegions.cpp +- models/Bands.cpp +- models/Modes.cpp +- models/FrequencyList.cpp +- models/StationList.cpp +- widgets/FrequencyLineEdit.cpp +- widgets/FrequencyDeltaLineEdit.cpp +- item_delegates/CandidateKeyFilter.cpp +- item_delegates/ForeignKeyDelegate.cpp +- item_delegates/MessageItemDelegate.cpp +- validators/LiveFrequencyValidator.cpp +- GetUserId.cpp +- Audio/AudioDevice.cpp +- Transceiver/Transceiver.cpp +- Transceiver/TransceiverBase.cpp +- Transceiver/EmulateSplitTransceiver.cpp +- Transceiver/TransceiverFactory.cpp +- Transceiver/PollingTransceiver.cpp +- Transceiver/HamlibTransceiver.cpp +- Transceiver/HRDTransceiver.cpp +- Transceiver/DXLabSuiteCommanderTransceiver.cpp +- Network/NetworkMessage.cpp +- Network/MessageClient.cpp +- widgets/LettersSpinBox.cpp +- widgets/HintedSpinBox.cpp +- widgets/RestrictedSpinBox.cpp +- widgets/HelpTextWindow.cpp +- SampleDownloader.cpp +- SampleDownloader/DirectoryDelegate.cpp +- SampleDownloader/Directory.cpp +- SampleDownloader/FileNode.cpp +- SampleDownloader/RemoteFile.cpp +- DisplayManual.cpp +- MultiSettings.cpp +- validators/MaidenheadLocatorValidator.cpp +- validators/CallsignValidator.cpp +- widgets/SplashScreen.cpp +- EqualizationToolsDialog.cpp +- widgets/DoubleClickablePushButton.cpp +- widgets/DoubleClickableRadioButton.cpp +- Network/LotWUsers.cpp +- models/DecodeHighlightingModel.cpp +- widgets/DecodeHighlightingListView.cpp +- models/FoxLog.cpp +- widgets/AbstractLogWindow.cpp +- widgets/FoxLogWindow.cpp +- widgets/CabrilloLogWindow.cpp +- item_delegates/CallsignDelegate.cpp +- item_delegates/MaidenheadLocatorDelegate.cpp +- item_delegates/FrequencyDelegate.cpp +- item_delegates/FrequencyDeltaDelegate.cpp +- item_delegates/SQLiteDateTimeDelegate.cpp +- models/CabrilloLog.cpp +- logbook/AD1CCty.cpp +- logbook/WorkedBefore.cpp +- logbook/Multiplier.cpp +- Network/NetworkAccessManager.cpp +- widgets/LazyFillComboBox.cpp +- widgets/CheckableItemComboBox.cpp +- widgets/BandComboBox.cpp + ) + + set (wsjt_qtmm_CXXSRCS +@@ -1089,9 +1019,6 @@ + if (WSJT_GENERATE_DOCS) + add_subdirectory (doc) + endif (WSJT_GENERATE_DOCS) +-if (EXISTS ${CMAKE_SOURCE_DIR}/tests AND IS_DIRECTORY ${CMAKE_SOURCE_DIR}/tests) +- add_subdirectory (tests) +-endif () + + # build a library of package functionality (without and optionally with OpenMP support) + add_library (wsjt_cxx STATIC ${wsjt_CSRCS} ${wsjt_CXXSRCS}) +@@ -1357,10 +1284,7 @@ + add_library (wsjt_qt STATIC ${wsjt_qt_CXXSRCS} ${wsjt_qt_GENUISRCS} ${GENAXSRCS}) + # set wsjtx_udp exports to static variants + target_compile_definitions (wsjt_qt PUBLIC UDP_STATIC_DEFINE) +-target_link_libraries (wsjt_qt Hamlib::Hamlib Boost::log qcp Qt5::Widgets Qt5::Network Qt5::Sql) +-if (WIN32) +- target_link_libraries (wsjt_qt Qt5::AxContainer Qt5::AxBase) +-endif (WIN32) ++target_link_libraries (wsjt_qt Qt5::Core) + + # build a library of package Qt functionality used in Fortran utilities + add_library (fort_qt STATIC ${fort_qt_CXXSRCS}) +@@ -1425,90 +1349,6 @@ + add_subdirectory (map65) + endif () + +-# build the main application +-generate_version_info (wsjtx_VERSION_RESOURCES +- NAME wsjtx +- BUNDLE ${PROJECT_BUNDLE_NAME} +- ICON ${WSJTX_ICON_FILE} +- ) +- +-add_executable (wsjtx MACOSX_BUNDLE +- ${wsjtx_CXXSRCS} +- ${wsjtx_GENUISRCS} +- ${WSJTX_ICON_FILE} +- ${wsjtx_RESOURCES_RCC} +- ${wsjtx_VERSION_RESOURCES} +- ) +- +-if (WSJT_CREATE_WINMAIN) +- set_target_properties (wsjtx PROPERTIES WIN32_EXECUTABLE ON) +-endif (WSJT_CREATE_WINMAIN) +- +-set_target_properties (wsjtx PROPERTIES +- MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Darwin/Info.plist.in" +- MACOSX_BUNDLE_INFO_STRING "${PROJECT_DESCRIPTION}" +- MACOSX_BUNDLE_ICON_FILE "${WSJTX_ICON_FILE}" +- MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH} +- MACOSX_BUNDLE_SHORT_VERSION_STRING "v${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}" +- MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}.${PROJECT_VERSION_PATCH}${SCS_VERSION_STR}" +- MACOSX_BUNDLE_BUNDLE_NAME "${PROJECT_BUNDLE_NAME}" +- MACOSX_BUNDLE_BUNDLE_EXECUTABLE_NAME "${PROJECT_NAME}" +- MACOSX_BUNDLE_COPYRIGHT "${PROJECT_COPYRIGHT}" +- MACOSX_BUNDLE_GUI_IDENTIFIER "org.k1jt.wsjtx" +- ) +- +-target_include_directories (wsjtx PRIVATE ${FFTW3_INCLUDE_DIRS}) +-if ((NOT ${OPENMP_FOUND}) OR APPLE) +- target_link_libraries (wsjtx wsjt_fort) +-else () +- target_link_libraries (wsjtx wsjt_fort_omp) +- if (OpenMP_C_FLAGS) +- set_target_properties (wsjtx PROPERTIES +- COMPILE_FLAGS "${OpenMP_C_FLAGS}" +- LINK_FLAGS "${OpenMP_C_FLAGS}" +- ) +- endif () +- set_target_properties (wsjtx PROPERTIES +- Fortran_MODULE_DIRECTORY ${CMAKE_BINARY_DIR}/fortran_modules_omp +- ) +- if (WIN32) +- set_target_properties (wsjtx PROPERTIES +- LINK_FLAGS -Wl,--stack,0x1000000,--heap,0x20000000 +- ) +- endif () +-endif () +-target_link_libraries (wsjtx Qt5::SerialPort wsjt_cxx wsjt_qt wsjt_qtmm ${FFTW3_LIBRARIES} ${LIBM_LIBRARIES}) +- +-# make a library for WSJT-X UDP servers +-# add_library (wsjtx_udp SHARED ${UDP_library_CXXSRCS}) +-add_library (wsjtx_udp-static STATIC ${UDP_library_CXXSRCS}) +-#target_include_directories (wsjtx_udp +-# INTERFACE +-# $ +-# ) +-target_include_directories (wsjtx_udp-static +- INTERFACE +- $ +- ) +-#set_target_properties (wsjtx_udp PROPERTIES +-# PUBLIC_HEADER "${UDP_library_HEADERS}" +-# ) +-set_target_properties (wsjtx_udp-static PROPERTIES +- OUTPUT_NAME wsjtx_udp +- ) +-target_compile_definitions (wsjtx_udp-static PUBLIC UDP_STATIC_DEFINE) +-target_link_libraries (wsjtx_udp-static Qt5::Network Qt5::Gui) +-generate_export_header (wsjtx_udp-static BASE_NAME udp) +- +-generate_version_info (udp_daemon_VERSION_RESOURCES +- NAME udp_daemon +- BUNDLE ${PROJECT_BUNDLE_NAME} +- ICON ${WSJTX_ICON_FILE} +- FILE_DESCRIPTION "Example WSJT-X UDP Message Protocol daemon" +- ) +-add_executable (udp_daemon UDPExamples/UDPDaemon.cpp ${udp_daemon_VERSION_RESOURCES}) +-target_link_libraries (udp_daemon wsjtx_udp-static) +- + generate_version_info (wsjtx_app_version_VERSION_RESOURCES + NAME wsjtx_app_version + BUNDLE ${PROJECT_BUNDLE_NAME} +@@ -1518,47 +1358,9 @@ + add_executable (wsjtx_app_version AppVersion/AppVersion.cpp ${wsjtx_app_version_VERSION_RESOURCES}) + target_link_libraries (wsjtx_app_version wsjt_qt) + +-generate_version_info (message_aggregator_VERSION_RESOURCES +- NAME message_aggregator +- BUNDLE ${PROJECT_BUNDLE_NAME} +- ICON ${WSJTX_ICON_FILE} +- FILE_DESCRIPTION "Example WSJT-X UDP Message Protocol application" +- ) +-add_resources (message_aggregator_RESOURCES /qss ${message_aggregator_STYLESHEETS}) +-configure_file (UDPExamples/message_aggregator.qrc.in message_aggregator.qrc @ONLY) +-qt5_add_resources (message_aggregator_RESOURCES_RCC +- ${CMAKE_CURRENT_BINARY_DIR}/message_aggregator.qrc +- contrib/QDarkStyleSheet/qdarkstyle/style.qrc +- ) +-add_executable (message_aggregator +- ${message_aggregator_CXXSRCS} +- ${message_aggregator_RESOURCES_RCC} +- ${message_aggregator_VERSION_RESOURCES} +- ) +-target_link_libraries (message_aggregator wsjt_qt Qt5::Widgets wsjtx_udp-static) +- +-if (WSJT_CREATE_WINMAIN) +- set_target_properties (message_aggregator PROPERTIES WIN32_EXECUTABLE ON) +-endif (WSJT_CREATE_WINMAIN) +- +-if (UNIX) +- if (NOT WSJT_SKIP_MANPAGES) +- add_subdirectory (manpages) +- add_dependencies (wsjtx manpages) +- endif (NOT WSJT_SKIP_MANPAGES) +- if (NOT APPLE) +- add_subdirectory (debian) +- add_dependencies (wsjtx debian) +- endif (NOT APPLE) +-endif (UNIX) +- + # + # installation + # +-install (TARGETS wsjtx +- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime +- BUNDLE DESTINATION . COMPONENT runtime +- ) + + # install (TARGETS wsjtx_udp EXPORT udp + # RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +@@ -1577,12 +1379,7 @@ + # DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/wsjtx + # ) + +-install (TARGETS udp_daemon message_aggregator wsjtx_app_version +- RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime +- BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime +- ) +- +-install (TARGETS jt9 wsprd fmtave fcal fmeasure ++install (TARGETS wsjtx_app_version jt9 wsprd + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime + BUNDLE DESTINATION ${CMAKE_INSTALL_BINDIR} COMPONENT runtime + ) +@@ -1595,38 +1392,6 @@ + ) + endif(WSJT_BUILD_UTILS) + +-install (PROGRAMS +- ${RIGCTL_EXE} +- DESTINATION ${CMAKE_INSTALL_BINDIR} +- #COMPONENT runtime +- RENAME rigctl-wsjtx${CMAKE_EXECUTABLE_SUFFIX} +- ) +- +-install (PROGRAMS +- ${RIGCTLD_EXE} +- DESTINATION ${CMAKE_INSTALL_BINDIR} +- #COMPONENT runtime +- RENAME rigctld-wsjtx${CMAKE_EXECUTABLE_SUFFIX} +- ) +- +-install (PROGRAMS +- ${RIGCTLCOM_EXE} +- DESTINATION ${CMAKE_INSTALL_BINDIR} +- #COMPONENT runtime +- RENAME rigctlcom-wsjtx${CMAKE_EXECUTABLE_SUFFIX} +- ) +- +-install (FILES +- README +- COPYING +- AUTHORS +- THANKS +- NEWS +- BUGS +- DESTINATION ${CMAKE_INSTALL_DOCDIR} +- #COMPONENT runtime +- ) +- + install (FILES + cty.dat + cty.dat_copyright.txt +@@ -1635,13 +1400,6 @@ + #COMPONENT runtime + ) + +-install (DIRECTORY +- example_log_configurations +- DESTINATION ${CMAKE_INSTALL_DOCDIR} +- FILES_MATCHING REGEX "^.*[^~]$" +- #COMPONENT runtime +- ) +- + # + # Mac installer files + # +@@ -1693,22 +1451,6 @@ + "${CMAKE_CURRENT_BINARY_DIR}/wsjtx_config.h" + ) + +- +-if (NOT WIN32 AND NOT APPLE) +- # install a desktop file so wsjtx appears in the application start +- # menu with an icon +- install ( +- FILES wsjtx.desktop message_aggregator.desktop +- DESTINATION share/applications +- #COMPONENT runtime +- ) +- install ( +- FILES icons/Unix/wsjtx_icon.png +- DESTINATION share/pixmaps +- #COMPONENT runtime +- ) +-endif (NOT WIN32 AND NOT APPLE) +- + if (APPLE) + set (CMAKE_POSTFLIGHT_SCRIPT + "${wsjtx_BINARY_DIR}/postflight.sh") diff --git a/docker/scripts/install-connectors.sh b/docker/scripts/install-connectors.sh new file mode 100755 index 000000000..f056f2978 --- /dev/null +++ b/docker/scripts/install-connectors.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +set -euxo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + mkdir build + cd build + cmake .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="libfftw3-single3" +BUILD_PACKAGES="git cmake make gcc g++ libsamplerate-dev libfftw3-dev" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/jketterl/owrx_connector.git +# latest develop as of 2024-01-01 (fixed startup race condition) +cmakebuild owrx_connector 62219d40e180abb539ad61fcd9625b90c34f0e26 + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-afedri.sh b/docker/scripts/install-dependencies-afedri.sh new file mode 100755 index 000000000..f71e0743d --- /dev/null +++ b/docker/scripts/install-dependencies-afedri.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + mkdir build + cd build + cmake .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="" +BUILD_PACKAGES="git cmake make gcc g++" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/alexander-sholohov/SoapyAfedri.git +cmakebuild SoapyAfedri 1.0.1 + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-airspy.sh b/docker/scripts/install-dependencies-airspy.sh new file mode 100755 index 000000000..72032bace --- /dev/null +++ b/docker/scripts/install-dependencies-airspy.sh @@ -0,0 +1,44 @@ +#!/bin/bash +set -euxo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + mkdir build + cd build + cmake .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="libusb-1.0-0" +BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/airspy/airspyone_host.git +# latest from master as of 2020-09-04 +cmakebuild airspyone_host 652fd7f1a8f85687641e0bd91f739694d7258ecc + +git clone https://github.com/pothosware/SoapyAirspy.git +cmakebuild SoapyAirspy 10d697b209e7f1acc8b2c8d24851d46170ef77e3 + +git clone https://github.com/airspy/airspyhf.git +# latest from master as of 2020-09-04 +cmakebuild airspyhf 8891387edddcd185e2949e9814e9ef35f46f0722 + +git clone https://github.com/pothosware/SoapyAirspyHF.git +# latest from master as of 2020-09-04 +cmakebuild SoapyAirspyHF 5488dac5b44f1432ce67b40b915f7e61d3bd4853 + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-bladerf.sh b/docker/scripts/install-dependencies-bladerf.sh new file mode 100755 index 000000000..2b2e53205 --- /dev/null +++ b/docker/scripts/install-dependencies-bladerf.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -euxo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + mkdir build + cd build + cmake .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="libusb-1.0-0" +BUILD_PACKAGES="git cmake make gcc g++ libusb-1.0-0-dev" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/Nuand/bladeRF.git +cmakebuild bladeRF 2023.02 + +git clone https://github.com/pothosware/SoapyBladeRF.git +# latest from master as of 2023-08-30 +cmakebuild SoapyBladeRF 85f6dc554ed4c618304d99395b19c4e1523675b0 + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-fcdpp.sh b/docker/scripts/install-dependencies-fcdpp.sh new file mode 100755 index 000000000..49f14394d --- /dev/null +++ b/docker/scripts/install-dependencies-fcdpp.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -euxo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + mkdir build + cd build + cmake .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="libhidapi-hidraw0 libhidapi-libusb0 libasound2" +BUILD_PACKAGES="git cmake make gcc g++ libhidapi-dev libasound2-dev" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/pothosware/SoapyFCDPP.git +cmakebuild SoapyFCDPP soapy-fcdpp-0.1.1 + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-hackrf.sh b/docker/scripts/install-dependencies-hackrf.sh new file mode 100755 index 000000000..19a458a33 --- /dev/null +++ b/docker/scripts/install-dependencies-hackrf.sh @@ -0,0 +1,41 @@ +#!/bin/bash +set -euxo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + mkdir build + cd build + cmake .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="libusb-1.0-0 libfftw3-single3 udev" +BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev libfftw3-dev pkg-config" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/mossmann/hackrf.git +cd hackrf +# latest from master as of 2020-09-04 +git checkout 6e5cbda2945c3bab0e6e1510eae418eda60c358e +cmakebuild host +cd .. +rm -rf hackrf + +git clone https://github.com/pothosware/SoapyHackRF.git +# latest from master as of 2020-09-04 +cmakebuild SoapyHackRF 7d530872f96c1cbe0ed62617c32c48ce7e103e1d + +SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-hpsdr.sh b/docker/scripts/install-dependencies-hpsdr.sh new file mode 100755 index 000000000..24dd1e64e --- /dev/null +++ b/docker/scripts/install-dependencies-hpsdr.sh @@ -0,0 +1,46 @@ +#!/bin/bash +set -euxo pipefail +export MAKEFLAGS="-j4" + +BUILD_PACKAGES="git wget gcc libc6-dev" + +apt-get update +apt-get -y install --no-install-recommends $BUILD_PACKAGES + +pushd /tmp + +ARCH=$(uname -m) +GOVERSION=1.20.10 + +case ${ARCH} in + x86_64) + PACKAGE=go${GOVERSION}.linux-amd64.tar.gz + ;; + armv*) + PACKAGE=go${GOVERSION}.linux-armv6l.tar.gz + ;; + aarch64) + PACKAGE=go${GOVERSION}.linux-arm64.tar.gz + ;; +esac + +wget https://golang.org/dl/${PACKAGE} +tar xfz $PACKAGE + +git clone https://github.com/jancona/hpsdrconnector.git +pushd hpsdrconnector +git checkout v0.6.4 +/tmp/go/bin/go build +install -m 0755 hpsdrconnector /usr/local/bin + +popd + +rm -rf hpsdrconnector +rm -rf go +rm $PACKAGE + +popd + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-limesdr.sh b/docker/scripts/install-dependencies-limesdr.sh new file mode 100755 index 000000000..4f83298ef --- /dev/null +++ b/docker/scripts/install-dependencies-limesdr.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail +export MAKEFLAGS="-j4" + +cd /tmp + +STATIC_PACKAGES="libusb-1.0-0 libatomic1" +BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +SIMD_FLAGS="" +if [[ 'x86_64' == `uname -m` ]] ; then + SIMD_FLAGS="-DDEFAULT_SIMD_FLAGS=SSE3" +fi + +git clone https://github.com/myriadrf/LimeSuite.git +cd LimeSuite +# latest from master as of 2020-09-04 +git checkout 9526621f8b4c9e2a7f638b5ef50c45560dcad22a +mkdir builddir +cd builddir +cmake .. -DENABLE_EXAMPLES=OFF -DENABLE_DESKTOP=OFF -DENABLE_LIME_UTIL=OFF -DENABLE_QUICKTEST=OFF -DENABLE_OCTAVE=OFF -DENABLE_GUI=OFF -DCMAKE_CXX_STANDARD_LIBRARIES="-latomic" ${SIMD_FLAGS} +make +make install +cd ../.. +rm -rf LimeSuite + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-perseus.sh b/docker/scripts/install-dependencies-perseus.sh new file mode 100755 index 000000000..1d8f1c977 --- /dev/null +++ b/docker/scripts/install-dependencies-perseus.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +set -euxo pipefail +export MAKEFLAGS="-j4" + +cd /tmp + +STATIC_PACKAGES="libusb-1.0-0 libudev1" +BUILD_PACKAGES="git make gcc autoconf automake libtool libusb-1.0-0-dev xxd" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/Microtelecom/libperseus-sdr.git +cd libperseus-sdr +# latest from master as of 2020-09-04 +git checkout c2c95daeaa08bf0daed0e8ada970ab17cc264e1b +./bootstrap.sh +./configure +make +make install +ldconfig /etc/ld.so.conf.d +cd .. +rm -rf libperseus-sdr + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-plutosdr.sh b/docker/scripts/install-dependencies-plutosdr.sh new file mode 100755 index 000000000..aa801b560 --- /dev/null +++ b/docker/scripts/install-dependencies-plutosdr.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +set -euo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + mkdir build + cd build + cmake .. ${3:-} + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="libusb-1.0-0 libxml2" +BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ libxml2-dev flex bison pkg-config" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/analogdevicesinc/libiio.git +cmakebuild libiio v0.21 -DCMAKE_INSTALL_PREFIX=/usr/local + +git clone https://github.com/analogdevicesinc/libad9361-iio.git +cmakebuild libad9361-iio v0.2 + +git clone https://github.com/pothosware/SoapyPlutoSDR.git +# latest from master as of 2020-09-04 +cmakebuild SoapyPlutoSDR 93717b32ef052e0dfa717aa2c1a4eb27af16111f + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-radioberry.sh b/docker/scripts/install-dependencies-radioberry.sh new file mode 100755 index 000000000..44688e0d6 --- /dev/null +++ b/docker/scripts/install-dependencies-radioberry.sh @@ -0,0 +1,37 @@ +#!/bin/bash +set -euxo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + mkdir build + cd build + cmake .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="" +BUILD_PACKAGES="git cmake make gcc g++" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/pa3gsb/Radioberry-2.x +cd Radioberry-2.x/SBC/rpi-4 + +# latest from master as of 2020-09-04 +cmakebuild SoapyRadioberrySDR 8d17de6b4dc076e628900a82f05c7cf0b16cbe24 +cd ../../.. +rm -rf Radioberry-2.x + +SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-rtlsdr-soapy.sh b/docker/scripts/install-dependencies-rtlsdr-soapy.sh new file mode 100755 index 000000000..79077147f --- /dev/null +++ b/docker/scripts/install-dependencies-rtlsdr-soapy.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +set -euo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + mkdir build + cd build + cmake .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="libusb-1.0-0" +BUILD_PACKAGES="git libusb-1.0-0-dev cmake make gcc g++ pkg-config" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/osmocom/rtl-sdr.git +cmakebuild rtl-sdr v2.0.1 + +git clone https://github.com/pothosware/SoapyRTLSDR.git +# latest from master as of 2023-09-13 +cmakebuild SoapyRTLSDR 068aa77a4c938b239c9d80cd42c4ee7986458e8f + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-rtlsdr.sh b/docker/scripts/install-dependencies-rtlsdr.sh new file mode 100755 index 000000000..ccaa53827 --- /dev/null +++ b/docker/scripts/install-dependencies-rtlsdr.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -euxo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + mkdir build + cd build + cmake .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="libusb-1.0.0" +BUILD_PACKAGES="git libusb-1.0.0-dev cmake make gcc g++ pkg-config" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/osmocom/rtl-sdr.git +cmakebuild rtl-sdr v2.0.1 + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-runds.sh b/docker/scripts/install-dependencies-runds.sh new file mode 100755 index 000000000..fdf263014 --- /dev/null +++ b/docker/scripts/install-dependencies-runds.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -euxo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + mkdir build + cd build + cmake .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="libfftw3-single3" +BUILD_PACKAGES="git cmake make gcc g++ pkg-config libfftw3-dev" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/jketterl/runds_connector.git +# latest develop as of 2023-07-04 (cmake exports) +cmakebuild runds_connector 435364002d756735015707e7f59aa40e8d743585 + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-sdrplay.sh b/docker/scripts/install-dependencies-sdrplay.sh new file mode 100755 index 000000000..d09cb9119 --- /dev/null +++ b/docker/scripts/install-dependencies-sdrplay.sh @@ -0,0 +1,54 @@ +#!/bin/bash +set -euxo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + mkdir build + cd build + cmake .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="libusb-1.0.0 udev" +BUILD_PACKAGES="git cmake make patch wget sudo gcc g++ libusb-1.0-0-dev" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +ARCH=$(uname -m) + +case $ARCH in + x86_64|aarch64) + BINARY=SDRplay_RSP_API-Linux-3.14.0.run + ;; + armv*) + BINARY=SDRplay_RSP_API-ARM32-3.07.2.run + ;; +esac + +wget --no-http-keep-alive https://www.sdrplay.com/software/$BINARY +sh $BINARY --noexec --target sdrplay +patch --verbose -Np0 < /install-lib.$ARCH.patch + +cd sdrplay +./install_lib.sh +cd .. +rm -rf sdrplay +rm $BINARY + +git clone https://github.com/pothosware/SoapySDRPlay3.git +# latest from master as of 2021-06-19 (reliability fixes) +cmakebuild SoapySDRPlay3 a869f25364a1f0d5b16169ff908aa21a2ace475d + +SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-soapyremote.sh b/docker/scripts/install-dependencies-soapyremote.sh new file mode 100755 index 000000000..a74c46520 --- /dev/null +++ b/docker/scripts/install-dependencies-soapyremote.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + mkdir build + cd build + cmake .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="avahi-daemon libavahi-client3" +BUILD_PACKAGES="git cmake make gcc g++ libavahi-client-dev" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/pothosware/SoapyRemote.git +cmakebuild SoapyRemote soapy-remote-0.5.2 + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-soapysdr.sh b/docker/scripts/install-dependencies-soapysdr.sh new file mode 100755 index 000000000..bd312b440 --- /dev/null +++ b/docker/scripts/install-dependencies-soapysdr.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -euxo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + mkdir build + cd build + cmake .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="libudev1" +BUILD_PACKAGES="git cmake make patch wget sudo gcc g++" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/pothosware/SoapySDR +# latest from master as of 2020-09-04 +cmakebuild SoapySDR 580b94f3dad46899f34ec0a060dbb4534e844e57 + +SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies-uhd.sh b/docker/scripts/install-dependencies-uhd.sh new file mode 100755 index 000000000..87a5732cc --- /dev/null +++ b/docker/scripts/install-dependencies-uhd.sh @@ -0,0 +1,59 @@ +#!/bin/bash +set -euo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + mkdir build + cd build + cmake .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="libusb-1.0.0 libboost-chrono1.74.0 libboost-date-time1.74.0 libboost-filesystem1.74.0 libboost-program-options1.74.0 libboost-regex1.74.0 libboost-test1.74.0 libboost-serialization1.74.0 libboost-thread1.74.0 libboost-system1.74.0 python3-numpy python3-mako" +BUILD_PACKAGES="git cmake make gcc g++ libusb-1.0-0-dev libboost-dev libboost-chrono-dev libboost-date-time-dev libboost-filesystem-dev libboost-program-options-dev libboost-regex-dev libboost-test-dev libboost-serialization-dev libboost-thread-dev libboost-system-dev" + +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/EttusResearch/uhd.git +mkdir -p uhd/host/build +cd uhd/host/build +git checkout v4.1.0.4 +# see https://github.com/EttusResearch/uhd/issues/350 +case `uname -m` in + arm*) + cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_UTILS=OFF -DENABLE_PYTHON_API=OFF -DENABLE_EXAMPLES=OFF -DENABLE_TESTS=OFF -DENABLE_OCTOCLOCK=OFF -DENABLE_MAN_PAGES=OFF -DSTRIP_BINARIES=ON \ + -DCMAKE_CXX_FLAGS:STRING="-march=armv7-a -mfloat-abi=hard -mfpu=neon -mtune=cortex-a8 -Wno-psabi" \ + -DCMAKE_C_FLAGS:STRING="-march=armv7-a -mfloat-abi=hard -mfpu=neon -mtune=cortex-a8 -Wno-psabi" \ + -DCMAKE_ASM_FLAGS:STRING="-march=armv7-a -mfloat-abi=hard -mfpu=neon -mtune=cortex-a8 -g" .. + ;; + aarch64*) + cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_UTILS=OFF -DENABLE_PYTHON_API=OFF -DENABLE_EXAMPLES=OFF -DENABLE_TESTS=OFF -DENABLE_OCTOCLOCK=OFF -DENABLE_MAN_PAGES=OFF -DSTRIP_BINARIES=ON \ + -DCMAKE_CXX_FLAGS:STRING="-march=armv8-a -mtune=cortex-a72 -Wno-psabi" \ + -DCMAKE_C_FLAGS:STRING="-march=armv8-a -mtune=cortex-a72 -Wno-psabi" \ + -DCMAKE_ASM_FLAGS:STRING="-march=armv8-a -mtune=cortex-a72 -g" .. + ;; + x86_64) + cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_UTILS=OFF -DENABLE_PYTHON_API=OFF -DENABLE_EXAMPLES=OFF -DENABLE_TESTS=OFF -DENABLE_OCTOCLOCK=OFF -DENABLE_MAN_PAGES=OFF -DSTRIP_BINARIES=ON .. + ;; +esac +make +make install +cd ../../.. +rm -rf uhd + +git clone https://github.com/pothosware/SoapyUHD.git +cmakebuild SoapyUHD soapy-uhd-0.4.1 + +SUDO_FORCE_REMOVE=yes apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-dependencies.sh b/docker/scripts/install-dependencies.sh new file mode 100755 index 000000000..c10f97f1f --- /dev/null +++ b/docker/scripts/install-dependencies.sh @@ -0,0 +1,159 @@ +#!/bin/bash +set -euxo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + if [[ -f ".gitmodules" ]]; then + git submodule update --init + fi + mkdir build + cd build + cmake ${CMAKE_ARGS:-} .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="libfftw3-single3 libfftw3-double3 python3 python3-setuptools python3-paho-mqtt netcat-openbsd libsndfile1 liblapack3 libusb-1.0-0 libqt5core5a libreadline8 libgfortran5 libgomp1 libasound2 libudev1 ca-certificates libpulse0 libfaad2 libopus0 libboost-program-options1.74.0 libboost-log1.74.0 libcurl4 libncurses6 libliquid1 libconfig++9v5" +BUILD_PACKAGES="wget git libsndfile1-dev libfftw3-dev cmake make gcc g++ liblapack-dev texinfo gfortran libusb-1.0-0-dev qtbase5-dev qtmultimedia5-dev qttools5-dev libqt5serialport5-dev qttools5-dev-tools asciidoctor asciidoc libasound2-dev libudev-dev libhamlib-dev patch xsltproc qt5-qmake libfaad-dev libopus-dev libboost-dev libboost-program-options-dev libboost-log-dev libboost-regex-dev libpulse-dev libcurl4-openssl-dev libncurses-dev xz-utils libliquid-dev libconfig++-dev autoconf automake" +apt-get update +apt-get -y install auto-apt-proxy +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +case `uname -m` in + arm*) + PLATFORM=armhf + ;; + aarch64*) + PLATFORM=aarch64 + ;; + x86_64*) + PLATFORM=x86_64 + ;; +esac + +wget https://github.com/just-containers/s6-overlay/releases/download/v3.1.5.0/s6-overlay-noarch.tar.xz +tar -Jxpf /tmp/s6-overlay-noarch.tar.xz -C / +rm s6-overlay-noarch.tar.xz +wget https://github.com/just-containers/s6-overlay/releases/download/v3.1.5.0/s6-overlay-${PLATFORM}.tar.xz +tar -Jxpf /tmp/s6-overlay-${PLATFORM}.tar.xz -C / +rm s6-overlay-${PLATFORM}.tar.xz + +JS8CALL_VERSION=2.2.0 +JS8CALL_DIR=js8call +JS8CALL_TGZ=js8call-${JS8CALL_VERSION}.tgz +wget http://files.js8call.com/${JS8CALL_VERSION}/${JS8CALL_TGZ} +tar xfz ${JS8CALL_TGZ} +# patch allows us to build against the packaged hamlib +patch -Np1 -d ${JS8CALL_DIR} < /js8call-hamlib.patch +rm /js8call-hamlib.patch +cmakebuild ${JS8CALL_DIR} +rm ${JS8CALL_TGZ} + +WSJT_DIR=wsjtx-2.6.1 +WSJT_TGZ=${WSJT_DIR}.tgz +wget https://downloads.sourceforge.net/project/wsjt/${WSJT_DIR}/${WSJT_TGZ} +tar xfz ${WSJT_TGZ} +patch -Np0 -d ${WSJT_DIR} < /wsjtx-hamlib.patch +mv /wsjtx.patch ${WSJT_DIR} +cmakebuild ${WSJT_DIR} +rm ${WSJT_TGZ} + +git clone https://github.com/alexander-sholohov/msk144decoder.git +# latest from main as of 2023-02-21 +MAKEFLAGS="" cmakebuild msk144decoder fe2991681e455636e258e83c29fd4b2a72d16095 + +git clone --depth 1 -b 1.6 https://github.com/wb2osz/direwolf.git +cd direwolf +# hamlib is present (necessary for the wsjt-x and js8call builds) and would be used, but there's no real need. +# this patch prevents direwolf from linking to it, and it can be stripped at the end of the script. +patch -Np1 < /direwolf-hamlib.patch +mkdir build +cd build +cmake .. +make +make install +cd ../.. +rm -rf direwolf +# strip lots of generic documentation that will never be read inside a docker container +rm /usr/local/share/doc/direwolf/*.pdf +# examples are pointless, too +rm -rf /usr/local/share/doc/direwolf/examples/ + +git clone https://github.com/drowe67/codec2.git +cd codec2 +git checkout 1.2.0 +mkdir build +cd build +cmake .. +make +make install +install -m 0755 src/freedv_rx /usr/local/bin +cd ../.. +rm -rf codec2 + +wget https://downloads.sourceforge.net/project/drm/dream/2.1.1/dream-2.1.1-svn808.tar.gz +tar xvfz dream-2.1.1-svn808.tar.gz +pushd dream +patch -Np0 < /dream.patch +qmake CONFIG+=console +make +make install +popd +rm -rf dream +rm dream-2.1.1-svn808.tar.gz + +git clone https://github.com/mobilinkd/m17-cxx-demod.git +cmakebuild m17-cxx-demod v2.3 + +git clone --depth 1 -b v9.0 https://github.com/flightaware/dump1090 +cd dump1090 +make +install -m 0755 dump1090 /usr/local/bin +cd .. +rm -rf dump1090 + +git clone https://github.com/merbanan/rtl_433.git +# latest from master as of 2023-09-06 +CMAKE_ARGS="-DENABLE_RTLSDR=OFF" cmakebuild rtl_433 70d84d01e1be87b459f7a10825966f3262b7dd34 + +git clone https://github.com/szpajder/libacars.git +cmakebuild libacars v2.2.0 + +git clone https://github.com/szpajder/dumphfdl +cmakebuild dumphfdl v1.4.1 + +git clone https://github.com/szpajder/dumpvdl2.git +cmakebuild dumpvdl2 v2.3.0 + +git clone https://github.com/windytan/redsea.git +pushd redsea +# latest from master as of 2024-01-18 +git checkout c6e6b47ac2c7a9aac9409483b00ca61cd6eb47bd +./autogen.sh +./configure +make +make install +popd +rm -rf redsea + +git clone https://github.com/Opendigitalradio/dablin.git +CMAKE_ARGS="-DDISABLE_SDL=1" cmakebuild dablin 1.15.0 + +git clone https://github.com/hessu/aprs-symbols /usr/share/aprs-symbols +pushd /usr/share/aprs-symbols +git checkout 5c2abe2658ee4d2563f3c73b90c6f59124839802 +# remove unused files (including git meta information) +rm -rf .git aprs-symbols.ai aprs-sym-export.js +popd + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/install-owrx-tools.sh b/docker/scripts/install-owrx-tools.sh new file mode 100755 index 000000000..357e36b20 --- /dev/null +++ b/docker/scripts/install-owrx-tools.sh @@ -0,0 +1,78 @@ +#!/bin/bash +set -euxo pipefail +export MAKEFLAGS="-j4" + +function cmakebuild() { + cd $1 + if [[ ! -z "${2:-}" ]]; then + git checkout $2 + fi + mkdir build + cd build + cmake ${CMAKE_ARGS:-} .. + make + make install + cd ../.. + rm -rf $1 +} + +cd /tmp + +STATIC_PACKAGES="libfftw3-single3 libprotobuf32 libsamplerate0 libicu72 libudev1" +BUILD_PACKAGES="git autoconf automake libtool libfftw3-dev pkg-config cmake make gcc g++ libprotobuf-dev protobuf-compiler libsamplerate-dev libicu-dev libpython3-dev libudev-dev" +apt-get update +apt-get -y install --no-install-recommends $STATIC_PACKAGES $BUILD_PACKAGES + +git clone https://github.com/jketterl/js8py.git +pushd js8py +# latest develop as of 2022-11-30 (structured callsign data) +git checkout f7e394b7892d26cbdcce5d43c0b4081a2a6a48f6 +python3 setup.py install +popd +rm -rf js8py + +git clone https://github.com/jketterl/csdr.git +# latest develop as of 2024-01-25 (exemodule setargs) +cmakebuild csdr 344179a616cdbadf501479ce9ed1b836543e657b + +git clone https://github.com/jketterl/pycsdr.git +cd pycsdr +# latest develop as of 2024-01-25 (execmodule setargs) +git checkout 9063b8a119e366c31d089596641a24a427e3cbdc +./setup.py install install_headers +cd .. +rm -rf pycsdr + +git clone https://github.com/jketterl/csdr-eti.git +# latest develop as of 2024-02-13 (fix for aarch64) +cmakebuild csdr-eti e174007f9c247047dba60f092f794800297c594f + +git clone https://github.com/jketterl/pycsdr-eti.git +cd pycsdr-eti +# latest develop as of 2024-02-12 (service id filter) +git checkout 676663b4d796fbadd18dfcae0c3b80eb1b1f9147 +./setup.py install +cd .. +rm -rf pycsdr-eti + +git clone https://github.com/jketterl/codecserver.git +mkdir -p /usr/local/etc/codecserver +cp codecserver/conf/codecserver.conf /usr/local/etc/codecserver +# latest develop as of 2023-07-03 (error handling) +cmakebuild codecserver 0f3703ce285acd85fcd28f6620d7795dc173cb50 + +git clone https://github.com/jketterl/digiham.git +# latest develop as of 2023-07-02 (codecserver protocol version) +cmakebuild digiham 262e6dfd9a2c56778bd4b597240756ad0fb9861d + +git clone https://github.com/jketterl/pydigiham.git +cd pydigiham +# latest develop as of 2023-06-30 (csdr cleanup) +git checkout 894aa87ea9a3534d1e7109da86194c7cd5e0b7c7 +./setup.py install +cd .. +rm -rf pydigiham + +apt-get -y purge --autoremove $BUILD_PACKAGES +apt-get clean +rm -rf /var/lib/apt/lists/* diff --git a/docker/scripts/run.sh b/docker/scripts/run.sh new file mode 100755 index 000000000..92ea968c3 --- /dev/null +++ b/docker/scripts/run.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -euo pipefail + +mkdir -p /etc/openwebrx/openwebrx.conf.d +mkdir -p /var/lib/openwebrx +mkdir -p /tmp/openwebrx/ +if [[ ! -f /etc/openwebrx/openwebrx.conf.d/20-temporary-directory.conf ]] ; then + cat << EOF > /etc/openwebrx/openwebrx.conf.d/20-temporary-directory.conf +[core] +temporary_directory = /tmp/openwebrx +EOF +fi +if [[ ! -f /etc/openwebrx/bands.json ]] ; then + cp bands.json /etc/openwebrx/ +fi +if [[ ! -f /etc/openwebrx/openwebrx.conf ]] ; then + cp openwebrx.conf /etc/openwebrx/ +fi +if [[ ! -z "${OPENWEBRX_ADMIN_USER:-}" ]] && [[ ! -z "${OPENWEBRX_ADMIN_PASSWORD:-}" ]] ; then + if ! python3 openwebrx.py admin --silent hasuser "${OPENWEBRX_ADMIN_USER}" ; then + OWRX_PASSWORD="${OPENWEBRX_ADMIN_PASSWORD}" python3 openwebrx.py admin --noninteractive adduser "${OPENWEBRX_ADMIN_USER}" + fi +fi + + +_term() { + echo "Caught signal!" + kill -TERM "$child" 2>/dev/null +} + +trap _term SIGTERM SIGINT + +python3 openwebrx.py $@ & + +child=$! +wait "$child" diff --git a/htdocs/__init__.py b/htdocs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/htdocs/apple-touch-icon.png b/htdocs/apple-touch-icon.png new file mode 100644 index 000000000..5bc3c155a Binary files /dev/null and b/htdocs/apple-touch-icon.png differ diff --git a/htdocs/css/admin.css b/htdocs/css/admin.css new file mode 100644 index 000000000..02e9a8adb --- /dev/null +++ b/htdocs/css/admin.css @@ -0,0 +1,166 @@ +@import url("openwebrx-header.css"); +@import url("openwebrx-globals.css"); + +html, body { + height: unset; +} + +body { + margin-bottom: 5rem; +} + +hr { + background: #444; +} + +.buttons { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: #222; + z-index: 2; + padding: 10px; + text-align: right; + border-top: 1px solid #444; +} + +.row .map-input { + margin: 15px 15px 0; +} + +.settings-section h3 { + margin-top: 1em; + margin-bottom: 1em; +} + +h1 { + margin: 1em 0; + text-align: center; +} + +.matrix { + display: grid; +} + +.q65-matrix { + grid-template-columns: repeat(5, auto); +} + +.imageupload .image-container { + max-width: 100%; + padding: 7px; +} + +.imageupload img.webrx-top-photo { + max-height: 350px; + max-width: 100%; +} + +.settings-grid > div { + padding: 20px; +} + +.settings-grid .btn { + width: 100%; + height: 100px; + padding: 20px; + font-size: 1.2rem; +} + +.tab-body { + overflow: auto; + border: 1px solid #444; + border-top: none; + border-bottom-left-radius: 0.25rem; + border-bottom-right-radius: 0.25rem; +} + +.tab-body .form-group { + padding-right: 15px; +} + +.bookmarks table .frequency, .bookmark-list table .frequency { + text-align: right; +} + +.bookmarks table input, .bookmarks table select { + width: initial; + text-align: inherit; + display: initial; +} + +.bookmark-list table .form-check-input { + margin-left: 0; +} + +.actions { + margin: 1rem 0; +} + +.actions .btn { + width: 100%; +} + +.wsjt-decoding-depths-table { + width: auto; + margin: 0; +} + +.wsjt-decoding-depths-table td:first-child { + padding-left: 0; +} + +.sdr-device-list .list-group-item, +.sdr-profile-list .list-group-item { + background: initial; +} + +.sdr-device-list .sdr-profile-list { + max-height: 20rem; + overflow-y: auto; +} + +.removable-group.removable, .add-group { + display: flex; + flex-direction: row; +} + +.removable-group.removable .removable-item, .add-group .add-group-select { + flex: 1 0 0; + margin-right: .25rem; +} + +.removable-group.removable .option-remove-button, .add-group .option-add-button { + flex: 0 0 70px; +} + +.option-add-button, .option-remove-button { + width: 70px; +} + +.scheduler-static-time-inputs { + display: flex; + flex-direction: row; +} + +.scheduler-static-time-inputs > * { + flex: 0 0 auto; + width: unset; +} + +.scheduler-static-time-inputs > select { + flex: 1 0 auto; +} + +.breadcrumb { + margin-top: .5rem; +} + +.imageupload.is-invalid ~ .invalid-feedback { + display: block; +} + +.device-log-messages { + max-height: 500px; +} \ No newline at end of file diff --git a/htdocs/css/bootstrap.min.css b/htdocs/css/bootstrap.min.css new file mode 100644 index 000000000..43d80a04d --- /dev/null +++ b/htdocs/css/bootstrap.min.css @@ -0,0 +1,12 @@ +/*! + * Bootswatch v4.5.0 + * Homepage: https://bootswatch.com + * Copyright 2012-2020 Thomas Park + * Licensed under MIT + * Based on Bootstrap +*//*! + * Bootstrap v4.5.0 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors + * Copyright 2011-2020 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */@import url("https://fonts.googleapis.com/css?family=Lato:400,700,400italic&display=swap");:root{--blue: #375a7f;--indigo: #6610f2;--purple: #6f42c1;--pink: #e83e8c;--red: #E74C3C;--orange: #fd7e14;--yellow: #F39C12;--green: #00bc8c;--teal: #20c997;--cyan: #3498DB;--white: #fff;--gray: #888;--gray-dark: #303030;--primary: #375a7f;--secondary: #444;--success: #00bc8c;--info: #3498DB;--warning: #F39C12;--danger: #E74C3C;--light: #adb5bd;--dark: #303030;--breakpoint-xs: 0;--breakpoint-sm: 576px;--breakpoint-md: 768px;--breakpoint-lg: 992px;--breakpoint-xl: 1200px;--font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}*,*::before,*::after{-webkit-box-sizing:border-box;box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:"Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-size:0.9375rem;font-weight:400;line-height:1.5;color:#fff;text-align:left;background-color:#222}[tabindex="-1"]:focus:not(:focus-visible){outline:0 !important}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:0.5rem}p{margin-top:0;margin-bottom:1rem}abbr[title],abbr[data-original-title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;border-bottom:0;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#00bc8c;text-decoration:none;background-color:transparent}a:hover{color:#007053;text-decoration:underline}a:not([href]){color:inherit;text-decoration:none}a:not([href]):hover{color:inherit;text-decoration:none}pre,code,kbd,samp{font-family:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:0.75rem;padding-bottom:0.75rem;color:#888;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:0.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role="button"]{cursor:pointer}select{word-wrap:normal}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button:not(:disabled),[type="button"]:not(:disabled),[type="reset"]:not(:disabled),[type="submit"]:not(:disabled){cursor:pointer}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{padding:0;border-style:none}input[type="radio"],input[type="checkbox"]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{outline-offset:-2px;-webkit-appearance:none}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none !important}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{margin-bottom:0.5rem;font-weight:500;line-height:1.2}h1,.h1{font-size:3rem}h2,.h2{font-size:2.5rem}h3,.h3{font-size:2rem}h4,.h4{font-size:1.40625rem}h5,.h5{font-size:1.171875rem}h6,.h6{font-size:0.9375rem}.lead{font-size:1.171875rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,0.1)}small,.small{font-size:80%;font-weight:400}mark,.mark{padding:0.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:0.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.171875rem}.blockquote-footer{display:block;font-size:80%;color:#888}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:0.25rem;background-color:#222;border:1px solid #dee2e6;border-radius:0.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:0.5rem;line-height:1}.figure-caption{font-size:90%;color:#888}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:0.2rem 0.4rem;font-size:87.5%;color:#fff;background-color:#222;border-radius:0.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:inherit}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width: 576px){.container{max-width:540px}}@media (min-width: 768px){.container{max-width:720px}}@media (min-width: 992px){.container{max-width:960px}}@media (min-width: 1200px){.container{max-width:1140px}}.container-fluid,.container-sm,.container-md,.container-lg,.container-xl{width:100%;padding-right:15px;padding-left:15px;margin-right:auto;margin-left:auto}@media (min-width: 576px){.container,.container-sm{max-width:540px}}@media (min-width: 768px){.container,.container-sm,.container-md{max-width:720px}}@media (min-width: 992px){.container,.container-sm,.container-md,.container-lg{max-width:960px}}@media (min-width: 1200px){.container,.container-sm,.container-md,.container-lg,.container-xl{max-width:1140px}}.row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-15px;margin-left:-15px}.no-gutters{margin-right:0;margin-left:0}.no-gutters>.col,.no-gutters>[class*="col-"]{padding-right:0;padding-left:0}.col-1,.col-2,.col-3,.col-4,.col-5,.col-6,.col-7,.col-8,.col-9,.col-10,.col-11,.col-12,.col,.col-auto,.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm,.col-sm-auto,.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12,.col-md,.col-md-auto,.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg,.col-lg-auto,.col-xl-1,.col-xl-2,.col-xl-3,.col-xl-4,.col-xl-5,.col-xl-6,.col-xl-7,.col-xl-8,.col-xl-9,.col-xl-10,.col-xl-11,.col-xl-12,.col-xl,.col-xl-auto{position:relative;width:100%;padding-right:15px;padding-left:15px}.col{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-1{margin-left:8.3333333333%}.offset-2{margin-left:16.6666666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.3333333333%}.offset-5{margin-left:41.6666666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.3333333333%}.offset-8{margin-left:66.6666666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.3333333333%}.offset-11{margin-left:91.6666666667%}@media (min-width: 576px){.col-sm{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-sm-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-sm-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-sm-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-sm-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-sm-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-sm-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-sm-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-sm-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-sm-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-sm-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-sm-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-sm-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-sm-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-sm-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-sm-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-sm-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-sm-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-sm-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-sm-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-sm-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-sm-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-sm-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-sm-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-sm-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-sm-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-sm-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-sm-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-sm-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-sm-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-sm-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-sm-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-sm-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-sm-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-sm-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.3333333333%}.offset-sm-2{margin-left:16.6666666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.3333333333%}.offset-sm-5{margin-left:41.6666666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.3333333333%}.offset-sm-8{margin-left:66.6666666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.3333333333%}.offset-sm-11{margin-left:91.6666666667%}}@media (min-width: 768px){.col-md{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-md-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-md-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-md-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-md-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-md-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-md-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-md-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-md-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-md-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-md-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-md-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-md-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-md-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-md-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-md-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-md-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-md-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-md-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-md-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-md-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-md-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-md-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-md-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-md-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-md-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-md-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-md-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-md-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-md-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-md-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-md-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-md-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-md-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-md-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.3333333333%}.offset-md-2{margin-left:16.6666666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.3333333333%}.offset-md-5{margin-left:41.6666666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.3333333333%}.offset-md-8{margin-left:66.6666666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.3333333333%}.offset-md-11{margin-left:91.6666666667%}}@media (min-width: 992px){.col-lg{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-lg-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-lg-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-lg-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-lg-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-lg-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-lg-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-lg-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-lg-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-lg-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-lg-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-lg-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-lg-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-lg-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-lg-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-lg-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-lg-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-lg-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-lg-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-lg-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-lg-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-lg-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-lg-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-lg-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-lg-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-lg-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-lg-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-lg-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-lg-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-lg-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-lg-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-lg-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-lg-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-lg-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-lg-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.3333333333%}.offset-lg-2{margin-left:16.6666666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.3333333333%}.offset-lg-5{margin-left:41.6666666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.3333333333%}.offset-lg-8{margin-left:66.6666666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.3333333333%}.offset-lg-11{margin-left:91.6666666667%}}@media (min-width: 1200px){.col-xl{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;min-width:0;max-width:100%}.row-cols-xl-1>*{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.row-cols-xl-2>*{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.row-cols-xl-3>*{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.row-cols-xl-4>*{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.row-cols-xl-5>*{-webkit-box-flex:0;-ms-flex:0 0 20%;flex:0 0 20%;max-width:20%}.row-cols-xl-6>*{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-xl-auto{-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;width:auto;max-width:100%}.col-xl-1{-webkit-box-flex:0;-ms-flex:0 0 8.3333333333%;flex:0 0 8.3333333333%;max-width:8.3333333333%}.col-xl-2{-webkit-box-flex:0;-ms-flex:0 0 16.6666666667%;flex:0 0 16.6666666667%;max-width:16.6666666667%}.col-xl-3{-webkit-box-flex:0;-ms-flex:0 0 25%;flex:0 0 25%;max-width:25%}.col-xl-4{-webkit-box-flex:0;-ms-flex:0 0 33.3333333333%;flex:0 0 33.3333333333%;max-width:33.3333333333%}.col-xl-5{-webkit-box-flex:0;-ms-flex:0 0 41.6666666667%;flex:0 0 41.6666666667%;max-width:41.6666666667%}.col-xl-6{-webkit-box-flex:0;-ms-flex:0 0 50%;flex:0 0 50%;max-width:50%}.col-xl-7{-webkit-box-flex:0;-ms-flex:0 0 58.3333333333%;flex:0 0 58.3333333333%;max-width:58.3333333333%}.col-xl-8{-webkit-box-flex:0;-ms-flex:0 0 66.6666666667%;flex:0 0 66.6666666667%;max-width:66.6666666667%}.col-xl-9{-webkit-box-flex:0;-ms-flex:0 0 75%;flex:0 0 75%;max-width:75%}.col-xl-10{-webkit-box-flex:0;-ms-flex:0 0 83.3333333333%;flex:0 0 83.3333333333%;max-width:83.3333333333%}.col-xl-11{-webkit-box-flex:0;-ms-flex:0 0 91.6666666667%;flex:0 0 91.6666666667%;max-width:91.6666666667%}.col-xl-12{-webkit-box-flex:0;-ms-flex:0 0 100%;flex:0 0 100%;max-width:100%}.order-xl-first{-webkit-box-ordinal-group:0;-ms-flex-order:-1;order:-1}.order-xl-last{-webkit-box-ordinal-group:14;-ms-flex-order:13;order:13}.order-xl-0{-webkit-box-ordinal-group:1;-ms-flex-order:0;order:0}.order-xl-1{-webkit-box-ordinal-group:2;-ms-flex-order:1;order:1}.order-xl-2{-webkit-box-ordinal-group:3;-ms-flex-order:2;order:2}.order-xl-3{-webkit-box-ordinal-group:4;-ms-flex-order:3;order:3}.order-xl-4{-webkit-box-ordinal-group:5;-ms-flex-order:4;order:4}.order-xl-5{-webkit-box-ordinal-group:6;-ms-flex-order:5;order:5}.order-xl-6{-webkit-box-ordinal-group:7;-ms-flex-order:6;order:6}.order-xl-7{-webkit-box-ordinal-group:8;-ms-flex-order:7;order:7}.order-xl-8{-webkit-box-ordinal-group:9;-ms-flex-order:8;order:8}.order-xl-9{-webkit-box-ordinal-group:10;-ms-flex-order:9;order:9}.order-xl-10{-webkit-box-ordinal-group:11;-ms-flex-order:10;order:10}.order-xl-11{-webkit-box-ordinal-group:12;-ms-flex-order:11;order:11}.order-xl-12{-webkit-box-ordinal-group:13;-ms-flex-order:12;order:12}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.3333333333%}.offset-xl-2{margin-left:16.6666666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.3333333333%}.offset-xl-5{margin-left:41.6666666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.3333333333%}.offset-xl-8{margin-left:66.6666666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.3333333333%}.offset-xl-11{margin-left:91.6666666667%}}.table{width:100%;margin-bottom:1rem;color:#fff}.table th,.table td{padding:0.75rem;vertical-align:top;border-top:1px solid #444}.table thead th{vertical-align:bottom;border-bottom:2px solid #444}.table tbody+tbody{border-top:2px solid #444}.table-sm th,.table-sm td{padding:0.3rem}.table-bordered{border:1px solid #444}.table-bordered th,.table-bordered td{border:1px solid #444}.table-bordered thead th,.table-bordered thead td{border-bottom-width:2px}.table-borderless th,.table-borderless td,.table-borderless thead th,.table-borderless tbody+tbody{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:#303030}.table-hover tbody tr:hover{color:#fff;background-color:rgba(0,0,0,0.075)}.table-primary,.table-primary>th,.table-primary>td{background-color:#c7d1db}.table-primary th,.table-primary td,.table-primary thead th,.table-primary tbody+tbody{border-color:#97a9bc}.table-hover .table-primary:hover{background-color:#b7c4d1}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#b7c4d1}.table-secondary,.table-secondary>th,.table-secondary>td{background-color:#cbcbcb}.table-secondary th,.table-secondary td,.table-secondary thead th,.table-secondary tbody+tbody{border-color:#9e9e9e}.table-hover .table-secondary:hover{background-color:#bebebe}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#bebebe}.table-success,.table-success>th,.table-success>td{background-color:#b8ecdf}.table-success th,.table-success td,.table-success thead th,.table-success tbody+tbody{border-color:#7adcc3}.table-hover .table-success:hover{background-color:#a4e7d6}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#a4e7d6}.table-info,.table-info>th,.table-info>td{background-color:#c6e2f5}.table-info th,.table-info td,.table-info thead th,.table-info tbody+tbody{border-color:#95c9ec}.table-hover .table-info:hover{background-color:#b0d7f1}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#b0d7f1}.table-warning,.table-warning>th,.table-warning>td{background-color:#fce3bd}.table-warning th,.table-warning td,.table-warning thead th,.table-warning tbody+tbody{border-color:#f9cc84}.table-hover .table-warning:hover{background-color:#fbd9a5}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#fbd9a5}.table-danger,.table-danger>th,.table-danger>td{background-color:#f8cdc8}.table-danger th,.table-danger td,.table-danger thead th,.table-danger tbody+tbody{border-color:#f3a29a}.table-hover .table-danger:hover{background-color:#f5b8b1}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#f5b8b1}.table-light,.table-light>th,.table-light>td{background-color:#e8eaed}.table-light th,.table-light td,.table-light thead th,.table-light tbody+tbody{border-color:#d4d9dd}.table-hover .table-light:hover{background-color:#dadde2}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#dadde2}.table-dark,.table-dark>th,.table-dark>td{background-color:#c5c5c5}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#939393}.table-hover .table-dark:hover{background-color:#b8b8b8}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b8b8b8}.table-active,.table-active>th,.table-active>td{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,0.075)}.table .thead-dark th{color:#fff;background-color:#303030;border-color:#434343}.table .thead-light th{color:#444;background-color:#ebebeb;border-color:#444}.table-dark{color:#fff;background-color:#303030}.table-dark th,.table-dark td,.table-dark thead th{border-color:#434343}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,0.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,0.075)}@media (max-width: 575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width: 767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width: 991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width: 1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 0.75rem;font-size:0.9375rem;font-weight:400;line-height:1.5;color:#444;background-color:#fff;background-clip:padding-box;border:1px solid #222;border-radius:0.25rem;-webkit-transition:border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control{-webkit-transition:none;transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #444}.form-control:focus{color:#444;background-color:#fff;border-color:#739ac2;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.form-control::-webkit-input-placeholder{color:#888;opacity:1}.form-control::-ms-input-placeholder{color:#888;opacity:1}.form-control::placeholder{color:#888;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#ebebeb;opacity:1}input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{-webkit-appearance:none;-moz-appearance:none;appearance:none}select.form-control:focus::-ms-value{color:#444;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(0.375rem + 1px);padding-bottom:calc(0.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(0.5rem + 1px);padding-bottom:calc(0.5rem + 1px);font-size:1.171875rem;line-height:1.5}.col-form-label-sm{padding-top:calc(0.25rem + 1px);padding-bottom:calc(0.25rem + 1px);font-size:0.8203125rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:0.375rem 0;margin-bottom:0;font-size:0.9375rem;line-height:1.5;color:#fff;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + 0.5rem + 2px);padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5;border-radius:0.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:0.5rem 1rem;font-size:1.171875rem;line-height:1.5;border-radius:0.3rem}select.form-control[size],select.form-control[multiple]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:0.25rem}.form-row{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*="col-"]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:0.3rem;margin-left:-1.25rem}.form-check-input[disabled] ~ .form-check-label,.form-check-input:disabled ~ .form-check-label{color:#888}.form-check-label{margin-bottom:0}.form-check-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding-left:0;margin-right:0.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:0.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:0.25rem;font-size:80%;color:#00bc8c}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:0.25rem 0.5rem;margin-top:.1rem;font-size:0.8203125rem;line-height:1.5;color:#fff;background-color:rgba(0,188,140,0.9);border-radius:0.25rem}.was-validated :valid ~ .valid-feedback,.was-validated :valid ~ .valid-tooltip,.is-valid ~ .valid-feedback,.is-valid ~ .valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-valid{border-color:#00bc8c;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:#00bc8c;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .custom-select:valid,.custom-select.is-valid{border-color:#00bc8c;padding-right:calc(0.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%2300bc8c' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .custom-select:valid:focus,.custom-select.is-valid:focus{border-color:#00bc8c;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.was-validated .form-check-input:valid ~ .form-check-label,.form-check-input.is-valid ~ .form-check-label{color:#00bc8c}.was-validated .form-check-input:valid ~ .valid-feedback,.was-validated .form-check-input:valid ~ .valid-tooltip,.form-check-input.is-valid ~ .valid-feedback,.form-check-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-control-input:valid ~ .custom-control-label,.custom-control-input.is-valid ~ .custom-control-label{color:#00bc8c}.was-validated .custom-control-input:valid ~ .custom-control-label::before,.custom-control-input.is-valid ~ .custom-control-label::before{border-color:#00bc8c}.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before,.custom-control-input.is-valid:checked ~ .custom-control-label::before{border-color:#00efb2;background-color:#00efb2}.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before,.custom-control-input.is-valid:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before{border-color:#00bc8c}.was-validated .custom-file-input:valid ~ .custom-file-label,.custom-file-input.is-valid ~ .custom-file-label{border-color:#00bc8c}.was-validated .custom-file-input:valid:focus ~ .custom-file-label,.custom-file-input.is-valid:focus ~ .custom-file-label{border-color:#00bc8c;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.25)}.invalid-feedback{display:none;width:100%;margin-top:0.25rem;font-size:80%;color:#E74C3C}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:0.25rem 0.5rem;margin-top:.1rem;font-size:0.8203125rem;line-height:1.5;color:#fff;background-color:rgba(231,76,60,0.9);border-radius:0.25rem}.was-validated :invalid ~ .invalid-feedback,.was-validated :invalid ~ .invalid-tooltip,.is-invalid ~ .invalid-feedback,.is-invalid ~ .invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:#E74C3C;padding-right:calc(1.5em + 0.75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23E74C3C' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23E74C3C' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(0.375em + 0.1875rem) center;background-size:calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:#E74C3C;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + 0.75rem);background-position:top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem)}.was-validated .custom-select:invalid,.custom-select.is-invalid{border-color:#E74C3C;padding-right:calc(0.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23E74C3C' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23E74C3C' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem)}.was-validated .custom-select:invalid:focus,.custom-select.is-invalid:focus{border-color:#E74C3C;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.was-validated .form-check-input:invalid ~ .form-check-label,.form-check-input.is-invalid ~ .form-check-label{color:#E74C3C}.was-validated .form-check-input:invalid ~ .invalid-feedback,.was-validated .form-check-input:invalid ~ .invalid-tooltip,.form-check-input.is-invalid ~ .invalid-feedback,.form-check-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-control-input:invalid ~ .custom-control-label,.custom-control-input.is-invalid ~ .custom-control-label{color:#E74C3C}.was-validated .custom-control-input:invalid ~ .custom-control-label::before,.custom-control-input.is-invalid ~ .custom-control-label::before{border-color:#E74C3C}.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before,.custom-control-input.is-invalid:checked ~ .custom-control-label::before{border-color:#ed7669;background-color:#ed7669}.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before,.custom-control-input.is-invalid:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before{border-color:#E74C3C}.was-validated .custom-file-input:invalid ~ .custom-file-label,.custom-file-input.is-invalid ~ .custom-file-label{border-color:#E74C3C}.was-validated .custom-file-input:invalid:focus ~ .custom-file-label,.custom-file-input.is-invalid:focus ~ .custom-file-label{border-color:#E74C3C;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.25)}.form-inline{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.form-inline .form-check{width:100%}@media (min-width: 576px){.form-inline label{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-ms-flex:0 0 auto;flex:0 0 auto;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .input-group,.form-inline .custom-select{width:auto}.form-inline .form-check{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-top:0;margin-right:0.25rem;margin-left:0}.form-inline .custom-control{-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#fff;text-align:center;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:0.375rem 0.75rem;font-size:0.9375rem;line-height:1.5;border-radius:0.25rem;-webkit-transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.btn{-webkit-transition:none;transition:none}}.btn:hover{color:#fff;text-decoration:none}.btn:focus,.btn.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.btn.disabled,.btn:disabled{opacity:0.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-primary:hover{color:#fff;background-color:#2b4764;border-color:#28415b}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#2b4764;border-color:#28415b;-webkit-box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5);box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-primary:not(:disabled):not(.disabled):active,.btn-primary:not(:disabled):not(.disabled).active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#28415b;border-color:#243a53}.btn-primary:not(:disabled):not(.disabled):active:focus,.btn-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-primary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5);box-shadow:0 0 0 0.2rem rgba(85,115,146,0.5)}.btn-secondary{color:#fff;background-color:#444;border-color:#444}.btn-secondary:hover{color:#fff;background-color:#313131;border-color:#2b2a2a}.btn-secondary:focus,.btn-secondary.focus{color:#fff;background-color:#313131;border-color:#2b2a2a;-webkit-box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5);box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#444;border-color:#444}.btn-secondary:not(:disabled):not(.disabled):active,.btn-secondary:not(:disabled):not(.disabled).active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#2b2a2a;border-color:#242424}.btn-secondary:not(:disabled):not(.disabled):active:focus,.btn-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-secondary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5);box-shadow:0 0 0 0.2rem rgba(96,96,96,0.5)}.btn-success{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-success:hover{color:#fff;background-color:#009670;border-color:#008966}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#009670;border-color:#008966;-webkit-box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5);box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-success:not(:disabled):not(.disabled):active,.btn-success:not(:disabled):not(.disabled).active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#008966;border-color:#007c5d}.btn-success:not(:disabled):not(.disabled):active:focus,.btn-success:not(:disabled):not(.disabled).active:focus,.show>.btn-success.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5);box-shadow:0 0 0 0.2rem rgba(38,198,157,0.5)}.btn-info{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-info:hover{color:#fff;background-color:#2384c6;border-color:#217dbb}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#2384c6;border-color:#217dbb;-webkit-box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5);box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-info:not(:disabled):not(.disabled):active,.btn-info:not(:disabled):not(.disabled).active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#217dbb;border-color:#1f76b0}.btn-info:not(:disabled):not(.disabled):active:focus,.btn-info:not(:disabled):not(.disabled).active:focus,.show>.btn-info.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5);box-shadow:0 0 0 0.2rem rgba(82,167,224,0.5)}.btn-warning{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-warning:hover{color:#fff;background-color:#d4860b;border-color:#c87f0a}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#d4860b;border-color:#c87f0a;-webkit-box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5);box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5)}.btn-warning.disabled,.btn-warning:disabled{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-warning:not(:disabled):not(.disabled):active,.btn-warning:not(:disabled):not(.disabled).active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#c87f0a;border-color:#bc770a}.btn-warning:not(:disabled):not(.disabled):active:focus,.btn-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-warning.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5);box-shadow:0 0 0 0.2rem rgba(245,171,54,0.5)}.btn-danger{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-danger:hover{color:#fff;background-color:#e12e1c;border-color:#d62c1a}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#e12e1c;border-color:#d62c1a;-webkit-box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5);box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-danger:not(:disabled):not(.disabled):active,.btn-danger:not(:disabled):not(.disabled).active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#d62c1a;border-color:#ca2a19}.btn-danger:not(:disabled):not(.disabled):active:focus,.btn-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-danger.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5);box-shadow:0 0 0 0.2rem rgba(235,103,89,0.5)}.btn-light{color:#222;background-color:#adb5bd;border-color:#adb5bd}.btn-light:hover{color:#fff;background-color:#98a2ac;border-color:#919ca6}.btn-light:focus,.btn-light.focus{color:#fff;background-color:#98a2ac;border-color:#919ca6;-webkit-box-shadow:0 0 0 0.2rem rgba(152,159,166,0.5);box-shadow:0 0 0 0.2rem rgba(152,159,166,0.5)}.btn-light.disabled,.btn-light:disabled{color:#222;background-color:#adb5bd;border-color:#adb5bd}.btn-light:not(:disabled):not(.disabled):active,.btn-light:not(:disabled):not(.disabled).active,.show>.btn-light.dropdown-toggle{color:#fff;background-color:#919ca6;border-color:#8a95a1}.btn-light:not(:disabled):not(.disabled):active:focus,.btn-light:not(:disabled):not(.disabled).active:focus,.show>.btn-light.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(152,159,166,0.5);box-shadow:0 0 0 0.2rem rgba(152,159,166,0.5)}.btn-dark{color:#fff;background-color:#303030;border-color:#303030}.btn-dark:hover{color:#fff;background-color:#1d1d1d;border-color:#171616}.btn-dark:focus,.btn-dark.focus{color:#fff;background-color:#1d1d1d;border-color:#171616;-webkit-box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5);box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#303030;border-color:#303030}.btn-dark:not(:disabled):not(.disabled):active,.btn-dark:not(:disabled):not(.disabled).active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#171616;border-color:#101010}.btn-dark:not(:disabled):not(.disabled):active:focus,.btn-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-dark.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5);box-shadow:0 0 0 0.2rem rgba(79,79,79,0.5)}.btn-outline-primary{color:#375a7f;border-color:#375a7f}.btn-outline-primary:hover{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-outline-primary:focus,.btn-outline-primary.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#375a7f;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled):active,.btn-outline-primary:not(:disabled):not(.disabled).active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#375a7f;border-color:#375a7f}.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5)}.btn-outline-secondary{color:#444;border-color:#444}.btn-outline-secondary:hover{color:#fff;background-color:#444;border-color:#444}.btn-outline-secondary:focus,.btn-outline-secondary.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5);box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#444;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled):active,.btn-outline-secondary:not(:disabled):not(.disabled).active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#444;border-color:#444}.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5);box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5)}.btn-outline-success{color:#00bc8c;border-color:#00bc8c}.btn-outline-success:hover{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-outline-success:focus,.btn-outline-success.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#00bc8c;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled):active,.btn-outline-success:not(:disabled):not(.disabled).active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#00bc8c;border-color:#00bc8c}.btn-outline-success:not(:disabled):not(.disabled):active:focus,.btn-outline-success:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-success.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5)}.btn-outline-info{color:#3498DB;border-color:#3498DB}.btn-outline-info:hover{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-outline-info:focus,.btn-outline-info.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5);box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#3498DB;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled):active,.btn-outline-info:not(:disabled):not(.disabled).active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#3498DB;border-color:#3498DB}.btn-outline-info:not(:disabled):not(.disabled):active:focus,.btn-outline-info:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-info.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5);box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5)}.btn-outline-warning{color:#F39C12;border-color:#F39C12}.btn-outline-warning:hover{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-outline-warning:focus,.btn-outline-warning.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5);box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#F39C12;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled):active,.btn-outline-warning:not(:disabled):not(.disabled).active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#F39C12;border-color:#F39C12}.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5);box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5)}.btn-outline-danger{color:#E74C3C;border-color:#E74C3C}.btn-outline-danger:hover{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-outline-danger:focus,.btn-outline-danger.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#E74C3C;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled):active,.btn-outline-danger:not(:disabled):not(.disabled).active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#E74C3C;border-color:#E74C3C}.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5)}.btn-outline-light{color:#adb5bd;border-color:#adb5bd}.btn-outline-light:hover{color:#222;background-color:#adb5bd;border-color:#adb5bd}.btn-outline-light:focus,.btn-outline-light.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5);box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#adb5bd;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled):active,.btn-outline-light:not(:disabled):not(.disabled).active,.show>.btn-outline-light.dropdown-toggle{color:#222;background-color:#adb5bd;border-color:#adb5bd}.btn-outline-light:not(:disabled):not(.disabled):active:focus,.btn-outline-light:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-light.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5);box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5)}.btn-outline-dark{color:#303030;border-color:#303030}.btn-outline-dark:hover{color:#fff;background-color:#303030;border-color:#303030}.btn-outline-dark:focus,.btn-outline-dark.focus{-webkit-box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5);box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#303030;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled):active,.btn-outline-dark:not(:disabled):not(.disabled).active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#303030;border-color:#303030}.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{-webkit-box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5);box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5)}.btn-link{font-weight:400;color:#00bc8c;text-decoration:none}.btn-link:hover{color:#007053;text-decoration:underline}.btn-link:focus,.btn-link.focus{text-decoration:underline}.btn-link:disabled,.btn-link.disabled{color:#888;pointer-events:none}.btn-lg,.btn-group-lg>.btn{padding:0.5rem 1rem;font-size:1.171875rem;line-height:1.5;border-radius:0.3rem}.btn-sm,.btn-group-sm>.btn{padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5;border-radius:0.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:0.5rem}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{-webkit-transition:opacity 0.15s linear;transition:opacity 0.15s linear}@media (prefers-reduced-motion: reduce){.fade{-webkit-transition:none;transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height 0.35s ease;transition:height 0.35s ease}@media (prefers-reduced-motion: reduce){.collapsing{-webkit-transition:none;transition:none}}.dropup,.dropright,.dropdown,.dropleft{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid;border-right:0.3em solid transparent;border-bottom:0;border-left:0.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:0.5rem 0;margin:0.125rem 0 0;font-size:0.9375rem;color:#fff;text-align:left;list-style:none;background-color:#222;background-clip:padding-box;border:1px solid #444;border-radius:0.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width: 576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width: 768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width: 992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width: 1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:0.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0;border-right:0.3em solid transparent;border-bottom:0.3em solid;border-left:0.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:0.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid transparent;border-right:0;border-bottom:0.3em solid transparent;border-left:0.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:0.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:0.255em;vertical-align:0.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:0.255em;vertical-align:0.255em;content:"";border-top:0.3em solid transparent;border-right:0.3em solid;border-bottom:0.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^="top"],.dropdown-menu[x-placement^="right"],.dropdown-menu[x-placement^="bottom"],.dropdown-menu[x-placement^="left"]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:0.5rem 0;overflow:hidden;border-top:1px solid #444}.dropdown-item{display:block;width:100%;padding:0.25rem 1.5rem;clear:both;font-weight:400;color:#fff;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:hover,.dropdown-item:focus{color:#fff;text-decoration:none;background-color:#375a7f}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#375a7f}.dropdown-item.disabled,.dropdown-item:disabled{color:#888;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:0.5rem 1.5rem;margin-bottom:0;font-size:0.8203125rem;color:#888;white-space:nowrap}.dropdown-item-text{display:block;padding:0.25rem 1.5rem;color:#fff}.btn-group,.btn-group-vertical{position:relative;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover{z-index:1}.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child){margin-left:-1px}.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:0.5625rem;padding-left:0.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:0.375rem;padding-left:0.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:0.75rem;padding-left:0.75rem}.btn-group-vertical{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type="radio"],.btn-group-toggle>.btn input[type="checkbox"],.btn-group-toggle>.btn-group>.btn input[type="radio"],.btn-group-toggle>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:stretch;-ms-flex-align:stretch;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-control-plaintext,.input-group>.custom-select,.input-group>.custom-file{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.form-control+.form-control,.input-group>.form-control+.custom-select,.input-group>.form-control+.custom-file,.input-group>.form-control-plaintext+.form-control,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.custom-file,.input-group>.custom-select+.form-control,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.custom-file,.input-group>.custom-file+.form-control,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.custom-file{margin-left:-1px}.input-group>.form-control:focus,.input-group>.custom-select:focus,.input-group>.custom-file .custom-file-input:focus ~ .custom-file-label{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.form-control:not(:last-child),.input-group>.custom-select:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.form-control:not(:first-child),.input-group>.custom-select:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-prepend,.input-group-append{display:-webkit-box;display:-ms-flexbox;display:flex}.input-group-prepend .btn,.input-group-append .btn{position:relative;z-index:2}.input-group-prepend .btn:focus,.input-group-append .btn:focus{z-index:3}.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.input-group-text,.input-group-append .input-group-text+.btn{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:0.375rem 0.75rem;margin-bottom:0;font-size:0.9375rem;font-weight:400;line-height:1.5;color:#adb5bd;text-align:center;white-space:nowrap;background-color:#444;border:1px solid #222;border-radius:0.25rem}.input-group-text input[type="radio"],.input-group-text input[type="checkbox"]{margin-top:0}.input-group-lg>.form-control:not(textarea),.input-group-lg>.custom-select{height:calc(1.5em + 1rem + 2px)}.input-group-lg>.form-control,.input-group-lg>.custom-select,.input-group-lg>.input-group-prepend>.input-group-text,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-append>.btn{padding:0.5rem 1rem;font-size:1.171875rem;line-height:1.5;border-radius:0.3rem}.input-group-sm>.form-control:not(textarea),.input-group-sm>.custom-select{height:calc(1.5em + 0.5rem + 2px)}.input-group-sm>.form-control,.input-group-sm>.custom-select,.input-group-sm>.input-group-prepend>.input-group-text,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-append>.btn{padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5;border-radius:0.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text,.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.40625rem;padding-left:1.5rem}.custom-control-inline{display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.203125rem;opacity:0}.custom-control-input:checked ~ .custom-control-label::before{color:#fff;border-color:#375a7f;background-color:#375a7f}.custom-control-input:focus ~ .custom-control-label::before{-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-control-input:focus:not(:checked) ~ .custom-control-label::before{border-color:#739ac2}.custom-control-input:not(:disabled):active ~ .custom-control-label::before{color:#fff;background-color:#97b3d2;border-color:#97b3d2}.custom-control-input[disabled] ~ .custom-control-label,.custom-control-input:disabled ~ .custom-control-label{color:#888}.custom-control-input[disabled] ~ .custom-control-label::before,.custom-control-input:disabled ~ .custom-control-label::before{background-color:#ebebeb}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:0.203125rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:0.203125rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50% / 50% 50%}.custom-checkbox .custom-control-label::before{border-radius:0.25rem}.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before{border-color:#375a7f;background-color:#375a7f}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:0.5rem}.custom-switch .custom-control-label::after{top:calc(0.203125rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:0.5rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:transform 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-transform 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-switch .custom-control-label::after{-webkit-transition:none;transition:none}}.custom-switch .custom-control-input:checked ~ .custom-control-label::after{background-color:#fff;-webkit-transform:translateX(0.75rem);transform:translateX(0.75rem)}.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(55,90,127,0.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 1.75rem 0.375rem 0.75rem;font-size:0.9375rem;font-weight:400;line-height:1.5;color:#444;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23303030' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px;border:1px solid #222;border-radius:0.25rem;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-select:focus{border-color:#739ac2;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-select:focus::-ms-value{color:#444;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:0.75rem;background-image:none}.custom-select:disabled{color:#888;background-color:#ebebeb}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #444}.custom-select-sm{height:calc(1.5em + 0.5rem + 2px);padding-top:0.25rem;padding-bottom:0.25rem;padding-left:0.5rem;font-size:0.8203125rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:0.5rem;padding-bottom:0.5rem;padding-left:1rem;font-size:1.171875rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + 0.75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + 0.75rem + 2px);margin:0;opacity:0}.custom-file-input:focus ~ .custom-file-label{border-color:#739ac2;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-file-input[disabled] ~ .custom-file-label,.custom-file-input:disabled ~ .custom-file-label{background-color:#ebebeb}.custom-file-input:lang(en) ~ .custom-file-label::after{content:"Browse"}.custom-file-input ~ .custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + 0.75rem + 2px);padding:0.375rem 0.75rem;font-weight:400;line-height:1.5;color:#adb5bd;background-color:#fff;border:1px solid #222;border-radius:0.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + 0.75rem);padding:0.375rem 0.75rem;line-height:1.5;color:#adb5bd;content:"Browse";background-color:#444;border-left:inherit;border-radius:0 0.25rem 0.25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.custom-range:focus{outline:none}.custom-range:focus::-webkit-slider-thumb{-webkit-box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #222,0 0 0 0.2rem rgba(55,90,127,0.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-0.25rem;background-color:#375a7f;border:0;border-radius:1rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#97b3d2}.custom-range::-webkit-slider-runnable-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#375a7f;border:0;border-radius:1rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-moz-range-thumb{-webkit-transition:none;transition:none}}.custom-range::-moz-range-thumb:active{background-color:#97b3d2}.custom-range::-moz-range-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:0.2rem;margin-left:0.2rem;background-color:#375a7f;border:0;border-radius:1rem;-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-ms-thumb{-webkit-transition:none;transition:none}}.custom-range::-ms-thumb:active{background-color:#97b3d2}.custom-range::-ms-track{width:100%;height:0.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:0.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{-webkit-transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-control-label::before,.custom-file-label,.custom-select{-webkit-transition:none;transition:none}}.nav{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:0.5rem 2rem}.nav-link:hover,.nav-link:focus{text-decoration:none}.nav-link.disabled{color:#adb5bd;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #444}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:0.25rem;border-top-right-radius:0.25rem}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{border-color:#444 #444 transparent}.nav-tabs .nav-link.disabled{color:#adb5bd;background-color:transparent;border-color:transparent}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:#fff;background-color:#222;border-color:#444 #444 transparent}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:0.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#375a7f}.nav-fill .nav-item{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;text-align:center}.nav-justified .nav-item{-ms-flex-preferred-size:0;flex-basis:0;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem}.navbar .container,.navbar .container-fluid,.navbar .container-sm,.navbar .container-md,.navbar .container-lg,.navbar .container-xl{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:0.32421875rem;padding-bottom:0.32421875rem;margin-right:1rem;font-size:1.171875rem;line-height:inherit;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-nav{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:0.5rem;padding-bottom:0.5rem}.navbar-collapse{-ms-flex-preferred-size:100%;flex-basis:100%;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.navbar-toggler{padding:0.25rem 0.75rem;font-size:1.171875rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:0.25rem}.navbar-toggler:hover,.navbar-toggler:focus{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width: 575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 576px){.navbar-expand-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-sm .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width: 767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-md,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 768px){.navbar-expand-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-md .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-md,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width: 991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 992px){.navbar-expand-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-lg .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width: 1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 1200px){.navbar-expand-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand-xl .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row nowrap;flex-flow:row nowrap;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-sm,.navbar-expand>.container-md,.navbar-expand>.container-lg,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:0.5rem;padding-left:0.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-sm,.navbar-expand>.container-md,.navbar-expand>.container-lg,.navbar-expand>.container-xl{-ms-flex-wrap:nowrap;flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important;-ms-flex-preferred-size:auto;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:#222}.navbar-light .navbar-brand:hover,.navbar-light .navbar-brand:focus{color:#222}.navbar-light .navbar-nav .nav-link{color:rgba(34,34,34,0.7)}.navbar-light .navbar-nav .nav-link:hover,.navbar-light .navbar-nav .nav-link:focus{color:#222}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,0.3)}.navbar-light .navbar-nav .show>.nav-link,.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .nav-link.active{color:#222}.navbar-light .navbar-toggler{color:rgba(34,34,34,0.7);border-color:rgba(34,34,34,0.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2834, 34, 34, 0.7%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(34,34,34,0.7)}.navbar-light .navbar-text a{color:#222}.navbar-light .navbar-text a:hover,.navbar-light .navbar-text a:focus{color:#222}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:hover,.navbar-dark .navbar-brand:focus{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,0.6)}.navbar-dark .navbar-nav .nav-link:hover,.navbar-dark .navbar-nav .nav-link:focus{color:#fff}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,0.25)}.navbar-dark .navbar-nav .show>.nav-link,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .nav-link.active{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,0.6);border-color:rgba(255,255,255,0.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.6%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,0.6)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:hover,.navbar-dark .navbar-text a:focus{color:#fff}.card{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#303030;background-clip:border-box;border:1px solid rgba(0,0,0,0.125);border-radius:0.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(0.25rem - 1px);border-bottom-left-radius:calc(0.25rem - 1px)}.card-body{-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:0.75rem}.card-subtitle{margin-top:-0.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:0.75rem 1.25rem;margin-bottom:0;background-color:#444;border-bottom:1px solid rgba(0,0,0,0.125)}.card-header:first-child{border-radius:calc(0.25rem - 1px) calc(0.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:0.75rem 1.25rem;background-color:#444;border-top:1px solid rgba(0,0,0,0.125)}.card-footer:last-child{border-radius:0 0 calc(0.25rem - 1px) calc(0.25rem - 1px)}.card-header-tabs{margin-right:-0.625rem;margin-bottom:-0.75rem;margin-left:-0.625rem;border-bottom:0}.card-header-pills{margin-right:-0.625rem;margin-left:-0.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img,.card-img-top,.card-img-bottom{-ms-flex-negative:0;flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(0.25rem - 1px);border-top-right-radius:calc(0.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(0.25rem - 1px);border-bottom-left-radius:calc(0.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width: 576px){.card-deck{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width: 576px){.card-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-flow:row wrap;flex-flow:row wrap}.card-group>.card{-webkit-box-flex:1;-ms-flex:1 0 0%;flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-img-top,.card-group>.card:not(:last-child) .card-header{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-img-bottom,.card-group>.card:not(:last-child) .card-footer{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-img-top,.card-group>.card:not(:first-child) .card-header{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-img-bottom,.card-group>.card:not(:first-child) .card-footer{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:0.75rem}@media (min-width: 576px){.card-columns{-webkit-column-count:3;column-count:3;-webkit-column-gap:1.25rem;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;padding:0.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#444;border-radius:0.25rem}.breadcrumb-item{display:-webkit-box;display:-ms-flexbox;display:flex}.breadcrumb-item+.breadcrumb-item{padding-left:0.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:0.5rem;color:#888;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#888}.pagination{display:-webkit-box;display:-ms-flexbox;display:flex;padding-left:0;list-style:none;border-radius:0.25rem}.page-link{position:relative;display:block;padding:0.5rem 0.75rem;margin-left:0;line-height:1.25;color:#fff;background-color:#00bc8c;border:0 solid transparent}.page-link:hover{z-index:2;color:#fff;text-decoration:none;background-color:#00efb2;border-color:transparent}.page-link:focus{z-index:3;outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:0.25rem;border-bottom-left-radius:0.25rem}.page-item:last-child .page-link{border-top-right-radius:0.25rem;border-bottom-right-radius:0.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#00efb2;border-color:transparent}.page-item.disabled .page-link{color:#fff;pointer-events:none;cursor:auto;background-color:#007053;border-color:transparent}.pagination-lg .page-link{padding:0.75rem 1.5rem;font-size:1.171875rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:0.3rem;border-bottom-left-radius:0.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:0.3rem;border-bottom-right-radius:0.3rem}.pagination-sm .page-link{padding:0.25rem 0.5rem;font-size:0.8203125rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:0.2rem;border-bottom-left-radius:0.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:0.2rem;border-bottom-right-radius:0.2rem}.badge{display:inline-block;padding:0.25em 0.4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:0.25rem;-webkit-transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;transition:color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.badge{-webkit-transition:none;transition:none}}a.badge:hover,a.badge:focus{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:0.6em;padding-left:0.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#375a7f}a.badge-primary:hover,a.badge-primary:focus{color:#fff;background-color:#28415b}a.badge-primary:focus,a.badge-primary.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5);box-shadow:0 0 0 0.2rem rgba(55,90,127,0.5)}.badge-secondary{color:#fff;background-color:#444}a.badge-secondary:hover,a.badge-secondary:focus{color:#fff;background-color:#2b2a2a}a.badge-secondary:focus,a.badge-secondary.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5);box-shadow:0 0 0 0.2rem rgba(68,68,68,0.5)}.badge-success{color:#fff;background-color:#00bc8c}a.badge-success:hover,a.badge-success:focus{color:#fff;background-color:#008966}a.badge-success:focus,a.badge-success.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5);box-shadow:0 0 0 0.2rem rgba(0,188,140,0.5)}.badge-info{color:#fff;background-color:#3498DB}a.badge-info:hover,a.badge-info:focus{color:#fff;background-color:#217dbb}a.badge-info:focus,a.badge-info.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5);box-shadow:0 0 0 0.2rem rgba(52,152,219,0.5)}.badge-warning{color:#fff;background-color:#F39C12}a.badge-warning:hover,a.badge-warning:focus{color:#fff;background-color:#c87f0a}a.badge-warning:focus,a.badge-warning.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5);box-shadow:0 0 0 0.2rem rgba(243,156,18,0.5)}.badge-danger{color:#fff;background-color:#E74C3C}a.badge-danger:hover,a.badge-danger:focus{color:#fff;background-color:#d62c1a}a.badge-danger:focus,a.badge-danger.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5);box-shadow:0 0 0 0.2rem rgba(231,76,60,0.5)}.badge-light{color:#222;background-color:#adb5bd}a.badge-light:hover,a.badge-light:focus{color:#222;background-color:#919ca6}a.badge-light:focus,a.badge-light.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5);box-shadow:0 0 0 0.2rem rgba(173,181,189,0.5)}.badge-dark{color:#fff;background-color:#303030}a.badge-dark:hover,a.badge-dark:focus{color:#fff;background-color:#171616}a.badge-dark:focus,a.badge-dark.focus{outline:0;-webkit-box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5);box-shadow:0 0 0 0.2rem rgba(48,48,48,0.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#303030;border-radius:0.3rem}@media (min-width: 576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:0.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:0.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3.90625rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:0.75rem 1.25rem;color:inherit}.alert-primary{color:#1d2f42;background-color:#d7dee5;border-color:#c7d1db}.alert-primary hr{border-top-color:#b7c4d1}.alert-primary .alert-link{color:#0d161f}.alert-secondary{color:#232323;background-color:#dadada;border-color:#cbcbcb}.alert-secondary hr{border-top-color:#bebebe}.alert-secondary .alert-link{color:#0a0909}.alert-success{color:#006249;background-color:#ccf2e8;border-color:#b8ecdf}.alert-success hr{border-top-color:#a4e7d6}.alert-success .alert-link{color:#002f23}.alert-info{color:#1b4f72;background-color:#d6eaf8;border-color:#c6e2f5}.alert-info hr{border-top-color:#b0d7f1}.alert-info .alert-link{color:#113249}.alert-warning{color:#7e5109;background-color:#fdebd0;border-color:#fce3bd}.alert-warning hr{border-top-color:#fbd9a5}.alert-warning .alert-link{color:#4e3206}.alert-danger{color:#78281f;background-color:#fadbd8;border-color:#f8cdc8}.alert-danger hr{border-top-color:#f5b8b1}.alert-danger .alert-link{color:#4f1a15}.alert-light{color:#5a5e62;background-color:#eff0f2;border-color:#e8eaed}.alert-light hr{border-top-color:#dadde2}.alert-light .alert-link{color:#424547}.alert-dark{color:#191919;background-color:#d6d6d6;border-color:#c5c5c5}.alert-dark hr{border-top-color:#b8b8b8}.alert-dark .alert-link{color:black}@-webkit-keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:-webkit-box;display:-ms-flexbox;display:flex;height:1rem;overflow:hidden;line-height:0;font-size:0.703125rem;background-color:#444;border-radius:0.25rem}.progress-bar{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#375a7f;-webkit-transition:width 0.6s ease;transition:width 0.6s ease}@media (prefers-reduced-motion: reduce){.progress-bar{-webkit-transition:none;transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:progress-bar-stripes 1s linear infinite;animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion: reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.media{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start}.media-body{-webkit-box-flex:1;-ms-flex:1;flex:1}.list-group{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:0.25rem}.list-group-item-action{width:100%;color:#444;text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:#444;text-decoration:none;background-color:#444}.list-group-item-action:active{color:#fff;background-color:#ebebeb}.list-group-item{position:relative;display:block;padding:0.75rem 1.25rem;background-color:#303030;border:1px solid #444}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#888;pointer-events:none;background-color:#303030}.list-group-item.active{z-index:2;color:#fff;background-color:#375a7f;border-color:#375a7f}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width: 576px){.list-group-horizontal-sm{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 768px){.list-group-horizontal-md{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 992px){.list-group-horizontal-lg{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 1200px){.list-group-horizontal-xl{-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:0.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:0.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#1d2f42;background-color:#c7d1db}.list-group-item-primary.list-group-item-action:hover,.list-group-item-primary.list-group-item-action:focus{color:#1d2f42;background-color:#b7c4d1}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#1d2f42;border-color:#1d2f42}.list-group-item-secondary{color:#232323;background-color:#cbcbcb}.list-group-item-secondary.list-group-item-action:hover,.list-group-item-secondary.list-group-item-action:focus{color:#232323;background-color:#bebebe}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#232323;border-color:#232323}.list-group-item-success{color:#006249;background-color:#b8ecdf}.list-group-item-success.list-group-item-action:hover,.list-group-item-success.list-group-item-action:focus{color:#006249;background-color:#a4e7d6}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#006249;border-color:#006249}.list-group-item-info{color:#1b4f72;background-color:#c6e2f5}.list-group-item-info.list-group-item-action:hover,.list-group-item-info.list-group-item-action:focus{color:#1b4f72;background-color:#b0d7f1}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#1b4f72;border-color:#1b4f72}.list-group-item-warning{color:#7e5109;background-color:#fce3bd}.list-group-item-warning.list-group-item-action:hover,.list-group-item-warning.list-group-item-action:focus{color:#7e5109;background-color:#fbd9a5}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#7e5109;border-color:#7e5109}.list-group-item-danger{color:#78281f;background-color:#f8cdc8}.list-group-item-danger.list-group-item-action:hover,.list-group-item-danger.list-group-item-action:focus{color:#78281f;background-color:#f5b8b1}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#78281f;border-color:#78281f}.list-group-item-light{color:#5a5e62;background-color:#e8eaed}.list-group-item-light.list-group-item-action:hover,.list-group-item-light.list-group-item-action:focus{color:#5a5e62;background-color:#dadde2}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#5a5e62;border-color:#5a5e62}.list-group-item-dark{color:#191919;background-color:#c5c5c5}.list-group-item-dark.list-group-item-action:hover,.list-group-item-dark.list-group-item-action:focus{color:#191919;background-color:#b8b8b8}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#191919;border-color:#191919}.close{float:right;font-size:1.40625rem;font-weight:700;line-height:1;color:#fff;text-shadow:none;opacity:.5}.close:hover{color:#fff;text-decoration:none}.close:not(:disabled):not(.disabled):hover,.close:not(:disabled):not(.disabled):focus{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:0.875rem;background-color:#444;background-clip:padding-box;border:1px solid rgba(0,0,0,0.1);-webkit-box-shadow:0 0.25rem 0.75rem rgba(0,0,0,0.1);box-shadow:0 0.25rem 0.75rem rgba(0,0,0,0.1);-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px);opacity:0;border-radius:0.25rem}.toast:not(:last-child){margin-bottom:0.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;padding:0.25rem 0.75rem;color:#888;background-color:#303030;background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,0.05)}.toast-body{padding:0.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:0.5rem;pointer-events:none}.modal.fade .modal-dialog{-webkit-transition:-webkit-transform 0.3s ease-out;transition:-webkit-transform 0.3s ease-out;transition:transform 0.3s ease-out;transition:transform 0.3s ease-out, -webkit-transform 0.3s ease-out;-webkit-transform:translate(0, -50px);transform:translate(0, -50px)}@media (prefers-reduced-motion: reduce){.modal.fade .modal-dialog{-webkit-transition:none;transition:none}}.modal.show .modal-dialog{-webkit-transform:none;transform:none}.modal.modal-static .modal-dialog{-webkit-transform:scale(1.02);transform:scale(1.02)}.modal-dialog-scrollable{display:-webkit-box;display:-ms-flexbox;display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-header,.modal-dialog-scrollable .modal-footer{-ms-flex-negative:0;flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:-webkit-min-content;height:-moz-min-content;height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;width:100%;pointer-events:auto;background-color:#303030;background-clip:padding-box;border:1px solid #444;border-radius:0.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:0.5}.modal-header{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:start;-ms-flex-align:start;align-items:flex-start;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #444;border-top-left-radius:calc(0.3rem - 1px);border-top-right-radius:calc(0.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto;padding:1rem}.modal-footer{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;padding:0.75rem;border-top:1px solid #444;border-bottom-right-radius:calc(0.3rem - 1px);border-bottom-left-radius:calc(0.3rem - 1px)}.modal-footer>*{margin:0.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width: 576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:-webkit-min-content;height:-moz-min-content;height:min-content}.modal-sm{max-width:300px}}@media (min-width: 992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width: 1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:"Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.8203125rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:0.9}.tooltip .arrow{position:absolute;display:block;width:0.8rem;height:0.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-top,.bs-tooltip-auto[x-placement^="top"]{padding:0.4rem 0}.bs-tooltip-top .arrow,.bs-tooltip-auto[x-placement^="top"] .arrow{bottom:0}.bs-tooltip-top .arrow::before,.bs-tooltip-auto[x-placement^="top"] .arrow::before{top:0;border-width:0.4rem 0.4rem 0;border-top-color:#000}.bs-tooltip-right,.bs-tooltip-auto[x-placement^="right"]{padding:0 0.4rem}.bs-tooltip-right .arrow,.bs-tooltip-auto[x-placement^="right"] .arrow{left:0;width:0.4rem;height:0.8rem}.bs-tooltip-right .arrow::before,.bs-tooltip-auto[x-placement^="right"] .arrow::before{right:0;border-width:0.4rem 0.4rem 0.4rem 0;border-right-color:#000}.bs-tooltip-bottom,.bs-tooltip-auto[x-placement^="bottom"]{padding:0.4rem 0}.bs-tooltip-bottom .arrow,.bs-tooltip-auto[x-placement^="bottom"] .arrow{top:0}.bs-tooltip-bottom .arrow::before,.bs-tooltip-auto[x-placement^="bottom"] .arrow::before{bottom:0;border-width:0 0.4rem 0.4rem;border-bottom-color:#000}.bs-tooltip-left,.bs-tooltip-auto[x-placement^="left"]{padding:0 0.4rem}.bs-tooltip-left .arrow,.bs-tooltip-auto[x-placement^="left"] .arrow{right:0;width:0.4rem;height:0.8rem}.bs-tooltip-left .arrow::before,.bs-tooltip-auto[x-placement^="left"] .arrow::before{left:0;border-width:0.4rem 0 0.4rem 0.4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:0.25rem 0.5rem;color:#fff;text-align:center;background-color:#000;border-radius:0.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:"Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:0.8203125rem;word-wrap:break-word;background-color:#303030;background-clip:padding-box;border:1px solid rgba(0,0,0,0.2);border-radius:0.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:0.5rem;margin:0 0.3rem}.popover .arrow::before,.popover .arrow::after{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-top,.bs-popover-auto[x-placement^="top"]{margin-bottom:0.5rem}.bs-popover-top>.arrow,.bs-popover-auto[x-placement^="top"]>.arrow{bottom:calc(-0.5rem - 1px)}.bs-popover-top>.arrow::before,.bs-popover-auto[x-placement^="top"]>.arrow::before{bottom:0;border-width:0.5rem 0.5rem 0;border-top-color:rgba(0,0,0,0.25)}.bs-popover-top>.arrow::after,.bs-popover-auto[x-placement^="top"]>.arrow::after{bottom:1px;border-width:0.5rem 0.5rem 0;border-top-color:#303030}.bs-popover-right,.bs-popover-auto[x-placement^="right"]{margin-left:0.5rem}.bs-popover-right>.arrow,.bs-popover-auto[x-placement^="right"]>.arrow{left:calc(-0.5rem - 1px);width:0.5rem;height:1rem;margin:0.3rem 0}.bs-popover-right>.arrow::before,.bs-popover-auto[x-placement^="right"]>.arrow::before{left:0;border-width:0.5rem 0.5rem 0.5rem 0;border-right-color:rgba(0,0,0,0.25)}.bs-popover-right>.arrow::after,.bs-popover-auto[x-placement^="right"]>.arrow::after{left:1px;border-width:0.5rem 0.5rem 0.5rem 0;border-right-color:#303030}.bs-popover-bottom,.bs-popover-auto[x-placement^="bottom"]{margin-top:0.5rem}.bs-popover-bottom>.arrow,.bs-popover-auto[x-placement^="bottom"]>.arrow{top:calc(-0.5rem - 1px)}.bs-popover-bottom>.arrow::before,.bs-popover-auto[x-placement^="bottom"]>.arrow::before{top:0;border-width:0 0.5rem 0.5rem 0.5rem;border-bottom-color:rgba(0,0,0,0.25)}.bs-popover-bottom>.arrow::after,.bs-popover-auto[x-placement^="bottom"]>.arrow::after{top:1px;border-width:0 0.5rem 0.5rem 0.5rem;border-bottom-color:#303030}.bs-popover-bottom .popover-header::before,.bs-popover-auto[x-placement^="bottom"] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-0.5rem;content:"";border-bottom:1px solid #444}.bs-popover-left,.bs-popover-auto[x-placement^="left"]{margin-right:0.5rem}.bs-popover-left>.arrow,.bs-popover-auto[x-placement^="left"]>.arrow{right:calc(-0.5rem - 1px);width:0.5rem;height:1rem;margin:0.3rem 0}.bs-popover-left>.arrow::before,.bs-popover-auto[x-placement^="left"]>.arrow::before{right:0;border-width:0.5rem 0 0.5rem 0.5rem;border-left-color:rgba(0,0,0,0.25)}.bs-popover-left>.arrow::after,.bs-popover-auto[x-placement^="left"]>.arrow::after{right:1px;border-width:0.5rem 0 0.5rem 0.5rem;border-left-color:#303030}.popover-header{padding:0.5rem 0.75rem;margin-bottom:0;font-size:0.9375rem;background-color:#444;border-bottom:1px solid #373737;border-top-left-radius:calc(0.3rem - 1px);border-top-right-radius:calc(0.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:0.5rem 0.75rem;color:#fff}.carousel{position:relative}.carousel.pointer-event{-ms-touch-action:pan-y;touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transition:-webkit-transform 0.6s ease-in-out;transition:-webkit-transform 0.6s ease-in-out;transition:transform 0.6s ease-in-out;transition:transform 0.6s ease-in-out, -webkit-transform 0.6s ease-in-out}@media (prefers-reduced-motion: reduce){.carousel-item{-webkit-transition:none;transition:none}}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-left),.active.carousel-item-right{-webkit-transform:translateX(100%);transform:translateX(100%)}.carousel-item-prev:not(.carousel-item-right),.active.carousel-item-left{-webkit-transform:translateX(-100%);transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;-webkit-transition-property:opacity;transition-property:opacity;-webkit-transform:none;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;-webkit-transition:opacity 0s 0.6s;transition:opacity 0s 0.6s}@media (prefers-reduced-motion: reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{-webkit-transition:none;transition:none}}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:0.5;-webkit-transition:opacity 0.15s ease;transition:opacity 0.15s ease}@media (prefers-reduced-motion: reduce){.carousel-control-prev,.carousel-control-next{-webkit-transition:none;transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:0.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50% / 100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{-webkit-box-sizing:content-box;box-sizing:content-box;-webkit-box-flex:0;-ms-flex:0 1 auto;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;-webkit-transition:opacity 0.6s ease;transition:opacity 0.6s ease}@media (prefers-reduced-motion: reduce){.carousel-indicators li{-webkit-transition:none;transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@-webkit-keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes spinner-border{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:0.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:spinner-border .75s linear infinite;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:0.2em}@-webkit-keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}@keyframes spinner-grow{0%{-webkit-transform:scale(0);transform:scale(0)}50%{opacity:1;-webkit-transform:none;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:spinner-grow .75s linear infinite;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.bg-primary{background-color:#375a7f !important}a.bg-primary:hover,a.bg-primary:focus,button.bg-primary:hover,button.bg-primary:focus{background-color:#28415b !important}.bg-secondary{background-color:#444 !important}a.bg-secondary:hover,a.bg-secondary:focus,button.bg-secondary:hover,button.bg-secondary:focus{background-color:#2b2a2a !important}.bg-success{background-color:#00bc8c !important}a.bg-success:hover,a.bg-success:focus,button.bg-success:hover,button.bg-success:focus{background-color:#008966 !important}.bg-info{background-color:#3498DB !important}a.bg-info:hover,a.bg-info:focus,button.bg-info:hover,button.bg-info:focus{background-color:#217dbb !important}.bg-warning{background-color:#F39C12 !important}a.bg-warning:hover,a.bg-warning:focus,button.bg-warning:hover,button.bg-warning:focus{background-color:#c87f0a !important}.bg-danger{background-color:#E74C3C !important}a.bg-danger:hover,a.bg-danger:focus,button.bg-danger:hover,button.bg-danger:focus{background-color:#d62c1a !important}.bg-light{background-color:#adb5bd !important}a.bg-light:hover,a.bg-light:focus,button.bg-light:hover,button.bg-light:focus{background-color:#919ca6 !important}.bg-dark{background-color:#303030 !important}a.bg-dark:hover,a.bg-dark:focus,button.bg-dark:hover,button.bg-dark:focus{background-color:#171616 !important}.bg-white{background-color:#fff !important}.bg-transparent{background-color:transparent !important}.border{border:1px solid #dee2e6 !important}.border-top{border-top:1px solid #dee2e6 !important}.border-right{border-right:1px solid #dee2e6 !important}.border-bottom{border-bottom:1px solid #dee2e6 !important}.border-left{border-left:1px solid #dee2e6 !important}.border-0{border:0 !important}.border-top-0{border-top:0 !important}.border-right-0{border-right:0 !important}.border-bottom-0{border-bottom:0 !important}.border-left-0{border-left:0 !important}.border-primary{border-color:#375a7f !important}.border-secondary{border-color:#444 !important}.border-success{border-color:#00bc8c !important}.border-info{border-color:#3498DB !important}.border-warning{border-color:#F39C12 !important}.border-danger{border-color:#E74C3C !important}.border-light{border-color:#adb5bd !important}.border-dark{border-color:#303030 !important}.border-white{border-color:#fff !important}.rounded-sm{border-radius:0.2rem !important}.rounded{border-radius:0.25rem !important}.rounded-top{border-top-left-radius:0.25rem !important;border-top-right-radius:0.25rem !important}.rounded-right{border-top-right-radius:0.25rem !important;border-bottom-right-radius:0.25rem !important}.rounded-bottom{border-bottom-right-radius:0.25rem !important;border-bottom-left-radius:0.25rem !important}.rounded-left{border-top-left-radius:0.25rem !important;border-bottom-left-radius:0.25rem !important}.rounded-lg{border-radius:0.3rem !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.rounded-0{border-radius:0 !important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}@media (min-width: 576px){.d-sm-none{display:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-sm-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 768px){.d-md-none{display:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-md-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 992px){.d-lg-none{display:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-lg-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media (min-width: 1200px){.d-xl-none{display:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-xl-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}@media print{.d-print-none{display:none !important}.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:-webkit-box !important;display:-ms-flexbox !important;display:flex !important}.d-print-inline-flex{display:-webkit-inline-box !important;display:-ms-inline-flexbox !important;display:inline-flex !important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.8571428571%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}@media (min-width: 576px){.flex-sm-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-sm-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-sm-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-sm-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-sm-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-sm-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-sm-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-sm-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-sm-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-sm-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-sm-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-sm-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-sm-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-sm-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-sm-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-sm-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-sm-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-sm-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-sm-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-sm-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-sm-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-sm-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-sm-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-sm-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-sm-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-sm-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-sm-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-sm-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-sm-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-sm-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-sm-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-sm-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-sm-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-sm-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 768px){.flex-md-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-md-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-md-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-md-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-md-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-md-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-md-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-md-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-md-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-md-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-md-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-md-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-md-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-md-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-md-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-md-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-md-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-md-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-md-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-md-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-md-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-md-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-md-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-md-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-md-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-md-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-md-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-md-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-md-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-md-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-md-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-md-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-md-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-md-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 992px){.flex-lg-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-lg-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-lg-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-lg-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-lg-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-lg-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-lg-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-lg-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-lg-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-lg-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-lg-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-lg-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-lg-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-lg-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-lg-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-lg-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-lg-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-lg-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-lg-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-lg-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-lg-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-lg-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-lg-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-lg-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-lg-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-lg-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-lg-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-lg-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-lg-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-lg-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-lg-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-lg-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-lg-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-lg-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}@media (min-width: 1200px){.flex-xl-row{-webkit-box-orient:horizontal !important;-webkit-box-direction:normal !important;-ms-flex-direction:row !important;flex-direction:row !important}.flex-xl-column{-webkit-box-orient:vertical !important;-webkit-box-direction:normal !important;-ms-flex-direction:column !important;flex-direction:column !important}.flex-xl-row-reverse{-webkit-box-orient:horizontal !important;-webkit-box-direction:reverse !important;-ms-flex-direction:row-reverse !important;flex-direction:row-reverse !important}.flex-xl-column-reverse{-webkit-box-orient:vertical !important;-webkit-box-direction:reverse !important;-ms-flex-direction:column-reverse !important;flex-direction:column-reverse !important}.flex-xl-wrap{-ms-flex-wrap:wrap !important;flex-wrap:wrap !important}.flex-xl-nowrap{-ms-flex-wrap:nowrap !important;flex-wrap:nowrap !important}.flex-xl-wrap-reverse{-ms-flex-wrap:wrap-reverse !important;flex-wrap:wrap-reverse !important}.flex-xl-fill{-webkit-box-flex:1 !important;-ms-flex:1 1 auto !important;flex:1 1 auto !important}.flex-xl-grow-0{-webkit-box-flex:0 !important;-ms-flex-positive:0 !important;flex-grow:0 !important}.flex-xl-grow-1{-webkit-box-flex:1 !important;-ms-flex-positive:1 !important;flex-grow:1 !important}.flex-xl-shrink-0{-ms-flex-negative:0 !important;flex-shrink:0 !important}.flex-xl-shrink-1{-ms-flex-negative:1 !important;flex-shrink:1 !important}.justify-content-xl-start{-webkit-box-pack:start !important;-ms-flex-pack:start !important;justify-content:flex-start !important}.justify-content-xl-end{-webkit-box-pack:end !important;-ms-flex-pack:end !important;justify-content:flex-end !important}.justify-content-xl-center{-webkit-box-pack:center !important;-ms-flex-pack:center !important;justify-content:center !important}.justify-content-xl-between{-webkit-box-pack:justify !important;-ms-flex-pack:justify !important;justify-content:space-between !important}.justify-content-xl-around{-ms-flex-pack:distribute !important;justify-content:space-around !important}.align-items-xl-start{-webkit-box-align:start !important;-ms-flex-align:start !important;align-items:flex-start !important}.align-items-xl-end{-webkit-box-align:end !important;-ms-flex-align:end !important;align-items:flex-end !important}.align-items-xl-center{-webkit-box-align:center !important;-ms-flex-align:center !important;align-items:center !important}.align-items-xl-baseline{-webkit-box-align:baseline !important;-ms-flex-align:baseline !important;align-items:baseline !important}.align-items-xl-stretch{-webkit-box-align:stretch !important;-ms-flex-align:stretch !important;align-items:stretch !important}.align-content-xl-start{-ms-flex-line-pack:start !important;align-content:flex-start !important}.align-content-xl-end{-ms-flex-line-pack:end !important;align-content:flex-end !important}.align-content-xl-center{-ms-flex-line-pack:center !important;align-content:center !important}.align-content-xl-between{-ms-flex-line-pack:justify !important;align-content:space-between !important}.align-content-xl-around{-ms-flex-line-pack:distribute !important;align-content:space-around !important}.align-content-xl-stretch{-ms-flex-line-pack:stretch !important;align-content:stretch !important}.align-self-xl-auto{-ms-flex-item-align:auto !important;align-self:auto !important}.align-self-xl-start{-ms-flex-item-align:start !important;align-self:flex-start !important}.align-self-xl-end{-ms-flex-item-align:end !important;align-self:flex-end !important}.align-self-xl-center{-ms-flex-item-align:center !important;align-self:center !important}.align-self-xl-baseline{-ms-flex-item-align:baseline !important;align-self:baseline !important}.align-self-xl-stretch{-ms-flex-item-align:stretch !important;align-self:stretch !important}}.float-left{float:left !important}.float-right{float:right !important}.float-none{float:none !important}@media (min-width: 576px){.float-sm-left{float:left !important}.float-sm-right{float:right !important}.float-sm-none{float:none !important}}@media (min-width: 768px){.float-md-left{float:left !important}.float-md-right{float:right !important}.float-md-none{float:none !important}}@media (min-width: 992px){.float-lg-left{float:left !important}.float-lg-right{float:right !important}.float-lg-none{float:none !important}}@media (min-width: 1200px){.float-xl-left{float:left !important}.float-xl-right{float:right !important}.float-xl-none{float:none !important}}.user-select-all{-webkit-user-select:all !important;-moz-user-select:all !important;-ms-user-select:all !important;user-select:all !important}.user-select-auto{-webkit-user-select:auto !important;-moz-user-select:auto !important;-ms-user-select:auto !important;user-select:auto !important}.user-select-none{-webkit-user-select:none !important;-moz-user-select:none !important;-ms-user-select:none !important;user-select:none !important}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:-webkit-sticky !important;position:sticky !important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports (position: -webkit-sticky) or (position: sticky){.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{-webkit-box-shadow:0 0.125rem 0.25rem rgba(0,0,0,0.075) !important;box-shadow:0 0.125rem 0.25rem rgba(0,0,0,0.075) !important}.shadow{-webkit-box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important;box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important}.shadow-lg{-webkit-box-shadow:0 1rem 3rem rgba(0,0,0,0.175) !important;box-shadow:0 1rem 3rem rgba(0,0,0,0.175) !important}.shadow-none{-webkit-box-shadow:none !important;box-shadow:none !important}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mw-100{max-width:100% !important}.mh-100{max-height:100% !important}.min-vw-100{min-width:100vw !important}.min-vh-100{min-height:100vh !important}.vw-100{width:100vw !important}.vh-100{height:100vh !important}.m-0{margin:0 !important}.mt-0,.my-0{margin-top:0 !important}.mr-0,.mx-0{margin-right:0 !important}.mb-0,.my-0{margin-bottom:0 !important}.ml-0,.mx-0{margin-left:0 !important}.m-1{margin:0.25rem !important}.mt-1,.my-1{margin-top:0.25rem !important}.mr-1,.mx-1{margin-right:0.25rem !important}.mb-1,.my-1{margin-bottom:0.25rem !important}.ml-1,.mx-1{margin-left:0.25rem !important}.m-2{margin:0.5rem !important}.mt-2,.my-2{margin-top:0.5rem !important}.mr-2,.mx-2{margin-right:0.5rem !important}.mb-2,.my-2{margin-bottom:0.5rem !important}.ml-2,.mx-2{margin-left:0.5rem !important}.m-3{margin:1rem !important}.mt-3,.my-3{margin-top:1rem !important}.mr-3,.mx-3{margin-right:1rem !important}.mb-3,.my-3{margin-bottom:1rem !important}.ml-3,.mx-3{margin-left:1rem !important}.m-4{margin:1.5rem !important}.mt-4,.my-4{margin-top:1.5rem !important}.mr-4,.mx-4{margin-right:1.5rem !important}.mb-4,.my-4{margin-bottom:1.5rem !important}.ml-4,.mx-4{margin-left:1.5rem !important}.m-5{margin:3rem !important}.mt-5,.my-5{margin-top:3rem !important}.mr-5,.mx-5{margin-right:3rem !important}.mb-5,.my-5{margin-bottom:3rem !important}.ml-5,.mx-5{margin-left:3rem !important}.p-0{padding:0 !important}.pt-0,.py-0{padding-top:0 !important}.pr-0,.px-0{padding-right:0 !important}.pb-0,.py-0{padding-bottom:0 !important}.pl-0,.px-0{padding-left:0 !important}.p-1{padding:0.25rem !important}.pt-1,.py-1{padding-top:0.25rem !important}.pr-1,.px-1{padding-right:0.25rem !important}.pb-1,.py-1{padding-bottom:0.25rem !important}.pl-1,.px-1{padding-left:0.25rem !important}.p-2{padding:0.5rem !important}.pt-2,.py-2{padding-top:0.5rem !important}.pr-2,.px-2{padding-right:0.5rem !important}.pb-2,.py-2{padding-bottom:0.5rem !important}.pl-2,.px-2{padding-left:0.5rem !important}.p-3{padding:1rem !important}.pt-3,.py-3{padding-top:1rem !important}.pr-3,.px-3{padding-right:1rem !important}.pb-3,.py-3{padding-bottom:1rem !important}.pl-3,.px-3{padding-left:1rem !important}.p-4{padding:1.5rem !important}.pt-4,.py-4{padding-top:1.5rem !important}.pr-4,.px-4{padding-right:1.5rem !important}.pb-4,.py-4{padding-bottom:1.5rem !important}.pl-4,.px-4{padding-left:1.5rem !important}.p-5{padding:3rem !important}.pt-5,.py-5{padding-top:3rem !important}.pr-5,.px-5{padding-right:3rem !important}.pb-5,.py-5{padding-bottom:3rem !important}.pl-5,.px-5{padding-left:3rem !important}.m-n1{margin:-0.25rem !important}.mt-n1,.my-n1{margin-top:-0.25rem !important}.mr-n1,.mx-n1{margin-right:-0.25rem !important}.mb-n1,.my-n1{margin-bottom:-0.25rem !important}.ml-n1,.mx-n1{margin-left:-0.25rem !important}.m-n2{margin:-0.5rem !important}.mt-n2,.my-n2{margin-top:-0.5rem !important}.mr-n2,.mx-n2{margin-right:-0.5rem !important}.mb-n2,.my-n2{margin-bottom:-0.5rem !important}.ml-n2,.mx-n2{margin-left:-0.5rem !important}.m-n3{margin:-1rem !important}.mt-n3,.my-n3{margin-top:-1rem !important}.mr-n3,.mx-n3{margin-right:-1rem !important}.mb-n3,.my-n3{margin-bottom:-1rem !important}.ml-n3,.mx-n3{margin-left:-1rem !important}.m-n4{margin:-1.5rem !important}.mt-n4,.my-n4{margin-top:-1.5rem !important}.mr-n4,.mx-n4{margin-right:-1.5rem !important}.mb-n4,.my-n4{margin-bottom:-1.5rem !important}.ml-n4,.mx-n4{margin-left:-1.5rem !important}.m-n5{margin:-3rem !important}.mt-n5,.my-n5{margin-top:-3rem !important}.mr-n5,.mx-n5{margin-right:-3rem !important}.mb-n5,.my-n5{margin-bottom:-3rem !important}.ml-n5,.mx-n5{margin-left:-3rem !important}.m-auto{margin:auto !important}.mt-auto,.my-auto{margin-top:auto !important}.mr-auto,.mx-auto{margin-right:auto !important}.mb-auto,.my-auto{margin-bottom:auto !important}.ml-auto,.mx-auto{margin-left:auto !important}@media (min-width: 576px){.m-sm-0{margin:0 !important}.mt-sm-0,.my-sm-0{margin-top:0 !important}.mr-sm-0,.mx-sm-0{margin-right:0 !important}.mb-sm-0,.my-sm-0{margin-bottom:0 !important}.ml-sm-0,.mx-sm-0{margin-left:0 !important}.m-sm-1{margin:0.25rem !important}.mt-sm-1,.my-sm-1{margin-top:0.25rem !important}.mr-sm-1,.mx-sm-1{margin-right:0.25rem !important}.mb-sm-1,.my-sm-1{margin-bottom:0.25rem !important}.ml-sm-1,.mx-sm-1{margin-left:0.25rem !important}.m-sm-2{margin:0.5rem !important}.mt-sm-2,.my-sm-2{margin-top:0.5rem !important}.mr-sm-2,.mx-sm-2{margin-right:0.5rem !important}.mb-sm-2,.my-sm-2{margin-bottom:0.5rem !important}.ml-sm-2,.mx-sm-2{margin-left:0.5rem !important}.m-sm-3{margin:1rem !important}.mt-sm-3,.my-sm-3{margin-top:1rem !important}.mr-sm-3,.mx-sm-3{margin-right:1rem !important}.mb-sm-3,.my-sm-3{margin-bottom:1rem !important}.ml-sm-3,.mx-sm-3{margin-left:1rem !important}.m-sm-4{margin:1.5rem !important}.mt-sm-4,.my-sm-4{margin-top:1.5rem !important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem !important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem !important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem !important}.m-sm-5{margin:3rem !important}.mt-sm-5,.my-sm-5{margin-top:3rem !important}.mr-sm-5,.mx-sm-5{margin-right:3rem !important}.mb-sm-5,.my-sm-5{margin-bottom:3rem !important}.ml-sm-5,.mx-sm-5{margin-left:3rem !important}.p-sm-0{padding:0 !important}.pt-sm-0,.py-sm-0{padding-top:0 !important}.pr-sm-0,.px-sm-0{padding-right:0 !important}.pb-sm-0,.py-sm-0{padding-bottom:0 !important}.pl-sm-0,.px-sm-0{padding-left:0 !important}.p-sm-1{padding:0.25rem !important}.pt-sm-1,.py-sm-1{padding-top:0.25rem !important}.pr-sm-1,.px-sm-1{padding-right:0.25rem !important}.pb-sm-1,.py-sm-1{padding-bottom:0.25rem !important}.pl-sm-1,.px-sm-1{padding-left:0.25rem !important}.p-sm-2{padding:0.5rem !important}.pt-sm-2,.py-sm-2{padding-top:0.5rem !important}.pr-sm-2,.px-sm-2{padding-right:0.5rem !important}.pb-sm-2,.py-sm-2{padding-bottom:0.5rem !important}.pl-sm-2,.px-sm-2{padding-left:0.5rem !important}.p-sm-3{padding:1rem !important}.pt-sm-3,.py-sm-3{padding-top:1rem !important}.pr-sm-3,.px-sm-3{padding-right:1rem !important}.pb-sm-3,.py-sm-3{padding-bottom:1rem !important}.pl-sm-3,.px-sm-3{padding-left:1rem !important}.p-sm-4{padding:1.5rem !important}.pt-sm-4,.py-sm-4{padding-top:1.5rem !important}.pr-sm-4,.px-sm-4{padding-right:1.5rem !important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem !important}.pl-sm-4,.px-sm-4{padding-left:1.5rem !important}.p-sm-5{padding:3rem !important}.pt-sm-5,.py-sm-5{padding-top:3rem !important}.pr-sm-5,.px-sm-5{padding-right:3rem !important}.pb-sm-5,.py-sm-5{padding-bottom:3rem !important}.pl-sm-5,.px-sm-5{padding-left:3rem !important}.m-sm-n1{margin:-0.25rem !important}.mt-sm-n1,.my-sm-n1{margin-top:-0.25rem !important}.mr-sm-n1,.mx-sm-n1{margin-right:-0.25rem !important}.mb-sm-n1,.my-sm-n1{margin-bottom:-0.25rem !important}.ml-sm-n1,.mx-sm-n1{margin-left:-0.25rem !important}.m-sm-n2{margin:-0.5rem !important}.mt-sm-n2,.my-sm-n2{margin-top:-0.5rem !important}.mr-sm-n2,.mx-sm-n2{margin-right:-0.5rem !important}.mb-sm-n2,.my-sm-n2{margin-bottom:-0.5rem !important}.ml-sm-n2,.mx-sm-n2{margin-left:-0.5rem !important}.m-sm-n3{margin:-1rem !important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem !important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem !important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem !important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem !important}.m-sm-n4{margin:-1.5rem !important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem !important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem !important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem !important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem !important}.m-sm-n5{margin:-3rem !important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem !important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem !important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem !important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem !important}.m-sm-auto{margin:auto !important}.mt-sm-auto,.my-sm-auto{margin-top:auto !important}.mr-sm-auto,.mx-sm-auto{margin-right:auto !important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto !important}.ml-sm-auto,.mx-sm-auto{margin-left:auto !important}}@media (min-width: 768px){.m-md-0{margin:0 !important}.mt-md-0,.my-md-0{margin-top:0 !important}.mr-md-0,.mx-md-0{margin-right:0 !important}.mb-md-0,.my-md-0{margin-bottom:0 !important}.ml-md-0,.mx-md-0{margin-left:0 !important}.m-md-1{margin:0.25rem !important}.mt-md-1,.my-md-1{margin-top:0.25rem !important}.mr-md-1,.mx-md-1{margin-right:0.25rem !important}.mb-md-1,.my-md-1{margin-bottom:0.25rem !important}.ml-md-1,.mx-md-1{margin-left:0.25rem !important}.m-md-2{margin:0.5rem !important}.mt-md-2,.my-md-2{margin-top:0.5rem !important}.mr-md-2,.mx-md-2{margin-right:0.5rem !important}.mb-md-2,.my-md-2{margin-bottom:0.5rem !important}.ml-md-2,.mx-md-2{margin-left:0.5rem !important}.m-md-3{margin:1rem !important}.mt-md-3,.my-md-3{margin-top:1rem !important}.mr-md-3,.mx-md-3{margin-right:1rem !important}.mb-md-3,.my-md-3{margin-bottom:1rem !important}.ml-md-3,.mx-md-3{margin-left:1rem !important}.m-md-4{margin:1.5rem !important}.mt-md-4,.my-md-4{margin-top:1.5rem !important}.mr-md-4,.mx-md-4{margin-right:1.5rem !important}.mb-md-4,.my-md-4{margin-bottom:1.5rem !important}.ml-md-4,.mx-md-4{margin-left:1.5rem !important}.m-md-5{margin:3rem !important}.mt-md-5,.my-md-5{margin-top:3rem !important}.mr-md-5,.mx-md-5{margin-right:3rem !important}.mb-md-5,.my-md-5{margin-bottom:3rem !important}.ml-md-5,.mx-md-5{margin-left:3rem !important}.p-md-0{padding:0 !important}.pt-md-0,.py-md-0{padding-top:0 !important}.pr-md-0,.px-md-0{padding-right:0 !important}.pb-md-0,.py-md-0{padding-bottom:0 !important}.pl-md-0,.px-md-0{padding-left:0 !important}.p-md-1{padding:0.25rem !important}.pt-md-1,.py-md-1{padding-top:0.25rem !important}.pr-md-1,.px-md-1{padding-right:0.25rem !important}.pb-md-1,.py-md-1{padding-bottom:0.25rem !important}.pl-md-1,.px-md-1{padding-left:0.25rem !important}.p-md-2{padding:0.5rem !important}.pt-md-2,.py-md-2{padding-top:0.5rem !important}.pr-md-2,.px-md-2{padding-right:0.5rem !important}.pb-md-2,.py-md-2{padding-bottom:0.5rem !important}.pl-md-2,.px-md-2{padding-left:0.5rem !important}.p-md-3{padding:1rem !important}.pt-md-3,.py-md-3{padding-top:1rem !important}.pr-md-3,.px-md-3{padding-right:1rem !important}.pb-md-3,.py-md-3{padding-bottom:1rem !important}.pl-md-3,.px-md-3{padding-left:1rem !important}.p-md-4{padding:1.5rem !important}.pt-md-4,.py-md-4{padding-top:1.5rem !important}.pr-md-4,.px-md-4{padding-right:1.5rem !important}.pb-md-4,.py-md-4{padding-bottom:1.5rem !important}.pl-md-4,.px-md-4{padding-left:1.5rem !important}.p-md-5{padding:3rem !important}.pt-md-5,.py-md-5{padding-top:3rem !important}.pr-md-5,.px-md-5{padding-right:3rem !important}.pb-md-5,.py-md-5{padding-bottom:3rem !important}.pl-md-5,.px-md-5{padding-left:3rem !important}.m-md-n1{margin:-0.25rem !important}.mt-md-n1,.my-md-n1{margin-top:-0.25rem !important}.mr-md-n1,.mx-md-n1{margin-right:-0.25rem !important}.mb-md-n1,.my-md-n1{margin-bottom:-0.25rem !important}.ml-md-n1,.mx-md-n1{margin-left:-0.25rem !important}.m-md-n2{margin:-0.5rem !important}.mt-md-n2,.my-md-n2{margin-top:-0.5rem !important}.mr-md-n2,.mx-md-n2{margin-right:-0.5rem !important}.mb-md-n2,.my-md-n2{margin-bottom:-0.5rem !important}.ml-md-n2,.mx-md-n2{margin-left:-0.5rem !important}.m-md-n3{margin:-1rem !important}.mt-md-n3,.my-md-n3{margin-top:-1rem !important}.mr-md-n3,.mx-md-n3{margin-right:-1rem !important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem !important}.ml-md-n3,.mx-md-n3{margin-left:-1rem !important}.m-md-n4{margin:-1.5rem !important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem !important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem !important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem !important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem !important}.m-md-n5{margin:-3rem !important}.mt-md-n5,.my-md-n5{margin-top:-3rem !important}.mr-md-n5,.mx-md-n5{margin-right:-3rem !important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem !important}.ml-md-n5,.mx-md-n5{margin-left:-3rem !important}.m-md-auto{margin:auto !important}.mt-md-auto,.my-md-auto{margin-top:auto !important}.mr-md-auto,.mx-md-auto{margin-right:auto !important}.mb-md-auto,.my-md-auto{margin-bottom:auto !important}.ml-md-auto,.mx-md-auto{margin-left:auto !important}}@media (min-width: 992px){.m-lg-0{margin:0 !important}.mt-lg-0,.my-lg-0{margin-top:0 !important}.mr-lg-0,.mx-lg-0{margin-right:0 !important}.mb-lg-0,.my-lg-0{margin-bottom:0 !important}.ml-lg-0,.mx-lg-0{margin-left:0 !important}.m-lg-1{margin:0.25rem !important}.mt-lg-1,.my-lg-1{margin-top:0.25rem !important}.mr-lg-1,.mx-lg-1{margin-right:0.25rem !important}.mb-lg-1,.my-lg-1{margin-bottom:0.25rem !important}.ml-lg-1,.mx-lg-1{margin-left:0.25rem !important}.m-lg-2{margin:0.5rem !important}.mt-lg-2,.my-lg-2{margin-top:0.5rem !important}.mr-lg-2,.mx-lg-2{margin-right:0.5rem !important}.mb-lg-2,.my-lg-2{margin-bottom:0.5rem !important}.ml-lg-2,.mx-lg-2{margin-left:0.5rem !important}.m-lg-3{margin:1rem !important}.mt-lg-3,.my-lg-3{margin-top:1rem !important}.mr-lg-3,.mx-lg-3{margin-right:1rem !important}.mb-lg-3,.my-lg-3{margin-bottom:1rem !important}.ml-lg-3,.mx-lg-3{margin-left:1rem !important}.m-lg-4{margin:1.5rem !important}.mt-lg-4,.my-lg-4{margin-top:1.5rem !important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem !important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem !important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem !important}.m-lg-5{margin:3rem !important}.mt-lg-5,.my-lg-5{margin-top:3rem !important}.mr-lg-5,.mx-lg-5{margin-right:3rem !important}.mb-lg-5,.my-lg-5{margin-bottom:3rem !important}.ml-lg-5,.mx-lg-5{margin-left:3rem !important}.p-lg-0{padding:0 !important}.pt-lg-0,.py-lg-0{padding-top:0 !important}.pr-lg-0,.px-lg-0{padding-right:0 !important}.pb-lg-0,.py-lg-0{padding-bottom:0 !important}.pl-lg-0,.px-lg-0{padding-left:0 !important}.p-lg-1{padding:0.25rem !important}.pt-lg-1,.py-lg-1{padding-top:0.25rem !important}.pr-lg-1,.px-lg-1{padding-right:0.25rem !important}.pb-lg-1,.py-lg-1{padding-bottom:0.25rem !important}.pl-lg-1,.px-lg-1{padding-left:0.25rem !important}.p-lg-2{padding:0.5rem !important}.pt-lg-2,.py-lg-2{padding-top:0.5rem !important}.pr-lg-2,.px-lg-2{padding-right:0.5rem !important}.pb-lg-2,.py-lg-2{padding-bottom:0.5rem !important}.pl-lg-2,.px-lg-2{padding-left:0.5rem !important}.p-lg-3{padding:1rem !important}.pt-lg-3,.py-lg-3{padding-top:1rem !important}.pr-lg-3,.px-lg-3{padding-right:1rem !important}.pb-lg-3,.py-lg-3{padding-bottom:1rem !important}.pl-lg-3,.px-lg-3{padding-left:1rem !important}.p-lg-4{padding:1.5rem !important}.pt-lg-4,.py-lg-4{padding-top:1.5rem !important}.pr-lg-4,.px-lg-4{padding-right:1.5rem !important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem !important}.pl-lg-4,.px-lg-4{padding-left:1.5rem !important}.p-lg-5{padding:3rem !important}.pt-lg-5,.py-lg-5{padding-top:3rem !important}.pr-lg-5,.px-lg-5{padding-right:3rem !important}.pb-lg-5,.py-lg-5{padding-bottom:3rem !important}.pl-lg-5,.px-lg-5{padding-left:3rem !important}.m-lg-n1{margin:-0.25rem !important}.mt-lg-n1,.my-lg-n1{margin-top:-0.25rem !important}.mr-lg-n1,.mx-lg-n1{margin-right:-0.25rem !important}.mb-lg-n1,.my-lg-n1{margin-bottom:-0.25rem !important}.ml-lg-n1,.mx-lg-n1{margin-left:-0.25rem !important}.m-lg-n2{margin:-0.5rem !important}.mt-lg-n2,.my-lg-n2{margin-top:-0.5rem !important}.mr-lg-n2,.mx-lg-n2{margin-right:-0.5rem !important}.mb-lg-n2,.my-lg-n2{margin-bottom:-0.5rem !important}.ml-lg-n2,.mx-lg-n2{margin-left:-0.5rem !important}.m-lg-n3{margin:-1rem !important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem !important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem !important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem !important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem !important}.m-lg-n4{margin:-1.5rem !important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem !important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem !important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem !important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem !important}.m-lg-n5{margin:-3rem !important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem !important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem !important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem !important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem !important}.m-lg-auto{margin:auto !important}.mt-lg-auto,.my-lg-auto{margin-top:auto !important}.mr-lg-auto,.mx-lg-auto{margin-right:auto !important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto !important}.ml-lg-auto,.mx-lg-auto{margin-left:auto !important}}@media (min-width: 1200px){.m-xl-0{margin:0 !important}.mt-xl-0,.my-xl-0{margin-top:0 !important}.mr-xl-0,.mx-xl-0{margin-right:0 !important}.mb-xl-0,.my-xl-0{margin-bottom:0 !important}.ml-xl-0,.mx-xl-0{margin-left:0 !important}.m-xl-1{margin:0.25rem !important}.mt-xl-1,.my-xl-1{margin-top:0.25rem !important}.mr-xl-1,.mx-xl-1{margin-right:0.25rem !important}.mb-xl-1,.my-xl-1{margin-bottom:0.25rem !important}.ml-xl-1,.mx-xl-1{margin-left:0.25rem !important}.m-xl-2{margin:0.5rem !important}.mt-xl-2,.my-xl-2{margin-top:0.5rem !important}.mr-xl-2,.mx-xl-2{margin-right:0.5rem !important}.mb-xl-2,.my-xl-2{margin-bottom:0.5rem !important}.ml-xl-2,.mx-xl-2{margin-left:0.5rem !important}.m-xl-3{margin:1rem !important}.mt-xl-3,.my-xl-3{margin-top:1rem !important}.mr-xl-3,.mx-xl-3{margin-right:1rem !important}.mb-xl-3,.my-xl-3{margin-bottom:1rem !important}.ml-xl-3,.mx-xl-3{margin-left:1rem !important}.m-xl-4{margin:1.5rem !important}.mt-xl-4,.my-xl-4{margin-top:1.5rem !important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem !important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem !important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem !important}.m-xl-5{margin:3rem !important}.mt-xl-5,.my-xl-5{margin-top:3rem !important}.mr-xl-5,.mx-xl-5{margin-right:3rem !important}.mb-xl-5,.my-xl-5{margin-bottom:3rem !important}.ml-xl-5,.mx-xl-5{margin-left:3rem !important}.p-xl-0{padding:0 !important}.pt-xl-0,.py-xl-0{padding-top:0 !important}.pr-xl-0,.px-xl-0{padding-right:0 !important}.pb-xl-0,.py-xl-0{padding-bottom:0 !important}.pl-xl-0,.px-xl-0{padding-left:0 !important}.p-xl-1{padding:0.25rem !important}.pt-xl-1,.py-xl-1{padding-top:0.25rem !important}.pr-xl-1,.px-xl-1{padding-right:0.25rem !important}.pb-xl-1,.py-xl-1{padding-bottom:0.25rem !important}.pl-xl-1,.px-xl-1{padding-left:0.25rem !important}.p-xl-2{padding:0.5rem !important}.pt-xl-2,.py-xl-2{padding-top:0.5rem !important}.pr-xl-2,.px-xl-2{padding-right:0.5rem !important}.pb-xl-2,.py-xl-2{padding-bottom:0.5rem !important}.pl-xl-2,.px-xl-2{padding-left:0.5rem !important}.p-xl-3{padding:1rem !important}.pt-xl-3,.py-xl-3{padding-top:1rem !important}.pr-xl-3,.px-xl-3{padding-right:1rem !important}.pb-xl-3,.py-xl-3{padding-bottom:1rem !important}.pl-xl-3,.px-xl-3{padding-left:1rem !important}.p-xl-4{padding:1.5rem !important}.pt-xl-4,.py-xl-4{padding-top:1.5rem !important}.pr-xl-4,.px-xl-4{padding-right:1.5rem !important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem !important}.pl-xl-4,.px-xl-4{padding-left:1.5rem !important}.p-xl-5{padding:3rem !important}.pt-xl-5,.py-xl-5{padding-top:3rem !important}.pr-xl-5,.px-xl-5{padding-right:3rem !important}.pb-xl-5,.py-xl-5{padding-bottom:3rem !important}.pl-xl-5,.px-xl-5{padding-left:3rem !important}.m-xl-n1{margin:-0.25rem !important}.mt-xl-n1,.my-xl-n1{margin-top:-0.25rem !important}.mr-xl-n1,.mx-xl-n1{margin-right:-0.25rem !important}.mb-xl-n1,.my-xl-n1{margin-bottom:-0.25rem !important}.ml-xl-n1,.mx-xl-n1{margin-left:-0.25rem !important}.m-xl-n2{margin:-0.5rem !important}.mt-xl-n2,.my-xl-n2{margin-top:-0.5rem !important}.mr-xl-n2,.mx-xl-n2{margin-right:-0.5rem !important}.mb-xl-n2,.my-xl-n2{margin-bottom:-0.5rem !important}.ml-xl-n2,.mx-xl-n2{margin-left:-0.5rem !important}.m-xl-n3{margin:-1rem !important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem !important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem !important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem !important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem !important}.m-xl-n4{margin:-1.5rem !important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem !important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem !important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem !important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem !important}.m-xl-n5{margin:-3rem !important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem !important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem !important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem !important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem !important}.m-xl-auto{margin:auto !important}.mt-xl-auto,.my-xl-auto{margin-top:auto !important}.mr-xl-auto,.mx-xl-auto{margin-right:auto !important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto !important}.ml-xl-auto,.mx-xl-auto{margin-left:auto !important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important}.text-justify{text-align:justify !important}.text-wrap{white-space:normal !important}.text-nowrap{white-space:nowrap !important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left !important}.text-right{text-align:right !important}.text-center{text-align:center !important}@media (min-width: 576px){.text-sm-left{text-align:left !important}.text-sm-right{text-align:right !important}.text-sm-center{text-align:center !important}}@media (min-width: 768px){.text-md-left{text-align:left !important}.text-md-right{text-align:right !important}.text-md-center{text-align:center !important}}@media (min-width: 992px){.text-lg-left{text-align:left !important}.text-lg-right{text-align:right !important}.text-lg-center{text-align:center !important}}@media (min-width: 1200px){.text-xl-left{text-align:left !important}.text-xl-right{text-align:right !important}.text-xl-center{text-align:center !important}}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.font-weight-light{font-weight:300 !important}.font-weight-lighter{font-weight:lighter !important}.font-weight-normal{font-weight:400 !important}.font-weight-bold{font-weight:700 !important}.font-weight-bolder{font-weight:bolder !important}.font-italic{font-style:italic !important}.text-white{color:#fff !important}.text-primary{color:#375a7f !important}a.text-primary:hover,a.text-primary:focus{color:#20344a !important}.text-secondary{color:#444 !important}a.text-secondary:hover,a.text-secondary:focus{color:#1e1e1e !important}.text-success{color:#00bc8c !important}a.text-success:hover,a.text-success:focus{color:#007053 !important}.text-info{color:#3498DB !important}a.text-info:hover,a.text-info:focus{color:#1d6fa5 !important}.text-warning{color:#F39C12 !important}a.text-warning:hover,a.text-warning:focus{color:#b06f09 !important}.text-danger{color:#E74C3C !important}a.text-danger:hover,a.text-danger:focus{color:#bf2718 !important}.text-light{color:#adb5bd !important}a.text-light:hover,a.text-light:focus{color:#838f9b !important}.text-dark{color:#303030 !important}a.text-dark:hover,a.text-dark:focus{color:#0a0a0a !important}.text-body{color:#fff !important}.text-muted{color:#888 !important}.text-black-50{color:rgba(0,0,0,0.5) !important}.text-white-50{color:rgba(255,255,255,0.5) !important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none !important}.text-break{word-wrap:break-word !important}.text-reset{color:inherit !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}@media print{*,*::before,*::after{text-shadow:none !important;-webkit-box-shadow:none !important;box-shadow:none !important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap !important}pre,blockquote{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px !important}.container{min-width:992px !important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #dee2e6 !important}.table-dark{color:inherit}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#444}.table .thead-dark th{color:inherit;border-color:#444}}.blockquote-footer{color:#888}.table-primary,.table-primary>th,.table-primary>td{background-color:#375a7f}.table-secondary,.table-secondary>th,.table-secondary>td{background-color:#444}.table-light,.table-light>th,.table-light>td{background-color:#adb5bd}.table-dark,.table-dark>th,.table-dark>td{background-color:#303030}.table-success,.table-success>th,.table-success>td{background-color:#00bc8c}.table-info,.table-info>th,.table-info>td{background-color:#3498DB}.table-danger,.table-danger>th,.table-danger>td{background-color:#E74C3C}.table-warning,.table-warning>th,.table-warning>td{background-color:#F39C12}.table-active,.table-active>th,.table-active>td{background-color:rgba(0,0,0,0.075)}.table-hover .table-primary:hover,.table-hover .table-primary:hover>th,.table-hover .table-primary:hover>td{background-color:#2f4d6d}.table-hover .table-secondary:hover,.table-hover .table-secondary:hover>th,.table-hover .table-secondary:hover>td{background-color:#373737}.table-hover .table-light:hover,.table-hover .table-light:hover>th,.table-hover .table-light:hover>td{background-color:#9fa8b2}.table-hover .table-dark:hover,.table-hover .table-dark:hover>th,.table-hover .table-dark:hover>td{background-color:#232323}.table-hover .table-success:hover,.table-hover .table-success:hover>th,.table-hover .table-success:hover>td{background-color:#00a379}.table-hover .table-info:hover,.table-hover .table-info:hover>th,.table-hover .table-info:hover>td{background-color:#258cd1}.table-hover .table-danger:hover,.table-hover .table-danger:hover>th,.table-hover .table-danger:hover>td{background-color:#e43725}.table-hover .table-warning:hover,.table-hover .table-warning:hover>th,.table-hover .table-warning:hover>td{background-color:#e08e0b}.table-hover .table-active:hover,.table-hover .table-active:hover>th,.table-hover .table-active:hover>td{background-color:rgba(0,0,0,0.075)}.input-group-addon{color:#fff}.nav-tabs .nav-link,.nav-tabs .nav-link.active,.nav-tabs .nav-link.active:focus,.nav-tabs .nav-link.active:hover,.nav-tabs .nav-item.open .nav-link,.nav-tabs .nav-item.open .nav-link:focus,.nav-tabs .nav-item.open .nav-link:hover,.nav-pills .nav-link,.nav-pills .nav-link.active,.nav-pills .nav-link.active:focus,.nav-pills .nav-link.active:hover,.nav-pills .nav-item.open .nav-link,.nav-pills .nav-item.open .nav-link:focus,.nav-pills .nav-item.open .nav-link:hover{color:#fff}.breadcrumb a{color:#fff}.pagination a:hover{text-decoration:none}.close{opacity:0.4}.close:hover,.close:focus{opacity:1}.alert{border:none;color:#fff}.alert a,.alert .alert-link{color:#fff;text-decoration:underline}.alert-primary{background-color:#375a7f}.alert-secondary{background-color:#444}.alert-success{background-color:#00bc8c}.alert-info{background-color:#3498DB}.alert-warning{background-color:#F39C12}.alert-danger{background-color:#E74C3C}.alert-light{background-color:#adb5bd}.alert-dark{background-color:#303030}.list-group-item-action{color:#fff}.list-group-item-action:hover,.list-group-item-action:focus{background-color:#444;color:#fff}.list-group-item-action .list-group-item-heading{color:#fff} diff --git a/htdocs/css/login.css b/htdocs/css/login.css new file mode 100644 index 000000000..ccd6c0265 --- /dev/null +++ b/htdocs/css/login.css @@ -0,0 +1,34 @@ +@import url("openwebrx-header.css"); +@import url("openwebrx-globals.css"); + +body { + display: flex; + flex-direction: column; +} + +.login-container { + flex: 1; + position: relative; +} + +.login { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + + width: 500px; + + padding: 20px; + border-radius: 10px; + border: 1px solid #575757; + box-shadow: 0 0 20px #000; +} + +.login .btn { + width: 100%; +} + +.btn-login { + height: 50px; +} \ No newline at end of file diff --git a/htdocs/css/map.css b/htdocs/css/map.css new file mode 100644 index 000000000..70702b967 --- /dev/null +++ b/htdocs/css/map.css @@ -0,0 +1,65 @@ +@import url("openwebrx-header.css"); +@import url("openwebrx-globals.css"); + +body { + display: flex; + flex-direction: column; +} + +.openwebrx-map { + flex: 1 1 auto; +} + +h3 { + margin: 10px 0; + text-align: center; +} + +ul { + margin-block-start: 5px; + margin-block-end: 5px; + padding-inline-start: 25px; +} + +/* don't show the filter in it's initial position */ +.openwebrx-map-legend { + display: none; + background-color: #fff; + padding: 10px; + margin: 10px; + user-select: none; +} + +/* show it as soon as google maps has moved it to its container */ +.openwebrx-map .openwebrx-map-legend { + display: block; +} + +.openwebrx-map-legend ul { + list-style-type: none; + padding: 0; +} + +.openwebrx-map-legend ul li { + cursor: pointer; +} + +.openwebrx-map-legend ul li.disabled { + opacity: .3; + filter: grayscale(70%); +} + +.openwebrx-map-legend li.square .illustration { + display: inline-block; + width: 30px; + height: 20px; + margin-right: 10px; + border-width: 2px; + border-style: solid; +} + +.openwebrx-map-legend select { + background-color: #FFF; + border-color: #DDD; + padding: 5px; +} diff --git a/htdocs/css/openwebrx-globals.css b/htdocs/css/openwebrx-globals.css new file mode 100644 index 000000000..575984772 --- /dev/null +++ b/htdocs/css/openwebrx-globals.css @@ -0,0 +1,7 @@ +html, body +{ + margin: 0; + padding: 0; + height: 100%; + font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; +} diff --git a/htdocs/css/openwebrx-header.css b/htdocs/css/openwebrx-header.css new file mode 100644 index 000000000..4e8601b71 --- /dev/null +++ b/htdocs/css/openwebrx-header.css @@ -0,0 +1,227 @@ +.webrx-top-container { + position: relative; + z-index:1000; + background-color: #575757; + + background-image: url(../gfx/openwebrx-top-photo.jpg); + background-position-x: center; + background-position-y: top; + background-repeat: no-repeat; + background-size: cover; + + overflow: hidden; +} + +.openwebrx-description-container { + transition-property: height, opacity; + transition-duration: 1s; + transition-timing-function: ease-out; + opacity: 0; + height: 0; + /* originally, top-bar + description was 350px */ + max-height: 283px; + overflow: hidden; +} + +.openwebrx-description-container.expanded { + opacity: 1; + height: 283px; +} + +.webrx-top-bar { + height:67px; + + background: rgba(128, 128, 128, 0.15); + margin:0; + padding:0; + user-select: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + overflow: hidden; + + display: flex; + flex-direction: row; +} + +.webrx-top-bar > * { + flex: 0; +} + +.webrx-top-container, .webrx-top-container * { + line-height: initial; + box-sizing: initial; +} + +.webrx-top-logo { + width: 261px; + padding: 12px; + filter: drop-shadow(0 0 2.5px rgba(0, 0, 0, .9)); + /* overwritten by media queries */ + display: none; +} + +.webrx-rx-avatar { + background-color: rgba(154, 154, 154, .5); + margin: 7px; + + width: 46px; + height: 46px; + padding: 4px; + border-radius: 8px; + box-sizing: content-box; +} + +.webrx-rx-texts { + /* minimum layout width */ + width: 0; + /* will be getting wider with flex */ + flex: 1; + overflow: hidden; + margin: auto 0; +} + +.webrx-rx-texts div, .webrx-rx-texts h1 { + margin: 0 10px; + padding: 3px; + white-space:nowrap; + overflow: hidden; + color: #909090; + text-align: left; +} + +.webrx-rx-title { + font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; + font-size: 11pt; + font-weight: bold; +} + +.webrx-rx-desc { + font-size: 10pt; +} + +.openwebrx-main-buttons .button { + display: block; + width: 55px; + cursor:pointer; +} + +.openwebrx-main-buttons .button[data-toggle-panel] { + /* will be enabled by javascript if the panel is present in the DOM */ + display: none; +} + +.openwebrx-main-buttons .button img, +.openwebrx-main-buttons .button svg { + height: 38px; + filter: drop-shadow(0 0 4px rgba(0, 0, 0, 0.5)); +} + +.openwebrx-main-buttons a { + color: inherit; + text-decoration: inherit; +} + +.openwebrx-main-buttons .button:hover { + background-color: rgba(255, 255, 255, 0.3); +} + +.openwebrx-main-buttons .button:active { + background-color: rgba(255, 255, 255, 0.55); +} + + +.openwebrx-main-buttons { + padding: 5px 15px; + display: flex; + list-style: none; + margin:0; + color: white; + text-shadow: 0px 0px 4px #000000; + text-align: center; + font-size: 9pt; + font-weight: bold; +} + +.webrx-rx-photo-title { + margin: 10px 15px; + color: white; + font-size: 16pt; + text-shadow: 1px 1px 4px #444; + opacity: 1; +} + +.webrx-rx-photo-desc { + margin: 10px 15px; + color: white; + font-size: 10pt; + font-weight: bold; + text-shadow: 0px 0px 6px #444; + opacity: 1; + line-height: 1.5em; +} + +.webrx-rx-photo-desc a { + color: #5ca8ff; + text-shadow: none; +} + +.openwebrx-photo-trigger { + cursor: pointer; +} + +/* + * Responsive stuff + */ + +@media (min-width: 576px) { + .webrx-rx-texts { + display: initial; + } +} + +@media (min-width: 768px) { +} + +@media (min-width: 992px) { + .webrx-top-logo { + display: initial; + } +} + +@media (min-width: 1200px) { +} + +/* + * RX details arrow up/down switching + */ + +.openwebrx-rx-details-arrow { + position: absolute; + bottom: 0; + left: 50%; + transform: translate(-50%, 0); + + margin: 0; + padding: 0; + line-height: 0; + display: block; +} + +.openwebrx-rx-details-arrow svg { + height: 12px; +} + +.openwebrx-rx-details-arrow .up { + display: none; +} + +.openwebrx-rx-details-arrow--up .down { + display: none; +} + +.openwebrx-rx-details-arrow--up .up { + display: initial; +} \ No newline at end of file diff --git a/htdocs/css/openwebrx.css b/htdocs/css/openwebrx.css new file mode 100644 index 000000000..fae763788 --- /dev/null +++ b/htdocs/css/openwebrx.css @@ -0,0 +1,1442 @@ +/* + + This file is part of OpenWebRX, + an open-source SDR receiver software with a web UI. + Copyright (c) 2013-2015 by Andras Retzler + Copyright (c) 2019-2021 by Jakob Ketterl + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +*/ +@import url("openwebrx-header.css"); +@import url("openwebrx-globals.css"); + +html, body { + overflow: hidden; +} + +select +{ + font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; +} + +input +{ + vertical-align:middle; +} + +input[type=range] { + -webkit-appearance: none; + margin: 0 0; + background: transparent !important; + --track-background: #B6B6B6; +} + +input[type=range]:focus { + outline: none; +} + +input[type=range]::-webkit-slider-runnable-track +{ + height: 5px; + cursor: pointer; + animate: 0.2s; + box-shadow: 0px 0px 0px #000000; + background: #B6B6B6; + /*border-radius: 11px;*/ + border: 1px solid #8A8A8A; + background: var(--track-background); +} + +input[type=range]::-webkit-slider-thumb +{ + box-shadow: 1px 1px 1px #828282; + border: 1px solid #8A8A8A; + height: 15px; + width: 15px; + border-radius: 10px; + background: #FFFFFF; + cursor: pointer; + -webkit-appearance: none; + margin-top: -7px; +} + +input[type=range]:focus::-webkit-slider-runnable-track +{ + background: #B6B6B6; + background: var(--track-background); +} + +input[type=range]::-moz-range-track +{ + height: 3px; + cursor: pointer; + animate: 0.2s; + box-shadow: 0px 0px 0px #000000; + background: #B6B6B6; + background: var(--track-background); + border-radius: 11px; + border: 1px solid #8A8A8A; +} + +input[type=range]::-moz-range-thumb +{ + box-shadow: 1px 1px 1px #828282; + border: 1px solid #8A8A8A; + height: 12px; + width: 12px; + border-radius: 10px; + background: #FFFFFF; + cursor: pointer; +} + +input[type=range]::-ms-track +{ + width: 100%; + height: 7px; + cursor: pointer; + animate: 0.2s; + background: transparent; + border-color: transparent; + color: transparent; +} + +input[type=range]::-ms-fill-lower + { + background: #B6B6B6; + border: 1px solid #8A8A8A; + border-radius: 22px; + box-shadow: 0px 0px 0px #000000; +} + +input[type=range]::-ms-fill-upper +{ + background: #B6B6B6; + border: 1px solid #8A8A8A; + border-radius: 22px; + box-shadow: 0px 0px 0px #000000; +} + +input[type=range]::-ms-thumb +{ + box-shadow: 1px 1px 1px #828282; + border: 1px solid #8A8A8A; + height: 24px; + width: 7px; + border-radius: 0px; + background: #FFFFFF; + cursor: pointer; +} + +input[type=range]:focus::-ms-fill-lower +{ + background: #B6B6B6; +} + +input[type=range]:focus::-ms-fill-upper +{ + background: #B6B6B6; +} + +input[type=range]:disabled { + opacity: 0.5; +} + +#webrx-page-container +{ + height: 100%; + position: relative; + display: flex; + flex-direction: column; +} + +#openwebrx-scale-container +{ + height: 47px; + overflow: hidden; + z-index:1000; + position: relative; +} + +#openwebrx-frequency-container { + background-image: url("../gfx/openwebrx-scale-background.png"); + background-repeat: repeat-x; + background-size: cover; + background-color: #444; + z-index: 1001; +} + +#openwebrx-bookmarks-container +{ + height: 25px; + position: relative; + z-index: 1000; +} + +#openwebrx-bookmarks-container .bookmark { + font-size: 12px; + background-color: #FFFF00; + border: 1px solid #000; + border-radius: 5px; + padding: 2px 5px; + cursor: pointer; + white-space: nowrap; + max-height: 14px; + max-width: 50px; + + position: absolute; + bottom: 5px; + transform: translate(-50%, 0); +} + +#openwebrx-bookmarks-container .bookmark .bookmark-content { + overflow: hidden; + text-overflow: ellipsis; +} + +#openwebrx-bookmarks-container .bookmark .bookmark-actions { + display: none; + text-align: right; +} + +.bookmark-actions .action { + line-height: 0; +} + +.bookmark-actions .action img { + width: 14px; +} + +#openwebrx-bookmarks-container .bookmark.selected { + z-index: 1010; +} + +#openwebrx-bookmarks-container .bookmark:hover { + z-index: 1011; + max-height: none; + max-width: none; +} + +#openwebrx-bookmarks-container .bookmark[editable]:hover .bookmark-actions { + display: block; + margin-bottom: 5px; +} + +#openwebrx-bookmarks-container .bookmark:after { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + width: 0; + height: 0; + border: 5px solid transparent; + border-top-color: #FFFF00; + border-bottom: 0; + margin-left: -5px; + margin-bottom: -5px; +} + +#openwebrx-bookmarks-container .bookmark[data-source=local] { + background-color: #0FF; +} + +#openwebrx-bookmarks-container .bookmark[data-source=local]:after { + border-top-color: #0FF; +} + +#openwebrx-bookmarks-container .bookmark[data-source=dial_frequencies] { + background-color: #0F0; +} + +#openwebrx-bookmarks-container .bookmark[data-source=dial_frequencies]:after { + border-top-color: #0F0; +} + +#webrx-canvas-background { + flex-grow: 1; + background-image: url('../gfx/openwebrx-background-cool-blue.png'); + background-repeat: no-repeat; + background-color: #1e5f7f; + background-size: cover; + display: flex; + flex-direction: column; +} + +@supports(background-image: -webkit-image-set(url('../gfx/openwebrx-background-cool-blue.webp') 1x)) { + #webrx-canvas-background { + background-image: -webkit-image-set(url('../gfx/openwebrx-background-cool-blue.webp') 1x); + } +} + +@supports(background-image: image-set(url('../gfx/openwebrx-background-cool-blue.webp') 1x)) { + #webrx-canvas-background { + background-image: image-set(url('../gfx/openwebrx-background-cool-blue.webp') 1x); + } +} + +#webrx-canvas-container +{ + position: relative; + overflow: visible; + cursor: crosshair; + flex-grow: 1; +} + +#webrx-canvas-container canvas +{ + position: absolute; + top: 0; + border-style: none; + image-rendering: crisp-edges; + image-rendering: -webkit-optimize-contrast; + width: 100%; + height: 200px; + will-change: transform; +} + +#openwebrx-log-scroll +{ + /*overflow-y:auto;*/ + height: 125px; + width: 619px +} + +.nano .nano-pane { background: #444; } +.nano .nano-slider { background: #eee !important; } + +.webrx-error +{ + font-weight: bold; + color: #ff6262; +} + +@font-face { + font-family: 'roboto-mono'; + src: url('../fonts/RobotoMono-Regular.woff2') format('woff2'), + url('../fonts/RobotoMono-Regular.woff') format('woff'), + url('../fonts/RobotoMono-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +.webrx-actual-freq { + width: 100%; + text-align: left; + padding: 0; + margin: 0; + display: flex; + flex-direction: row; + cursor: pointer; +} + +.webrx-actual-freq > * { + flex: 1; +} + +.webrx-actual-freq .input-group { + display: flex; + flex-direction: row; +} + +.webrx-actual-freq .input-group > * { + flex: 0 0 auto; +} + +.webrx-actual-freq .input-group input { + flex: 1 0 auto; + margin-right: 0; + border-right: 1px solid #373737; + -moz-appearance: textfield; +} + +.webrx-actual-freq .input-group input::-webkit-outer-spin-button, +.webrx-actual-freq .input-group input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.input-group > :not(:last-child) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group > :not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} + +.input-group :first-child { + padding-left: 5px; +} + +.input-group :last-child { + padding-right: 5px +} + +.webrx-actual-freq .input-group input, .webrx-actual-freq .input-group select { + outline: none; + font-size: 16pt; +} + +.webrx-actual-freq input { + font-family: 'roboto-mono'; + width: 0; + box-sizing: border-box; + border: 0; + padding: 0; + background-color: inherit; + color: inherit; +} + +.webrx-actual-freq, .webrx-actual-freq input { + font-size: 16pt; + font-family: 'roboto-mono'; +} + +.webrx-actual-freq .digit { + cursor: ns-resize; +} + +.webrx-actual-freq .digit:hover { + color: #FFFF50; + border-radius: 5px; + background: -webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) ); + background: -moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% ); +} + +.webrx-mouse-freq { + width: 100%; + text-align: left; + font-size: 10pt; + color: #AAA; + font-family: 'roboto-mono'; + margin-bottom: 5px; +} + +#openwebrx-panels-container-left, +#openwebrx-panels-container-right { + position: absolute; + bottom: 0; + display: flex; + flex-direction: column; + justify-content: flex-end; + height: 0; + overflow: visible; +} + +#openwebrx-panels-container-left { + left: 0; + align-items: flex-start; +} + +#openwebrx-panels-container-right { + right: 0; + align-items: flex-end; +} + +.openwebrx-panel +{ + transform: perspective( 600px ) rotateX( 90deg ); + background-color: #575757; + padding: 10px; + color: white; + font-size: 10pt; + border-radius: 15px; + -moz-border-radius: 15px; + margin: 5.9px; + box-sizing: content-box; +} + +.openwebrx-panel a +{ + color: #5ca8ff; + text-shadow: none; +} + +.openwebrx-panel-inner +{ + overflow-y: auto; + overflow-x: hidden; + height: 100%; +} + +.openwebrx-button +{ + background-color: #373737; + padding: 4.2px; + border-radius: 5px; + -moz-border-radius: 5px; + color: White; + font-weight: bold; + margin-right: 1px; + cursor: pointer; + background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) ); + background:-moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% ); + user-select: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + display: inline-block; +} + +.openwebrx-button:hover, .openwebrx-demodulator-button.highlighted, .openwebrx-button.highlighted +{ + /*background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #3F3F3F), color-stop(1, #777777) ); + background:-moz-linear-gradient( center top, #373737 5%, #4F4F4F 100% );*/ + background: #474747; + color: #FFFF50; +} + +.openwebrx-button:active +{ + background: #777777; + color: #FFFF50; +} + +.openwebrx-button:last-child { + margin-right: 0; +} + +.openwebrx-button.disabled { + opacity: 0.5; +} + +.openwebrx-demodulator-button +{ + height: 19px; + font-size: 12pt; + text-align: center; + flex: 1; + margin-right: 5px; +} + +.openwebrx-demodulator-button.same-mod { + color: #FFC; +} + +.openwebrx-square-button img +{ + height: 27px; +} + +.openwebrx-round-button +{ + margin-right: -2px; + width: 35px; + height: 35px; + border-radius: 25px; +} + +.openwebrx-round-button img +{ + height: 30px; +} + +.openwebrx-round-button-small +{ + margin-right: -3px; + width: 20px; + height: 20px; + border-radius: 25px; +} + +.openwebrx-round-button-small img +{ + height: 20px; +} + +img.openwebrx-mirror-img +{ + transform: scale(-1, 1); +} + + +.openwebrx-round-rightarrow img +{ + position: relative; + left: 12px; + top: 3px; +} + +.openwebrx-round-leftarrow img +{ + position: relative; + left: 7px; + top: 3px; +} + +#openwebrx-client-log-title +{ + margin-bottom: 5px; + font-weight: bold; +} + +.openwebrx-progressbar +{ + position: relative; + border-radius: 5px; + background-color: #003850; /*#006235;*/ + display: inline-block; + text-align: center; + font-size: 8pt; + font-weight: bold; + text-shadow: 0px 0px 4px #000000; + cursor: default; + user-select: none; + -webkit-touch-callout: none; + -webkit-user-select: none; + -khtml-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + overflow: hidden; + z-index: 1 +} + +.openwebrx-progressbar-bar { + background-color: #00aba6; + border-radius: 5px; + height: 100%; + width: 100%; + transition-property: transform, background-color; + transition-duration: 1s; + transition-timing-function: ease-in-out; + transform: translate(-100%) translateZ(0); + will-change: transform, background-color; + z-index: 0; +} + +.openwebrx-progressbar--over .openwebrx-progressbar-bar { + background-color: #ff6262; +} + +.openwebrx-progressbar-text +{ + position: absolute; + left:50%; + top:50%; + transform: translate(-50%, -50%); + white-space: nowrap; + z-index: 2; +} + +#openwebrx-panel-status +{ + margin: 0 0 0 5.9px; + padding: 0px; + background-color:rgba(0, 0, 0, 0); +} + +#openwebrx-panel-status div.openwebrx-progressbar +{ + width: 200px; + height: 20px; +} + +#openwebrx-panel-receiver +{ + width:110px; +} + + +#openwebrx-panel-receiver .frequencies-container { + display: flex; + flex-direction: row; + gap: 5px; +} + +#openwebrx-panel-receiver .frequencies { + flex-grow: 1; +} + +#openwebrx-panel-receiver .openwebrx-bookmark-button { + width: 27px; + height: 27px; + text-align: center; +} + +.openwebrx-panel-slider +{ + position: relative; + top: -2px; + width: 95px; +} + +.openwebrx-panel-line +{ + padding-top: 5px; +} + +.openwebrx-panel-flex-line { + display: flex; + flex-direction: row; +} + +.openwebrx-panel-line:first-child { + padding-top: 0; +} + +.openwebrx-modes-grid { + display: flex; + flex-direction: row; + flex-wrap: wrap; + margin: -5px -5px 0 0; +} + +.openwebrx-modes-grid .openwebrx-demodulator-button { + margin: 0; + white-space: nowrap; + flex: 1 0 38px; + margin: 5px 5px 0 0; +} + +@supports(gap: 5px) { + .openwebrx-modes-grid { + margin: 0; + gap: 5px; + } + + .openwebrx-modes-grid .openwebrx-demodulator-button { + margin: 0; + } +} + +#openwebrx-smeter { + border-color: #888; + border-style: solid; + border-width: 0px; + width: 255px; + height: 7px; + background-color: #373737; + border-radius: 3px; + overflow: hidden; +} + +.openwebrx-smeter-bar { + transition-property: transform; + transition-duration: 0.2s; + transition-timing-function: linear; + will-change: transform; + transform: translate(-100%) translateZ(0); + width: 100%; + height: 100%; + background: linear-gradient(to top, #ff5939 , #961700); + margin: 0; + padding: 0; + border-radius: 3px; +} + +#openwebrx-smeter-db +{ + color: #aaa; + display: inline-block; + font-size: 10pt; + float: right; + margin-right: 5px; + margin-top: 24px; + font-family: 'roboto-mono'; +} + +.openwebrx-overlay { + position: absolute; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + opacity: 0.8; + background-color: #777; + left: 0; + top: 0; + z-index: 1001; + color: white; + font-weight: bold; + font-size: 20pt; +} + +#openwebrx-autoplay-overlay +{ + cursor: pointer; + transition: opacity 0.3s linear; +} + +#openwebrx-autoplay-overlay svg { + width: 150px; +} + +.openwebrx-overlay .overlay-content { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + text-align: center; +} + +#openwebrx-error-overlay .overlay-content { + background-color: #000; + padding: 50px; + border-radius: 20px; +} + +#openwebrx-digimode-canvas-container +{ + margin: -10px -10px -10px -10px; + border-radius: 15px; + height: 200px; + background-color: #333; + position: relative; + overflow: hidden; +} + +#openwebrx-digimode-canvas-container canvas +{ + position: absolute; + top: 0; + pointer-events: none; + transition: width 500ms, left 500ms; + will-change: transform; +} + +.openwebrx-panel select, +.openwebrx-panel input, +.openwebrx-dialog select, +.openwebrx-dialog input { + border-radius: 5px; + background-color: #373737; + color: White; + font-weight: normal; + font-size: 13pt; + margin-right: 1px; + background:linear-gradient(#373737, #4F4F4F); + border-color: transparent; + border-width: 0px; +} + +@supports(-moz-appearance: none) { + .openwebrx-panel select, + .openwebrx-dialog select { + -moz-appearance: none; + background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%20%20xmlns%3Av%3D%22https%3A%2F%2Fvecta.io%2Fnano%22%3E%3Cpath%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8s-1.9-9.2-5.5-12.8z%22%20fill%3D%22%23fff%22%2F%3E%3C%2Fsvg%3E'), + linear-gradient(#373737, #4F4F4F); + background-repeat: no-repeat, repeat; + background-position: right .3em top 50%, 0 0; + background-size: .65em auto, 100%; + } + + .openwebrx-panel .input-group select, + .openwebrx-dialog .input-group select { + padding-right: 1em; + } +} + +.openwebrx-panel select option, +.openwebrx-dialog select option { + border-width: 0px; + background-color: #373737; + color: White; +} + +.openwebrx-secondary-demod-listbox { + width: 173px; + height: 27px; + padding-left:3px; + flex: 4; +} + +#openwebrx-sdr-profiles-listbox { + width: 100%; + font-size: 10pt; + height: 27px; +} + +#openwebrx-cursor-blink +{ + animation: cursor-blink 1s infinite; + /*animation: cursor-3d 2s infinite;*/ + animation-timing-function: linear; + animation-direction: alternate; + height: 1em; + width: 8px; + background-color: White; + display: inline-block; + position: relative; + top: 1px; + /*perspective: 60px;*/ + +} + +@keyframes cursor-blink +{ + 0%{ opacity: 0; } + 50% { opacity: 1; } + 100%{ opacity: 0; } +} + +@keyframes cursor-3d +{ + 0%{ transform: rotateX(0deg) rotateX(Ydeg); } + 50% { transform: rotateX(180deg) rotateY(360deg); opacity: 0.1; } + 100%{ transform: rotateX(360deg) rotateY(720deg); } +} + +#openwebrx-digimode-content +{ + word-wrap: break-word; + position: absolute; + bottom: 0; + width: 100%; +} + +#openwebrx-digimode-content-container +{ + overflow-y: hidden; + display: none; + height: 50px; + position: relative; +} + +#openwebrx-digimode-content-container .gradient +{ + width: 100%; + height: 20px; + background: linear-gradient(to top, rgba(87,87,87,0) 0%,rgba(87,87,87,1) 100%); + position: absolute; + top: 0; + z-index: 10; +} + +#openwebrx-digimode-select-channel +{ + transition: all 500ms; + background-color: Yellow; + display: none; + position: absolute; + pointer-events: none; + height: 100%; + width: 0; + top: 0; + left: 0; + opacity: 0.7; + border-style: solid; + border-width: 0; + border-color: Red; +} + +.openwebrx-meta-panel { + display: flex; + flex-direction: row; + gap: 10px; + /* compatibility with iOS 14.2 */ + flex: 0 0 auto; +} + +.openwebrx-meta-slot { + flex: 1; + width: 145px; + height: 196px; + + background-color: #676767; + padding: 2px 0; + color: #333; + + text-align: center; + + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; +} + +.openwebrx-meta-slot > * { + flex: 1 0 0; + line-height: 1.2em; +} + +.openwebrx-meta-slot, .openwebrx-meta-slot .mute { + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; +} + +.openwebrx-meta-slot .mute { + display: none; + cursor: pointer; + + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + background-color: rgba(0,0,0,.3); +} + +.openwebrx-meta-slot .mute svg { + position: absolute; + top: 50%; + left: 0; + transform: translate(0, -50%); +} + +.openwebrx-meta-slot.muted .mute { + display: block; +} + +.openwebrx-meta-slot.active { + background-color: #95bbdf; +} + +.openwebrx-meta-slot.sync .openwebrx-dmr-slot:before { + content:""; + display: inline-block; + margin: 0 5px; + width: 12px; + height: 12px; + background-color: #ABFF00; + border-radius: 50%; + box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 7px 1px, inset #304701 0 -1px 9px, #89FF00 0 2px 12px; +} + +.openwebrx-meta-slot .openwebrx-meta-user-image { + flex: 0 1 100%; + background-position: center; + background-repeat: no-repeat; + line-height: 0; + overflow: hidden; +} + +.openwebrx-meta-slot .openwebrx-meta-user-image img { + max-width: 100%; + max-height: 100%; + display: none; +} + +.openwebrx-meta-slot.active.direct .openwebrx-meta-user-image .directcall, +.openwebrx-meta-slot.active.individual .openwebrx-meta-user-image .directcall, +#openwebrx-panel-metadata-ysf .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall, +#openwebrx-panel-metadata-dstar .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall, +#openwebrx-panel-metadata-m17 .openwebrx-meta-slot.active .openwebrx-meta-user-image .directcall { + display: initial; +} + +.openwebrx-meta-slot.active.group .openwebrx-meta-user-image .groupcall, +.openwebrx-meta-slot.active.conference .openwebrx-meta-user-image .groupcall { + display: initial; +} + +.openwebrx-meta-slot.group .openwebrx-dmr-target:not(:empty):before { + content: "Talkgroup: "; +} + +.openwebrx-meta-slot.direct .openwebrx-dmr-target:not(:empty):before { + content: "Direct: "; +} + +.openwebrx-dmr-timeslot-panel * { + cursor: pointer; + user-select: none; +} + +.openwebrx-ysf-mode:not(:empty):before { + content: "Mode: "; +} + +.openwebrx-ysf-up:not(:empty):before { + content: "Up: "; +} + +.openwebrx-ysf-down:not(:empty):before { + content: "Down: "; +} + +.openwebrx-m17-source:not(:empty):before { + content: "SRC: "; +} + +.openwebrx-m17-destination:not(:empty):before { + content: "DEST: "; +} + +.openwebrx-dstar-yourcall:not(:empty):before { + content: "UR: "; +} + +.openwebrx-dstar-departure:not(:empty):before { + content: "RPT1: "; +} + +.openwebrx-dstar-destination:not(:empty):before { + content: "RPT2: "; +} + +.openwebrx-meta-slot.individual .openwebrx-nxdn-destination:not(:empty):before { + content: "Direct: "; +} + +.openwebrx-meta-slot.conference .openwebrx-nxdn-destination:not(:empty):before { + content: "Conference: "; +} + +.openwebrx-maps-pin svg { + width: 15px; + height: 15px; + vertical-align: middle; +} + +.openwebrx-message-panel { + min-height: 180px; + position: relative; +} + +.openwebrx-message-panel#openwebrx-panel-adsb-message { + min-height: 380px; +} + +.openwebrx-message-panel table { + display: block; + overflow: auto; + height: 100%; + width: 100%; +} + +.openwebrx-message-panel th, +.openwebrx-message-panel td { + min-width: 50px; + text-align: left; + vertical-align: top; + padding: 1px 3px; +} + +.openwebrx-message-panel th { + position: sticky; + top: 0; + background-color: #575757; +} + +.openwebrx-message-panel h4 { + margin: 0 0 .25em; +} + +.openwebrx-message-panel .acars-message { + white-space: pre; + font-family: roboto-mono, monospace; +} + +#openwebrx-panel-wsjt-message .message { + width: 380px; +} + +#openwebrx-panel-wsjt-message .decimal { + text-align: right; + width: 35px; +} + +#openwebrx-panel-wsjt-message .decimal.freq { + width: 70px; +} + +#openwebrx-panel-js8-message .message { + width: 465px; + max-width: 465px; +} + +#openwebrx-panel-js8-message td.message { + white-space: nowrap; + overflow: hidden; + display: flex; + flex-direction: row-reverse; +} + +#openwebrx-panel-js8-message .message div { + flex: 1; +} + +#openwebrx-panel-js8-message .decimal { + text-align: right; + width: 35px; +} + +#openwebrx-panel-js8-message .decimal.freq { + width: 70px; +} + +#openwebrx-panel-packet-message .message { + width: 410px; + max-width: 410px; +} + +#openwebrx-panel-packet-message .callsign { + width: 80px; +} + +#openwebrx-panel-packet-message .coord { + width: 40px; + text-align: center; +} + +#openwebrx-panel-pocsag-message .address { + width: 100px; +} + +#openwebrx-panel-pocsag-message .message { + width: 486px; + max-width: 486px; + white-space: pre; +} + +.aprs-symbol { + display: inline-block; + width: 15px; + height: 15px; + background-size: 240px 90px; +} + +.aprs-symboltable-normal { + background-image: url(../../aprs-symbols/aprs-symbols-24-0.png) +} + +.aprs-symboltable-alternate { + background-image: url(../../aprs-symbols/aprs-symbols-24-1.png) +} + +.aprs-symboltable-overlay { + background-image: url(../../aprs-symbols/aprs-symbols-24-2.png) +} + +.openwebrx-dialog { + background-color: #575757; + padding: 10px; + color: white; + position: fixed; + font-size: 10pt; + border-radius: 15px; + -moz-border-radius: 15px; + position: fixed; + left: 50%; + top: 50%; + transform: translate(-50%, 0); +} + +.openwebrx-dialog .form-field { + padding: 5px; + display: flex; + flex-direction: row; +} + +.openwebrx-dialog .form-field:first-child { + padding-top: 0; +} + +.openwebrx-dialog label { + display: inline-block; + flex-grow: 0; + width: 70px; + padding-right: 20px; + margin-top: auto; + margin-bottom: auto; +} + +.openwebrx-dialog .form-field input, +.openwebrx-dialog .form-field select { + flex-grow: 1; + height: 27px; +} + +.openwebrx-dialog .form-field input { + padding: 0 5px; +} + +.openwebrx-dialog .buttons { + text-align: right; + padding: 5px 5px 0; + border-top: 1px solid #666; +} + +.openwebrx-dialog .buttons .openwebrx-button { + font-size: 12pt; + min-width: 50px; + text-align: center; + padding: 5px 10px; +} + +#openwebrx-panel-digimodes[data-mode^="bpsk"] #openwebrx-digimode-content-container, +#openwebrx-panel-digimodes[data-mode^="rtty"] #openwebrx-digimode-content-container, +#openwebrx-panel-digimodes[data-mode^="bpsk"] #openwebrx-digimode-select-channel, +#openwebrx-panel-digimodes[data-mode^="rtty"] #openwebrx-digimode-select-channel +{ + display: block; +} + +#openwebrx-panel-digimodes[data-mode^="bpsk"] #openwebrx-digimode-canvas-container, +#openwebrx-panel-digimodes[data-mode^="rtty"] #openwebrx-digimode-canvas-container +{ + height: 150px; + margin-bottom: 0; +} + +.openwebrx-zoom-button svg { + height: 27px; +} + +.openwebrx-slider-button svg { + position:relative; + top: 1px; + height: 14px; +} + +.openwebrx-mute-button svg.muted { + display: none; +} + +.openwebrx-mute-button.muted svg.muted { + display: initial; +} + +.openwebrx-mute-button.muted svg.unmuted { + display: none; +} + +.bookmark .bookmark-actions .openwebrx-button svg { + height: 14px; +} + +#openwebrx-waterfall-colors-auto .continuous { + display: none; +} + +#openwebrx-waterfall-colors-auto.highlighted .continuous { + display: initial; +} + +#openwebrx-waterfall-colors-auto.highlighted .auto { + display: none; +} + +.openwebrx-waterfall-container { + flex-grow: 1; + display: flex; + flex-direction: column; + position: relative; +} + +.openwebrx-waterfall-container > * { + flex: 0 0 auto; +} + +#openwebrx-panel-metadata-wfm { + width: 300px; + max-height: 300px; +} + +.rds-container { + width: 100%; + text-align: center; + overflow: hidden auto; +} + +.rds-container > *, .rds-radiotext-plus > * { + margin: 2px 0; +} + +.rds-container .rds-stationname { + font-family: roboto-mono; + font-size: 18pt; + padding: 10px 0; +} + +.rds-container .rds-stationname, +.rds-container .rds-identifier, +.rds-container .rds-prog_type { + min-height: 1lh; +} + +.rds-container .rds-radiotext-plus .rds-rtplus-item:not(:empty):before { + content: "♫ "; +} + +.rds-container .rds-radiotext-plus .rds-rtplus-programme:not(:empty):before { + content: "📅 "; +} + +.rds-container .rds-radiotext-plus ul.rds-rtplus-news { + list-style-type: "📰 "; + padding-left: 1.5lh; +} + +.rds-container .rds-radiotext-plus .rds-rtplus-weather:not(:empty):before { + content: "⛅ "; +} + +.rds-container .rds-radiotext-plus .rds-rtplus-homepage:not(:empty):before { + content: "🔗 "; +} + +#openwebrx-panel-metadata-dab { + width: 300px; +} + +#openwebrx-panel-metadata-dab .dab-container { + width: 100%; +} + +.dab-container > * { + margin: 2px 0; + text-align: center; + overflow: hidden auto; +} + +.dab-container label { + display: block; + margin: 5px 0; +} + +.dab-container select#dab-service-id { + width: 100%; + padding: 3px; +} + +.dab-container .dab-ensemble-id:not(:empty):before { + content: "Ensemble ID: "; +} + +.dab-container .dab-ensemble-label:not(:empty):before { + content: "Ensemble: "; +} + +.under-construction { + background-color: #ffd914; + color: #111111; + text-align: center; +} + +.under-construction h4 { + font-size: 15pt; +} + +.under-construction h4 { + margin: 0 +} + +.under-construction p { + margin: 5px 0; +} + +.under-construction-description { + max-height: 0; + overflow: hidden; + transition: max-height .2s ease-in-out; +} + +.under-construction:hover .under-construction-description { + max-height: 500px; +} diff --git a/htdocs/favicon.ico b/htdocs/favicon.ico index f8c9a2ae9..6a07f1b1e 100644 Binary files a/htdocs/favicon.ico and b/htdocs/favicon.ico differ diff --git a/htdocs/features.html b/htdocs/features.html new file mode 100644 index 000000000..53099b6d0 --- /dev/null +++ b/htdocs/features.html @@ -0,0 +1,25 @@ + + OpenWebRX Feature report + + + + + + + + + ${header} +
+ ${breadcrumb} +

OpenWebRX Feature Report

+ + + + + + + +
FeatureRequirementDescriptionAvailable
+ ${breadcrumb} +
+ \ No newline at end of file diff --git a/htdocs/features.js b/htdocs/features.js new file mode 100644 index 000000000..0add3b437 --- /dev/null +++ b/htdocs/features.js @@ -0,0 +1,23 @@ +$(function(){ + var converter = new showdown.Converter({openLinksInNewWindow: true}); + $.ajax('api/features').done(function(data){ + var $table = $('table.features'); + $.each(data, function(name, details) { + var requirements = $.map(details.requirements, function(r, name){ + return '' + + '' + + '' + name + '' + + '' + converter.makeHtml(r.description) + '' + + '' + (r.available ? 'YES' : 'NO') + '' + + ''; + }); + $table.append( + '' + + '' + name + '' + + '' + (details.available ? 'YES' : 'NO') + '' + + '' + + requirements.join("") + ); + }) + }); +}); diff --git a/htdocs/fonts/RobotoMono-Regular.ttf b/htdocs/fonts/RobotoMono-Regular.ttf new file mode 100644 index 000000000..7c4ce36a4 Binary files /dev/null and b/htdocs/fonts/RobotoMono-Regular.ttf differ diff --git a/htdocs/fonts/RobotoMono-Regular.woff b/htdocs/fonts/RobotoMono-Regular.woff new file mode 100644 index 000000000..1b805eaf2 Binary files /dev/null and b/htdocs/fonts/RobotoMono-Regular.woff differ diff --git a/htdocs/fonts/RobotoMono-Regular.woff2 b/htdocs/fonts/RobotoMono-Regular.woff2 new file mode 100644 index 000000000..dab25851b Binary files /dev/null and b/htdocs/fonts/RobotoMono-Regular.woff2 differ diff --git a/htdocs/gfx/favicon128.png b/htdocs/gfx/favicon128.png new file mode 100644 index 000000000..ad42441f1 Binary files /dev/null and b/htdocs/gfx/favicon128.png differ diff --git a/htdocs/gfx/favicon32.png b/htdocs/gfx/favicon32.png new file mode 100644 index 000000000..2c534af55 Binary files /dev/null and b/htdocs/gfx/favicon32.png differ diff --git a/htdocs/gfx/favicon44.png b/htdocs/gfx/favicon44.png new file mode 100644 index 000000000..d21f326e7 Binary files /dev/null and b/htdocs/gfx/favicon44.png differ diff --git a/htdocs/gfx/favicon64.png b/htdocs/gfx/favicon64.png new file mode 100644 index 000000000..b08e03fc7 Binary files /dev/null and b/htdocs/gfx/favicon64.png differ diff --git a/htdocs/gfx/favicon96.png b/htdocs/gfx/favicon96.png new file mode 100644 index 000000000..ee9412988 Binary files /dev/null and b/htdocs/gfx/favicon96.png differ diff --git a/htdocs/gfx/font-expletus-sans/ExpletusSans-Medium.ttf b/htdocs/gfx/font-expletus-sans/ExpletusSans-Medium.ttf deleted file mode 100644 index dfc87f871..000000000 Binary files a/htdocs/gfx/font-expletus-sans/ExpletusSans-Medium.ttf and /dev/null differ diff --git a/htdocs/gfx/font-expletus-sans/OFL.txt b/htdocs/gfx/font-expletus-sans/OFL.txt deleted file mode 100644 index 5979654ee..000000000 --- a/htdocs/gfx/font-expletus-sans/OFL.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright (c) 2011, Jasper de Waard (jasper@designtown.nl), -with Reserved Font Name "Expletus Sans". -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/htdocs/gfx/openwebrx-3d-spectrum.png b/htdocs/gfx/openwebrx-3d-spectrum.png deleted file mode 100644 index 06ad49502..000000000 Binary files a/htdocs/gfx/openwebrx-3d-spectrum.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-avatar-background.png b/htdocs/gfx/openwebrx-avatar-background.png deleted file mode 100644 index e52cb0b95..000000000 Binary files a/htdocs/gfx/openwebrx-avatar-background.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-avatar.png b/htdocs/gfx/openwebrx-avatar.png index 7e9736fdb..fc2052912 100644 Binary files a/htdocs/gfx/openwebrx-avatar.png and b/htdocs/gfx/openwebrx-avatar.png differ diff --git a/htdocs/gfx/openwebrx-background-cool-blue.png b/htdocs/gfx/openwebrx-background-cool-blue.png index 7430bd8a8..236b366b3 100644 Binary files a/htdocs/gfx/openwebrx-background-cool-blue.png and b/htdocs/gfx/openwebrx-background-cool-blue.png differ diff --git a/htdocs/gfx/openwebrx-background-cool-blue.webp b/htdocs/gfx/openwebrx-background-cool-blue.webp new file mode 100644 index 000000000..51f7852bf Binary files /dev/null and b/htdocs/gfx/openwebrx-background-cool-blue.webp differ diff --git a/htdocs/gfx/openwebrx-background-lingrad.png b/htdocs/gfx/openwebrx-background-lingrad.png deleted file mode 100644 index 48537f7a0..000000000 Binary files a/htdocs/gfx/openwebrx-background-lingrad.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-directcall.svg b/htdocs/gfx/openwebrx-directcall.svg new file mode 100644 index 000000000..344011213 --- /dev/null +++ b/htdocs/gfx/openwebrx-directcall.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/htdocs/gfx/openwebrx-groupcall.svg b/htdocs/gfx/openwebrx-groupcall.svg new file mode 100644 index 000000000..5083a5763 --- /dev/null +++ b/htdocs/gfx/openwebrx-groupcall.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/htdocs/gfx/openwebrx-ha5kfu-top-logo.png b/htdocs/gfx/openwebrx-ha5kfu-top-logo.png deleted file mode 100644 index 2686eef0a..000000000 Binary files a/htdocs/gfx/openwebrx-ha5kfu-top-logo.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-logo-big.png b/htdocs/gfx/openwebrx-logo-big.png deleted file mode 100644 index dcafb2ee3..000000000 Binary files a/htdocs/gfx/openwebrx-logo-big.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-panel-log.png b/htdocs/gfx/openwebrx-panel-log.png deleted file mode 100644 index 58e6fd5f0..000000000 Binary files a/htdocs/gfx/openwebrx-panel-log.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-panel-receiver.png b/htdocs/gfx/openwebrx-panel-receiver.png deleted file mode 100644 index 5c80c3b64..000000000 Binary files a/htdocs/gfx/openwebrx-panel-receiver.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-panel-status.png b/htdocs/gfx/openwebrx-panel-status.png deleted file mode 100644 index 064b54f71..000000000 Binary files a/htdocs/gfx/openwebrx-panel-status.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-play-button.png b/htdocs/gfx/openwebrx-play-button.png deleted file mode 100644 index 4a0652178..000000000 Binary files a/htdocs/gfx/openwebrx-play-button.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-rx-details-arrow-up.png b/htdocs/gfx/openwebrx-rx-details-arrow-up.png deleted file mode 100644 index 0baccd041..000000000 Binary files a/htdocs/gfx/openwebrx-rx-details-arrow-up.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-rx-details-arrow.png b/htdocs/gfx/openwebrx-rx-details-arrow.png deleted file mode 100644 index 9995118f0..000000000 Binary files a/htdocs/gfx/openwebrx-rx-details-arrow.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-scale-background.png b/htdocs/gfx/openwebrx-scale-background.png index 7fbb4d249..91453c589 100644 Binary files a/htdocs/gfx/openwebrx-scale-background.png and b/htdocs/gfx/openwebrx-scale-background.png differ diff --git a/htdocs/gfx/openwebrx-speaker-muted.png b/htdocs/gfx/openwebrx-speaker-muted.png deleted file mode 100644 index 0d5457039..000000000 Binary files a/htdocs/gfx/openwebrx-speaker-muted.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-speaker.png b/htdocs/gfx/openwebrx-speaker.png deleted file mode 100644 index 6c88e23c4..000000000 Binary files a/htdocs/gfx/openwebrx-speaker.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-squelch-button.png b/htdocs/gfx/openwebrx-squelch-button.png deleted file mode 100644 index f67177c5a..000000000 Binary files a/htdocs/gfx/openwebrx-squelch-button.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-top-logo.png b/htdocs/gfx/openwebrx-top-logo.png deleted file mode 100644 index 477242524..000000000 Binary files a/htdocs/gfx/openwebrx-top-logo.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-top-photo.jpg b/htdocs/gfx/openwebrx-top-photo.jpg index cf521c752..afc8e7e02 100644 Binary files a/htdocs/gfx/openwebrx-top-photo.jpg and b/htdocs/gfx/openwebrx-top-photo.jpg differ diff --git a/htdocs/gfx/openwebrx-waterfall-auto.png b/htdocs/gfx/openwebrx-waterfall-auto.png deleted file mode 100644 index 7e41302ac..000000000 Binary files a/htdocs/gfx/openwebrx-waterfall-auto.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-waterfall-default.png b/htdocs/gfx/openwebrx-waterfall-default.png deleted file mode 100644 index 1cd39fa59..000000000 Binary files a/htdocs/gfx/openwebrx-waterfall-default.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-zoom-in-total.png b/htdocs/gfx/openwebrx-zoom-in-total.png deleted file mode 100644 index 3646a377a..000000000 Binary files a/htdocs/gfx/openwebrx-zoom-in-total.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-zoom-in.png b/htdocs/gfx/openwebrx-zoom-in.png deleted file mode 100644 index c8df0c8ac..000000000 Binary files a/htdocs/gfx/openwebrx-zoom-in.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-zoom-out-total.png b/htdocs/gfx/openwebrx-zoom-out-total.png deleted file mode 100644 index d83b61df4..000000000 Binary files a/htdocs/gfx/openwebrx-zoom-out-total.png and /dev/null differ diff --git a/htdocs/gfx/openwebrx-zoom-out.png b/htdocs/gfx/openwebrx-zoom-out.png deleted file mode 100644 index 60cd91209..000000000 Binary files a/htdocs/gfx/openwebrx-zoom-out.png and /dev/null differ diff --git a/htdocs/gfx/svg-defs.svg b/htdocs/gfx/svg-defs.svg new file mode 100644 index 000000000..251b05170 --- /dev/null +++ b/htdocs/gfx/svg-defs.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/htdocs/inactive.html b/htdocs/inactive.html deleted file mode 100644 index c7214c5c8..000000000 --- a/htdocs/inactive.html +++ /dev/null @@ -1,85 +0,0 @@ - - -OpenWebRX - - - - -
- -
- Sorry, the receiver is inactive due to internal error. -
-
- - - diff --git a/htdocs/include/header.include.html b/htdocs/include/header.include.html new file mode 100644 index 000000000..203d6e41b --- /dev/null +++ b/htdocs/include/header.include.html @@ -0,0 +1,25 @@ +
+
+ + Receiver avatar +
+

${receiver_name}

+
${receiver_location} | Loc: ${locator}, ASL: ${receiver_asl} m
+
+
+

Status
+

Log
+

Receiver
+
Map
+
Settings
+
+
+
+
${photo_title}
+
${photo_desc}
+
+ + + + +
diff --git a/htdocs/index.html b/htdocs/index.html new file mode 100644 index 000000000..1e1c4c438 --- /dev/null +++ b/htdocs/index.html @@ -0,0 +1,269 @@ + + + + + OpenWebRX | Open Source SDR Web App for Everyone! + + + + + + + + + + + + + + + + +
+ ${header} +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+

Under construction

+
+

This receiver is running a development version of OpenWebRX. Additional features may be available, but there may also be bugs.

+

Issue tracker | Mailing list

+
+
+ + + + + + + + + + + + + + + + +
+
+
+
OpenWebRX client log
+
+ Author contact: Jakob Ketterl, DD5JFK | + OpenWebRX homepage +
+
Support and information: Groups.io Mailinglist
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+ + +
+ +
+ + +
+ +
+
+
+ +
+ +
+ +
+ +
+
+
+
+
+
+
0 dB
+
+
+
+
+
+
+
+
+
+ +
+
+ + + + diff --git a/htdocs/index.wrx b/htdocs/index.wrx deleted file mode 100644 index 5e0050602..000000000 --- a/htdocs/index.wrx +++ /dev/null @@ -1,180 +0,0 @@ - - - - - OpenWebRX | Open Source SDR Web App for Everyone! - - - - - - - - - - - -
-
-
- -
%[RX_PHOTO_TITLE]
-
%[RX_PHOTO_DESC]
-
-
-
- - - - -
%[RX_TITLE]
-
%[RX_LOC] | Loc: %[RX_QRA], ASL: %[RX_ASL] m, [maps]
-
- - -
-
-
    -

  • Status
  • -

  • Log
  • -

  • Receiver
  • -
-
-
-
-
-
- -
-
-
-
- -
-
-
-
---.--- MHz
-
---.--- MHz
-
-
FM
-
AM
-
LSB
-
USB
-
CW
-
-
-
DIG
- -
-
-
- -
- -
-
-
- -
- -
-
-
-
-
-
-
-
0 dB
-
-
-
-
-
-
-
-
-
-
-
OpenWebRX client log
- Author: András Retzler, HA7ILM
You can support OpenWebRX development via PayPal!
-
-
-
-
-
-
Audio buffer [0 ms]
-
Audio output [0 sps]
-
Audio stream [0 kbps]
-
Network usage [0 kbps]
-
Server CPU [0%]
-
Clients [1]
-
-
- Under construction -
We're working on the code right now, so the application might fail. -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -

Start OpenWebRX -
-
- - diff --git a/htdocs/jquery.nanoscroller.js b/htdocs/jquery.nanoscroller.js deleted file mode 100644 index edcbfaa44..000000000 --- a/htdocs/jquery.nanoscroller.js +++ /dev/null @@ -1,1000 +0,0 @@ -/*! nanoScrollerJS - v0.8.7 - 2015 -* http://jamesflorentino.github.com/nanoScrollerJS/ -* Copyright (c) 2015 James Florentino; Licensed MIT */ -(function(factory) { - if (typeof define === 'function' && define.amd) { - return define(['jquery'], function($) { - return factory($, window, document); - }); - } else if (typeof exports === 'object') { - return module.exports = factory(require('jquery'), window, document); - } else { - return factory(jQuery, window, document); - } -})(function($, window, document) { - "use strict"; - var BROWSER_IS_IE7, BROWSER_SCROLLBAR_WIDTH, DOMSCROLL, DOWN, DRAG, ENTER, KEYDOWN, KEYUP, MOUSEDOWN, MOUSEENTER, MOUSEMOVE, MOUSEUP, MOUSEWHEEL, NanoScroll, PANEDOWN, RESIZE, SCROLL, SCROLLBAR, TOUCHMOVE, UP, WHEEL, cAF, defaults, getBrowserScrollbarWidth, hasTransform, isFFWithBuggyScrollbar, rAF, transform, _elementStyle, _prefixStyle, _vendor; - defaults = { - - /** - a classname for the pane element. - @property paneClass - @type String - @default 'nano-pane' - */ - paneClass: 'nano-pane', - - /** - a classname for the slider element. - @property sliderClass - @type String - @default 'nano-slider' - */ - sliderClass: 'nano-slider', - - /** - a classname for the content element. - @property contentClass - @type String - @default 'nano-content' - */ - contentClass: 'nano-content', - - /** - a classname for enabled mode - @property enabledClass - @type String - @default 'has-scrollbar' - */ - enabledClass: 'has-scrollbar', - - /** - a classname for flashed mode - @property flashedClass - @type String - @default 'flashed' - */ - flashedClass: 'flashed', - - /** - a classname for active mode - @property activeClass - @type String - @default 'active' - */ - activeClass: 'active', - - /** - a setting to enable native scrolling in iOS devices. - @property iOSNativeScrolling - @type Boolean - @default false - */ - iOSNativeScrolling: false, - - /** - a setting to prevent the rest of the page being - scrolled when user scrolls the `.content` element. - @property preventPageScrolling - @type Boolean - @default false - */ - preventPageScrolling: false, - - /** - a setting to disable binding to the resize event. - @property disableResize - @type Boolean - @default false - */ - disableResize: false, - - /** - a setting to make the scrollbar always visible. - @property alwaysVisible - @type Boolean - @default false - */ - alwaysVisible: false, - - /** - a default timeout for the `flash()` method. - @property flashDelay - @type Number - @default 1500 - */ - flashDelay: 1500, - - /** - a minimum height for the `.slider` element. - @property sliderMinHeight - @type Number - @default 20 - */ - sliderMinHeight: 20, - - /** - a maximum height for the `.slider` element. - @property sliderMaxHeight - @type Number - @default null - */ - sliderMaxHeight: null, - - /** - an alternate document context. - @property documentContext - @type Document - @default null - */ - documentContext: null, - - /** - an alternate window context. - @property windowContext - @type Window - @default null - */ - windowContext: null - }; - - /** - @property SCROLLBAR - @type String - @static - @final - @private - */ - SCROLLBAR = 'scrollbar'; - - /** - @property SCROLL - @type String - @static - @final - @private - */ - SCROLL = 'scroll'; - - /** - @property MOUSEDOWN - @type String - @final - @private - */ - MOUSEDOWN = 'mousedown'; - - /** - @property MOUSEENTER - @type String - @final - @private - */ - MOUSEENTER = 'mouseenter'; - - /** - @property MOUSEMOVE - @type String - @static - @final - @private - */ - MOUSEMOVE = 'mousemove'; - - /** - @property MOUSEWHEEL - @type String - @final - @private - */ - MOUSEWHEEL = 'mousewheel'; - - /** - @property MOUSEUP - @type String - @static - @final - @private - */ - MOUSEUP = 'mouseup'; - - /** - @property RESIZE - @type String - @final - @private - */ - RESIZE = 'resize'; - - /** - @property DRAG - @type String - @static - @final - @private - */ - DRAG = 'drag'; - - /** - @property ENTER - @type String - @static - @final - @private - */ - ENTER = 'enter'; - - /** - @property UP - @type String - @static - @final - @private - */ - UP = 'up'; - - /** - @property PANEDOWN - @type String - @static - @final - @private - */ - PANEDOWN = 'panedown'; - - /** - @property DOMSCROLL - @type String - @static - @final - @private - */ - DOMSCROLL = 'DOMMouseScroll'; - - /** - @property DOWN - @type String - @static - @final - @private - */ - DOWN = 'down'; - - /** - @property WHEEL - @type String - @static - @final - @private - */ - WHEEL = 'wheel'; - - /** - @property KEYDOWN - @type String - @static - @final - @private - */ - KEYDOWN = 'keydown'; - - /** - @property KEYUP - @type String - @static - @final - @private - */ - KEYUP = 'keyup'; - - /** - @property TOUCHMOVE - @type String - @static - @final - @private - */ - TOUCHMOVE = 'touchmove'; - - /** - @property BROWSER_IS_IE7 - @type Boolean - @static - @final - @private - */ - BROWSER_IS_IE7 = window.navigator.appName === 'Microsoft Internet Explorer' && /msie 7./i.test(window.navigator.appVersion) && window.ActiveXObject; - - /** - @property BROWSER_SCROLLBAR_WIDTH - @type Number - @static - @default null - @private - */ - BROWSER_SCROLLBAR_WIDTH = null; - rAF = window.requestAnimationFrame; - cAF = window.cancelAnimationFrame; - _elementStyle = document.createElement('div').style; - _vendor = (function() { - var i, transform, vendor, vendors, _i, _len; - vendors = ['t', 'webkitT', 'MozT', 'msT', 'OT']; - for (i = _i = 0, _len = vendors.length; _i < _len; i = ++_i) { - vendor = vendors[i]; - transform = vendors[i] + 'ransform'; - if (transform in _elementStyle) { - return vendors[i].substr(0, vendors[i].length - 1); - } - } - return false; - })(); - _prefixStyle = function(style) { - if (_vendor === false) { - return false; - } - if (_vendor === '') { - return style; - } - return _vendor + style.charAt(0).toUpperCase() + style.substr(1); - }; - transform = _prefixStyle('transform'); - hasTransform = transform !== false; - - /** - Returns browser's native scrollbar width - @method getBrowserScrollbarWidth - @return {Number} the scrollbar width in pixels - @static - @private - */ - getBrowserScrollbarWidth = function() { - var outer, outerStyle, scrollbarWidth; - outer = document.createElement('div'); - outerStyle = outer.style; - outerStyle.position = 'absolute'; - outerStyle.width = '100px'; - outerStyle.height = '100px'; - outerStyle.overflow = SCROLL; - outerStyle.top = '-9999px'; - document.body.appendChild(outer); - scrollbarWidth = outer.offsetWidth - outer.clientWidth; - document.body.removeChild(outer); - return scrollbarWidth; - }; - isFFWithBuggyScrollbar = function() { - var isOSXFF, ua, version; - ua = window.navigator.userAgent; - isOSXFF = /(?=.+Mac OS X)(?=.+Firefox)/.test(ua); - if (!isOSXFF) { - return false; - } - version = /Firefox\/\d{2}\./.exec(ua); - if (version) { - version = version[0].replace(/\D+/g, ''); - } - return isOSXFF && +version > 23; - }; - - /** - @class NanoScroll - @param element {HTMLElement|Node} the main element - @param options {Object} nanoScroller's options - @constructor - */ - NanoScroll = (function() { - function NanoScroll(el, options) { - this.el = el; - this.options = options; - BROWSER_SCROLLBAR_WIDTH || (BROWSER_SCROLLBAR_WIDTH = getBrowserScrollbarWidth()); - this.$el = $(this.el); - this.doc = $(this.options.documentContext || document); - this.win = $(this.options.windowContext || window); - this.body = this.doc.find('body'); - this.$content = this.$el.children("." + this.options.contentClass); - this.$content.attr('tabindex', this.options.tabIndex || 0); - this.content = this.$content[0]; - this.previousPosition = 0; - if (this.options.iOSNativeScrolling && (this.el.style.WebkitOverflowScrolling != null)) { - this.nativeScrolling(); - } else { - this.generate(); - } - this.createEvents(); - this.addEvents(); - this.reset(); - } - - - /** - Prevents the rest of the page being scrolled - when user scrolls the `.nano-content` element. - @method preventScrolling - @param event {Event} - @param direction {String} Scroll direction (up or down) - @private - */ - - NanoScroll.prototype.preventScrolling = function(e, direction) { - if (!this.isActive) { - return; - } - if (e.type === DOMSCROLL) { - if (direction === DOWN && e.originalEvent.detail > 0 || direction === UP && e.originalEvent.detail < 0) { - e.preventDefault(); - } - } else if (e.type === MOUSEWHEEL) { - if (!e.originalEvent || !e.originalEvent.wheelDelta) { - return; - } - if (direction === DOWN && e.originalEvent.wheelDelta < 0 || direction === UP && e.originalEvent.wheelDelta > 0) { - e.preventDefault(); - } - } - }; - - - /** - Enable iOS native scrolling - @method nativeScrolling - @private - */ - - NanoScroll.prototype.nativeScrolling = function() { - this.$content.css({ - WebkitOverflowScrolling: 'touch' - }); - this.iOSNativeScrolling = true; - this.isActive = true; - }; - - - /** - Updates those nanoScroller properties that - are related to current scrollbar position. - @method updateScrollValues - @private - */ - - NanoScroll.prototype.updateScrollValues = function() { - var content, direction; - content = this.content; - this.maxScrollTop = content.scrollHeight - content.clientHeight; - this.prevScrollTop = this.contentScrollTop || 0; - this.contentScrollTop = content.scrollTop; - direction = this.contentScrollTop > this.previousPosition ? "down" : this.contentScrollTop < this.previousPosition ? "up" : "same"; - this.previousPosition = this.contentScrollTop; - if (direction !== "same") { - this.$el.trigger('update', { - position: this.contentScrollTop, - maximum: this.maxScrollTop, - direction: direction - }); - } - if (!this.iOSNativeScrolling) { - this.maxSliderTop = this.paneHeight - this.sliderHeight; - this.sliderTop = this.maxScrollTop === 0 ? 0 : this.contentScrollTop * this.maxSliderTop / this.maxScrollTop; - } - }; - - - /** - Updates CSS styles for current scroll position. - Uses CSS 2d transfroms and `window.requestAnimationFrame` if available. - @method setOnScrollStyles - @private - */ - - NanoScroll.prototype.setOnScrollStyles = function() { - var cssValue; - if (hasTransform) { - cssValue = {}; - cssValue[transform] = "translate(0, " + this.sliderTop + "px)"; - } else { - cssValue = { - top: this.sliderTop - }; - } - if (rAF) { - if (cAF && this.scrollRAF) { - cAF(this.scrollRAF); - } - this.scrollRAF = rAF((function(_this) { - return function() { - _this.scrollRAF = null; - return _this.slider.css(cssValue); - }; - })(this)); - } else { - this.slider.css(cssValue); - } - }; - - - /** - Creates event related methods - @method createEvents - @private - */ - - NanoScroll.prototype.createEvents = function() { - this.events = { - down: (function(_this) { - return function(e) { - _this.isBeingDragged = true; - _this.offsetY = e.pageY - _this.slider.offset().top; - if (!_this.slider.is(e.target)) { - _this.offsetY = 0; - } - _this.pane.addClass(_this.options.activeClass); - _this.doc.bind(MOUSEMOVE, _this.events[DRAG]).bind(MOUSEUP, _this.events[UP]); - _this.body.bind(MOUSEENTER, _this.events[ENTER]); - return false; - }; - })(this), - drag: (function(_this) { - return function(e) { - _this.sliderY = e.pageY - _this.$el.offset().top - _this.paneTop - (_this.offsetY || _this.sliderHeight * 0.5); - _this.scroll(); - if (_this.contentScrollTop >= _this.maxScrollTop && _this.prevScrollTop !== _this.maxScrollTop) { - _this.$el.trigger('scrollend'); - } else if (_this.contentScrollTop === 0 && _this.prevScrollTop !== 0) { - _this.$el.trigger('scrolltop'); - } - return false; - }; - })(this), - up: (function(_this) { - return function(e) { - _this.isBeingDragged = false; - _this.pane.removeClass(_this.options.activeClass); - _this.doc.unbind(MOUSEMOVE, _this.events[DRAG]).unbind(MOUSEUP, _this.events[UP]); - _this.body.unbind(MOUSEENTER, _this.events[ENTER]); - return false; - }; - })(this), - resize: (function(_this) { - return function(e) { - _this.reset(); - }; - })(this), - panedown: (function(_this) { - return function(e) { - _this.sliderY = (e.offsetY || e.originalEvent.layerY) - (_this.sliderHeight * 0.5); - _this.scroll(); - _this.events.down(e); - return false; - }; - })(this), - scroll: (function(_this) { - return function(e) { - _this.updateScrollValues(); - if (_this.isBeingDragged) { - return; - } - if (!_this.iOSNativeScrolling) { - _this.sliderY = _this.sliderTop; - _this.setOnScrollStyles(); - } - if (e == null) { - return; - } - if (_this.contentScrollTop >= _this.maxScrollTop) { - if (_this.options.preventPageScrolling) { - _this.preventScrolling(e, DOWN); - } - if (_this.prevScrollTop !== _this.maxScrollTop) { - _this.$el.trigger('scrollend'); - } - } else if (_this.contentScrollTop === 0) { - if (_this.options.preventPageScrolling) { - _this.preventScrolling(e, UP); - } - if (_this.prevScrollTop !== 0) { - _this.$el.trigger('scrolltop'); - } - } - }; - })(this), - wheel: (function(_this) { - return function(e) { - var delta; - if (e == null) { - return; - } - delta = e.delta || e.wheelDelta || (e.originalEvent && e.originalEvent.wheelDelta) || -e.detail || (e.originalEvent && -e.originalEvent.detail); - if (delta) { - _this.sliderY += -delta / 3; - } - _this.scroll(); - return false; - }; - })(this), - enter: (function(_this) { - return function(e) { - var _ref; - if (!_this.isBeingDragged) { - return; - } - if ((e.buttons || e.which) !== 1) { - return (_ref = _this.events)[UP].apply(_ref, arguments); - } - }; - })(this) - }; - }; - - - /** - Adds event listeners with jQuery. - @method addEvents - @private - */ - - NanoScroll.prototype.addEvents = function() { - var events; - this.removeEvents(); - events = this.events; - if (!this.options.disableResize) { - this.win.bind(RESIZE, events[RESIZE]); - } - if (!this.iOSNativeScrolling) { - this.slider.bind(MOUSEDOWN, events[DOWN]); - this.pane.bind(MOUSEDOWN, events[PANEDOWN]).bind("" + MOUSEWHEEL + " " + DOMSCROLL, events[WHEEL]); - } - this.$content.bind("" + SCROLL + " " + MOUSEWHEEL + " " + DOMSCROLL + " " + TOUCHMOVE, events[SCROLL]); - }; - - - /** - Removes event listeners with jQuery. - @method removeEvents - @private - */ - - NanoScroll.prototype.removeEvents = function() { - var events; - events = this.events; - this.win.unbind(RESIZE, events[RESIZE]); - if (!this.iOSNativeScrolling) { - this.slider.unbind(); - this.pane.unbind(); - } - this.$content.unbind("" + SCROLL + " " + MOUSEWHEEL + " " + DOMSCROLL + " " + TOUCHMOVE, events[SCROLL]); - }; - - - /** - Generates nanoScroller's scrollbar and elements for it. - @method generate - @chainable - @private - */ - - NanoScroll.prototype.generate = function() { - var contentClass, cssRule, currentPadding, options, pane, paneClass, sliderClass; - options = this.options; - paneClass = options.paneClass, sliderClass = options.sliderClass, contentClass = options.contentClass; - if (!(pane = this.$el.children("." + paneClass)).length && !pane.children("." + sliderClass).length) { - this.$el.append("
"); - } - this.pane = this.$el.children("." + paneClass); - this.slider = this.pane.find("." + sliderClass); - if (BROWSER_SCROLLBAR_WIDTH === 0 && isFFWithBuggyScrollbar()) { - currentPadding = window.getComputedStyle(this.content, null).getPropertyValue('padding-right').replace(/[^0-9.]+/g, ''); - cssRule = { - right: -14, - paddingRight: +currentPadding + 14 - }; - } else if (BROWSER_SCROLLBAR_WIDTH) { - cssRule = { - right: -BROWSER_SCROLLBAR_WIDTH - }; - this.$el.addClass(options.enabledClass); - } - if (cssRule != null) { - this.$content.css(cssRule); - } - return this; - }; - - - /** - @method restore - @private - */ - - NanoScroll.prototype.restore = function() { - this.stopped = false; - if (!this.iOSNativeScrolling) { - this.pane.show(); - } - this.addEvents(); - }; - - - /** - Resets nanoScroller's scrollbar. - @method reset - @chainable - @example - $(".nano").nanoScroller(); - */ - - NanoScroll.prototype.reset = function() { - var content, contentHeight, contentPosition, contentStyle, contentStyleOverflowY, paneBottom, paneHeight, paneOuterHeight, paneTop, parentMaxHeight, right, sliderHeight; - if (this.iOSNativeScrolling) { - this.contentHeight = this.content.scrollHeight; - return; - } - if (!this.$el.find("." + this.options.paneClass).length) { - this.generate().stop(); - } - if (this.stopped) { - this.restore(); - } - content = this.content; - contentStyle = content.style; - contentStyleOverflowY = contentStyle.overflowY; - if (BROWSER_IS_IE7) { - this.$content.css({ - height: this.$content.height() - }); - } - contentHeight = content.scrollHeight + BROWSER_SCROLLBAR_WIDTH; - parentMaxHeight = parseInt(this.$el.css("max-height"), 10); - if (parentMaxHeight > 0) { - this.$el.height(""); - this.$el.height(content.scrollHeight > parentMaxHeight ? parentMaxHeight : content.scrollHeight); - } - paneHeight = this.pane.outerHeight(false); - paneTop = parseInt(this.pane.css('top'), 10); - paneBottom = parseInt(this.pane.css('bottom'), 10); - paneOuterHeight = paneHeight + paneTop + paneBottom; - sliderHeight = Math.round(paneOuterHeight / contentHeight * paneHeight); - if (sliderHeight < this.options.sliderMinHeight) { - sliderHeight = this.options.sliderMinHeight; - } else if ((this.options.sliderMaxHeight != null) && sliderHeight > this.options.sliderMaxHeight) { - sliderHeight = this.options.sliderMaxHeight; - } - if (contentStyleOverflowY === SCROLL && contentStyle.overflowX !== SCROLL) { - sliderHeight += BROWSER_SCROLLBAR_WIDTH; - } - this.maxSliderTop = paneOuterHeight - sliderHeight; - this.contentHeight = contentHeight; - this.paneHeight = paneHeight; - this.paneOuterHeight = paneOuterHeight; - this.sliderHeight = sliderHeight; - this.paneTop = paneTop; - this.slider.height(sliderHeight); - this.events.scroll(); - this.pane.show(); - this.isActive = true; - if ((content.scrollHeight === content.clientHeight) || (this.pane.outerHeight(true) >= content.scrollHeight && contentStyleOverflowY !== SCROLL)) { - this.pane.hide(); - this.isActive = false; - } else if (this.el.clientHeight === content.scrollHeight && contentStyleOverflowY === SCROLL) { - this.slider.hide(); - } else { - this.slider.show(); - } - this.pane.css({ - opacity: (this.options.alwaysVisible ? 1 : ''), - visibility: (this.options.alwaysVisible ? 'visible' : '') - }); - contentPosition = this.$content.css('position'); - if (contentPosition === 'static' || contentPosition === 'relative') { - right = parseInt(this.$content.css('right'), 10); - if (right) { - this.$content.css({ - right: '', - marginRight: right - }); - } - } - return this; - }; - - - /** - @method scroll - @private - @example - $(".nano").nanoScroller({ scroll: 'top' }); - */ - - NanoScroll.prototype.scroll = function() { - if (!this.isActive) { - return; - } - this.sliderY = Math.max(0, this.sliderY); - this.sliderY = Math.min(this.maxSliderTop, this.sliderY); - this.$content.scrollTop(this.maxScrollTop * this.sliderY / this.maxSliderTop); - if (!this.iOSNativeScrolling) { - this.updateScrollValues(); - this.setOnScrollStyles(); - } - return this; - }; - - - /** - Scroll at the bottom with an offset value - @method scrollBottom - @param offsetY {Number} - @chainable - @example - $(".nano").nanoScroller({ scrollBottom: value }); - */ - - NanoScroll.prototype.scrollBottom = function(offsetY) { - if (!this.isActive) { - return; - } - this.$content.scrollTop(this.contentHeight - this.$content.height() - offsetY).trigger(MOUSEWHEEL); - this.stop().restore(); - return this; - }; - - - /** - Scroll at the top with an offset value - @method scrollTop - @param offsetY {Number} - @chainable - @example - $(".nano").nanoScroller({ scrollTop: value }); - */ - - NanoScroll.prototype.scrollTop = function(offsetY) { - if (!this.isActive) { - return; - } - this.$content.scrollTop(+offsetY).trigger(MOUSEWHEEL); - this.stop().restore(); - return this; - }; - - - /** - Scroll to an element - @method scrollTo - @param node {Node} A node to scroll to. - @chainable - @example - $(".nano").nanoScroller({ scrollTo: $('#a_node') }); - */ - - NanoScroll.prototype.scrollTo = function(node) { - if (!this.isActive) { - return; - } - this.scrollTop(this.$el.find(node).get(0).offsetTop); - return this; - }; - - - /** - To stop the operation. - This option will tell the plugin to disable all event bindings and hide the gadget scrollbar from the UI. - @method stop - @chainable - @example - $(".nano").nanoScroller({ stop: true }); - */ - - NanoScroll.prototype.stop = function() { - if (cAF && this.scrollRAF) { - cAF(this.scrollRAF); - this.scrollRAF = null; - } - this.stopped = true; - this.removeEvents(); - if (!this.iOSNativeScrolling) { - this.pane.hide(); - } - return this; - }; - - - /** - Destroys nanoScroller and restores browser's native scrollbar. - @method destroy - @chainable - @example - $(".nano").nanoScroller({ destroy: true }); - */ - - NanoScroll.prototype.destroy = function() { - if (!this.stopped) { - this.stop(); - } - if (!this.iOSNativeScrolling && this.pane.length) { - this.pane.remove(); - } - if (BROWSER_IS_IE7) { - this.$content.height(''); - } - this.$content.removeAttr('tabindex'); - if (this.$el.hasClass(this.options.enabledClass)) { - this.$el.removeClass(this.options.enabledClass); - this.$content.css({ - right: '' - }); - } - return this; - }; - - - /** - To flash the scrollbar gadget for an amount of time defined in plugin settings (defaults to 1,5s). - Useful if you want to show the user (e.g. on pageload) that there is more content waiting for him. - @method flash - @chainable - @example - $(".nano").nanoScroller({ flash: true }); - */ - - NanoScroll.prototype.flash = function() { - if (this.iOSNativeScrolling) { - return; - } - if (!this.isActive) { - return; - } - this.reset(); - this.pane.addClass(this.options.flashedClass); - setTimeout((function(_this) { - return function() { - _this.pane.removeClass(_this.options.flashedClass); - }; - })(this), this.options.flashDelay); - return this; - }; - - return NanoScroll; - - })(); - $.fn.nanoScroller = function(settings) { - return this.each(function() { - var options, scrollbar; - if (!(scrollbar = this.nanoscroller)) { - options = $.extend({}, defaults, settings); - this.nanoscroller = scrollbar = new NanoScroll(this, options); - } - if (settings && typeof settings === "object") { - $.extend(scrollbar.options, settings); - if (settings.scrollBottom != null) { - return scrollbar.scrollBottom(settings.scrollBottom); - } - if (settings.scrollTop != null) { - return scrollbar.scrollTop(settings.scrollTop); - } - if (settings.scrollTo) { - return scrollbar.scrollTo(settings.scrollTo); - } - if (settings.scroll === 'bottom') { - return scrollbar.scrollBottom(0); - } - if (settings.scroll === 'top') { - return scrollbar.scrollTop(0); - } - if (settings.scroll && settings.scroll instanceof $) { - return scrollbar.scrollTo(settings.scroll); - } - if (settings.stop) { - return scrollbar.stop(); - } - if (settings.destroy) { - return scrollbar.destroy(); - } - if (settings.flash) { - return scrollbar.flash(); - } - } - return scrollbar.reset(); - }); - }; - $.fn.nanoScroller.Constructor = NanoScroll; -}); - -//# sourceMappingURL=jquery.nanoscroller.js.map diff --git a/htdocs/lib/AprsMarker.js b/htdocs/lib/AprsMarker.js new file mode 100644 index 000000000..e1673e292 --- /dev/null +++ b/htdocs/lib/AprsMarker.js @@ -0,0 +1,109 @@ +function AprsMarker() {} + +AprsMarker.prototype = new google.maps.OverlayView(); + +AprsMarker.prototype.isFacingEast = function(symbol) { + var candidates = '' + if (symbol.table === '/') { + // primary table + candidates = '(*<=>CFPUXYZabefgjkpsuv['; + } else { + // alternate table + candidates = '(T`efhjktuvw'; + } + return candidates.includes(symbol.symbol); +}; + +AprsMarker.prototype.draw = function() { + var div = this.div; + var overlay = this.overlay; + if (!div || !overlay) return; + + if (this.symbol) { + var tableId = this.symbol.table === '/' ? 0 : 1; + div.style.background = 'url(aprs-symbols/aprs-symbols-24-' + tableId + '@2x.png)'; + div.style['background-size'] = '384px 144px'; + div.style['background-position-x'] = -(this.symbol.index % 16) * 24 + 'px'; + div.style['background-position-y'] = -Math.floor(this.symbol.index / 16) * 24 + 'px'; + } + + if (this.course) { + if (this.symbol && !this.isFacingEast(this.symbol)) { + // assume symbol points to the north + div.style.transform = 'rotate(' + this.course + ' deg)'; + } else if (this.course > 180) { + // symbol is pointing east + // don't rotate more than 180 degrees, rather mirror + div.style.transform = 'scalex(-1) rotate(' + (270 - this.course) + 'deg)' + } else { + // symbol is pointing east + div.style.transform = 'rotate(' + (this.course - 90) + 'deg)'; + } + } else { + div.style.transform = null; + } + + if (this.symbol.table !== '/' && this.symbol.table !== '\\') { + overlay.style.display = 'block'; + overlay.style['background-position-x'] = -(this.symbol.tableindex % 16) * 24 + 'px'; + overlay.style['background-position-y'] = -Math.floor(this.symbol.tableindex / 16) * 24 + 'px'; + } else { + overlay.style.display = 'none'; + } + + if (this.opacity) { + div.style.opacity = this.opacity; + } else { + div.style.opacity = null; + } + + var point = this.getProjection().fromLatLngToDivPixel(this.position); + + if (point) { + div.style.left = point.x - 12 + 'px'; + div.style.top = point.y - 12 + 'px'; + } +}; + +AprsMarker.prototype.setOptions = function(options) { + google.maps.OverlayView.prototype.setOptions.apply(this, arguments); + this.draw(); +}; + +AprsMarker.prototype.onAdd = function() { + var div = this.div = document.createElement('div'); + + div.style.position = 'absolute'; + div.style.cursor = 'pointer'; + div.style.width = '24px'; + div.style.height = '24px'; + + var overlay = this.overlay = document.createElement('div'); + overlay.style.width = '24px'; + overlay.style.height = '24px'; + overlay.style.background = 'url(aprs-symbols/aprs-symbols-24-2@2x.png)'; + overlay.style['background-size'] = '384px 144px'; + overlay.style.display = 'none'; + + div.appendChild(overlay); + + var self = this; + div.addEventListener("click", function(event) { + event.stopPropagation(); + google.maps.event.trigger(self, "click", event); + }); + + var panes = this.getPanes(); + panes.overlayImage.appendChild(div); +}; + +AprsMarker.prototype.onRemove = function() { + if (this.div) { + this.div.parentNode.removeChild(this.div); + this.div = null; + } +}; + +AprsMarker.prototype.getAnchorPoint = function() { + return new google.maps.Point(0, -12); +}; diff --git a/htdocs/lib/AudioEngine.js b/htdocs/lib/AudioEngine.js new file mode 100644 index 000000000..a3c037136 --- /dev/null +++ b/htdocs/lib/AudioEngine.js @@ -0,0 +1,497 @@ +// this controls if the new AudioWorklet API should be used if available. +// the engine will still fall back to the ScriptProcessorNode if this is set to true but not available in the browser. +var useAudioWorklets = true; + +function AudioEngine(maxBufferLength, audioReporter) { + this.audioReporter = audioReporter; + this.initStats(); + this.resetStats(); + + this.onStartCallbacks = []; + + this.started = false; + this.audioContext = this.buildAudioContext(); + if (!this.audioContext) { + return; + } + + var me = this; + this.audioContext.onstatechange = function() { + if (me.audioContext.state !== 'running') return; + me._start(); + } + + this.audioCodec = new ImaAdpcmCodec(); + this.compression = 'none'; + + this.setupResampling(); + this.resampler = new Interpolator(this.resamplingFactor); + this.hdResampler = new Interpolator(this.hdResamplingFactor); + + this.maxBufferSize = maxBufferLength * this.getSampleRate(); +} + +AudioEngine.prototype.buildAudioContext = function() { + var ctxClass = window.AudioContext || window.webkitAudioContext; + if (!ctxClass) { + return; + } + + // known good sample rates + var goodRates = [48000, 44100, 96000] + + // let the browser chose the sample rate, if it is good, use it + var ctx = new ctxClass({latencyHint: 'playback'}); + if (goodRates.indexOf(ctx.sampleRate) >= 0) { + return ctx; + } + + // if that didn't work, try if any of the good rates work + if (goodRates.some(function(sr) { + try { + ctx = new ctxClass({sampleRate: sr, latencyHint: 'playback'}); + return true; + } catch (e) { + return false; + } + }, this)) { + return ctx; + } + + // fallback: let the browser decide + // this may cause playback problems down the line + return new ctxClass({latencyHint: 'playback'}); +} + +AudioEngine.prototype.resume = function(){ + this.audioContext.resume(); +} + +AudioEngine.prototype._start = function() { + var me = this; + + // if failed to find a valid resampling factor... + if (me.resamplingFactor === 0) { + return; + } + + // been started before? + if (me.started) { + return; + } + + // are we allowed to play audio? + if (!me.isAllowed()) { + return; + } + me.started = true; + + var runCallbacks = function(workletType) { + var callbacks = me.onStartCallbacks; + me.onStartCallbacks = false; + callbacks.forEach(function(c) { c(workletType); }); + }; + + me.gainNode = me.audioContext.createGain(); + me.gainNode.connect(me.audioContext.destination); + + if (useAudioWorklets && me.audioContext.audioWorklet) { + me.audioContext.audioWorklet.addModule('static/lib/AudioProcessor.js').then(function(){ + me.audioNode = new AudioWorkletNode(me.audioContext, 'openwebrx-audio-processor', { + numberOfInputs: 0, + numberOfOutputs: 1, + outputChannelCount: [1], + processorOptions: { + maxBufferSize: me.maxBufferSize + } + }); + me.audioNode.connect(me.gainNode); + me.audioNode.port.addEventListener('message', function(m){ + var json = JSON.parse(m.data); + if (typeof(json.buffersize) !== 'undefined') { + me.audioReporter({ + buffersize: json.buffersize + }); + } + if (typeof(json.samplesProcessed) !== 'undefined') { + me.audioSamples.add(json.samplesProcessed); + } + }); + me.audioNode.port.start(); + runCallbacks('AudioWorklet'); + }); + } else { + me.audioBuffers = []; + + if (!AudioBuffer.prototype.copyToChannel) { //Chrome 36 does not have it, Firefox does + AudioBuffer.prototype.copyToChannel = function (input, channel) //input is Float32Array + { + var cd = this.getChannelData(channel); + for (var i = 0; i < input.length; i++) cd[i] = input[i]; + } + } + + var bufferSize; + if (me.audioContext.sampleRate < 44100 * 2) + bufferSize = 4096; + else if (me.audioContext.sampleRate >= 44100 * 2 && me.audioContext.sampleRate < 44100 * 4) + bufferSize = 4096 * 2; + else if (me.audioContext.sampleRate > 44100 * 4) + bufferSize = 4096 * 4; + + + function audio_onprocess(e) { + var total = 0; + var out = new Float32Array(bufferSize); + while (me.audioBuffers.length) { + var b = me.audioBuffers.shift(); + // not enough space to fit all data, so splice and put back in the queue + if (total + b.length > bufferSize) { + var spaceLeft = bufferSize - total; + var tokeep = b.subarray(0, spaceLeft); + out.set(tokeep, total); + var tobuffer = b.subarray(spaceLeft, b.length); + me.audioBuffers.unshift(tobuffer); + total += spaceLeft; + break; + } else { + out.set(b, total); + total += b.length; + } + } + + e.outputBuffer.copyToChannel(out, 0); + me.audioSamples.add(total); + + } + + //on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor + var method = 'createScriptProcessor'; + if (me.audioContext.createJavaScriptNode) { + method = 'createJavaScriptNode'; + } + me.audioNode = me.audioContext[method](bufferSize, 0, 1); + me.audioNode.onaudioprocess = audio_onprocess; + me.audioNode.connect(me.gainNode); + runCallbacks('ScriptProcessorNode') + } + + setInterval(me.reportStats.bind(me), 1000); +}; + +AudioEngine.prototype.onStart = function(callback) { + if (this.onStartCallbacks) { + this.onStartCallbacks.push(callback); + } else { + callback(); + } +}; + +AudioEngine.prototype.isAllowed = function() { + return this.audioContext.state === 'running'; +}; + +AudioEngine.prototype.reportStats = function() { + if (this.audioNode.port) { + this.audioNode.port.postMessage(JSON.stringify({cmd:'getStats'})); + } else { + this.audioReporter({ + buffersize: this.getBuffersize() + }); + } +}; + +AudioEngine.prototype.initStats = function() { + var me = this; + var buildReporter = function(key) { + return function(v){ + var report = {}; + report[key] = v; + me.audioReporter(report); + } + + }; + + this.audioBytes = new Measurement(); + this.audioBytes.report(10000, 1000, buildReporter('audioByteRate')); + + this.audioSamples = new Measurement(); + this.audioSamples.report(10000, 1000, buildReporter('audioRate')); +}; + +AudioEngine.prototype.resetStats = function() { + this.audioBytes.reset(); + this.audioSamples.reset(); +}; + +AudioEngine.prototype.setupResampling = function() { //both at the server and the client + var targetRate = this.audioContext.sampleRate; + var audio_params = this.findRate(8000, 12000); + if (!audio_params) { + this.resamplingFactor = 0; + this.outputRate = 0; + divlog('Your audio card sampling rate (' + targetRate + ') is not supported.
Please change your operating system default settings in order to fix this.', 1); + } else { + this.resamplingFactor = audio_params.resamplingFactor; + this.outputRate = audio_params.outputRate; + } + + var hd_audio_params = this.findRate(36000, 48000); + if (!hd_audio_params) { + this.hdResamplingFactor = 0; + this.hdOutputRate = 0; + divlog('Your audio card sampling rate (' + targetRate + ') is not supported for HD audio
Please change your operating system default settings in order to fix this.', 1); + } else { + this.hdResamplingFactor = hd_audio_params.resamplingFactor; + this.hdOutputRate = hd_audio_params.outputRate; + } +}; + +AudioEngine.prototype.findRate = function(low, high) { + var targetRate = this.audioContext.sampleRate; + var i = 1; + while (true) { + var audio_server_output_rate = Math.floor(targetRate / i); + if (audio_server_output_rate < low) { + return; + } else if (audio_server_output_rate >= low && audio_server_output_rate <= high) { + return { + resamplingFactor: i, + outputRate: audio_server_output_rate + } + } + i++; + }; +} + +AudioEngine.prototype.getOutputRate = function() { + return this.outputRate; +}; + +AudioEngine.prototype.getHdOutputRate = function() { + return this.hdOutputRate; +} + +AudioEngine.prototype.getSampleRate = function() { + return this.audioContext.sampleRate; +}; + +AudioEngine.prototype.processAudio = function(data, resampler) { + if (!this.audioNode) return; + this.audioBytes.add(data.byteLength); + var buffer; + if (this.compression === "adpcm") { + //resampling & ADPCM + buffer = this.audioCodec.decodeWithSync(new Uint8Array(data)); + } else { + buffer = new Int16Array(data); + } + buffer = resampler.process(buffer); + if (this.audioNode.port) { + // AudioWorklets supported + this.audioNode.port.postMessage(buffer); + } else { + // silently drop excess samples + if (this.getBuffersize() + buffer.length <= this.maxBufferSize) { + this.audioBuffers.push(buffer); + } + } +} + +AudioEngine.prototype.pushAudio = function(data) { + this.processAudio(data, this.resampler); +}; + +AudioEngine.prototype.pushHdAudio = function(data) { + this.processAudio(data, this.hdResampler); +} + +AudioEngine.prototype.setCompression = function(compression) { + this.compression = compression; +}; + +AudioEngine.prototype.setVolume = function(volume) { + this.gainNode.gain.value = volume; +}; + +AudioEngine.prototype.getBuffersize = function() { + // only available when using ScriptProcessorNode + if (!this.audioBuffers) return 0; + return this.audioBuffers.map(function(b){ return b.length; }).reduce(function(a, b){ return a + b; }, 0); +}; + +function ImaAdpcmCodec() { + this.reset(); +} + +ImaAdpcmCodec.prototype.reset = function() { + this.stepIndex = 0; + this.predictor = 0; + this.step = 0; + this.synchronized = 0; + this.syncWord = "SYNC"; + this.syncCounter = 0; + this.phase = 0; + this.syncBuffer = new Uint8Array(4); + this.syncBufferIndex = 0; +}; + +ImaAdpcmCodec.imaIndexTable = [ -1, -1, -1, -1, 2, 4, 6, 8, -1, -1, -1, -1, 2, 4, 6, 8 ]; + +ImaAdpcmCodec.imaStepTable = [ + 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, + 19, 21, 23, 25, 28, 31, 34, 37, 41, 45, + 50, 55, 60, 66, 73, 80, 88, 97, 107, 118, + 130, 143, 157, 173, 190, 209, 230, 253, 279, 307, + 337, 371, 408, 449, 494, 544, 598, 658, 724, 796, + 876, 963, 1060, 1166, 1282, 1411, 1552, 1707, 1878, 2066, + 2272, 2499, 2749, 3024, 3327, 3660, 4026, 4428, 4871, 5358, + 5894, 6484, 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, + 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, 32767 + ]; + +ImaAdpcmCodec.prototype.decode = function(data) { + var output = new Int16Array(data.length * 2); + for (var i = 0; i < data.length; i++) { + output[i * 2] = this.decodeNibble(data[i] & 0x0F); + output[i * 2 + 1] = this.decodeNibble((data[i] >> 4) & 0x0F); + } + return output; +}; + +ImaAdpcmCodec.prototype.decodeWithSync = function(data) { + var output = new Int16Array(data.length * 2); + var oi = 0; + for (var index = 0; index < data.length; index++) { + switch (this.phase) { + case 0: + // search for sync word + if (data[index] !== this.syncWord.charCodeAt(this.synchronized++)) { + // reset if data is unexpected + this.synchronized = 0; + } + // if sync word has been found pass on to next phase + if (this.synchronized === 4) { + this.syncBufferIndex = 0; + this.phase = 1; + } + break; + case 1: + // read codec runtime data from stream + this.syncBuffer[this.syncBufferIndex++] = data[index]; + // if data is complete, apply and pass on to next phase + if (this.syncBufferIndex === 4) { + var syncData = new Int16Array(this.syncBuffer.buffer); + this.stepIndex = syncData[0]; + this.predictor = syncData[1]; + this.syncCounter = 1000; + this.phase = 2; + } + break; + case 2: + // decode actual audio data + output[oi++] = this.decodeNibble(data[index] & 0x0F); + output[oi++] = this.decodeNibble(data[index] >> 4); + // if the next sync keyword is due, reset and return to phase 0 + if (this.syncCounter-- === 0) { + this.synchronized = 0; + this.phase = 0; + } + break; + } + } + return output.slice(0, oi); +}; + +ImaAdpcmCodec.prototype.decodeNibble = function(nibble) { + this.stepIndex += ImaAdpcmCodec.imaIndexTable[nibble]; + this.stepIndex = Math.min(Math.max(this.stepIndex, 0), 88); + + var diff = this.step >> 3; + if (nibble & 1) diff += this.step >> 2; + if (nibble & 2) diff += this.step >> 1; + if (nibble & 4) diff += this.step; + if (nibble & 8) diff = -diff; + + this.predictor += diff; + this.predictor = Math.min(Math.max(this.predictor, -32768), 32767); + + this.step = ImaAdpcmCodec.imaStepTable[this.stepIndex]; + + return this.predictor; +}; + +function Interpolator(factor) { + this.factor = factor; + this.lowpass = new Lowpass(factor) +} + +Interpolator.prototype.process = function(data) { + var output = new Float32Array(data.length * this.factor); + for (var i = 0; i < data.length; i++) { + output[i * this.factor] = (data[i] + 0.5) / 32768; + } + return this.lowpass.process(output); +}; + +function Lowpass(interpolation) { + this.interpolation = interpolation; + var transitionBandwidth = 0.05; + this.numtaps = Math.round(4 / transitionBandwidth); + if (this.numtaps % 2 == 0) this.numtaps += 1; + + var cutoff = 1 / interpolation; + this.coefficients = this.getCoefficients(cutoff / 2); + + this.delay = new Float32Array(this.numtaps); + for (var i = 0; i < this.numtaps; i++){ + this.delay[i] = 0; + } + this.delayIndex = 0; +} + +Lowpass.prototype.getCoefficients = function(cutoffRate) { + var middle = Math.floor(this.numtaps / 2); + // hamming window + var window_function = function(r){ + var rate = 0.5 + r / 2; + return 0.54 - 0.46 * Math.cos(2 * Math.PI * rate); + } + var output = []; + output[middle] = 2 * Math.PI * cutoffRate * window_function(0); + for (var i = 1; i <= middle; i++) { + output[middle - i] = output[middle + i] = (Math.sin(2 * Math.PI * cutoffRate * i) / i) * window_function(i / middle); + } + return this.normalizeCoefficients(output); +}; + +Lowpass.prototype.normalizeCoefficients = function(input) { + var sum = 0; + var output = []; + for (var i = 0; i < input.length; i++) { + sum += input[i]; + } + for (var i = 0; i < input.length; i++) { + output[i] = input[i] / sum; + } + return output; +}; + +Lowpass.prototype.process = function(input) { + output = new Float32Array(input.length); + for (var oi = 0; oi < input.length; oi++) { + this.delay[this.delayIndex] = input[oi]; + this.delayIndex = (this.delayIndex + 1) % this.numtaps; + + var acc = 0; + var index = this.delayIndex; + for (var i = 0; i < this.numtaps; ++i) { + var index = index != 0 ? index - 1 : this.numtaps - 1; + acc += this.delay[index] * this.coefficients[i]; + if (isNaN(acc)) debugger; + } + // gain by interpolation + output[oi] = this.interpolation * acc; + } + return output; +}; \ No newline at end of file diff --git a/htdocs/lib/AudioProcessor.js b/htdocs/lib/AudioProcessor.js new file mode 100644 index 000000000..a3e9abf93 --- /dev/null +++ b/htdocs/lib/AudioProcessor.js @@ -0,0 +1,61 @@ +class OwrxAudioProcessor extends AudioWorkletProcessor { + constructor(options){ + super(options); + // initialize ringbuffer, make sure it aligns with the expected buffer size of 128 + this.bufferSize = Math.round(options.processorOptions.maxBufferSize / 128) * 128; + this.audioBuffer = new Float32Array(this.bufferSize); + this.inPos = 0; + this.outPos = 0; + this.samplesProcessed = 0; + this.port.addEventListener('message', (m) => { + if (typeof(m.data) === 'string') { + const json = JSON.parse(m.data); + if (json.cmd && json.cmd === 'getStats') { + this.reportStats(); + } + } else { + // the ringbuffer size is aligned to the output buffer size, which means that the input buffers might + // need to wrap around the end of the ringbuffer, back to the start. + // it is better to have this processing here instead of in the time-critical process function. + if (this.inPos + m.data.length <= this.bufferSize) { + // we have enough space, so just copy data over. + this.audioBuffer.set(m.data, this.inPos); + } else { + // we don't have enough space, so we need to split the data. + const remaining = this.bufferSize - this.inPos; + this.audioBuffer.set(m.data.subarray(0, remaining), this.inPos); + this.audioBuffer.set(m.data.subarray(remaining)); + } + this.inPos = (this.inPos + m.data.length) % this.bufferSize; + } + }); + this.port.addEventListener('messageerror', console.error); + this.port.start(); + } + process(inputs, outputs) { + if (this.remaining() < 128) { + outputs[0].forEach(output => output.fill(0)); + return true; + } + outputs[0].forEach((output) => { + output.set(this.audioBuffer.subarray(this.outPos, this.outPos + 128)); + }); + this.outPos = (this.outPos + 128) % this.bufferSize; + this.samplesProcessed += 128; + return true; + } + remaining() { + const mod = (this.inPos - this.outPos) % this.bufferSize; + if (mod >= 0) return mod; + return mod + this.bufferSize; + } + reportStats() { + this.port.postMessage(JSON.stringify({ + buffersize: this.remaining(), + samplesProcessed: this.samplesProcessed + })); + this.samplesProcessed = 0; + } +} + +registerProcessor('openwebrx-audio-processor', OwrxAudioProcessor); \ No newline at end of file diff --git a/htdocs/lib/BookmarkBar.js b/htdocs/lib/BookmarkBar.js new file mode 100644 index 000000000..7c40892f3 --- /dev/null +++ b/htdocs/lib/BookmarkBar.js @@ -0,0 +1,147 @@ +function BookmarkBar() { + var me = this; + me.localBookmarks = new BookmarkLocalStorage(); + me.$container = $("#openwebrx-bookmarks-container"); + me.bookmarks = {}; + + me.$container.on('click', '.bookmark', function(e){ + var $bookmark = $(e.target).closest('.bookmark'); + me.$container.find('.bookmark').removeClass('selected'); + var b = $bookmark.data(); + if (!b || !b.frequency || !b.modulation) return; + me.getDemodulator().set_offset_frequency(b.frequency - center_freq); + if (b.modulation) { + me.getDemodulatorPanel().setMode(b.modulation, b.underlying); + } + $bookmark.addClass('selected'); + }); + + me.$container.on('click', '.action[data-action=edit]', function(e){ + e.stopPropagation(); + var $bookmark = $(e.target).closest('.bookmark'); + me.showEditDialog($bookmark.data()); + }); + + me.$container.on('click', '.action[data-action=delete]', function(e){ + e.stopPropagation(); + var $bookmark = $(e.target).closest('.bookmark'); + me.localBookmarks.deleteBookmark($bookmark.data()); + me.loadLocalBookmarks(); + }); + + var $bookmarkButton = $('#openwebrx-panel-receiver').find('.openwebrx-bookmark-button'); + if (typeof(Storage) !== 'undefined') { + $bookmarkButton.show(); + } else { + $bookmarkButton.hide(); + } + $bookmarkButton.click(function(){ + me.showEditDialog(); + }); + + me.$dialog = $("#openwebrx-dialog-bookmark"); + me.$dialog.find('.openwebrx-button[data-action=cancel]').click(function(){ + me.$dialog.hide(); + }); + me.$dialog.find('.openwebrx-button[data-action=submit]').click(function(){ + me.storeBookmark(); + }); + me.$dialog.find('form').on('submit', function(e){ + e.preventDefault(); + me.storeBookmark(); + }); +} + +BookmarkBar.prototype.position = function(){ + var range = get_visible_freq_range(); + $('#openwebrx-bookmarks-container').find('.bookmark').each(function(){ + $(this).css('left', scale_px_from_freq($(this).data('frequency'), range)); + }); +}; + +BookmarkBar.prototype.loadLocalBookmarks = function(){ + var bwh = bandwidth / 2; + var start = center_freq - bwh; + var end = center_freq + bwh; + var bookmarks = this.localBookmarks.getBookmarks().filter(function(b){ + return b.frequency >= start && b.frequency <= end; + }); + this.replace_bookmarks(bookmarks, 'local', true); +}; + +BookmarkBar.prototype.replace_bookmarks = function(bookmarks, source, editable) { + editable = !!editable; + bookmarks = bookmarks.map(function(b){ + b.source = source; + b.editable = editable; + return b; + }); + this.bookmarks[source] = bookmarks; + this.render(); +}; + +BookmarkBar.prototype.render = function(){ + var bookmarks = Object.values(this.bookmarks).reduce(function(l, v){ return l.concat(v); }); + bookmarks = bookmarks.sort(function(a, b){ return a.frequency - b.frequency; }); + var elements = bookmarks.map(function(b){ + var $bookmark = $( + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + b.name + '
' + + '
' + ); + $bookmark.data(b); + return $bookmark; + }); + this.$container.find('.bookmark').remove(); + this.$container.append(elements); + this.position(); +}; + +BookmarkBar.prototype.showEditDialog = function(bookmark) { + if (!bookmark) { + bookmark = { + name: "", + frequency: center_freq + this.getDemodulator().get_offset_frequency(), + modulation: this.getDemodulator().get_secondary_demod() || this.getDemodulator().get_modulation() + } + } + this.$dialog.bookmarkDialog().setValues(bookmark); + this.$dialog.show(); + this.$dialog.find('#name').focus(); +}; + +BookmarkBar.prototype.storeBookmark = function() { + var me = this; + var bookmark = this.$dialog.bookmarkDialog().getValues(); + if (!bookmark) return; + bookmark.frequency = Number(bookmark.frequency); + + var bookmarks = me.localBookmarks.getBookmarks(); + + if (!bookmark.id) { + if (bookmarks.length) { + bookmark.id = 1 + Math.max.apply(Math, bookmarks.map(function(b){ return b.id || 0; })); + } else { + bookmark.id = 1; + } + } + + bookmarks = bookmarks.filter(function(b) { return b.id !== bookmark.id; }); + bookmarks.push(bookmark); + + me.localBookmarks.setBookmarks(bookmarks); + me.loadLocalBookmarks(); + me.$dialog.hide(); +}; + +BookmarkBar.prototype.getDemodulatorPanel = function() { + return $('#openwebrx-panel-receiver').demodulatorPanel(); +}; + +BookmarkBar.prototype.getDemodulator = function() { + return this.getDemodulatorPanel().getDemodulator(); +}; diff --git a/htdocs/lib/BookmarkDialog.js b/htdocs/lib/BookmarkDialog.js new file mode 100644 index 000000000..4a0a1841b --- /dev/null +++ b/htdocs/lib/BookmarkDialog.js @@ -0,0 +1,36 @@ +$.fn.bookmarkDialog = function() { + var $el = this; + return { + setModes: function(modes) { + $el.find('#modulation').html(modes.filter(function(m){ + return m.isAvailable(); + }).map(function(m) { + return ''; + }).join('')); + return this; + }, + setValues: function(bookmark) { + var $form = $el.find('form'); + ['name', 'frequency', 'modulation'].forEach(function(key){ + $form.find('#' + key).val(bookmark[key]); + }); + $el.data('id', bookmark.id || false); + return this; + }, + getValues: function() { + var bookmark = {}; + var valid = true; + ['name', 'frequency', 'modulation'].forEach(function(key){ + var $input = $el.find('#' + key); + valid = valid && $input[0].checkValidity(); + bookmark[key] = $input.val(); + }); + if (!valid) { + $el.find("form :submit").click(); + return; + } + bookmark.id = $el.data('id'); + return bookmark; + } + } +} \ No newline at end of file diff --git a/htdocs/lib/BookmarkLocalStorage.js b/htdocs/lib/BookmarkLocalStorage.js new file mode 100644 index 000000000..edb499299 --- /dev/null +++ b/htdocs/lib/BookmarkLocalStorage.js @@ -0,0 +1,17 @@ +BookmarkLocalStorage = function(){ +}; + +BookmarkLocalStorage.prototype.getBookmarks = function(){ + return JSON.parse(window.localStorage.getItem("bookmarks")) || []; +}; + +BookmarkLocalStorage.prototype.setBookmarks = function(bookmarks){ + window.localStorage.setItem("bookmarks", JSON.stringify(bookmarks)); +}; + +BookmarkLocalStorage.prototype.deleteBookmark = function(data) { + if (data.id) data = data.id; + var bookmarks = this.getBookmarks(); + bookmarks = bookmarks.filter(function(b) { return b.id !== data; }); + this.setBookmarks(bookmarks); +}; diff --git a/htdocs/lib/Demodulator.js b/htdocs/lib/Demodulator.js new file mode 100644 index 000000000..601bc86bf --- /dev/null +++ b/htdocs/lib/Demodulator.js @@ -0,0 +1,427 @@ +function Filter(demodulator) { + this.demodulator = demodulator; + this.min_passband = 100; +} + +Filter.prototype.getLimits = function() { + var max_bw; + if (['pocsag', 'packet'].indexOf(this.demodulator.get_secondary_demod()) >= 0) { + max_bw = 12500; + } else if (['dmr', 'dstar', 'nxdn', 'ysf', 'm17'].indexOf(this.demodulator.get_modulation()) >= 0) { + max_bw = 6250; + } else if (this.demodulator.get_modulation() === 'wfm') { + max_bw = 100000; + } else if (this.demodulator.get_modulation() === 'drm') { + max_bw = 50000; + } else if (this.demodulator.get_modulation() === "freedv") { + max_bw = 4000; + } else if (this.demodulator.get_secondary_demod() === "ism") { + max_bw = 600000; + } else { + max_bw = (audioEngine.getOutputRate() / 2) - 1; + } + return { + high: max_bw, + low: -max_bw + }; +}; + +function Envelope(demodulator) { + this.demodulator = demodulator; + this.dragged_range = Demodulator.draggable_ranges.none; +} + +Envelope.prototype.draw = function(visible_range){ + this.visible_range = visible_range; + var line = center_freq + this.demodulator.offset_frequency; + + // ____ + // Draws a standard filter envelope like this: _/ \_ + // Parameters are given in offset frequency (Hz). + // Envelope is drawn on the scale canvas. + // A "drag range" object is returned, containing information about the draggable areas of the envelope + // (beginning, ending and the line showing the offset frequency). + var env_bounding_line_w = 5; // + var env_att_w = 5; // _______ ___env_h2 in px ___|_____ + var env_h1 = 17; // _/| \_ ___env_h1 in px _/ |_ \_ + var env_h2 = 5; // |||env_att_line_w |_env_lineplus + var env_lineplus = 1; // ||env_bounding_line_w + var env_line_click_area = 6; + //range=get_visible_freq_range(); + + var from = center_freq + this.demodulator.offset_frequency; + var to = center_freq + this.demodulator.offset_frequency; + var fake_indicator = typeof(this.demodulator.low_cut) !== 'number' || typeof(this.demodulator.high_cut) !== 'number'; + if (fake_indicator) { + // fake values just so that the tuning indicator shows up + var fixedBw = 100000 + // if we know the if rate, we can display that + if (this.demodulator.ifRate) { + fixedBw = this.demodulator.ifRate / 2; + } + from -= fixedBw; + to += fixedBw; + } else { + from += this.demodulator.low_cut; + to += this.demodulator.high_cut; + } + var from_px = scale_px_from_freq(from, range); + var to_px = scale_px_from_freq(to, range); + if (to_px < from_px) /* swap'em */ { + var temp_px = to_px; + to_px = from_px; + from_px = temp_px; + } + + from_px -= (env_att_w + env_bounding_line_w); + to_px += (env_att_w + env_bounding_line_w); + // do drawing: + var color = this.color || '#ffff00'; // yellow + scale_ctx.strokeStyle = color; + scale_ctx.fillStyle = color; + var drag_ranges = {envelope_on_screen: false, line_on_screen: false}; + if (!(to_px < 0 || from_px > window.innerWidth)) // out of screen? + { + if (fake_indicator) { + scale_ctx.setLineDash([10, 5]); + } else { + drag_ranges.beginning = {x1: from_px, x2: from_px + env_bounding_line_w + env_att_w}; + drag_ranges.ending = {x1: to_px - env_bounding_line_w - env_att_w, x2: to_px}; + drag_ranges.whole_envelope = {x1: from_px, x2: to_px}; + drag_ranges.envelope_on_screen = true; + } + + scale_ctx.beginPath(); + scale_ctx.lineWidth = 3; + + scale_ctx.moveTo(from_px, env_h1); + scale_ctx.lineTo(from_px + env_bounding_line_w, env_h1); + scale_ctx.lineTo(from_px + env_bounding_line_w + env_att_w, env_h2); + scale_ctx.lineTo(to_px - env_bounding_line_w - env_att_w, env_h2); + scale_ctx.lineTo(to_px - env_bounding_line_w, env_h1); + scale_ctx.lineTo(to_px, env_h1); + scale_ctx.globalAlpha = 0.3; + scale_ctx.fill(); + scale_ctx.globalAlpha = 1; + scale_ctx.stroke(); + scale_ctx.setLineDash([]); + + scale_ctx.lineWidth = 1; + scale_ctx.font = "bold 11px sans-serif"; + scale_ctx.textBaseline = "top"; + scale_ctx.textAlign = "left"; + if (typeof(this.demodulator.high_cut) === 'number') { + scale_ctx.fillText(this.demodulator.high_cut.toString(), to_px + env_att_w, env_h2); + } + scale_ctx.textAlign = "right"; + if (typeof(this.demodulator.low_cut) === 'number') { + scale_ctx.fillText(this.demodulator.low_cut.toString(), from_px - env_att_w, env_h2); + } + } + if (typeof line !== "undefined") // out of screen? + { + var line_px = scale_px_from_freq(line, range); + if (!(line_px < 0 || line_px > window.innerWidth)) { + drag_ranges.line = {x1: line_px - env_line_click_area / 2, x2: line_px + env_line_click_area / 2}; + drag_ranges.line_on_screen = true; + scale_ctx.beginPath(); + scale_ctx.moveTo(line_px, env_h1 + env_lineplus); + scale_ctx.lineTo(line_px, env_h2 - env_lineplus); + scale_ctx.lineWidth = 3; + scale_ctx.stroke(); + } + } + this.drag_ranges = drag_ranges; +}; + +Envelope.prototype.drag_start = function(x, key_modifiers){ + this.key_modifiers = key_modifiers; + this.dragged_range = this.where_clicked(x, this.drag_ranges, key_modifiers); + this.drag_origin = { + x: x, + low_cut: this.demodulator.low_cut, + high_cut: this.demodulator.high_cut, + offset_frequency: this.demodulator.offset_frequency + }; + return this.dragged_range !== Demodulator.draggable_ranges.none; +}; + +Envelope.prototype.where_clicked = function(x, drag_ranges, key_modifiers) { // Check exactly what the user has clicked based on ranges returned by envelope_draw(). + var in_range = function (x, range) { + return range.x1 <= x && range.x2 >= x; + }; + var dr = Demodulator.draggable_ranges; + + if (key_modifiers.shiftKey) { + //Check first: shift + center drag emulates BFO knob + if (drag_ranges.line_on_screen && in_range(x, drag_ranges.line)) return dr.bfo; + //Check second: shift + envelope drag emulates PBF knob + if (drag_ranges.envelope_on_screen && in_range(x, drag_ranges.whole_envelope)) return dr.pbs; + } + if (drag_ranges.envelope_on_screen) { + // For low and high cut: + if (in_range(x, drag_ranges.beginning)) return dr.beginning; + if (in_range(x, drag_ranges.ending)) return dr.ending; + // Last priority: having clicked anything else on the envelope, without holding the shift key + if (in_range(x, drag_ranges.whole_envelope)) return dr.anything_else; + } + return dr.none; //User doesn't drag the envelope for this demodulator +}; + + +Envelope.prototype.drag_move = function(x) { + var dr = Demodulator.draggable_ranges; + var new_value; + if (this.dragged_range === dr.none) return false; // we return if user is not dragging (us) at all + var freq_change = Math.round(this.visible_range.hps * (x - this.drag_origin.x)); + + //dragging the line in the middle of the filter envelope while holding Shift does emulate + //the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged + //Filter passband moves in the opposite direction than dragged, hence the minus below. + var minus = (this.dragged_range === dr.bfo) ? -1 : 1; + //dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob + //(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset + //frequency. + if (this.dragged_range === dr.beginning || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) { + //we don't let low_cut go beyond its limits + if ((new_value = this.drag_origin.low_cut + minus * freq_change) < this.demodulator.filter.getLimits().low) { + new_value = this.demodulator.filter.getLimits().low; + } + //nor the filter passband be too small + if (this.demodulator.high_cut - new_value < this.demodulator.filter.min_passband) { + new_value = this.demodulator.high_cut - this.demodulator.filter.min_passband; + } + //sanity check to prevent GNU Radio "firdes check failed: fa <= fb" + if (new_value >= this.demodulator.high_cut) return true; + this.demodulator.setLowCut(new_value); + } + if (this.dragged_range === dr.ending || this.dragged_range === dr.bfo || this.dragged_range === dr.pbs) { + //we don't let high_cut go beyond its limits + if ((new_value = this.drag_origin.high_cut + minus * freq_change) > this.demodulator.filter.getLimits().high) { + new_value = this.demodulator.filter.getLimits().high; + } + //nor the filter passband be too small + if (new_value - this.demodulator.low_cut < this.demodulator.filter.min_passband) { + new_value = this.demodulator.low_cut + this.demodulator.filter.min_passband; + } + //sanity check to prevent GNU Radio "firdes check failed: fa <= fb" + if (new_value <= this.demodulator.low_cut) return true; + this.demodulator.setHighCut(new_value); + } + if (this.dragged_range === dr.anything_else || this.dragged_range === dr.bfo) { + //when any other part of the envelope is dragged, the offset frequency is changed (whole passband also moves with it) + new_value = this.drag_origin.offset_frequency + freq_change; + if (new_value > bandwidth / 2 || new_value < -bandwidth / 2) return true; //we don't allow tuning above Nyquist frequency :-) + this.demodulator.set_offset_frequency(new_value); + } + //now do the actual modifications: + //mkenvelopes(this.visible_range); + //this.demodulator.set(); + return true; +}; + +Envelope.prototype.drag_end = function(){ + var to_return = this.dragged_range !== Demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset + this.dragged_range = Demodulator.draggable_ranges.none; + return to_return; +}; + + +//******* class Demodulator_default_analog ******* +// This can be used as a base for basic audio demodulators. +// It already supports most basic modulations used for ham radio and commercial services: AM/FM/LSB/USB + +function Demodulator(offset_frequency, modulation) { + this.offset_frequency = offset_frequency; + this.envelope = new Envelope(this); + this.color = Demodulator.get_next_color(); + this.modulation = modulation; + this.filter = new Filter(this); + this.squelch_level = -150; + this.dmr_filter = 3; + this.dab_service_id = 0; + this.started = false; + this.state = {}; + this.secondary_demod = false; + var mode = Modes.findByModulation(modulation); + this.low_cut = mode && mode.bandpass && mode.bandpass.low_cut || null; + this.high_cut = mode && mode.bandpass && mode.bandpass.high_cut || null; + this.ifRate = mode && mode.ifRate; + this.listeners = { + "frequencychange": [], + "squelchchange": [] + }; +} + +//ranges on filter envelope that can be dragged: +Demodulator.draggable_ranges = { + none: 0, + beginning: 1 /*from*/, + ending: 2 /*to*/, + anything_else: 3, + bfo: 4 /*line (while holding shift)*/, + pbs: 5 +}; //to which parameter these correspond in envelope_draw() + +Demodulator.color_index = 0; +Demodulator.colors = ["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"]; + +Demodulator.get_next_color = function() { + if (this.color_index >= this.colors.length) this.color_index = 0; + return (this.colors[this.color_index++]); +} + + + +Demodulator.prototype.on = function(event, handler) { + this.listeners[event].push(handler); +}; + +Demodulator.prototype.emit = function(event, params) { + this.listeners[event].forEach(function(fn) { + fn(params); + }); +}; + +Demodulator.prototype.set_offset_frequency = function(to_what) { + if (typeof(to_what) == 'undefined' || to_what > bandwidth / 2 || to_what < -bandwidth / 2) return; + to_what = Math.round(to_what); + if (this.offset_frequency === to_what) { + return; + } + this.offset_frequency = to_what; + this.set(); + this.emit("frequencychange", to_what); + mkenvelopes(get_visible_freq_range()); +}; + +Demodulator.prototype.get_offset_frequency = function() { + return this.offset_frequency; +}; + +Demodulator.prototype.get_modulation = function() { + return this.modulation; +}; + +Demodulator.prototype.start = function() { + this.started = true; + this.set(); + ws.send(JSON.stringify({ + "type": "dspcontrol", + "action": "start" + })); +}; + +// TODO check if this is actually used +Demodulator.prototype.stop = function() { +}; + +Demodulator.prototype.send = function(params) { + ws.send(JSON.stringify({"type": "dspcontrol", "params": params})); +} + +Demodulator.prototype.set = function () { //this function sends demodulator parameters to the server + if (!this.started) return; + var params = { + "low_cut": this.low_cut, + "high_cut": this.high_cut, + "offset_freq": this.offset_frequency, + "mod": this.modulation, + "dmr_filter": this.dmr_filter, + "dab_service_id": this.dab_service_id, + "squelch_level": this.squelch_level, + "secondary_mod": this.secondary_demod, + "secondary_offset_freq": this.secondary_offset_freq + }; + var to_send = {}; + for (var key in params) { + if (!(key in this.state) || params[key] !== this.state[key]) { + to_send[key] = params[key]; + } + } + if (Object.keys(to_send).length > 0) { + this.send(to_send); + for (var key in to_send) { + this.state[key] = to_send[key]; + } + } + mkenvelopes(get_visible_freq_range()); +}; + +Demodulator.prototype.setSquelch = function(squelch) { + if (this.squelch_level == squelch) { + return; + } + this.squelch_level = squelch; + this.set(); + this.emit("squelchchange", squelch); +}; + +Demodulator.prototype.getSquelch = function() { + return this.squelch_level; +}; + +Demodulator.prototype.setDmrFilter = function(dmr_filter) { + this.dmr_filter = dmr_filter; + this.set(); +}; + +Demodulator.prototype.setDabServiceId = function(dab_service_id) { + this.dab_service_id = dab_service_id; + this.set(); +} + +Demodulator.prototype.setBandpass = function(bandpass) { + this.bandpass = bandpass; + this.low_cut = bandpass.low_cut; + this.high_cut = bandpass.high_cut; + this.set(); +}; + +Demodulator.prototype.disableBandpass = function() { + delete this.bandpass; + this.low_cut = null; + this.high_cut = null; + this.set() +} + +Demodulator.prototype.setLowCut = function(low_cut) { + this.low_cut = low_cut; + this.set(); +}; + +Demodulator.prototype.setHighCut = function(high_cut) { + this.high_cut = high_cut; + this.set(); +}; + +Demodulator.prototype.getBandpass = function() { + return { + low_cut: this.low_cut, + high_cut: this.high_cut + }; +}; + +Demodulator.prototype.setIfRate = function(ifRate) { + this.ifRate = ifRate; +}; + +Demodulator.prototype.set_secondary_demod = function(secondary_demod) { + if (this.secondary_demod === secondary_demod) { + return; + } + this.secondary_demod = secondary_demod; + this.set(); +}; + +Demodulator.prototype.get_secondary_demod = function() { + return this.secondary_demod; +}; + +Demodulator.prototype.set_secondary_offset_freq = function(secondary_offset) { + if (this.secondary_offset_freq === secondary_offset) { + return; + } + this.secondary_offset_freq = secondary_offset; + this.set(); +}; diff --git a/htdocs/lib/DemodulatorPanel.js b/htdocs/lib/DemodulatorPanel.js new file mode 100644 index 000000000..6f35e6b79 --- /dev/null +++ b/htdocs/lib/DemodulatorPanel.js @@ -0,0 +1,385 @@ +function DemodulatorPanel(el) { + var self = this; + self.el = el; + self.demodulator = null; + self.mode = null; + self.squelchMargin = 10; + self.initialParams = {}; + + var displayEl = el.find('.webrx-actual-freq') + this.tuneableFrequencyDisplay = displayEl.tuneableFrequencyDisplay(); + displayEl.on('frequencychange', function(event, freq) { + self.getDemodulator().set_offset_frequency(freq - self.center_freq); + }); + + this.mouseFrequencyDisplay = el.find('.webrx-mouse-freq').frequencyDisplay(); + + Modes.registerModePanel(this); + el.on('click', '.openwebrx-demodulator-button', function() { + var modulation = $(this).data('modulation'); + if (modulation) { + if (self.mode && self.mode.type === 'digimode' && self.mode.underlying.indexOf(modulation) >= 0) { + // keep the mode, just switch underlying modulation + self.setMode(self.mode.modulation, modulation) + } else { + self.setMode(modulation); + } + } else { + self.disableDigiMode(); + } + }); + el.on('change', '.openwebrx-secondary-demod-listbox', function() { + var value = $(this).val(); + if (value === 'none') { + self.disableDigiMode(); + } else { + self.setMode(value); + } + }); + el.on('click', '.openwebrx-squelch-auto', function() { + if (!self.squelchAvailable()) return; + el.find('.openwebrx-squelch-slider').val(getLogSmeterValue(smeter_level) + self.getSquelchMargin()); + self.updateSquelch(); + }); + el.on('change', '.openwebrx-squelch-slider', function() { + self.updateSquelch(); + }); + window.addEventListener('hashchange', function() { + self.onHashChange(); + }); +}; + +DemodulatorPanel.prototype.render = function() { + var available = Modes.getModes().filter(function(m){ return m.isAvailable(); }); + var normalModes = available.filter(function(m){ return m.type === 'analog'; }); + var digiModes = available.filter(function(m){ return m.type === 'digimode'; }); + + var html = [] + + var buttons = normalModes.map(function(m){ + return $( + '
' + m.name + '
' + ); + }); + + var $modegrid = $('
'); + $modegrid.append.apply($modegrid, buttons); + html.push($modegrid); + + html.push($( + '
' + + '
DIG
' + + '' + + '
' + )); + + this.el.find(".openwebrx-modes").html(html); +}; + +DemodulatorPanel.prototype.setMode = function(requestedModulation, underlyingModulation) { + var mode = Modes.findByModulation(requestedModulation); + if (!mode) { + return; + } + + if (this.mode === mode && this.underlyingModulation === underlyingModulation) { + return; + } + if (!mode.isAvailable()) { + divlog('Modulation "' + mode.name + '" not supported. Please check the feature report', true); + return; + } + + var modulation; + if (mode.type === 'digimode') { + modulation = underlyingModulation = underlyingModulation || mode.underlying[0]; + } else { + underlyingModulation = undefined; + modulation = mode.modulation; + } + + var current = this.collectParams(); + if (this.demodulator) { + current.offset_frequency = this.demodulator.get_offset_frequency(); + current.squelch_level = this.demodulator.getSquelch(); + } + + this.stopDemodulator(); + this.demodulator = new Demodulator(current.offset_frequency, modulation); + this.demodulator.setSquelch(current.squelch_level); + + var self = this; + var updateFrequency = function(freq) { + self.tuneableFrequencyDisplay.setFrequency(self.center_freq + freq); + self.updateHash(); + }; + this.demodulator.on("frequencychange", updateFrequency); + updateFrequency(this.demodulator.get_offset_frequency()); + var updateSquelch = function(squelch) { + self.el.find('.openwebrx-squelch-slider') + .val(squelch) + .attr('title', 'Squelch (' + squelch + ' dB)'); + self.updateHash(); + }; + this.demodulator.on('squelchchange', updateSquelch); + updateSquelch(this.demodulator.getSquelch()); + + if (mode.type === 'digimode') { + this.demodulator.set_secondary_demod(mode.modulation); + var uMode = Modes.findByModulation(underlyingModulation); + var bandpass = mode.bandpass || (uMode && uMode.bandpass); + if (bandpass) { + this.demodulator.setBandpass(bandpass); + } else { + this.demodulator.disableBandpass(); + } + var ifRate = mode.ifRate || (uMode && uMode.ifRate); + this.demodulator.setIfRate(ifRate); + } else { + this.demodulator.set_secondary_demod(false); + } + + this.demodulator.start(); + this.mode = mode; + this.underlyingModulation = underlyingModulation; + + this.updateButtons(); + this.updatePanels(); + this.updateHash(); +}; + +DemodulatorPanel.prototype.disableDigiMode = function() { + this.setMode(this.getDemodulator().get_modulation()); +}; + +DemodulatorPanel.prototype.updatePanels = function() { + var modulation = this.getDemodulator().get_secondary_demod(); + $('#openwebrx-panel-digimodes').attr('data-mode', modulation); + var mode = Modes.findByModulation(modulation); + toggle_panel("openwebrx-panel-digimodes", modulation && (!mode || mode.secondaryFft)); + // WSJT-X modes share the same panel + toggle_panel("openwebrx-panel-wsjt-message", ['ft8', 'wspr', 'jt65', 'jt9', 'ft4', 'fst4', 'fst4w', "q65", "msk144"].indexOf(modulation) >= 0); + // these modes come with their own + ['js8', 'packet', 'pocsag', 'adsb', 'ism', 'hfdl', 'vdl2'].forEach(function(m) { + toggle_panel('openwebrx-panel-' + m + '-message', modulation === m); + }); + + modulation = this.getDemodulator().get_modulation(); + var showing = 'openwebrx-panel-metadata-' + modulation; + var metaPanels = $(".openwebrx-meta-panel"); + metaPanels.each(function (_, p) { + toggle_panel(p.id, p.id === showing && !p.classList.contains('disabled')); + }); + metaPanels.metaPanel().each(function() { + this.clear(); + }); +}; + +DemodulatorPanel.prototype.getDemodulator = function() { + return this.demodulator; +}; + +DemodulatorPanel.prototype.collectParams = function() { + var defaults = { + offset_frequency: 0, + squelch_level: -150, + mod: 'nfm' + } + return $.extend(new Object(), defaults, this.validateInitialParams(this.initialParams), this.transformHashParams(this.parseHash())); +}; + +DemodulatorPanel.prototype.startDemodulator = function() { + if (!Modes.initComplete() || !this.center_freq) return; + var params = this.collectParams(); + this._apply(params); +}; + +DemodulatorPanel.prototype.stopDemodulator = function() { + if (!this.demodulator) { + return; + } + this.demodulator.stop(); + this.demodulator = null; + this.mode = null; +} + +DemodulatorPanel.prototype._apply = function(params) { + if (params.secondary_mod) { + this.setMode(params.secondary_mod, params.mod) + } else { + this.setMode(params.mod); + } + this.getDemodulator().set_offset_frequency(params.offset_frequency); + this.getDemodulator().setSquelch(params.squelch_level); + this.updateButtons(); +}; + +DemodulatorPanel.prototype.setInitialParams = function(params) { + $.extend(this.initialParams, params); +}; + +DemodulatorPanel.prototype.resetInitialParams = function() { + this.initialParams = {}; +}; + +DemodulatorPanel.prototype.onHashChange = function() { + this._apply(this.transformHashParams(this.parseHash())); +}; + +DemodulatorPanel.prototype.transformHashParams = function(params) { + var ret = { + mod: params.mod + }; + if (typeof(params.secondary_mod) !== 'undefined') ret.secondary_mod = params.secondary_mod; + if (typeof(params.offset_frequency) !== 'undefined') ret.offset_frequency = params.offset_frequency; + if (typeof(params.sql) !== 'undefined') ret.squelch_level = parseInt(params.sql); + return ret; +}; + +DemodulatorPanel.prototype.squelchAvailable = function () { + return this.mode && this.mode.squelch; +} + +DemodulatorPanel.prototype.updateButtons = function() { + var $buttons = this.el.find(".openwebrx-demodulator-button"); + $buttons.removeClass("highlighted").removeClass('same-mod'); + var demod = this.getDemodulator() + if (!demod) return; + this.el.find('[data-modulation=' + demod.get_modulation() + ']').addClass("highlighted"); + var secondary_demod = demod.get_secondary_demod() + if (secondary_demod) { + this.el.find(".openwebrx-button-dig").addClass("highlighted"); + this.el.find('.openwebrx-secondary-demod-listbox').val(secondary_demod); + var mode = Modes.findByModulation(secondary_demod); + if (mode) { + var self = this; + mode.underlying.filter(function(m) { + return m !== demod.get_modulation(); + }).forEach(function(m) { + self.el.find('[data-modulation=' + m + ']').addClass('same-mod') + }); + } + } else { + this.el.find('.openwebrx-secondary-demod-listbox').val('none'); + } + var squelch_disabled = !this.squelchAvailable(); + this.el.find('.openwebrx-squelch-slider').prop('disabled', squelch_disabled); + this.el.find('.openwebrx-squelch-auto')[squelch_disabled ? 'addClass' : 'removeClass']('disabled'); +} + +DemodulatorPanel.prototype.setCenterFrequency = function(center_freq) { + var me = this; + if (me.centerFreqTimeout) { + clearTimeout(me.centerFreqTimeout); + me.centerFreqTimeout = false; + } + this.centerFreqTimeout = setTimeout(function() { + me.stopDemodulator(); + me.center_freq = center_freq; + me.startDemodulator(); + me.centerFreqTimeout = false; + }, 50); +}; + +DemodulatorPanel.prototype.parseHash = function() { + if (!window.location.hash) { + return {}; + } + var params = window.location.hash.substring(1).split(",").map(function(x) { + var harr = x.split('='); + return [harr[0], harr.slice(1).join('=')]; + }).reduce(function(params, p){ + params[p[0]] = p[1]; + return params; + }, {}); + + return this.validateHash(params); +}; + +DemodulatorPanel.prototype.validateHash = function(params) { + var self = this; + params = Object.keys(params).filter(function(key) { + if (key == 'freq' || key == 'mod' || key == 'secondary_mod' || key == 'sql') { + return params.freq && Math.abs(params.freq - self.center_freq) <= bandwidth / 2; + } + return true; + }).reduce(function(p, key) { + p[key] = params[key]; + return p; + }, {}); + + if (params['freq']) { + params['offset_frequency'] = params['freq'] - self.center_freq; + delete params['freq']; + } + + return params; +}; + +DemodulatorPanel.prototype.validateInitialParams = function(params) { + return Object.fromEntries( + Object.entries(params).filter(function(a) { + if (a[0] == "offset_frequency") { + return Math.abs(a[1]) <= bandwidth / 2; + } + return true; + }) + ); +}; + +DemodulatorPanel.prototype.updateHash = function() { + var demod = this.getDemodulator(); + if (!demod) return; + var self = this; + window.location.hash = $.map({ + freq: demod.get_offset_frequency() + self.center_freq, + mod: demod.get_modulation(), + secondary_mod: demod.get_secondary_demod(), + sql: demod.getSquelch() + }, function(value, key){ + if (typeof(value) === 'undefined' || value === false) return undefined; + return key + '=' + value; + }).filter(function(v) { + return !!v; + }).join(','); +}; + +DemodulatorPanel.prototype.updateSquelch = function() { + var sliderValue = parseInt(this.el.find(".openwebrx-squelch-slider").val()); + var demod = this.getDemodulator(); + if (demod) demod.setSquelch(sliderValue); +}; + +DemodulatorPanel.prototype.setSquelchMargin = function(margin) { + if (typeof(margin) === 'undefined' || this.squelchMargin == margin) return; + this.squelchMargin = margin; +}; + +DemodulatorPanel.prototype.getSquelchMargin = function() { + return this.squelchMargin; +}; + +DemodulatorPanel.prototype.setMouseFrequency = function(freq) { + this.mouseFrequencyDisplay.setFrequency(freq); +}; + +DemodulatorPanel.prototype.setTuningPrecision = function(precision) { + this.tuneableFrequencyDisplay.setTuningPrecision(precision); + this.mouseFrequencyDisplay.setTuningPrecision(precision); +}; + +$.fn.demodulatorPanel = function(){ + if (!this.data('panel')) { + this.data('panel', new DemodulatorPanel(this)); + } + return this.data('panel'); +}; \ No newline at end of file diff --git a/htdocs/lib/FrequencyDisplay.js b/htdocs/lib/FrequencyDisplay.js new file mode 100644 index 000000000..13390772c --- /dev/null +++ b/htdocs/lib/FrequencyDisplay.js @@ -0,0 +1,183 @@ +function FrequencyDisplay(element) { + this.suffixes = { + '': 0, + 'k': 3, + 'M': 6, + 'G': 9, + 'T': 12 + }; + this.element = $(element); + this.digits = []; + this.precision = 2; + this.setupElements(); + this.setFrequency(0); +} + +FrequencyDisplay.prototype.setupElements = function() { + this.displayContainer = $('
'); + this.digitContainer = $(''); + this.unitContainer = $(' Hz'); + this.displayContainer.html([this.digitContainer, this.unitContainer]); + this.element.html(this.displayContainer); +}; + +FrequencyDisplay.prototype.getSuffix = function() { + var me = this; + return Object.keys(me.suffixes).filter(function(key){ + return me.suffixes[key] == me.exponent; + })[0] || ""; +}; + +FrequencyDisplay.prototype.setFrequency = function(freq) { + this.frequency = freq; + if (this.frequency === 0 || Number.isNaN(this.frequency)) { + this.exponent = 0 + } else { + this.exponent = Math.floor(Math.log10(this.frequency) / 3) * 3; + } + + var digits = Math.max(0, this.exponent - this.precision); + var formatted = (freq / 10 ** this.exponent).toLocaleString( + undefined, + {maximumFractionDigits: digits, minimumFractionDigits: digits} + ); + var children = this.digitContainer.children(); + for (var i = 0; i < formatted.length; i++) { + if (!this.digits[i]) { + this.digits[i] = $(''); + var before = children[i]; + if (before) { + $(before).after(this.digits[i]); + } else { + this.digitContainer.append(this.digits[i]); + } + } + this.digits[i][(isNaN(formatted[i]) ? 'remove' : 'add') + 'Class']('digit'); + this.digits[i].html(formatted[i]); + } + while (this.digits.length > formatted.length) { + this.digits.pop().remove(); + } + this.unitContainer.text(' ' + this.getSuffix() + 'Hz'); +}; + +FrequencyDisplay.prototype.setTuningPrecision = function(precision) { + if (typeof(precision) == 'undefined') return; + this.precision = precision; + this.setFrequency(this.frequency); +}; + +function TuneableFrequencyDisplay(element) { + FrequencyDisplay.call(this, element); + this.setupEvents(); +} + +TuneableFrequencyDisplay.prototype = new FrequencyDisplay(); + +TuneableFrequencyDisplay.prototype.setupElements = function() { + FrequencyDisplay.prototype.setupElements.call(this); + this.input = $(''); + this.suffixInput = $(''); + this.$select.on("change", function() { + me.service_id = parseInt($(this).val()); + $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().setDabServiceId(me.service_id); + }); + var $container = $( + '
' + + '
' + + '
' + + '
' + + '' + + '
' + ); + $container.append(this.$select); + $(this.el).append($container); + this.clear(); + this.programmeTimeout = false; +} + +DabMetaPanel.prototype = new MetaPanel(); + +DabMetaPanel.prototype.isSupported = function(data) { + return this.modes.includes(data.mode); +} + + +DabMetaPanel.prototype.update = function(data) { + if (!this.isSupported(data)) return; + + if ('ensemble_id' in data) { + $(this.el).find('.dab-ensemble-id').text('0x' + data.ensemble_id.toString(16)); + } + + if ('ensemble_label' in data) { + $(this.el).find('.dab-ensemble-label').text(data.ensemble_label); + } + + if ('timestamp' in data) { + var date = new Date(data.timestamp * 1000); + $(this.el).find('.dab-timestamp').text(date.toLocaleString([], {dateStyle: 'short', timeStyle: 'medium'})); + } + + if ('programmes' in data) { + var options = Object.entries(data.programmes).map(function(e) { + return ''; + }); + this.$select.html( + options.join('') + + '' + ); + + var me = this; + if (this.programmeTimeout) clearTimeout(this.programmeTimeout); + this.programmeTimeout = setTimeout(function() { + // user has selected a programme to play. don't interfere. + me.$select.val(this.service_id); + if (me.$select.val()) return; + me.$select.val(me.$select.find('option:first').val()).change(); + }, 1000); + } +} + +DabMetaPanel.prototype.clear = function() { + this.service_id = 0; + $(this.el).find('.dab-auto-clear').empty(); + this.$select.html( + '' + ); +} + +MetaPanel.types = { + dmr: DmrMetaPanel, + ysf: YsfMetaPanel, + dstar: DStarMetaPanel, + nxdn: NxdnMetaPanel, + m17: M17MetaPanel, + wfm: WfmMetaPanel, + dab: DabMetaPanel, +}; + +$.fn.metaPanel = function() { + return this.map(function() { + var $self = $(this); + if (!$self.data('metapanel')) { + var matches = /^openwebrx-panel-metadata-([a-z0-9]+)$/.exec($self.prop('id')); + var constructor = matches && MetaPanel.types[matches[1]] || MetaPanel; + $self.data('metapanel', new constructor($self)); + } + return $self.data('metapanel'); + }); +}; \ No newline at end of file diff --git a/htdocs/lib/Modes.js b/htdocs/lib/Modes.js new file mode 100644 index 000000000..0bdac90bb --- /dev/null +++ b/htdocs/lib/Modes.js @@ -0,0 +1,59 @@ +var Modes = { + modes: [], + features: {}, + panels: [], + setModes:function(json){ + this.modes = json.map(function(m){ return new Mode(m); }); + this.updatePanels(); + $('#openwebrx-dialog-bookmark').bookmarkDialog().setModes(this.modes); + }, + getModes:function(){ + return this.modes; + }, + setFeatures:function(features){ + this.features = features; + this.updatePanels(); + }, + findByModulation:function(modulation){ + matches = this.modes.filter(function(m) { return m.modulation === modulation; }); + if (matches.length) return matches[0] + }, + registerModePanel: function(el) { + this.panels.push(el); + }, + initComplete: function() { + return this.modes.length && Object.keys(this.features).length; + }, + updatePanels: function() { + this.panels.forEach(function(p) { + p.render(); + p.startDemodulator(); + }); + } +}; + +var Mode = function(json){ + this.modulation = json.modulation; + this.name = json.name; + this.type = json.type; + this.requirements = json.requirements; + this.squelch = json.squelch; + if (json.bandpass) { + this.bandpass = json.bandpass; + } + if (json.ifRate) { + this.ifRate = json.ifRate; + } + if (this.type === 'digimode') { + this.underlying = json.underlying; + this.secondaryFft = json.secondaryFft; + } +}; + +Mode.prototype.isAvailable = function(){ + return this.requirements.map(function(r){ + return Modes.features[r]; + }).reduce(function(a, b){ + return a && b; + }, true); +}; diff --git a/htdocs/lib/PlaneMarker.js b/htdocs/lib/PlaneMarker.js new file mode 100644 index 000000000..77635cb3c --- /dev/null +++ b/htdocs/lib/PlaneMarker.js @@ -0,0 +1,108 @@ +function PlaneMarker(){} + +PlaneMarker.prototype = new google.maps.OverlayView(); + +PlaneMarker.prototype.draw = function() { + var svg = this.svg; + if (!svg) return; + + var angle = this.groundtrack || this.heading; + if (angle) { + svg.style.transform = 'rotate(' + angle + 'deg)'; + } else { + svg.style.transform = null; + } + + if (this.opacity) { + svg.style.opacity = this.opacity; + } else { + svg.style.opacity = null; + } + + var point = this.getProjection().fromLatLngToDivPixel(this.position); + + if (point) { + svg.style.left = point.x - 15 + 'px'; + svg.style.top = point.y - 15 + 'px'; + } + + svg.setAttribute('fill', this.getMarkerColor()); +}; + +PlaneMarker.prototype.setOptions = function(options) { + google.maps.OverlayView.prototype.setOptions.apply(this, arguments); + this.draw(); +}; + +PlaneMarker.prototype.onAdd = function() { + var svg = this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('viewBox', '0 0 65 65'); + svg.setAttribute('fill', this.getMarkerColor()); + svg.setAttribute('stroke', 'black'); + + var path = document.createElementNS('http://www.w3.org/2000/svg', 'path') + path.setAttribute('d', 'M 0,0 M 1.9565564,41.694305 C 1.7174505,40.497708 1.6419973,38.448747 1.8096508,37.70494 1.8936398,37.332056 2.0796653,36.88191 2.222907,36.70461 2.4497603,36.423844 4.087816,35.47248 14.917931,29.331528 l 12.434577,-7.050718 -0.04295,-7.613412 c -0.03657,-6.4844888 -0.01164,-7.7625804 0.168134,-8.6194061 0.276129,-1.3160905 0.762276,-2.5869575 1.347875,-3.5235502 l 0.472298,-0.7553719 1.083746,-0.6085497 c 1.194146,-0.67053522 1.399524,-0.71738842 2.146113,-0.48960552 1.077005,0.3285939 2.06344,1.41299352 2.797602,3.07543322 0.462378,1.0469993 0.978731,2.7738408 1.047635,3.5036272 0.02421,0.2570284 0.06357,3.78334 0.08732,7.836246 0.02375,4.052905 0.0658,7.409251 0.09345,7.458546 0.02764,0.04929 5.600384,3.561772 12.38386,7.805502 l 12.333598,7.715871 0.537584,0.959688 c 0.626485,1.118378 0.651686,1.311286 0.459287,3.516442 -0.175469,2.011604 -0.608966,2.863924 -1.590344,3.127136 -0.748529,0.200763 -1.293144,0.03637 -10.184829,-3.07436 C 48.007733,41.72562 44.793806,40.60197 43.35084,40.098045 l -2.623567,-0.916227 -1.981212,-0.06614 c -1.089663,-0.03638 -1.985079,-0.05089 -1.989804,-0.03225 -0.0052,0.01863 -0.02396,2.421278 -0.04267,5.339183 -0.0395,6.147742 -0.143635,7.215456 -0.862956,8.845475 l -0.300457,0.680872 2.91906,1.361455 c 2.929379,1.366269 3.714195,1.835385 4.04589,2.41841 0.368292,0.647353 0.594634,2.901439 0.395779,3.941627 -0.0705,0.368571 -0.106308,0.404853 -0.765159,0.773916 L 41.4545,62.83158 39.259237,62.80426 c -6.030106,-0.07507 -16.19508,-0.495041 -16.870991,-0.697033 -0.359409,-0.107405 -0.523792,-0.227482 -0.741884,-0.541926 -0.250591,-0.361297 -0.28386,-0.522402 -0.315075,-1.52589 -0.06327,-2.03378 0.23288,-3.033615 1.077963,-3.639283 0.307525,-0.2204 4.818478,-2.133627 6.017853,-2.552345 0.247872,-0.08654 0.247455,-0.102501 -0.01855,-0.711959 -0.330395,-0.756986 -0.708622,-2.221756 -0.832676,-3.224748 -0.05031,-0.406952 -0.133825,-3.078805 -0.185533,-5.937448 -0.0517,-2.858644 -0.145909,-5.208974 -0.209316,-5.222958 -0.06341,-0.01399 -0.974464,-0.0493 -2.024551,-0.07845 L 23.247235,38.61921 18.831373,39.8906 C 4.9432155,43.88916 4.2929558,44.057819 3.4954426,43.86823 2.7487826,43.690732 2.2007966,42.916622 1.9565564,41.694305 z') + svg.appendChild(path); + + svg.style.position = 'absolute'; + svg.style.cursor = 'pointer'; + svg.style.width = '30px'; + svg.style.height = '30px'; + + var self = this; + svg.addEventListener("click", function(event) { + event.stopPropagation(); + google.maps.event.trigger(self, "click", event); + }); + + var panes = this.getPanes(); + panes.overlayImage.appendChild(svg); +}; + +PlaneMarker.prototype.onRemove = function() { + if (this.svg) { + this.svg.parentNode.removeChild(this.svg); + this.svg = null; + } +}; + +PlaneMarker.prototype.getMarkerColor = function() { + var toHsl = function(input) { + return 'hsl(' + input.h + ', ' + input.s + '%, ' + input.l + '%)' + }; + + if (!this.altitude) { + return toHsl({h: 0, s: 0, l: 40}); + } + if (this.altitude === "ground") { + return toHsl({h: 120, s: 100, l: 30}); + } + + // find the pair of points the current altitude lies between, + // and interpolate the hue between those points + var hpoints = [ + { alt: 2000, val: 20 }, // orange + { alt: 10000, val: 140 }, // light green + { alt: 40000, val: 300 } + ]; + var h = hpoints[0].val; + + for (var i = hpoints.length-1; i >= 0; --i) { + if (this.altitude > hpoints[i].alt) { + if (i === hpoints.length - 1) { + h = hpoints[i].val; + } else { + h = hpoints[i].val + (hpoints[i+1].val - hpoints[i].val) * (this.altitude - hpoints[i].alt) / (hpoints[i+1].alt - hpoints[i].alt) + } + break; + } + } + + if (h < 0) { + h = (h % 360) + 360; + } else if (h >= 360) { + h = h % 360; + } + + return toHsl({h: h, s: 85, l: 50}) +} \ No newline at end of file diff --git a/htdocs/lib/ProgressBar.js b/htdocs/lib/ProgressBar.js new file mode 100644 index 000000000..9b791adc8 --- /dev/null +++ b/htdocs/lib/ProgressBar.js @@ -0,0 +1,164 @@ +ProgressBar = function(el) { + this.$el = $(el); + this.$innerText = $('' + this.getDefaultText() + ''); + this.$innerBar = $('
'); + this.$el.empty().append(this.$innerText, this.$innerBar); +}; + +ProgressBar.prototype.getDefaultText = function() { + return ''; +} + +ProgressBar.prototype.set = function(val, text, over) { + this.setValue(val); + this.setText(text); + this.setOver(over); +}; + +ProgressBar.prototype.setValue = function(val) { + if (val < 0) val = 0; + if (val > 1) val = 1; + this.$innerBar.css({transform: 'translate(' + ((val - 1) * 100) + '%) translateZ(0)'}); +}; + +ProgressBar.prototype.setText = function(text) { + this.$innerText.html(text); +}; + +ProgressBar.prototype.setOver = function(over) { + this.$el[over ? 'addClass' : 'removeClass']('openwebrx-progressbar--over'); +}; + +AudioBufferProgressBar = function(el) { + ProgressBar.call(this, el); +}; + +AudioBufferProgressBar.prototype = new ProgressBar(); + +AudioBufferProgressBar.prototype.getDefaultText = function() { + return 'Audio buffer [0 ms]'; +}; + +AudioBufferProgressBar.prototype.setSampleRate = function(sampleRate) { + this.sampleRate = sampleRate; +}; + +AudioBufferProgressBar.prototype.setBuffersize = function(buffersize) { + var audio_buffer_value = buffersize / this.sampleRate; + var overrun = audio_buffer_value > audio_buffer_maximal_length_sec; + var underrun = audio_buffer_value === 0; + var text = "buffer"; + if (overrun) { + text = "overrun"; + } + if (underrun) { + text = "underrun"; + } + this.set(audio_buffer_value, "Audio " + text + " [" + (audio_buffer_value).toFixed(1) + " s]", overrun || underrun); +}; + + +NetworkSpeedProgressBar = function(el) { + ProgressBar.call(this, el); +}; + +NetworkSpeedProgressBar.prototype = new ProgressBar(); + +NetworkSpeedProgressBar.prototype.getDefaultText = function() { + return 'Network usage [0 kbps]'; +}; + +NetworkSpeedProgressBar.prototype.setSpeed = function(speed) { + var speedInKilobits = speed * 8 / 1000; + this.set(speedInKilobits / 2000, "Network usage [" + speedInKilobits.toFixed(1) + " kbps]", false); +}; + +AudioSpeedProgressBar = function(el) { + ProgressBar.call(this, el); +}; + +AudioSpeedProgressBar.prototype = new ProgressBar(); + +AudioSpeedProgressBar.prototype.getDefaultText = function() { + return 'Audio stream [0 kbps]'; +}; + +AudioSpeedProgressBar.prototype.setSpeed = function(speed) { + this.set(speed / 1000000, "Audio stream [" + (speed / 1000).toFixed(0) + " kbps]", false); +}; + +AudioOutputProgressBar = function(el, sampleRate) { + ProgressBar.call(this, el); +}; + +AudioOutputProgressBar.prototype = new ProgressBar(); + +AudioOutputProgressBar.prototype.getDefaultText = function() { + return 'Audio output [0 sps]'; +}; + +AudioOutputProgressBar.prototype.setSampleRate = function(sampleRate) { + this.maxRate = sampleRate * 1.25; + this.minRate = sampleRate * .25; +}; + +AudioOutputProgressBar.prototype.setAudioRate = function(audioRate) { + this.set(audioRate / this.maxRate, "Audio output [" + (audioRate / 1000).toFixed(1) + " ksps]", audioRate > this.maxRate || audioRate < this.minRate); +}; + +ClientsProgressBar = function(el) { + ProgressBar.call(this, el); + this.clients = 0; + this.maxClients = 0; +}; + +ClientsProgressBar.prototype = new ProgressBar(); + +ClientsProgressBar.prototype.getDefaultText = function() { + return 'Clients [1]'; +}; + +ClientsProgressBar.prototype.setClients = function(clients) { + this.clients = clients; + this.render(); +}; + +ClientsProgressBar.prototype.setMaxClients = function(maxClients) { + this.maxClients = maxClients; + this.render(); +}; + +ClientsProgressBar.prototype.render = function() { + this.set(this.clients / this.maxClients, "Clients [" + this.clients + "]", this.clients > this.maxClients * 0.85); +}; + +CpuProgressBar = function(el) { + ProgressBar.call(this, el); +}; + +CpuProgressBar.prototype = new ProgressBar(); + +CpuProgressBar.prototype.getDefaultText = function() { + return 'Server CPU [0%]'; +}; + +CpuProgressBar.prototype.setUsage = function(usage) { + this.set(usage, "Server CPU [" + Math.round(usage * 100) + "%]", usage > .85); +}; + +ProgressBar.types = { + cpu: CpuProgressBar, + audiobuffer: AudioBufferProgressBar, + audiospeed: AudioSpeedProgressBar, + audiooutput: AudioOutputProgressBar, + clients: ClientsProgressBar, + networkspeed: NetworkSpeedProgressBar +} + +$.fn.progressbar = function() { + if (!this.data('progressbar')) { + var constructor = ProgressBar.types[this.data('type')] || ProgressBar; + this.data('progressbar', new constructor(this)); + } + return this.data('progressbar'); +}; diff --git a/htdocs/lib/bootstrap.bundle.min.js b/htdocs/lib/bootstrap.bundle.min.js new file mode 100644 index 000000000..afeb4bd20 --- /dev/null +++ b/htdocs/lib/bootstrap.bundle.min.js @@ -0,0 +1,6 @@ +/*! + * Bootstrap v4.5.0 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery")):"function"==typeof define&&define.amd?define(["exports","jquery"],e):e((t=t||self).bootstrap={},t.jQuery)}(this,(function(t,e){"use strict";function n(t,e){for(var n=0;n=4)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}};l.jQueryDetection(),e.fn.emulateTransitionEnd=a,e.event.special[l.TRANSITION_END]={bindType:"transitionend",delegateType:"transitionend",handle:function(t){if(e(t.target).is(this))return t.handleObj.handler.apply(this,arguments)}};var c="alert",u=e.fn[c],h=function(){function t(t){this._element=t}var n=t.prototype;return n.close=function(t){var e=this._element;t&&(e=this._getRootElement(t)),this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},n.dispose=function(){e.removeData(this._element,"bs.alert"),this._element=null},n._getRootElement=function(t){var n=l.getSelectorFromElement(t),i=!1;return n&&(i=document.querySelector(n)),i||(i=e(t).closest(".alert")[0]),i},n._triggerCloseEvent=function(t){var n=e.Event("close.bs.alert");return e(t).trigger(n),n},n._removeElement=function(t){var n=this;if(e(t).removeClass("show"),e(t).hasClass("fade")){var i=l.getTransitionDurationFromElement(t);e(t).one(l.TRANSITION_END,(function(e){return n._destroyElement(t,e)})).emulateTransitionEnd(i)}else this._destroyElement(t)},n._destroyElement=function(t){e(t).detach().trigger("closed.bs.alert").remove()},t._jQueryInterface=function(n){return this.each((function(){var i=e(this),o=i.data("bs.alert");o||(o=new t(this),i.data("bs.alert",o)),"close"===n&&o[n](this)}))},t._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},i(t,null,[{key:"VERSION",get:function(){return"4.5.0"}}]),t}();e(document).on("click.bs.alert.data-api",'[data-dismiss="alert"]',h._handleDismiss(new h)),e.fn[c]=h._jQueryInterface,e.fn[c].Constructor=h,e.fn[c].noConflict=function(){return e.fn[c]=u,h._jQueryInterface};var f=e.fn.button,d=function(){function t(t){this._element=t}var n=t.prototype;return n.toggle=function(){var t=!0,n=!0,i=e(this._element).closest('[data-toggle="buttons"]')[0];if(i){var o=this._element.querySelector('input:not([type="hidden"])');if(o){if("radio"===o.type)if(o.checked&&this._element.classList.contains("active"))t=!1;else{var r=i.querySelector(".active");r&&e(r).removeClass("active")}t&&("checkbox"!==o.type&&"radio"!==o.type||(o.checked=!this._element.classList.contains("active")),e(o).trigger("change")),o.focus(),n=!1}}this._element.hasAttribute("disabled")||this._element.classList.contains("disabled")||(n&&this._element.setAttribute("aria-pressed",!this._element.classList.contains("active")),t&&e(this._element).toggleClass("active"))},n.dispose=function(){e.removeData(this._element,"bs.button"),this._element=null},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.button");i||(i=new t(this),e(this).data("bs.button",i)),"toggle"===n&&i[n]()}))},i(t,null,[{key:"VERSION",get:function(){return"4.5.0"}}]),t}();e(document).on("click.bs.button.data-api",'[data-toggle^="button"]',(function(t){var n=t.target,i=n;if(e(n).hasClass("btn")||(n=e(n).closest(".btn")[0]),!n||n.hasAttribute("disabled")||n.classList.contains("disabled"))t.preventDefault();else{var o=n.querySelector('input:not([type="hidden"])');if(o&&(o.hasAttribute("disabled")||o.classList.contains("disabled")))return void t.preventDefault();"LABEL"===i.tagName&&o&&"checkbox"===o.type&&t.preventDefault(),d._jQueryInterface.call(e(n),"toggle")}})).on("focus.bs.button.data-api blur.bs.button.data-api",'[data-toggle^="button"]',(function(t){var n=e(t.target).closest(".btn")[0];e(n).toggleClass("focus",/^focus(in)?$/.test(t.type))})),e(window).on("load.bs.button.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-toggle="buttons"] .btn')),e=0,n=t.length;e0,this._pointerEvent=Boolean(window.PointerEvent||window.MSPointerEvent),this._addEventListeners()}var n=t.prototype;return n.next=function(){this._isSliding||this._slide("next")},n.nextWhenVisible=function(){!document.hidden&&e(this._element).is(":visible")&&"hidden"!==e(this._element).css("visibility")&&this.next()},n.prev=function(){this._isSliding||this._slide("prev")},n.pause=function(t){t||(this._isPaused=!0),this._element.querySelector(".carousel-item-next, .carousel-item-prev")&&(l.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},n.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},n.to=function(t){var n=this;this._activeElement=this._element.querySelector(".active.carousel-item");var i=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)e(this._element).one("slid.bs.carousel",(function(){return n.to(t)}));else{if(i===t)return this.pause(),void this.cycle();var o=t>i?"next":"prev";this._slide(o,this._items[t])}},n.dispose=function(){e(this._element).off(m),e.removeData(this._element,"bs.carousel"),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},n._getConfig=function(t){return t=s(s({},v),t),l.typeCheckConfig(p,t,_),t},n._handleSwipe=function(){var t=Math.abs(this.touchDeltaX);if(!(t<=40)){var e=t/this.touchDeltaX;this.touchDeltaX=0,e>0&&this.prev(),e<0&&this.next()}},n._addEventListeners=function(){var t=this;this._config.keyboard&&e(this._element).on("keydown.bs.carousel",(function(e){return t._keydown(e)})),"hover"===this._config.pause&&e(this._element).on("mouseenter.bs.carousel",(function(e){return t.pause(e)})).on("mouseleave.bs.carousel",(function(e){return t.cycle(e)})),this._config.touch&&this._addTouchEventListeners()},n._addTouchEventListeners=function(){var t=this;if(this._touchSupported){var n=function(e){t._pointerEvent&&b[e.originalEvent.pointerType.toUpperCase()]?t.touchStartX=e.originalEvent.clientX:t._pointerEvent||(t.touchStartX=e.originalEvent.touches[0].clientX)},i=function(e){t._pointerEvent&&b[e.originalEvent.pointerType.toUpperCase()]&&(t.touchDeltaX=e.originalEvent.clientX-t.touchStartX),t._handleSwipe(),"hover"===t._config.pause&&(t.pause(),t.touchTimeout&&clearTimeout(t.touchTimeout),t.touchTimeout=setTimeout((function(e){return t.cycle(e)}),500+t._config.interval))};e(this._element.querySelectorAll(".carousel-item img")).on("dragstart.bs.carousel",(function(t){return t.preventDefault()})),this._pointerEvent?(e(this._element).on("pointerdown.bs.carousel",(function(t){return n(t)})),e(this._element).on("pointerup.bs.carousel",(function(t){return i(t)})),this._element.classList.add("pointer-event")):(e(this._element).on("touchstart.bs.carousel",(function(t){return n(t)})),e(this._element).on("touchmove.bs.carousel",(function(e){return function(e){e.originalEvent.touches&&e.originalEvent.touches.length>1?t.touchDeltaX=0:t.touchDeltaX=e.originalEvent.touches[0].clientX-t.touchStartX}(e)})),e(this._element).on("touchend.bs.carousel",(function(t){return i(t)})))}},n._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case 37:t.preventDefault(),this.prev();break;case 39:t.preventDefault(),this.next()}},n._getItemIndex=function(t){return this._items=t&&t.parentNode?[].slice.call(t.parentNode.querySelectorAll(".carousel-item")):[],this._items.indexOf(t)},n._getItemByDirection=function(t,e){var n="next"===t,i="prev"===t,o=this._getItemIndex(e),r=this._items.length-1;if((i&&0===o||n&&o===r)&&!this._config.wrap)return e;var s=(o+("prev"===t?-1:1))%this._items.length;return-1===s?this._items[this._items.length-1]:this._items[s]},n._triggerSlideEvent=function(t,n){var i=this._getItemIndex(t),o=this._getItemIndex(this._element.querySelector(".active.carousel-item")),r=e.Event("slide.bs.carousel",{relatedTarget:t,direction:n,from:o,to:i});return e(this._element).trigger(r),r},n._setActiveIndicatorElement=function(t){if(this._indicatorsElement){var n=[].slice.call(this._indicatorsElement.querySelectorAll(".active"));e(n).removeClass("active");var i=this._indicatorsElement.children[this._getItemIndex(t)];i&&e(i).addClass("active")}},n._slide=function(t,n){var i,o,r,s=this,a=this._element.querySelector(".active.carousel-item"),c=this._getItemIndex(a),u=n||a&&this._getItemByDirection(t,a),h=this._getItemIndex(u),f=Boolean(this._interval);if("next"===t?(i="carousel-item-left",o="carousel-item-next",r="left"):(i="carousel-item-right",o="carousel-item-prev",r="right"),u&&e(u).hasClass("active"))this._isSliding=!1;else if(!this._triggerSlideEvent(u,r).isDefaultPrevented()&&a&&u){this._isSliding=!0,f&&this.pause(),this._setActiveIndicatorElement(u);var d=e.Event("slid.bs.carousel",{relatedTarget:u,direction:r,from:c,to:h});if(e(this._element).hasClass("slide")){e(u).addClass(o),l.reflow(u),e(a).addClass(i),e(u).addClass(i);var p=parseInt(u.getAttribute("data-interval"),10);p?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,this._config.interval=p):this._config.interval=this._config.defaultInterval||this._config.interval;var m=l.getTransitionDurationFromElement(a);e(a).one(l.TRANSITION_END,(function(){e(u).removeClass(i+" "+o).addClass("active"),e(a).removeClass("active "+o+" "+i),s._isSliding=!1,setTimeout((function(){return e(s._element).trigger(d)}),0)})).emulateTransitionEnd(m)}else e(a).removeClass("active"),e(u).addClass("active"),this._isSliding=!1,e(this._element).trigger(d);f&&this.cycle()}},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.carousel"),o=s(s({},v),e(this).data());"object"==typeof n&&(o=s(s({},o),n));var r="string"==typeof n?n:o.slide;if(i||(i=new t(this,o),e(this).data("bs.carousel",i)),"number"==typeof n)i.to(n);else if("string"==typeof r){if("undefined"==typeof i[r])throw new TypeError('No method named "'+r+'"');i[r]()}else o.interval&&o.ride&&(i.pause(),i.cycle())}))},t._dataApiClickHandler=function(n){var i=l.getSelectorFromElement(this);if(i){var o=e(i)[0];if(o&&e(o).hasClass("carousel")){var r=s(s({},e(o).data()),e(this).data()),a=this.getAttribute("data-slide-to");a&&(r.interval=!1),t._jQueryInterface.call(e(o),r),a&&e(o).data("bs.carousel").to(a),n.preventDefault()}}},i(t,null,[{key:"VERSION",get:function(){return"4.5.0"}},{key:"Default",get:function(){return v}}]),t}();e(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",y._dataApiClickHandler),e(window).on("load.bs.carousel.data-api",(function(){for(var t=[].slice.call(document.querySelectorAll('[data-ride="carousel"]')),n=0,i=t.length;n0&&(this._selector=s,this._triggerArray.push(r))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var n=t.prototype;return n.toggle=function(){e(this._element).hasClass("show")?this.hide():this.show()},n.show=function(){var n,i,o=this;if(!this._isTransitioning&&!e(this._element).hasClass("show")&&(this._parent&&0===(n=[].slice.call(this._parent.querySelectorAll(".show, .collapsing")).filter((function(t){return"string"==typeof o._config.parent?t.getAttribute("data-parent")===o._config.parent:t.classList.contains("collapse")}))).length&&(n=null),!(n&&(i=e(n).not(this._selector).data("bs.collapse"))&&i._isTransitioning))){var r=e.Event("show.bs.collapse");if(e(this._element).trigger(r),!r.isDefaultPrevented()){n&&(t._jQueryInterface.call(e(n).not(this._selector),"hide"),i||e(n).data("bs.collapse",null));var s=this._getDimension();e(this._element).removeClass("collapse").addClass("collapsing"),this._element.style[s]=0,this._triggerArray.length&&e(this._triggerArray).removeClass("collapsed").attr("aria-expanded",!0),this.setTransitioning(!0);var a="scroll"+(s[0].toUpperCase()+s.slice(1)),c=l.getTransitionDurationFromElement(this._element);e(this._element).one(l.TRANSITION_END,(function(){e(o._element).removeClass("collapsing").addClass("collapse show"),o._element.style[s]="",o.setTransitioning(!1),e(o._element).trigger("shown.bs.collapse")})).emulateTransitionEnd(c),this._element.style[s]=this._element[a]+"px"}}},n.hide=function(){var t=this;if(!this._isTransitioning&&e(this._element).hasClass("show")){var n=e.Event("hide.bs.collapse");if(e(this._element).trigger(n),!n.isDefaultPrevented()){var i=this._getDimension();this._element.style[i]=this._element.getBoundingClientRect()[i]+"px",l.reflow(this._element),e(this._element).addClass("collapsing").removeClass("collapse show");var o=this._triggerArray.length;if(o>0)for(var r=0;r=0)return 1;return 0}();var N=D&&window.Promise?function(t){var e=!1;return function(){e||(e=!0,window.Promise.resolve().then((function(){e=!1,t()})))}}:function(t){var e=!1;return function(){e||(e=!0,setTimeout((function(){e=!1,t()}),k))}};function O(t){return t&&"[object Function]"==={}.toString.call(t)}function A(t,e){if(1!==t.nodeType)return[];var n=t.ownerDocument.defaultView.getComputedStyle(t,null);return e?n[e]:n}function I(t){return"HTML"===t.nodeName?t:t.parentNode||t.host}function x(t){if(!t)return document.body;switch(t.nodeName){case"HTML":case"BODY":return t.ownerDocument.body;case"#document":return t.body}var e=A(t),n=e.overflow,i=e.overflowX,o=e.overflowY;return/(auto|scroll|overlay)/.test(n+o+i)?t:x(I(t))}function j(t){return t&&t.referenceNode?t.referenceNode:t}var L=D&&!(!window.MSInputMethodContext||!document.documentMode),P=D&&/MSIE 10/.test(navigator.userAgent);function F(t){return 11===t?L:10===t?P:L||P}function R(t){if(!t)return document.documentElement;for(var e=F(10)?document.body:null,n=t.offsetParent||null;n===e&&t.nextElementSibling;)n=(t=t.nextElementSibling).offsetParent;var i=n&&n.nodeName;return i&&"BODY"!==i&&"HTML"!==i?-1!==["TH","TD","TABLE"].indexOf(n.nodeName)&&"static"===A(n,"position")?R(n):n:t?t.ownerDocument.documentElement:document.documentElement}function M(t){return null!==t.parentNode?M(t.parentNode):t}function B(t,e){if(!(t&&t.nodeType&&e&&e.nodeType))return document.documentElement;var n=t.compareDocumentPosition(e)&Node.DOCUMENT_POSITION_FOLLOWING,i=n?t:e,o=n?e:t,r=document.createRange();r.setStart(i,0),r.setEnd(o,0);var s,a,l=r.commonAncestorContainer;if(t!==l&&e!==l||i.contains(o))return"BODY"===(a=(s=l).nodeName)||"HTML"!==a&&R(s.firstElementChild)!==s?R(l):l;var c=M(t);return c.host?B(c.host,e):B(t,M(e).host)}function q(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"top",n="top"===e?"scrollTop":"scrollLeft",i=t.nodeName;if("BODY"===i||"HTML"===i){var o=t.ownerDocument.documentElement,r=t.ownerDocument.scrollingElement||o;return r[n]}return t[n]}function H(t,e){var n=arguments.length>2&&void 0!==arguments[2]&&arguments[2],i=q(e,"top"),o=q(e,"left"),r=n?-1:1;return t.top+=i*r,t.bottom+=i*r,t.left+=o*r,t.right+=o*r,t}function Q(t,e){var n="x"===e?"Left":"Top",i="Left"===n?"Right":"Bottom";return parseFloat(t["border"+n+"Width"],10)+parseFloat(t["border"+i+"Width"],10)}function W(t,e,n,i){return Math.max(e["offset"+t],e["scroll"+t],n["client"+t],n["offset"+t],n["scroll"+t],F(10)?parseInt(n["offset"+t])+parseInt(i["margin"+("Height"===t?"Top":"Left")])+parseInt(i["margin"+("Height"===t?"Bottom":"Right")]):0)}function U(t){var e=t.body,n=t.documentElement,i=F(10)&&getComputedStyle(n);return{height:W("Height",e,n,i),width:W("Width",e,n,i)}}var V=function(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")},Y=function(){function t(t,e){for(var n=0;n2&&void 0!==arguments[2]&&arguments[2],i=F(10),o="HTML"===e.nodeName,r=G(t),s=G(e),a=x(t),l=A(e),c=parseFloat(l.borderTopWidth,10),u=parseFloat(l.borderLeftWidth,10);n&&o&&(s.top=Math.max(s.top,0),s.left=Math.max(s.left,0));var h=K({top:r.top-s.top-c,left:r.left-s.left-u,width:r.width,height:r.height});if(h.marginTop=0,h.marginLeft=0,!i&&o){var f=parseFloat(l.marginTop,10),d=parseFloat(l.marginLeft,10);h.top-=c-f,h.bottom-=c-f,h.left-=u-d,h.right-=u-d,h.marginTop=f,h.marginLeft=d}return(i&&!n?e.contains(a):e===a&&"BODY"!==a.nodeName)&&(h=H(h,e)),h}function J(t){var e=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=t.ownerDocument.documentElement,i=$(t,n),o=Math.max(n.clientWidth,window.innerWidth||0),r=Math.max(n.clientHeight,window.innerHeight||0),s=e?0:q(n),a=e?0:q(n,"left"),l={top:s-i.top+i.marginTop,left:a-i.left+i.marginLeft,width:o,height:r};return K(l)}function Z(t){var e=t.nodeName;if("BODY"===e||"HTML"===e)return!1;if("fixed"===A(t,"position"))return!0;var n=I(t);return!!n&&Z(n)}function tt(t){if(!t||!t.parentElement||F())return document.documentElement;for(var e=t.parentElement;e&&"none"===A(e,"transform");)e=e.parentElement;return e||document.documentElement}function et(t,e,n,i){var o=arguments.length>4&&void 0!==arguments[4]&&arguments[4],r={top:0,left:0},s=o?tt(t):B(t,j(e));if("viewport"===i)r=J(s,o);else{var a=void 0;"scrollParent"===i?"BODY"===(a=x(I(e))).nodeName&&(a=t.ownerDocument.documentElement):a="window"===i?t.ownerDocument.documentElement:i;var l=$(a,s,o);if("HTML"!==a.nodeName||Z(s))r=l;else{var c=U(t.ownerDocument),u=c.height,h=c.width;r.top+=l.top-l.marginTop,r.bottom=u+l.top,r.left+=l.left-l.marginLeft,r.right=h+l.left}}var f="number"==typeof(n=n||0);return r.left+=f?n:n.left||0,r.top+=f?n:n.top||0,r.right-=f?n:n.right||0,r.bottom-=f?n:n.bottom||0,r}function nt(t){return t.width*t.height}function it(t,e,n,i,o){var r=arguments.length>5&&void 0!==arguments[5]?arguments[5]:0;if(-1===t.indexOf("auto"))return t;var s=et(n,i,r,o),a={top:{width:s.width,height:e.top-s.top},right:{width:s.right-e.right,height:s.height},bottom:{width:s.width,height:s.bottom-e.bottom},left:{width:e.left-s.left,height:s.height}},l=Object.keys(a).map((function(t){return X({key:t},a[t],{area:nt(a[t])})})).sort((function(t,e){return e.area-t.area})),c=l.filter((function(t){var e=t.width,i=t.height;return e>=n.clientWidth&&i>=n.clientHeight})),u=c.length>0?c[0].key:l[0].key,h=t.split("-")[1];return u+(h?"-"+h:"")}function ot(t,e,n){var i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,o=i?tt(e):B(e,j(n));return $(n,o,i)}function rt(t){var e=t.ownerDocument.defaultView.getComputedStyle(t),n=parseFloat(e.marginTop||0)+parseFloat(e.marginBottom||0),i=parseFloat(e.marginLeft||0)+parseFloat(e.marginRight||0);return{width:t.offsetWidth+i,height:t.offsetHeight+n}}function st(t){var e={left:"right",right:"left",bottom:"top",top:"bottom"};return t.replace(/left|right|bottom|top/g,(function(t){return e[t]}))}function at(t,e,n){n=n.split("-")[0];var i=rt(t),o={width:i.width,height:i.height},r=-1!==["right","left"].indexOf(n),s=r?"top":"left",a=r?"left":"top",l=r?"height":"width",c=r?"width":"height";return o[s]=e[s]+e[l]/2-i[l]/2,o[a]=n===a?e[a]-i[c]:e[st(a)],o}function lt(t,e){return Array.prototype.find?t.find(e):t.filter(e)[0]}function ct(t,e,n){return(void 0===n?t:t.slice(0,function(t,e,n){if(Array.prototype.findIndex)return t.findIndex((function(t){return t[e]===n}));var i=lt(t,(function(t){return t[e]===n}));return t.indexOf(i)}(t,"name",n))).forEach((function(t){t.function&&console.warn("`modifier.function` is deprecated, use `modifier.fn`!");var n=t.function||t.fn;t.enabled&&O(n)&&(e.offsets.popper=K(e.offsets.popper),e.offsets.reference=K(e.offsets.reference),e=n(e,t))})),e}function ut(){if(!this.state.isDestroyed){var t={instance:this,styles:{},arrowStyles:{},attributes:{},flipped:!1,offsets:{}};t.offsets.reference=ot(this.state,this.popper,this.reference,this.options.positionFixed),t.placement=it(this.options.placement,t.offsets.reference,this.popper,this.reference,this.options.modifiers.flip.boundariesElement,this.options.modifiers.flip.padding),t.originalPlacement=t.placement,t.positionFixed=this.options.positionFixed,t.offsets.popper=at(this.popper,t.offsets.reference,t.placement),t.offsets.popper.position=this.options.positionFixed?"fixed":"absolute",t=ct(this.modifiers,t),this.state.isCreated?this.options.onUpdate(t):(this.state.isCreated=!0,this.options.onCreate(t))}}function ht(t,e){return t.some((function(t){var n=t.name;return t.enabled&&n===e}))}function ft(t){for(var e=[!1,"ms","Webkit","Moz","O"],n=t.charAt(0).toUpperCase()+t.slice(1),i=0;i1&&void 0!==arguments[1]&&arguments[1],n=Tt.indexOf(t),i=Tt.slice(n+1).concat(Tt.slice(0,n));return e?i.reverse():i}var St="flip",Dt="clockwise",kt="counterclockwise";function Nt(t,e,n,i){var o=[0,0],r=-1!==["right","left"].indexOf(i),s=t.split(/(\+|\-)/).map((function(t){return t.trim()})),a=s.indexOf(lt(s,(function(t){return-1!==t.search(/,|\s/)})));s[a]&&-1===s[a].indexOf(",")&&console.warn("Offsets separated by white space(s) are deprecated, use a comma (,) instead.");var l=/\s*,\s*|\s+/,c=-1!==a?[s.slice(0,a).concat([s[a].split(l)[0]]),[s[a].split(l)[1]].concat(s.slice(a+1))]:[s];return(c=c.map((function(t,i){var o=(1===i?!r:r)?"height":"width",s=!1;return t.reduce((function(t,e){return""===t[t.length-1]&&-1!==["+","-"].indexOf(e)?(t[t.length-1]=e,s=!0,t):s?(t[t.length-1]+=e,s=!1,t):t.concat(e)}),[]).map((function(t){return function(t,e,n,i){var o=t.match(/((?:\-|\+)?\d*\.?\d*)(.*)/),r=+o[1],s=o[2];if(!r)return t;if(0===s.indexOf("%")){var a=void 0;switch(s){case"%p":a=n;break;case"%":case"%r":default:a=i}return K(a)[e]/100*r}if("vh"===s||"vw"===s){return("vh"===s?Math.max(document.documentElement.clientHeight,window.innerHeight||0):Math.max(document.documentElement.clientWidth,window.innerWidth||0))/100*r}return r}(t,o,e,n)}))}))).forEach((function(t,e){t.forEach((function(n,i){_t(n)&&(o[e]+=n*("-"===t[i-1]?-1:1))}))})),o}var Ot={placement:"bottom",positionFixed:!1,eventsEnabled:!0,removeOnDestroy:!1,onCreate:function(){},onUpdate:function(){},modifiers:{shift:{order:100,enabled:!0,fn:function(t){var e=t.placement,n=e.split("-")[0],i=e.split("-")[1];if(i){var o=t.offsets,r=o.reference,s=o.popper,a=-1!==["bottom","top"].indexOf(n),l=a?"left":"top",c=a?"width":"height",u={start:z({},l,r[l]),end:z({},l,r[l]+r[c]-s[c])};t.offsets.popper=X({},s,u[i])}return t}},offset:{order:200,enabled:!0,fn:function(t,e){var n=e.offset,i=t.placement,o=t.offsets,r=o.popper,s=o.reference,a=i.split("-")[0],l=void 0;return l=_t(+n)?[+n,0]:Nt(n,r,s,a),"left"===a?(r.top+=l[0],r.left-=l[1]):"right"===a?(r.top+=l[0],r.left+=l[1]):"top"===a?(r.left+=l[0],r.top-=l[1]):"bottom"===a&&(r.left+=l[0],r.top+=l[1]),t.popper=r,t},offset:0},preventOverflow:{order:300,enabled:!0,fn:function(t,e){var n=e.boundariesElement||R(t.instance.popper);t.instance.reference===n&&(n=R(n));var i=ft("transform"),o=t.instance.popper.style,r=o.top,s=o.left,a=o[i];o.top="",o.left="",o[i]="";var l=et(t.instance.popper,t.instance.reference,e.padding,n,t.positionFixed);o.top=r,o.left=s,o[i]=a,e.boundaries=l;var c=e.priority,u=t.offsets.popper,h={primary:function(t){var n=u[t];return u[t]l[t]&&!e.escapeWithReference&&(i=Math.min(u[n],l[t]-("right"===t?u.width:u.height))),z({},n,i)}};return c.forEach((function(t){var e=-1!==["left","top"].indexOf(t)?"primary":"secondary";u=X({},u,h[e](t))})),t.offsets.popper=u,t},priority:["left","right","top","bottom"],padding:5,boundariesElement:"scrollParent"},keepTogether:{order:400,enabled:!0,fn:function(t){var e=t.offsets,n=e.popper,i=e.reference,o=t.placement.split("-")[0],r=Math.floor,s=-1!==["top","bottom"].indexOf(o),a=s?"right":"bottom",l=s?"left":"top",c=s?"width":"height";return n[a]r(i[a])&&(t.offsets.popper[l]=r(i[a])),t}},arrow:{order:500,enabled:!0,fn:function(t,e){var n;if(!wt(t.instance.modifiers,"arrow","keepTogether"))return t;var i=e.element;if("string"==typeof i){if(!(i=t.instance.popper.querySelector(i)))return t}else if(!t.instance.popper.contains(i))return console.warn("WARNING: `arrow.element` must be child of its popper element!"),t;var o=t.placement.split("-")[0],r=t.offsets,s=r.popper,a=r.reference,l=-1!==["left","right"].indexOf(o),c=l?"height":"width",u=l?"Top":"Left",h=u.toLowerCase(),f=l?"left":"top",d=l?"bottom":"right",p=rt(i)[c];a[d]-ps[d]&&(t.offsets.popper[h]+=a[h]+p-s[d]),t.offsets.popper=K(t.offsets.popper);var m=a[h]+a[c]/2-p/2,g=A(t.instance.popper),v=parseFloat(g["margin"+u],10),_=parseFloat(g["border"+u+"Width"],10),b=m-t.offsets.popper[h]-v-_;return b=Math.max(Math.min(s[c]-p,b),0),t.arrowElement=i,t.offsets.arrow=(z(n={},h,Math.round(b)),z(n,f,""),n),t},element:"[x-arrow]"},flip:{order:600,enabled:!0,fn:function(t,e){if(ht(t.instance.modifiers,"inner"))return t;if(t.flipped&&t.placement===t.originalPlacement)return t;var n=et(t.instance.popper,t.instance.reference,e.padding,e.boundariesElement,t.positionFixed),i=t.placement.split("-")[0],o=st(i),r=t.placement.split("-")[1]||"",s=[];switch(e.behavior){case St:s=[i,o];break;case Dt:s=Ct(i);break;case kt:s=Ct(i,!0);break;default:s=e.behavior}return s.forEach((function(a,l){if(i!==a||s.length===l+1)return t;i=t.placement.split("-")[0],o=st(i);var c=t.offsets.popper,u=t.offsets.reference,h=Math.floor,f="left"===i&&h(c.right)>h(u.left)||"right"===i&&h(c.left)h(u.top)||"bottom"===i&&h(c.top)h(n.right),m=h(c.top)h(n.bottom),v="left"===i&&d||"right"===i&&p||"top"===i&&m||"bottom"===i&&g,_=-1!==["top","bottom"].indexOf(i),b=!!e.flipVariations&&(_&&"start"===r&&d||_&&"end"===r&&p||!_&&"start"===r&&m||!_&&"end"===r&&g),y=!!e.flipVariationsByContent&&(_&&"start"===r&&p||_&&"end"===r&&d||!_&&"start"===r&&g||!_&&"end"===r&&m),w=b||y;(f||v||w)&&(t.flipped=!0,(f||v)&&(i=s[l+1]),w&&(r=function(t){return"end"===t?"start":"start"===t?"end":t}(r)),t.placement=i+(r?"-"+r:""),t.offsets.popper=X({},t.offsets.popper,at(t.instance.popper,t.offsets.reference,t.placement)),t=ct(t.instance.modifiers,t,"flip"))})),t},behavior:"flip",padding:5,boundariesElement:"viewport",flipVariations:!1,flipVariationsByContent:!1},inner:{order:700,enabled:!1,fn:function(t){var e=t.placement,n=e.split("-")[0],i=t.offsets,o=i.popper,r=i.reference,s=-1!==["left","right"].indexOf(n),a=-1===["top","left"].indexOf(n);return o[s?"left":"top"]=r[n]-(a?o[s?"width":"height"]:0),t.placement=st(e),t.offsets.popper=K(o),t}},hide:{order:800,enabled:!0,fn:function(t){if(!wt(t.instance.modifiers,"hide","preventOverflow"))return t;var e=t.offsets.reference,n=lt(t.instance.modifiers,(function(t){return"preventOverflow"===t.name})).boundaries;if(e.bottomn.right||e.top>n.bottom||e.right2&&void 0!==arguments[2]?arguments[2]:{};V(this,t),this.scheduleUpdate=function(){return requestAnimationFrame(i.update)},this.update=N(this.update.bind(this)),this.options=X({},t.Defaults,o),this.state={isDestroyed:!1,isCreated:!1,scrollParents:[]},this.reference=e&&e.jquery?e[0]:e,this.popper=n&&n.jquery?n[0]:n,this.options.modifiers={},Object.keys(X({},t.Defaults.modifiers,o.modifiers)).forEach((function(e){i.options.modifiers[e]=X({},t.Defaults.modifiers[e]||{},o.modifiers?o.modifiers[e]:{})})),this.modifiers=Object.keys(this.options.modifiers).map((function(t){return X({name:t},i.options.modifiers[t])})).sort((function(t,e){return t.order-e.order})),this.modifiers.forEach((function(t){t.enabled&&O(t.onLoad)&&t.onLoad(i.reference,i.popper,i.options,t,i.state)})),this.update();var r=this.options.eventsEnabled;r&&this.enableEventListeners(),this.state.eventsEnabled=r}return Y(t,[{key:"update",value:function(){return ut.call(this)}},{key:"destroy",value:function(){return dt.call(this)}},{key:"enableEventListeners",value:function(){return gt.call(this)}},{key:"disableEventListeners",value:function(){return vt.call(this)}}]),t}();At.Utils=("undefined"!=typeof window?window:global).PopperUtils,At.placements=Et,At.Defaults=Ot;var It="dropdown",xt=e.fn[It],jt=new RegExp("38|40|27"),Lt={offset:0,flip:!0,boundary:"scrollParent",reference:"toggle",display:"dynamic",popperConfig:null},Pt={offset:"(number|string|function)",flip:"boolean",boundary:"(string|element)",reference:"(string|element)",display:"string",popperConfig:"(null|object)"},Ft=function(){function t(t,e){this._element=t,this._popper=null,this._config=this._getConfig(e),this._menu=this._getMenuElement(),this._inNavbar=this._detectNavbar(),this._addEventListeners()}var n=t.prototype;return n.toggle=function(){if(!this._element.disabled&&!e(this._element).hasClass("disabled")){var n=e(this._menu).hasClass("show");t._clearMenus(),n||this.show(!0)}},n.show=function(n){if(void 0===n&&(n=!1),!(this._element.disabled||e(this._element).hasClass("disabled")||e(this._menu).hasClass("show"))){var i={relatedTarget:this._element},o=e.Event("show.bs.dropdown",i),r=t._getParentFromElement(this._element);if(e(r).trigger(o),!o.isDefaultPrevented()){if(!this._inNavbar&&n){if("undefined"==typeof At)throw new TypeError("Bootstrap's dropdowns require Popper.js (https://popper.js.org/)");var s=this._element;"parent"===this._config.reference?s=r:l.isElement(this._config.reference)&&(s=this._config.reference,"undefined"!=typeof this._config.reference.jquery&&(s=this._config.reference[0])),"scrollParent"!==this._config.boundary&&e(r).addClass("position-static"),this._popper=new At(s,this._menu,this._getPopperConfig())}"ontouchstart"in document.documentElement&&0===e(r).closest(".navbar-nav").length&&e(document.body).children().on("mouseover",null,e.noop),this._element.focus(),this._element.setAttribute("aria-expanded",!0),e(this._menu).toggleClass("show"),e(r).toggleClass("show").trigger(e.Event("shown.bs.dropdown",i))}}},n.hide=function(){if(!this._element.disabled&&!e(this._element).hasClass("disabled")&&e(this._menu).hasClass("show")){var n={relatedTarget:this._element},i=e.Event("hide.bs.dropdown",n),o=t._getParentFromElement(this._element);e(o).trigger(i),i.isDefaultPrevented()||(this._popper&&this._popper.destroy(),e(this._menu).toggleClass("show"),e(o).toggleClass("show").trigger(e.Event("hidden.bs.dropdown",n)))}},n.dispose=function(){e.removeData(this._element,"bs.dropdown"),e(this._element).off(".bs.dropdown"),this._element=null,this._menu=null,null!==this._popper&&(this._popper.destroy(),this._popper=null)},n.update=function(){this._inNavbar=this._detectNavbar(),null!==this._popper&&this._popper.scheduleUpdate()},n._addEventListeners=function(){var t=this;e(this._element).on("click.bs.dropdown",(function(e){e.preventDefault(),e.stopPropagation(),t.toggle()}))},n._getConfig=function(t){return t=s(s(s({},this.constructor.Default),e(this._element).data()),t),l.typeCheckConfig(It,t,this.constructor.DefaultType),t},n._getMenuElement=function(){if(!this._menu){var e=t._getParentFromElement(this._element);e&&(this._menu=e.querySelector(".dropdown-menu"))}return this._menu},n._getPlacement=function(){var t=e(this._element.parentNode),n="bottom-start";return t.hasClass("dropup")?n=e(this._menu).hasClass("dropdown-menu-right")?"top-end":"top-start":t.hasClass("dropright")?n="right-start":t.hasClass("dropleft")?n="left-start":e(this._menu).hasClass("dropdown-menu-right")&&(n="bottom-end"),n},n._detectNavbar=function(){return e(this._element).closest(".navbar").length>0},n._getOffset=function(){var t=this,e={};return"function"==typeof this._config.offset?e.fn=function(e){return e.offsets=s(s({},e.offsets),t._config.offset(e.offsets,t._element)||{}),e}:e.offset=this._config.offset,e},n._getPopperConfig=function(){var t={placement:this._getPlacement(),modifiers:{offset:this._getOffset(),flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}};return"static"===this._config.display&&(t.modifiers.applyStyle={enabled:!1}),s(s({},t),this._config.popperConfig)},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.dropdown");if(i||(i=new t(this,"object"==typeof n?n:null),e(this).data("bs.dropdown",i)),"string"==typeof n){if("undefined"==typeof i[n])throw new TypeError('No method named "'+n+'"');i[n]()}}))},t._clearMenus=function(n){if(!n||3!==n.which&&("keyup"!==n.type||9===n.which))for(var i=[].slice.call(document.querySelectorAll('[data-toggle="dropdown"]')),o=0,r=i.length;o0&&s--,40===n.which&&sdocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},n._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},n._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Qt,popperConfig:null},Zt={HIDE:"hide.bs.tooltip",HIDDEN:"hidden.bs.tooltip",SHOW:"show.bs.tooltip",SHOWN:"shown.bs.tooltip",INSERTED:"inserted.bs.tooltip",CLICK:"click.bs.tooltip",FOCUSIN:"focusin.bs.tooltip",FOCUSOUT:"focusout.bs.tooltip",MOUSEENTER:"mouseenter.bs.tooltip",MOUSELEAVE:"mouseleave.bs.tooltip"},te=function(){function t(t,e){if("undefined"==typeof At)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var n=t.prototype;return n.enable=function(){this._isEnabled=!0},n.disable=function(){this._isEnabled=!1},n.toggleEnabled=function(){this._isEnabled=!this._isEnabled},n.toggle=function(t){if(this._isEnabled)if(t){var n=this.constructor.DATA_KEY,i=e(t.currentTarget).data(n);i||(i=new this.constructor(t.currentTarget,this._getDelegateConfig()),e(t.currentTarget).data(n,i)),i._activeTrigger.click=!i._activeTrigger.click,i._isWithActiveTrigger()?i._enter(null,i):i._leave(null,i)}else{if(e(this.getTipElement()).hasClass("show"))return void this._leave(null,this);this._enter(null,this)}},n.dispose=function(){clearTimeout(this._timeout),e.removeData(this.element,this.constructor.DATA_KEY),e(this.element).off(this.constructor.EVENT_KEY),e(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&e(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},n.show=function(){var t=this;if("none"===e(this.element).css("display"))throw new Error("Please use show on visible elements");var n=e.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){e(this.element).trigger(n);var i=l.findShadowRoot(this.element),o=e.contains(null!==i?i:this.element.ownerDocument.documentElement,this.element);if(n.isDefaultPrevented()||!o)return;var r=this.getTipElement(),s=l.getUID(this.constructor.NAME);r.setAttribute("id",s),this.element.setAttribute("aria-describedby",s),this.setContent(),this.config.animation&&e(r).addClass("fade");var a="function"==typeof this.config.placement?this.config.placement.call(this,r,this.element):this.config.placement,c=this._getAttachment(a);this.addAttachmentClass(c);var u=this._getContainer();e(r).data(this.constructor.DATA_KEY,this),e.contains(this.element.ownerDocument.documentElement,this.tip)||e(r).appendTo(u),e(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new At(this.element,r,this._getPopperConfig(c)),e(r).addClass("show"),"ontouchstart"in document.documentElement&&e(document.body).children().on("mouseover",null,e.noop);var h=function(){t.config.animation&&t._fixTransition();var n=t._hoverState;t._hoverState=null,e(t.element).trigger(t.constructor.Event.SHOWN),"out"===n&&t._leave(null,t)};if(e(this.tip).hasClass("fade")){var f=l.getTransitionDurationFromElement(this.tip);e(this.tip).one(l.TRANSITION_END,h).emulateTransitionEnd(f)}else h()}},n.hide=function(t){var n=this,i=this.getTipElement(),o=e.Event(this.constructor.Event.HIDE),r=function(){"show"!==n._hoverState&&i.parentNode&&i.parentNode.removeChild(i),n._cleanTipClass(),n.element.removeAttribute("aria-describedby"),e(n.element).trigger(n.constructor.Event.HIDDEN),null!==n._popper&&n._popper.destroy(),t&&t()};if(e(this.element).trigger(o),!o.isDefaultPrevented()){if(e(i).removeClass("show"),"ontouchstart"in document.documentElement&&e(document.body).children().off("mouseover",null,e.noop),this._activeTrigger.click=!1,this._activeTrigger.focus=!1,this._activeTrigger.hover=!1,e(this.tip).hasClass("fade")){var s=l.getTransitionDurationFromElement(i);e(i).one(l.TRANSITION_END,r).emulateTransitionEnd(s)}else r();this._hoverState=""}},n.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},n.isWithContent=function(){return Boolean(this.getTitle())},n.addAttachmentClass=function(t){e(this.getTipElement()).addClass("bs-tooltip-"+t)},n.getTipElement=function(){return this.tip=this.tip||e(this.config.template)[0],this.tip},n.setContent=function(){var t=this.getTipElement();this.setElementContent(e(t.querySelectorAll(".tooltip-inner")),this.getTitle()),e(t).removeClass("fade show")},n.setElementContent=function(t,n){"object"!=typeof n||!n.nodeType&&!n.jquery?this.config.html?(this.config.sanitize&&(n=Vt(n,this.config.whiteList,this.config.sanitizeFn)),t.html(n)):t.text(n):this.config.html?e(n).parent().is(t)||t.empty().append(n):t.text(e(n).text())},n.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},n._getPopperConfig=function(t){var e=this;return s(s({},{placement:t,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:".arrow"},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),this.config.popperConfig)},n._getOffset=function(){var t=this,e={};return"function"==typeof this.config.offset?e.fn=function(e){return e.offsets=s(s({},e.offsets),t.config.offset(e.offsets,t.element)||{}),e}:e.offset=this.config.offset,e},n._getContainer=function(){return!1===this.config.container?document.body:l.isElement(this.config.container)?e(this.config.container):e(document).find(this.config.container)},n._getAttachment=function(t){return $t[t.toUpperCase()]},n._setListeners=function(){var t=this;this.config.trigger.split(" ").forEach((function(n){if("click"===n)e(t.element).on(t.constructor.Event.CLICK,t.config.selector,(function(e){return t.toggle(e)}));else if("manual"!==n){var i="hover"===n?t.constructor.Event.MOUSEENTER:t.constructor.Event.FOCUSIN,o="hover"===n?t.constructor.Event.MOUSELEAVE:t.constructor.Event.FOCUSOUT;e(t.element).on(i,t.config.selector,(function(e){return t._enter(e)})).on(o,t.config.selector,(function(e){return t._leave(e)}))}})),this._hideModalHandler=function(){t.element&&t.hide()},e(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=s(s({},this.config),{},{trigger:"manual",selector:""}):this._fixTitle()},n._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},n._enter=function(t,n){var i=this.constructor.DATA_KEY;(n=n||e(t.currentTarget).data(i))||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),e(t.currentTarget).data(i,n)),t&&(n._activeTrigger["focusin"===t.type?"focus":"hover"]=!0),e(n.getTipElement()).hasClass("show")||"show"===n._hoverState?n._hoverState="show":(clearTimeout(n._timeout),n._hoverState="show",n.config.delay&&n.config.delay.show?n._timeout=setTimeout((function(){"show"===n._hoverState&&n.show()}),n.config.delay.show):n.show())},n._leave=function(t,n){var i=this.constructor.DATA_KEY;(n=n||e(t.currentTarget).data(i))||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),e(t.currentTarget).data(i,n)),t&&(n._activeTrigger["focusout"===t.type?"focus":"hover"]=!1),n._isWithActiveTrigger()||(clearTimeout(n._timeout),n._hoverState="out",n.config.delay&&n.config.delay.hide?n._timeout=setTimeout((function(){"out"===n._hoverState&&n.hide()}),n.config.delay.hide):n.hide())},n._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},n._getConfig=function(t){var n=e(this.element).data();return Object.keys(n).forEach((function(t){-1!==Kt.indexOf(t)&&delete n[t]})),"number"==typeof(t=s(s(s({},this.constructor.Default),n),"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),l.typeCheckConfig(Yt,t,this.constructor.DefaultType),t.sanitize&&(t.template=Vt(t.template,t.whiteList,t.sanitizeFn)),t},n._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},n._cleanTipClass=function(){var t=e(this.getTipElement()),n=t.attr("class").match(Xt);null!==n&&n.length&&t.removeClass(n.join(""))},n._handlePopperPlacementChange=function(t){this.tip=t.instance.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},n._fixTransition=function(){var t=this.getTipElement(),n=this.config.animation;null===t.getAttribute("x-placement")&&(e(t).removeClass("fade"),this.config.animation=!1,this.hide(),this.show(),this.config.animation=n)},t._jQueryInterface=function(n){return this.each((function(){var i=e(this).data("bs.tooltip"),o="object"==typeof n&&n;if((i||!/dispose|hide/.test(n))&&(i||(i=new t(this,o),e(this).data("bs.tooltip",i)),"string"==typeof n)){if("undefined"==typeof i[n])throw new TypeError('No method named "'+n+'"');i[n]()}}))},i(t,null,[{key:"VERSION",get:function(){return"4.5.0"}},{key:"Default",get:function(){return Jt}},{key:"NAME",get:function(){return Yt}},{key:"DATA_KEY",get:function(){return"bs.tooltip"}},{key:"Event",get:function(){return Zt}},{key:"EVENT_KEY",get:function(){return".bs.tooltip"}},{key:"DefaultType",get:function(){return Gt}}]),t}();e.fn[Yt]=te._jQueryInterface,e.fn[Yt].Constructor=te,e.fn[Yt].noConflict=function(){return e.fn[Yt]=zt,te._jQueryInterface};var ee="popover",ne=e.fn[ee],ie=new RegExp("(^|\\s)bs-popover\\S+","g"),oe=s(s({},te.Default),{},{placement:"right",trigger:"click",content:"",template:''}),re=s(s({},te.DefaultType),{},{content:"(string|element|function)"}),se={HIDE:"hide.bs.popover",HIDDEN:"hidden.bs.popover",SHOW:"show.bs.popover",SHOWN:"shown.bs.popover",INSERTED:"inserted.bs.popover",CLICK:"click.bs.popover",FOCUSIN:"focusin.bs.popover",FOCUSOUT:"focusout.bs.popover",MOUSEENTER:"mouseenter.bs.popover",MOUSELEAVE:"mouseleave.bs.popover"},ae=function(t){var n,o;function r(){return t.apply(this,arguments)||this}o=t,(n=r).prototype=Object.create(o.prototype),n.prototype.constructor=n,n.__proto__=o;var s=r.prototype;return s.isWithContent=function(){return this.getTitle()||this._getContent()},s.addAttachmentClass=function(t){e(this.getTipElement()).addClass("bs-popover-"+t)},s.getTipElement=function(){return this.tip=this.tip||e(this.config.template)[0],this.tip},s.setContent=function(){var t=e(this.getTipElement());this.setElementContent(t.find(".popover-header"),this.getTitle());var n=this._getContent();"function"==typeof n&&(n=n.call(this.element)),this.setElementContent(t.find(".popover-body"),n),t.removeClass("fade show")},s._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},s._cleanTipClass=function(){var t=e(this.getTipElement()),n=t.attr("class").match(ie);null!==n&&n.length>0&&t.removeClass(n.join(""))},r._jQueryInterface=function(t){return this.each((function(){var n=e(this).data("bs.popover"),i="object"==typeof t?t:null;if((n||!/dispose|hide/.test(t))&&(n||(n=new r(this,i),e(this).data("bs.popover",n)),"string"==typeof t)){if("undefined"==typeof n[t])throw new TypeError('No method named "'+t+'"');n[t]()}}))},i(r,null,[{key:"VERSION",get:function(){return"4.5.0"}},{key:"Default",get:function(){return oe}},{key:"NAME",get:function(){return ee}},{key:"DATA_KEY",get:function(){return"bs.popover"}},{key:"Event",get:function(){return se}},{key:"EVENT_KEY",get:function(){return".bs.popover"}},{key:"DefaultType",get:function(){return re}}]),r}(te);e.fn[ee]=ae._jQueryInterface,e.fn[ee].Constructor=ae,e.fn[ee].noConflict=function(){return e.fn[ee]=ne,ae._jQueryInterface};var le="scrollspy",ce=e.fn[le],ue={offset:10,method:"auto",target:""},he={offset:"number",method:"string",target:"(string|element)"},fe=function(){function t(t,n){var i=this;this._element=t,this._scrollElement="BODY"===t.tagName?window:t,this._config=this._getConfig(n),this._selector=this._config.target+" .nav-link,"+this._config.target+" .list-group-item,"+this._config.target+" .dropdown-item",this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,e(this._scrollElement).on("scroll.bs.scrollspy",(function(t){return i._process(t)})),this.refresh(),this._process()}var n=t.prototype;return n.refresh=function(){var t=this,n=this._scrollElement===this._scrollElement.window?"offset":"position",i="auto"===this._config.method?n:this._config.method,o="position"===i?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),[].slice.call(document.querySelectorAll(this._selector)).map((function(t){var n,r=l.getSelectorFromElement(t);if(r&&(n=document.querySelector(r)),n){var s=n.getBoundingClientRect();if(s.width||s.height)return[e(n)[i]().top+o,r]}return null})).filter((function(t){return t})).sort((function(t,e){return t[0]-e[0]})).forEach((function(e){t._offsets.push(e[0]),t._targets.push(e[1])}))},n.dispose=function(){e.removeData(this._element,"bs.scrollspy"),e(this._scrollElement).off(".bs.scrollspy"),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},n._getConfig=function(t){if("string"!=typeof(t=s(s({},ue),"object"==typeof t&&t?t:{})).target&&l.isElement(t.target)){var n=e(t.target).attr("id");n||(n=l.getUID(le),e(t.target).attr("id",n)),t.target="#"+n}return l.typeCheckConfig(le,t,he),t},n._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},n._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},n._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},n._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var i=this._targets[this._targets.length-1];this._activeTarget!==i&&this._activate(i)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var o=this._offsets.length;o--;){this._activeTarget!==this._targets[o]&&t>=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t li > .active":".active";i=(i=e.makeArray(e(o).find(s)))[i.length-1]}var a=e.Event("hide.bs.tab",{relatedTarget:this._element}),c=e.Event("show.bs.tab",{relatedTarget:i});if(i&&e(i).trigger(a),e(this._element).trigger(c),!c.isDefaultPrevented()&&!a.isDefaultPrevented()){r&&(n=document.querySelector(r)),this._activate(this._element,o);var u=function(){var n=e.Event("hidden.bs.tab",{relatedTarget:t._element}),o=e.Event("shown.bs.tab",{relatedTarget:i});e(i).trigger(n),e(t._element).trigger(o)};n?this._activate(n,n.parentNode,u):u()}}},n.dispose=function(){e.removeData(this._element,"bs.tab"),this._element=null},n._activate=function(t,n,i){var o=this,r=(!n||"UL"!==n.nodeName&&"OL"!==n.nodeName?e(n).children(".active"):e(n).find("> li > .active"))[0],s=i&&r&&e(r).hasClass("fade"),a=function(){return o._transitionComplete(t,r,i)};if(r&&s){var c=l.getTransitionDurationFromElement(r);e(r).removeClass("show").one(l.TRANSITION_END,a).emulateTransitionEnd(c)}else a()},n._transitionComplete=function(t,n,i){if(n){e(n).removeClass("active");var o=e(n.parentNode).find("> .dropdown-menu .active")[0];o&&e(o).removeClass("active"),"tab"===n.getAttribute("role")&&n.setAttribute("aria-selected",!1)}if(e(t).addClass("active"),"tab"===t.getAttribute("role")&&t.setAttribute("aria-selected",!0),l.reflow(t),t.classList.contains("fade")&&t.classList.add("show"),t.parentNode&&e(t.parentNode).hasClass("dropdown-menu")){var r=e(t).closest(".dropdown")[0];if(r){var s=[].slice.call(r.querySelectorAll(".dropdown-toggle"));e(s).addClass("active")}t.setAttribute("aria-expanded",!0)}i&&i()},t._jQueryInterface=function(n){return this.each((function(){var i=e(this),o=i.data("bs.tab");if(o||(o=new t(this),i.data("bs.tab",o)),"string"==typeof n){if("undefined"==typeof o[n])throw new TypeError('No method named "'+n+'"');o[n]()}}))},i(t,null,[{key:"VERSION",get:function(){return"4.5.0"}}]),t}();e(document).on("click.bs.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"], [data-toggle="list"]',(function(t){t.preventDefault(),pe._jQueryInterface.call(e(this),"show")})),e.fn.tab=pe._jQueryInterface,e.fn.tab.Constructor=pe,e.fn.tab.noConflict=function(){return e.fn.tab=de,pe._jQueryInterface};var me=e.fn.toast,ge={animation:"boolean",autohide:"boolean",delay:"number"},ve={animation:!0,autohide:!0,delay:500},_e=function(){function t(t,e){this._element=t,this._config=this._getConfig(e),this._timeout=null,this._setListeners()}var n=t.prototype;return n.show=function(){var t=this,n=e.Event("show.bs.toast");if(e(this._element).trigger(n),!n.isDefaultPrevented()){this._config.animation&&this._element.classList.add("fade");var i=function(){t._element.classList.remove("showing"),t._element.classList.add("show"),e(t._element).trigger("shown.bs.toast"),t._config.autohide&&(t._timeout=setTimeout((function(){t.hide()}),t._config.delay))};if(this._element.classList.remove("hide"),l.reflow(this._element),this._element.classList.add("showing"),this._config.animation){var o=l.getTransitionDurationFromElement(this._element);e(this._element).one(l.TRANSITION_END,i).emulateTransitionEnd(o)}else i()}},n.hide=function(){if(this._element.classList.contains("show")){var t=e.Event("hide.bs.toast");e(this._element).trigger(t),t.isDefaultPrevented()||this._close()}},n.dispose=function(){clearTimeout(this._timeout),this._timeout=null,this._element.classList.contains("show")&&this._element.classList.remove("show"),e(this._element).off("click.dismiss.bs.toast"),e.removeData(this._element,"bs.toast"),this._element=null,this._config=null},n._getConfig=function(t){return t=s(s(s({},ve),e(this._element).data()),"object"==typeof t&&t?t:{}),l.typeCheckConfig("toast",t,this.constructor.DefaultType),t},n._setListeners=function(){var t=this;e(this._element).on("click.dismiss.bs.toast",'[data-dismiss="toast"]',(function(){return t.hide()}))},n._close=function(){var t=this,n=function(){t._element.classList.add("hide"),e(t._element).trigger("hidden.bs.toast")};if(this._element.classList.remove("show"),this._config.animation){var i=l.getTransitionDurationFromElement(this._element);e(this._element).one(l.TRANSITION_END,n).emulateTransitionEnd(i)}else n()},t._jQueryInterface=function(n){return this.each((function(){var i=e(this),o=i.data("bs.toast");if(o||(o=new t(this,"object"==typeof n&&n),i.data("bs.toast",o)),"string"==typeof n){if("undefined"==typeof o[n])throw new TypeError('No method named "'+n+'"');o[n](this)}}))},i(t,null,[{key:"VERSION",get:function(){return"4.5.0"}},{key:"DefaultType",get:function(){return ge}},{key:"Default",get:function(){return ve}}]),t}();e.fn.toast=_e._jQueryInterface,e.fn.toast.Constructor=_e,e.fn.toast.noConflict=function(){return e.fn.toast=me,_e._jQueryInterface},t.Alert=h,t.Button=d,t.Carousel=y,t.Collapse=S,t.Dropdown=Ft,t.Modal=qt,t.Popover=ae,t.Scrollspy=fe,t.Tab=pe,t.Toast=_e,t.Tooltip=te,t.Util=l,Object.defineProperty(t,"__esModule",{value:!0})})); diff --git a/htdocs/lib/chroma.min.js b/htdocs/lib/chroma.min.js new file mode 100644 index 000000000..76dd1f840 --- /dev/null +++ b/htdocs/lib/chroma.min.js @@ -0,0 +1,58 @@ +/** + * chroma.js - JavaScript library for color conversions + * + * Copyright (c) 2011-2019, Gregor Aisch + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. The name Gregor Aisch may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL GREGOR AISCH OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, + * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * ------------------------------------------------------- + * + * chroma.js includes colors from colorbrewer2.org, which are released under + * the following license: + * + * Copyright (c) 2002 Cynthia Brewer, Mark Harrower, + * and The Pennsylvania State University. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, + * either express or implied. See the License for the specific + * language governing permissions and limitations under the License. + * + * ------------------------------------------------------ + * + * Named colors are taken from X11 Color Names. + * http://www.w3.org/TR/css3-color/#svg-color + * + * @preserve + */ + +!function(r,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):r.chroma=e()}(this,function(){"use strict";for(var t=function(r,e,t){return void 0===e&&(e=0),void 0===t&&(t=1),r>16,e>>8&255,255&e,1]}if(r.match(fr)){9===r.length&&(r=r.substr(1));var t=parseInt(r,16);return[t>>24&255,t>>16&255,t>>8&255,Math.round((255&t)/255*100)/100]}throw new Error("unknown hex color: "+r)},ur=o.type;A.prototype.hex=function(r){return nr(this._rgb,r)},N.hex=function(){for(var r=[],e=arguments.length;e--;)r[e]=arguments[e];return new(Function.prototype.bind.apply(A,[null].concat(r,["hex"])))},b.format.hex=or,b.autodetect.push({p:4,test:function(r){for(var e=[],t=arguments.length-1;0>16,r>>8&255,255&r,1];throw new Error("unknown num color: "+r)},xe=o.type;A.prototype.num=function(){return Me(this._rgb)},N.num=function(){for(var r=[],e=arguments.length;e--;)r[e]=arguments[e];return new(Function.prototype.bind.apply(A,[null].concat(r,["num"])))},b.format.num=_e,b.autodetect.push({p:5,test:function(){for(var r=[],e=arguments.length;e--;)r[e]=arguments[e];if(1===r.length&&"number"===xe(r[0])&&0<=r[0]&&r[0]<=16777215)return"num"}});var Ae=o.unpack,Ee=o.type,Pe=Math.round;A.prototype.rgb=function(r){return void 0===r&&(r=!0),!1===r?this._rgb.slice(0,3):this._rgb.slice(0,3).map(Pe)},A.prototype.rgba=function(t){return void 0===t&&(t=!0),this._rgb.slice(0,4).map(function(r,e){return e<3?!1===t?r:Pe(r):r})},N.rgb=function(){for(var r=[],e=arguments.length;e--;)r[e]=arguments[e];return new(Function.prototype.bind.apply(A,[null].concat(r,["rgb"])))},b.format.rgb=function(){for(var r=[],e=arguments.length;e--;)r[e]=arguments[e];var t=Ae(r,"rgba");return void 0===t[3]&&(t[3]=1),t},b.autodetect.push({p:3,test:function(){for(var r=[],e=arguments.length;e--;)r[e]=arguments[e];if(r=Ae(r,"rgba"),"array"===Ee(r)&&(3===r.length||4===r.length&&"number"==Ee(r[3])&&0<=r[3]&&r[3]<=1))return"rgb"}});var Fe=Math.log,Oe=function(r){var e,t,n,a=r/100;return n=a<66?(e=255,t=-155.25485562709179-.44596950469579133*(t=a-2)+104.49216199393888*Fe(t),a<20?0:.8274096064007395*(n=a-10)-254.76935184120902+115.67994401066147*Fe(n)):(e=351.97690566805693+.114206453784165*(e=a-55)-40.25366309332127*Fe(e),t=325.4494125711974+.07943456536662342*(t=a-50)-28.0852963507957*Fe(t),255),[e,t,n,1]},je=o.unpack,Ge=Math.round,qe=function(){for(var r=[],e=arguments.length;e--;)r[e]=arguments[e];for(var t,n=je(r,"rgb"),a=n[0],f=n[2],o=1e3,u=4e4;.4=f/a?u=t:o=t}return Ge(t)};A.prototype.temp=A.prototype.kelvin=A.prototype.temperature=function(){return qe(this._rgb)},N.temp=N.kelvin=N.temperature=function(){for(var r=[],e=arguments.length;e--;)r[e]=arguments[e];return new(Function.prototype.bind.apply(A,[null].concat(r,["temp"])))},b.format.temp=b.format.kelvin=b.format.temperature=Oe;var Le=o.type;A.prototype.alpha=function(r,e){return void 0===e&&(e=!1),void 0!==r&&"number"===Le(r)?e?(this._rgb[3]=r,this):new A([this._rgb[0],this._rgb[1],this._rgb[2],r],"rgb"):this._rgb[3]},A.prototype.clipped=function(){return this._rgb._clipped||!1},A.prototype.darken=function(r){void 0===r&&(r=1);var e=this.lab();return e[0]-=qr*r,new A(e,"lab").alpha(this.alpha(),!0)},A.prototype.brighten=function(r){return void 0===r&&(r=1),this.darken(-r)},A.prototype.darker=A.prototype.darken,A.prototype.brighter=A.prototype.brighten,A.prototype.get=function(r){var e=r.split("."),t=e[0],n=e[1],a=this[t]();if(n){var f=t.indexOf(n);if(-1=s[t];)t++;return t-1}(r)/(s.length-2):g!==p?(r-p)/(g-p):1;e||(n=w(n)),1!==y&&(n=tt(n,y)),n=d[0]+n*(1-d[0]-d[1]),n=Math.min(1,Math.max(0,n));var a=Math.floor(1e4*n);if(m&&v[a])t=v[a];else{if("array"===et(b))for(var f=0;ft.max&&(t.max=r),t.count+=1)}),t.domain=[t.min,t.max],t.limits=function(r,e){return Mt(t,r,e)},t},Mt=function(r,e,t){void 0===e&&(e="equal"),void 0===t&&(t=7),"array"==Y(r)&&(r=kt(r));var n=r.min,a=r.max,f=r.values.sort(function(r,e){return r-e});if(1===t)return[n,a];var o=[];if("c"===e.substr(0,1)&&(o.push(n),o.push(a)),"e"===e.substr(0,1)){o.push(n);for(var u=1;u 0");var c=Math.LOG10E*vt(n),i=Math.LOG10E*vt(a);o.push(n);for(var l=1;le;a=++e)if(c=d[a],b=d[a]+"ransform",b in F)return d[a].substr(0,d[a].length-1);return!1}(),G=function(a){return H===!1?!1:""===H?a:H+a.charAt(0).toUpperCase()+a.substr(1)},E=G("transform"),B=E!==!1,A=function(){var a,b,d;return a=c.createElement("div"),b=a.style,b.position="absolute",b.width="100px",b.height="100px",b.overflow=t,b.top="-9999px",c.body.appendChild(a),d=a.offsetWidth-a.clientWidth,c.body.removeChild(a),d},C=function(){var a,c,d;return c=b.navigator.userAgent,(a=/(?=.+Mac OS X)(?=.+Firefox)/.test(c))?(d=/Firefox\/\d{2}\./.exec(c),d&&(d=d[0].replace(/\D+/g,"")),a&&+d>23):!1},q=function(){function j(d,f){this.el=d,this.options=f,e||(e=A()),this.$el=a(this.el),this.doc=a(this.options.documentContext||c),this.win=a(this.options.windowContext||b),this.body=this.doc.find("body"),this.$content=this.$el.children("."+this.options.contentClass),this.$content.attr("tabindex",this.options.tabIndex||0),this.content=this.$content[0],this.previousPosition=0,this.options.iOSNativeScrolling&&null!=this.el.style.WebkitOverflowScrolling?this.nativeScrolling():this.generate(),this.createEvents(),this.addEvents(),this.reset()}return j.prototype.preventScrolling=function(a,b){if(this.isActive)if(a.type===f)(b===g&&a.originalEvent.detail>0||b===w&&a.originalEvent.detail<0)&&a.preventDefault();else if(a.type===p){if(!a.originalEvent||!a.originalEvent.wheelDelta)return;(b===g&&a.originalEvent.wheelDelta<0||b===w&&a.originalEvent.wheelDelta>0)&&a.preventDefault()}},j.prototype.nativeScrolling=function(){this.$content.css({WebkitOverflowScrolling:"touch"}),this.iOSNativeScrolling=!0,this.isActive=!0},j.prototype.updateScrollValues=function(){var a,b;a=this.content,this.maxScrollTop=a.scrollHeight-a.clientHeight,this.prevScrollTop=this.contentScrollTop||0,this.contentScrollTop=a.scrollTop,b=this.contentScrollTop>this.previousPosition?"down":this.contentScrollTop=a.maxScrollTop&&a.prevScrollTop!==a.maxScrollTop?a.$el.trigger("scrollend"):0===a.contentScrollTop&&0!==a.prevScrollTop&&a.$el.trigger("scrolltop"),!1}}(this),up:function(a){return function(b){return a.isBeingDragged=!1,a.pane.removeClass(a.options.activeClass),a.doc.unbind(n,a.events[h]).unbind(o,a.events[w]),a.body.unbind(m,a.events[i]),!1}}(this),resize:function(a){return function(b){a.reset()}}(this),panedown:function(a){return function(b){return a.sliderY=(b.offsetY||b.originalEvent.layerY)-.5*a.sliderHeight,a.scroll(),a.events.down(b),!1}}(this),scroll:function(a){return function(b){a.updateScrollValues(),a.isBeingDragged||(a.iOSNativeScrolling||(a.sliderY=a.sliderTop,a.setOnScrollStyles()),null!=b&&(a.contentScrollTop>=a.maxScrollTop?(a.options.preventPageScrolling&&a.preventScrolling(b,g),a.prevScrollTop!==a.maxScrollTop&&a.$el.trigger("scrollend")):0===a.contentScrollTop&&(a.options.preventPageScrolling&&a.preventScrolling(b,w),0!==a.prevScrollTop&&a.$el.trigger("scrolltop"))))}}(this),wheel:function(a){return function(b){var c;if(null!=b)return c=b.delta||b.wheelDelta||b.originalEvent&&b.originalEvent.wheelDelta||-b.detail||b.originalEvent&&-b.originalEvent.detail,c&&(a.sliderY+=-c/3),a.scroll(),!1}}(this),enter:function(a){return function(b){var c;if(a.isBeingDragged)return 1!==(b.buttons||b.which)?(c=a.events)[w].apply(c,arguments):void 0}}(this)}},j.prototype.addEvents=function(){var a;this.removeEvents(),a=this.events,this.options.disableResize||this.win.bind(s,a[s]),this.iOSNativeScrolling||(this.slider.bind(l,a[g]),this.pane.bind(l,a[r]).bind(""+p+" "+f,a[x])),this.$content.bind(""+t+" "+p+" "+f+" "+v,a[t])},j.prototype.removeEvents=function(){var a;a=this.events,this.win.unbind(s,a[s]),this.iOSNativeScrolling||(this.slider.unbind(),this.pane.unbind()),this.$content.unbind(""+t+" "+p+" "+f+" "+v,a[t])},j.prototype.generate=function(){var a,c,d,f,g,h,i;return f=this.options,h=f.paneClass,i=f.sliderClass,a=f.contentClass,(g=this.$el.children("."+h)).length||g.children("."+i).length||this.$el.append('
'),this.pane=this.$el.children("."+h),this.slider=this.pane.find("."+i),0===e&&C()?(d=b.getComputedStyle(this.content,null).getPropertyValue("padding-right").replace(/[^0-9.]+/g,""),c={right:-14,paddingRight:+d+14}):e&&(c={right:-e},this.$el.addClass(f.enabledClass)),null!=c&&this.$content.css(c),this},j.prototype.restore=function(){this.stopped=!1,this.iOSNativeScrolling||this.pane.show(),this.addEvents()},j.prototype.reset=function(){var a,b,c,f,g,h,i,j,k,l,m,n;return this.iOSNativeScrolling?void(this.contentHeight=this.content.scrollHeight):(this.$el.find("."+this.options.paneClass).length||this.generate().stop(),this.stopped&&this.restore(),a=this.content,f=a.style,g=f.overflowY,d&&this.$content.css({height:this.$content.height()}),b=a.scrollHeight+e,l=parseInt(this.$el.css("max-height"),10),l>0&&(this.$el.height(""),this.$el.height(a.scrollHeight>l?l:a.scrollHeight)),i=this.pane.outerHeight(!1),k=parseInt(this.pane.css("top"),10),h=parseInt(this.pane.css("bottom"),10),j=i+k+h,n=Math.round(j/b*i),nthis.options.sliderMaxHeight&&(n=this.options.sliderMaxHeight),g===t&&f.overflowX!==t&&(n+=e),this.maxSliderTop=j-n,this.contentHeight=b,this.paneHeight=i,this.paneOuterHeight=j,this.sliderHeight=n,this.paneTop=k,this.slider.height(n),this.events.scroll(),this.pane.show(),this.isActive=!0,a.scrollHeight===a.clientHeight||this.pane.outerHeight(!0)>=a.scrollHeight&&g!==t?(this.pane.hide(),this.isActive=!1):this.el.clientHeight===a.scrollHeight&&g===t?this.slider.hide():this.slider.show(),this.pane.css({opacity:this.options.alwaysVisible?1:"",visibility:this.options.alwaysVisible?"visible":""}),c=this.$content.css("position"),("static"===c||"relative"===c)&&(m=parseInt(this.$content.css("right"),10),m&&this.$content.css({right:"",marginRight:m})),this)},j.prototype.scroll=function(){return this.isActive?(this.sliderY=Math.max(0,this.sliderY),this.sliderY=Math.min(this.maxSliderTop,this.sliderY),this.$content.scrollTop(this.maxScrollTop*this.sliderY/this.maxSliderTop),this.iOSNativeScrolling||(this.updateScrollValues(),this.setOnScrollStyles()),this):void 0},j.prototype.scrollBottom=function(a){return this.isActive?(this.$content.scrollTop(this.contentHeight-this.$content.height()-a).trigger(p),this.stop().restore(),this):void 0},j.prototype.scrollTop=function(a){return this.isActive?(this.$content.scrollTop(+a).trigger(p),this.stop().restore(),this):void 0},j.prototype.scrollTo=function(a){return this.isActive?(this.scrollTop(this.$el.find(a).get(0).offsetTop),this):void 0},j.prototype.stop=function(){return y&&this.scrollRAF&&(y(this.scrollRAF),this.scrollRAF=null),this.stopped=!0,this.removeEvents(),this.iOSNativeScrolling||this.pane.hide(),this},j.prototype.destroy=function(){return this.stopped||this.stop(),!this.iOSNativeScrolling&&this.pane.length&&this.pane.remove(),d&&this.$content.height(""),this.$content.removeAttr("tabindex"),this.$el.hasClass(this.options.enabledClass)&&(this.$el.removeClass(this.options.enabledClass),this.$content.css({right:""})),this},j.prototype.flash=function(){return!this.iOSNativeScrolling&&this.isActive?(this.reset(),this.pane.addClass(this.options.flashedClass),setTimeout(function(a){return function(){a.pane.removeClass(a.options.flashedClass)}}(this),this.options.flashDelay),this):void 0},j}(),a.fn.nanoScroller=function(b){return this.each(function(){var c,d;if((d=this.nanoscroller)||(c=a.extend({},z,b),this.nanoscroller=d=new q(this,c)),b&&"object"==typeof b){if(a.extend(d.options,b),null!=b.scrollBottom)return d.scrollBottom(b.scrollBottom);if(null!=b.scrollTop)return d.scrollTop(b.scrollTop);if(b.scrollTo)return d.scrollTo(b.scrollTo);if("bottom"===b.scroll)return d.scrollBottom(0);if("top"===b.scroll)return d.scrollTop(0);if(b.scroll&&b.scroll instanceof a)return d.scrollTo(b.scroll);if(b.stop)return d.stop();if(b.destroy)return d.destroy();if(b.flash)return d.flash()}return d.reset()})},a.fn.nanoScroller.Constructor=q}); diff --git a/htdocs/lib/location-picker.min.js b/htdocs/lib/location-picker.min.js new file mode 100644 index 000000000..7b3dfc526 --- /dev/null +++ b/htdocs/lib/location-picker.min.js @@ -0,0 +1,4 @@ +/* Taken from https://github.com/cyphercodes/location-picker under GPLv3 license */ +/* Contains https://github.com/cyphercodes/location-picker/pull/11 to allow latitude and longitude to be 0 */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.locationPicker=t()}(this,function(){"use strict";return function(e,t){void 0===t&&(t={});var n=t.insertAt;if(e&&"undefined"!=typeof document){var o=document.head||document.getElementsByTagName("head")[0],i=document.createElement("style");i.type="text/css","top"===n&&o.firstChild?o.insertBefore(i,o.firstChild):o.appendChild(i),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(document.createTextNode(e))}}('.location-picker .centerMarker{position:absolute;background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADYAAABWCAYAAACEsWWHAAAGLElEQVR4AdXcA2xsXReH8f3Ztm3btm2br23btm3btm2rd257b835vVnJStL0sj3Tds6TrOTkzN5r/5925qgorQRPwQewDHbHebgXDQxlNXLfeTlmmZzzlNJOpMyXsA86TJ2O7PGl6DnbQr/CncYx1tNj4JzTde+0la5V/6/x+x+Z85Ov6/jWZ0TFduyL12JMjI05E4iev5pxQXwFV0lGOx7Xf/xRulb5n45vflrH1z4xqYo5MTd6RC9JrvGVmRB6C04dLzR/yw10fP2TGbJyRa/oOVHw1Fh7uqS+jAY0e3uaPXvupOM7n48w01LRO9aItSDX/nKrpf6FYRi85AJzfvy1WHxGKtaKNZPhyNIKoadjZ0nfIfvFYrNSsXYiMz2tilg00Bweas7fZO1YYFYrMkQWKVfl7ScadS7z12jcFhVZxsn9ayoHimHI71RbVWRKhiPrZA7pjZn+TFX4zDWW6lSAU/PoFw3auiJjcuqSryjyPJWH9LauyBhZEXxlcdd+VyFOjDGxFhVZk6vCoUwEv5KXSXlFUYuKrJE5+dXCvlt3Ia7TYkKtKjIndxr/XcOXIMzzgrZWFZkje/Kl8WL7QP/xR7Z0wWs/9X5HvutNdnjdq2z6qpdFxXbsi9daulZkT/YZ/zbsQNwTtWSRuz7/YXu9+bU2ePlLFlcxJsa2ZM3InnSEU8EH5J1v3iRWqhs+9QGbv+plS5TKirExp/K6kT0ckg8ULIO4RW/Jd2q81GTkWvGdC4dkmYLdoXvHLSs3zrffVCrmVl4/HJLdC86r/vmKA8X7ImCVqnxACYfkvIL7EE+OKjWNI11FsehRKUM4JPcVNBCPxSo1jcN4RbHoUSlDOCSNgiHEM79KTeMcVVEselTKEA7JUMEA4pqr9mLhkAwUPAqNX36n9m/Fxi+/K3mk4GaY+/df1/7gEQ7JzQUnwPwN16h8XTjbh/twSE4oWBf6jjio9ifocEjWLfgeDN14be0vqcIh+V7BCzFobKw556ffqO1FcGQPh3AJpxLgROjebrPa3rZE9uTEEgT4IwzffENtbzQje/LH8WLPRQPycXatKjInjXAp48GGMHjx+bUTi8zJhmUieAX60ez8359rIxVZ0czsrygLA9uM+6zVoiJrsnVZFHg55sP8jddqe6nImMyP7GVxYA0Yffyxtn4iHNkiY7J6WRJ4Fu6G3n13a1uxyJbcHZnL0oDvQHNwsJmPDNqqIlNkg8haJgOOgKGrL287sciUHFEmC16FTpi/2bptIxVZks7IWKYC/grN+fOa+bBnVisyRBaIbKUKOBsGzjp11sUiQ3J2qQreij7oWn3ZWZOKtZO+yFRaAVaAsSceb3Z874szLhVrxtoQWUqrwFNxeeWfoVX/2dflkaW0ErwPQ2h2Lvf3GZOKtdDMtd9XpgOsByMP3q/j25+bdqlYI9ZK1ivTBZ6JW6D3kH2nXSzWSG6Jtct0gs+iaWSkOfcvv5g2qehtdLSJqM+WmQC7wND110ybWPROdikzBV6Ih2H+5uu1XCp6Jg/HWmUmwa+gOa+rOedHX22ZVPSKngh+VWYDnAP9Jx3TMrHolZxTZgu8B8PxBHbun39eWSp6RC8MR+8ym2AbGLzo3Mpi0SPZpsw2eCm6oPP/f5myVMxNuqJnaQewKgzdMPXDf8xNVi3tAp6Nh2DemstPWirmJNHj2aWdwDIwfNstkxaLOckypd3Ac/AEdP73T0stFWOTJ6JHaUewBgycecpSi8XYZI3SruBlGGwODy/VT0djTIzFYMwt7QwOhZ69d16iWIxJDi3tDr5CPh/5xqcWKRWvxRgEX6mDWP5GOF0r/muRYl0r/ltyV8wpdQCbQ98xhy1SLF5LNi91AV+A0ccfXaRYvJZ8oU5iT0cXNP7wExOlYl/SFWNLncARMH+L9U0Ui33JEaVuYGXoP/lYE8ViX7JyHcW+uahrx9iXfLOOYq+Bse75JorFvuQ1pY6gE8b/kV1sJ52lruCuib+9GtvJXXUWu2ri72fFdnJVncXOhvgXFykmtpOz6yx2MMzfdB0pJraTg+sstiwM33mbxm9/qPG7H4ntZNk6iz0Hd5hA7ntOqTN4OfbDo1mx/fIyzTwJJedUPgRWtocAAAAASUVORK5CYII=") no-repeat;background-size:100%;top:50%;left:50%;z-index:1;margin-left:-14px;margin-top:-43px;height:44px;width:28px;cursor:pointer}'),function(){function e(e,t,n){void 0===t&&(t={}),void 0===n&&(n={});var o={setCurrentPosition:!0};Object.assign(o,t);var i={center:new google.maps.LatLng("number"==typeof o.lat?o.lat:34.4346,"number"==typeof o.lng?o.lng:35.8362),zoom:15};Object.assign(i,n),e instanceof HTMLElement?this.element=e:this.element=document.getElementById(e),this.map=new google.maps.Map(this.element,i);var r=document.createElement("div");r.classList.add("centerMarker"),this.element&&(this.element.classList.add("location-picker"),this.element.children[0].appendChild(r)),!o.setCurrentPosition||o.lat||o.lng||this.setCurrentPosition()}return e.prototype.getMarkerPosition=function(){var e=this.map.getCenter();return{lat:e.lat(),lng:e.lng()}},e.prototype.setLocation=function(e,t){this.map.setCenter(new google.maps.LatLng(e,t))},e.prototype.setCurrentPosition=function(){var e=this;navigator.geolocation?navigator.geolocation.getCurrentPosition(function(t){var n={lat:t.coords.latitude,lng:t.coords.longitude};e.map.setCenter(n)},function(){console.log("Could not determine your location...")}):console.log("Your browser does not support Geolocation.")},e}()}); + diff --git a/htdocs/nanoscroller.css b/htdocs/lib/nanoscroller.css similarity index 100% rename from htdocs/nanoscroller.css rename to htdocs/lib/nanoscroller.css diff --git a/htdocs/lib/nite-overlay.js b/htdocs/lib/nite-overlay.js new file mode 100644 index 000000000..2cb805e7f --- /dev/null +++ b/htdocs/lib/nite-overlay.js @@ -0,0 +1,147 @@ +/* Nite v1.7 + * A tiny library to create a night overlay over the map + * Author: Rossen Georgiev @ https://github.com/rossengeorgiev + * Requires: GMaps API 3 + */ + + +var nite = { + map: null, + date: null, + sun_position: null, + earth_radius_meters: 6371008, + marker_twilight_civil: null, + marker_twilight_nautical: null, + marker_twilight_astronomical: null, + marker_night: null, + + init: function(map) { + if(typeof google === 'undefined' + || typeof google.maps === 'undefined') throw "Nite Overlay: no google.maps detected"; + + this.map = map; + this.sun_position = this.calculatePositionOfSun(); + + this.marker_twilight_civil = new google.maps.Circle({ + map: this.map, + center: this.getShadowPosition(), + radius: this.getShadowRadiusFromAngle(0.566666), + fillColor: "#000", + fillOpacity: 0.1, + strokeOpacity: 0, + clickable: false, + editable: false, + zIndex: 1 + }); + this.marker_twilight_nautical = new google.maps.Circle({ + map: this.map, + center: this.getShadowPosition(), + radius: this.getShadowRadiusFromAngle(6), + fillColor: "#000", + fillOpacity: 0.1, + strokeOpacity: 0, + clickable: false, + editable: false, + zIndex: 1 + }); + this.marker_twilight_astronomical = new google.maps.Circle({ + map: this.map, + center: this.getShadowPosition(), + radius: this.getShadowRadiusFromAngle(12), + fillColor: "#000", + fillOpacity: 0.1, + strokeOpacity: 0, + clickable: false, + editable: false, + zIndex: 1 + }); + this.marker_night = new google.maps.Circle({ + map: this.map, + center: this.getShadowPosition(), + radius: this.getShadowRadiusFromAngle(18), + fillColor: "#000", + fillOpacity: 0.1, + strokeOpacity: 0, + clickable: false, + editable: false, + zIndex: 1 + }); + }, + getShadowRadiusFromAngle: function(angle) { + var shadow_radius = this.earth_radius_meters * Math.PI * 0.5; + var twilight_dist = ((this.earth_radius_meters * 2 * Math.PI) / 360) * angle; + return shadow_radius - twilight_dist; + }, + getSunPosition: function() { + return this.sun_position; + }, + getShadowPosition: function() { + return (this.sun_position) ? new google.maps.LatLng(-this.sun_position.lat(), this.sun_position.lng() + 180) : null; + }, + refresh: function() { + if(!this.isVisible()) return; + this.sun_position = this.calculatePositionOfSun(this.date); + var shadow_position = this.getShadowPosition(); + this.marker_twilight_civil.setCenter(shadow_position); + this.marker_twilight_nautical.setCenter(shadow_position); + this.marker_twilight_astronomical.setCenter(shadow_position); + this.marker_night.setCenter(shadow_position); + }, + jday: function(date) { + return (date.getTime() / 86400000.0) + 2440587.5; + }, + calculatePositionOfSun: function(date) { + date = (date instanceof Date) ? date : new Date(); + + var rad = 0.017453292519943295; + + // based on NOAA solar calculations + var ms_past_midnight = ((date.getUTCHours() * 60 + date.getUTCMinutes()) * 60 + date.getUTCSeconds()) * 1000 + date.getUTCMilliseconds(); + var jc = (this.jday(date) - 2451545)/36525; + var mean_long_sun = (280.46646+jc*(36000.76983+jc*0.0003032)) % 360; + var mean_anom_sun = 357.52911+jc*(35999.05029-0.0001537*jc); + var sun_eq = Math.sin(rad*mean_anom_sun)*(1.914602-jc*(0.004817+0.000014*jc))+Math.sin(rad*2*mean_anom_sun)*(0.019993-0.000101*jc)+Math.sin(rad*3*mean_anom_sun)*0.000289; + var sun_true_long = mean_long_sun + sun_eq; + var sun_app_long = sun_true_long - 0.00569 - 0.00478*Math.sin(rad*125.04-1934.136*jc); + var mean_obliq_ecliptic = 23+(26+((21.448-jc*(46.815+jc*(0.00059-jc*0.001813))))/60)/60; + var obliq_corr = mean_obliq_ecliptic + 0.00256*Math.cos(rad*125.04-1934.136*jc); + + var lat = Math.asin(Math.sin(rad*obliq_corr)*Math.sin(rad*sun_app_long)) / rad; + + var eccent = 0.016708634-jc*(0.000042037+0.0000001267*jc); + var y = Math.tan(rad*(obliq_corr/2))*Math.tan(rad*(obliq_corr/2)); + var rq_of_time = 4*((y*Math.sin(2*rad*mean_long_sun)-2*eccent*Math.sin(rad*mean_anom_sun)+4*eccent*y*Math.sin(rad*mean_anom_sun)*Math.cos(2*rad*mean_long_sun)-0.5*y*y*Math.sin(4*rad*mean_long_sun)-1.25*eccent*eccent*Math.sin(2*rad*mean_anom_sun))/rad); + var true_solar_time_in_deg = ((ms_past_midnight+rq_of_time*60000) % 86400000) / 240000; + + var lng = -((true_solar_time_in_deg < 0) ? true_solar_time_in_deg + 180 : true_solar_time_in_deg - 180); + + return new google.maps.LatLng(lat, lng); + }, + setDate: function(date) { + this.date = date; + this.refresh(); + }, + setMap: function(map) { + this.map = map; + this.marker_twilight_civil.setMap(this.map); + this.marker_twilight_nautical.setMap(this.map); + this.marker_twilight_astronomical.setMap(this.map); + this.marker_night.setMap(this.map); + }, + show: function() { + this.marker_twilight_civil.setVisible(true); + this.marker_twilight_nautical.setVisible(true); + this.marker_twilight_astronomical.setVisible(true); + this.marker_night.setVisible(true); + this.refresh(); + }, + hide: function() { + this.marker_twilight_civil.setVisible(false); + this.marker_twilight_nautical.setVisible(false); + this.marker_twilight_astronomical.setVisible(false); + this.marker_night.setVisible(false); + }, + isVisible: function() { + return this.marker_night.getVisible(); + } +} diff --git a/htdocs/lib/settings/BookmarkTable.js b/htdocs/lib/settings/BookmarkTable.js new file mode 100644 index 000000000..48f48f195 --- /dev/null +++ b/htdocs/lib/settings/BookmarkTable.js @@ -0,0 +1,402 @@ +function Editor(table) { + this.table = table; +} + +Editor.prototype.getInputHtml = function() { + return ''; +} + +Editor.prototype.render = function(el) { + this.input = $(this.getInputHtml()); + el.append(this.input); + this.setupEvents(); +}; + +Editor.prototype.setupEvents = function() { + var me = this; + this.input.on('blur', function() { me.submit(); }).on('change', function() { me.submit(); }).on('keydown', function(e){ + if (e.keyCode == 13) return me.submit(); + if (e.keyCode == 27) return me.cancel(); + }); +}; + +Editor.prototype.submit = function() { + if (!this.onSubmit) return; + var submit = this.onSubmit; + delete this.onSubmit; + submit(); +}; + +Editor.prototype.cancel = function() { + if (!this.onCancel) return; + var cancel = this.onCancel; + delete this.onCancel; + cancel(); +}; + +Editor.prototype.focus = function() { + this.input.focus(); +}; + +Editor.prototype.disable = function(flag) { + this.input.prop('disabled', flag); +}; + +Editor.prototype.setValue = function(value) { + this.input.val(value); +}; + +Editor.prototype.getValue = function() { + return this.input.val(); +}; + +Editor.prototype.getHtml = function() { + return this.getValue(); +}; + +function NameEditor(table) { + Editor.call(this, table); +} + +NameEditor.prototype = new Editor(); + +NameEditor.prototype.getInputHtml = function() { + return ''; +} + +function FrequencyEditor(table) { + Editor.call(this, table); +} + +FrequencyEditor.suffixes = { + '': 0, + 'K': 3, + 'M': 6, + 'G': 9, + 'T': 12 +}; + +FrequencyEditor.prototype = new Editor(); + +FrequencyEditor.prototype.getInputHtml = function() { + return '
' + + '' + + '
' + + '' + + '
' + + '
'; +}; + +FrequencyEditor.prototype.render = function(el) { + this.input = $(this.getInputHtml()); + el.append(this.input); + this.freqInput = el.find('input'); + this.expInput = el.find('select'); + this.setupEvents(); +}; + +FrequencyEditor.prototype.setupEvents = function() { + var me = this; + var inputs = [this.freqInput, this.expInput].map(function(i) { return i[0]; }); + inputs.forEach(function(input) { + $(input).on('blur', function(e){ + if (inputs.indexOf(e.relatedTarget) >= 0) { + return; + } + me.submit(); + }); + }); + + var me = this; + this.freqInput.on('keydown', function(e){ + if (e.keyCode == 13) return me.submit(); + if (e.keyCode == 27) return me.cancel(); + var c = String.fromCharCode(e.which); + if (c in FrequencyEditor.suffixes) { + me.expInput.val(FrequencyEditor.suffixes[c]); + } + }); +} + +FrequencyEditor.prototype.getValue = function() { + var frequency = parseFloat(this.freqInput.val()); + var exp = parseInt(this.expInput.val()); + return Math.floor(frequency * 10 ** exp); +}; + +FrequencyEditor.prototype.setValue = function(value) { + var value = parseFloat(value); + var exp = 0; + if (!Number.isNaN(value) && value > 0) { + exp = Math.floor(Math.log10(value) / 3) * 3; + } + this.freqInput.val(value / 10 ** exp); + this.expInput.val(exp); +}; + +FrequencyEditor.prototype.focus = function() { + this.freqInput.focus(); +}; + +var renderFrequency = function(freq) { + var exp = 0; + if (!Number.isNaN(freq)) { + exp = Math.floor(Math.log10(freq) / 3) * 3; + } + var frequency = freq / 10 ** exp; + var suffix = Object.entries(FrequencyEditor.suffixes).find(function(e) { + return e[1] == exp; + }); + if (!suffix) { + return freq + ' Hz'; + } + // fix lowercase 'kHz' + suffix = suffix[0] == 'K' ? 'k' : suffix[0]; + var expString = suffix[0] + 'Hz'; + return frequency + ' ' + expString; +} + +FrequencyEditor.prototype.getHtml = function() { + return renderFrequency(this.getValue()); +}; + +function ModulationEditor(table) { + Editor.call(this, table); + this.modes = table.data('modes'); +} + +ModulationEditor.prototype = new Editor(); + +ModulationEditor.prototype.getInputHtml = function() { + return ''; +}; + +ModulationEditor.prototype.getHtml = function() { + var $option = this.input.find('option:selected') + return $option.html(); +}; + +$.fn.bookmarktable = function() { + var editors = { + name: NameEditor, + frequency: FrequencyEditor, + modulation: ModulationEditor + }; + + $.each(this, function(){ + var $table = $(this).find('table'); + + $table.on('dblclick', 'td', function(e) { + var $cell = $(e.target); + var html = $cell.html(); + + var $row = $cell.parents('tr'); + var name = $cell.data('editor'); + var EditorCls = editors[name]; + if (!EditorCls) return; + + var editor = new EditorCls($table); + editor.render($cell.html('')); + editor.setValue($cell.data('value')); + editor.focus(); + + editor.onSubmit = function() { + editor.disable(true); + $.ajax(document.location.href + "/" + $row.data('id'), { + data: JSON.stringify(Object.fromEntries([[name, editor.getValue()]])), + contentType: 'application/json', + method: 'POST' + }).done(function(){ + $cell.data('value', editor.getValue()); + $cell.html(editor.getHtml()); + }); + }; + + editor.onCancel = function() { + $cell.html(html); + }; + }); + + var $modal = $('#deleteModal').modal({show:false}); + + $modal.on('hidden.bs.modal', function() { + var $row = $modal.data('row'); + if (!$row) return; + $row.find('.bookmark-delete').prop('disabled', false); + $modal.removeData('row'); + }); + + $modal.on('click', '.confirm', function() { + var $row = $modal.data('row'); + if (!$row) return; + $.ajax(document.location.href + "/" + $row.data('id'), { + data: "{}", + contentType: 'application/json', + method: 'DELETE' + }).done(function(){ + $row.remove(); + $modal.modal('hide'); + }); + }); + + $table.on('click', '.bookmark-delete', function(e) { + var $button = $(e.target); + $button.prop('disabled', true); + + var $row = $button.parents('tr'); + $modal.data('row', $row); + $modal.modal('show'); + }); + + $(this).on('click', '.bookmark-add', function() { + if ($table.find('tr[data-id="new"]').length) return; + + $table.find('.emptytext').remove(); + var row = $(''); + + var inputs = Object.fromEntries( + Object.entries(editors).map(function(e) { + return [e[0], new e[1]($table)]; + }) + ); + + row.append($.map(inputs, function(editor, name){ + var cell = $(''); + editor.render(cell); + return cell; + })); + row.append($( + '' + + '
' + + '' + + '' + + '
' + + '' + )); + + row.on('click', '.bookmark-cancel', function() { + row.remove(); + }); + + row.on('click', '.bookmark-save', function() { + var data = Object.fromEntries( + $.map(inputs, function(input, name){ + input.disable(true); + // double wrapped because jQuery.map() flattens the result + return [[name, input.getValue()]]; + }) + ); + + $.ajax(document.location.href, { + data: JSON.stringify([data]), + contentType: 'application/json', + method: 'POST' + }).done(function(data){ + if (data.length && data.length === 1 && 'bookmark_id' in data[0]) { + row.attr('data-id', data[0]['bookmark_id']); + var tds = row.find('td'); + + Object.values(inputs).forEach(function(input, index) { + var td = $(tds[index]); + td.data('value', input.getValue()); + td.html(input.getHtml()); + }); + + var $cell = row.find('td').last(); + var $group = $cell.find('.btn-group'); + if ($group.length) { + $group.remove; + $cell.html('
delete
'); + } + } + }); + + }); + + $table.append(row); + row[0].scrollIntoView(); + }); + + var $importModal = $('#importModal').modal({show: false}); + + $(this).find('.bookmark-import').on('click', function() { + var storage = new BookmarkLocalStorage(); + var bookmarks = storage.getBookmarks(); + if (bookmarks.length) { + var modes = $table.data('modes'); + var $list = $(''); + $list.append(bookmarks.map(function(b) { + var modulation = b.modulation; + if (modulation in modes) { + modulation = modes[modulation]; + } + var row = $( + '' + + '' + + '' + + '' + + '' + + '' + ); + row.data('bookmark', b); + return row; + })); + $importModal.find('.bookmark-list').html($list); + } else { + $importModal.find('.bookmark-list').html('No personal bookmarks found in this browser'); + } + $importModal.modal('show'); + }); + + $importModal.on('click', '.confirm', function() { + var $list = $importModal.find('.bookmark-list table'); + if ($list.length) { + var selected = $list.find('tr').filter(function(){ + return $(this).find('.select').is(':checked'); + }).map(function(){ + return $(this).data('bookmark'); + }).toArray(); + if (selected.length) { + $.ajax(document.location.href, { + data: JSON.stringify(selected), + contentType: 'application/json', + method: 'POST' + }).done(function(data){ + $table.find('.emptytext').remove(); + var modes = $table.data('modes'); + if (data.length && data.length == selected.length) { + $table.append(data.map(function(obj, index) { + var bookmark = selected[index]; + var modulation_name = bookmark.modulation; + if (modulation_name in modes) { + modulation_name = modes[modulation_name]; + } + return $( + '' + + '' + + '' + + '' + + '' + + '' + ) + })); + } + }); + } + } + $importModal.modal('hide'); + }); + }); +}; diff --git a/htdocs/lib/settings/ExponentialInput.js b/htdocs/lib/settings/ExponentialInput.js new file mode 100644 index 000000000..5f7e99f9a --- /dev/null +++ b/htdocs/lib/settings/ExponentialInput.js @@ -0,0 +1,45 @@ +$.fn.exponentialInput = function() { + var prefixes = { + 'K': 3, + 'M': 6, + 'G': 9, + 'T': 12 + }; + + this.each(function(){ + var $group = $(this); + var currentExponent = 0; + var $input = $group.find('input'); + + var setExponent = function() { + var newExponent = parseInt($exponent.val()); + var delta = currentExponent - newExponent; + if (delta >= 0) { + $input.val(parseFloat($input.val()) * 10 ** delta); + } else { + // should not be necessary to handle this separately, but floating point precision in javascript + // does not handle this well otherwise + $input.val(parseFloat($input.val()) / 10 ** -delta); + } + currentExponent = newExponent; + }; + + $input.on('keydown', function(e) { + var c = String.fromCharCode(e.which); + if (c in prefixes) { + currentExponent = prefixes[c]; + $exponent.val(prefixes[c]); + } + }); + + var $exponent = $group.find('select.exponent'); + $exponent.on('change', setExponent); + + // calculate initial exponent + var value = parseFloat($input.val()); + if (!Number.isNaN(value)) { + $exponent.val(Math.floor(Math.log10(Math.abs(value)) / 3) * 3); + setExponent(); + } + }) +}; \ No newline at end of file diff --git a/htdocs/lib/settings/GainInput.js b/htdocs/lib/settings/GainInput.js new file mode 100644 index 000000000..a5e26bfff --- /dev/null +++ b/htdocs/lib/settings/GainInput.js @@ -0,0 +1,17 @@ +$.fn.gainInput = function() { + this.each(function() { + var $container = $(this); + + var update = function(value){ + $container.find('.option').hide(); + $container.find('.option.' + value).show(); + } + + var $select = $container.find('select'); + $select.on('change', function(e) { + var value = $(e.target).val(); + update(value); + }); + update($select.val()); + }); +} \ No newline at end of file diff --git a/htdocs/lib/settings/ImageUpload.js b/htdocs/lib/settings/ImageUpload.js new file mode 100644 index 000000000..2337318df --- /dev/null +++ b/htdocs/lib/settings/ImageUpload.js @@ -0,0 +1,87 @@ +$.fn.imageUpload = function() { + $.each(this, function(){ + var $this = $(this); + var $uploadButton = $this.find('button.upload'); + var $restoreButton = $this.find('button.restore'); + var $img = $this.find('img'); + var originalUrl = $img.prop('src'); + var $input = $this.find('input'); + var id = $input.prop('id'); + var maxSize = $this.data('max-size'); + var $error; + var handleError = function(message) { + clearError(); + $error = $('
' + message + '
'); + $this.after($error); + $this.addClass('is-invalid'); + }; + var clearError = function(message) { + if ($error) $error.remove(); + $this.removeClass('is-invalid'); + }; + $uploadButton.click(function(){ + var input = document.createElement('input'); + input.type = 'file'; + input.accept = 'image/jpeg, image/png, image/webp'; + + input.onchange = function(e) { + $uploadButton.prop('disabled', true); + var $spinner = $(''); + $uploadButton.prepend($spinner); + + var reader = new FileReader() + reader.readAsArrayBuffer(e.target.files[0]); + reader.onprogress = function(e) { + if (e.loaded > maxSize) { + handleError('Maximum file size exceeded'); + $uploadButton.prop('disabled', false); + $spinner.remove(); + reader.abort(); + } + }; + reader.onload = function(e) { + if (e.loaded > maxSize) { + handleError('Maximum file size exceeded'); + $uploadButton.prop('disabled', false); + $spinner.remove(); + return; + } + $.ajax({ + url: '../imageupload?id=' + id, + type: 'POST', + data: e.target.result, + processData: false, + contentType: 'application/octet-stream', + }).done(function(data){ + $input.val(data.file); + $img.one('load', function() { + $uploadButton.prop('disabled', false); + $spinner.remove(); + }); + $img.prop('src', '../imageupload?file=' + data.file); + clearError(); + }).fail(function(xhr, error){ + try { + var res = JSON.parse(xhr.responseText); + handleError(res.error || error); + } catch (e) { + handleError(error); + } + $uploadButton.prop('disabled', false); + $spinner.remove(); + }); + } + }; + + input.click(); + return false; + }); + + $restoreButton.click(function(){ + $input.val('restore'); + $img.prop('src', originalUrl + "&mapped=false"); + clearError(); + return false; + }); + }); +} \ No newline at end of file diff --git a/htdocs/lib/settings/LogMessages.js b/htdocs/lib/settings/LogMessages.js new file mode 100644 index 000000000..ea2683eaa --- /dev/null +++ b/htdocs/lib/settings/LogMessages.js @@ -0,0 +1,5 @@ +$.fn.logMessages = function() { + $.each(this, function(){ + $(this).scrollTop(this.scrollHeight); + }); +}; \ No newline at end of file diff --git a/htdocs/lib/settings/MapInput.js b/htdocs/lib/settings/MapInput.js new file mode 100644 index 000000000..a35b8a9b0 --- /dev/null +++ b/htdocs/lib/settings/MapInput.js @@ -0,0 +1,23 @@ +$.fn.mapInput = function() { + this.each(function(el) { + var $el = $(this); + var field_id = $el.attr("for"); + var $lat = $('#' + field_id + '-lat'); + var $lon = $('#' + field_id + '-lon'); + $.getScript('https://maps.googleapis.com/maps/api/js?key=' + $el.data('key')).done(function(){ + $el.css('height', '200px'); + var lp = new locationPicker($el.get(0), { + lat: parseFloat($lat.val()), + lng: parseFloat($lon.val()) + }, { + zoom: 7 + }); + + google.maps.event.addListener(lp.map, 'idle', function(event){ + var pos = lp.getMarkerPosition(); + $lat.val(pos.lat); + $lon.val(pos.lng); + }); + }); + }); +}; \ No newline at end of file diff --git a/htdocs/lib/settings/OptionalSection.js b/htdocs/lib/settings/OptionalSection.js new file mode 100644 index 000000000..16eff54a2 --- /dev/null +++ b/htdocs/lib/settings/OptionalSection.js @@ -0,0 +1,29 @@ +$.fn.optionalSection = function(){ + this.each(function() { + var $section = $(this); + var $select = $section.find('.optional-select'); + var $optionalInputs = $section.find('.optional-inputs'); + $section.on('click', '.option-add-button', function(e){ + var field = $select.val(); + var group = $optionalInputs.find(".form-group[data-field='" + field + "']"); + group.find('input, select').filter(function(){ + // exclude template inputs + return !$(this).parents('.template').length; + }).prop('disabled', false); + $section.find('hr').before(group); + $select.find('option[value=\'' + field + '\']').remove(); + + return false; + }); + $section.on('click', '.option-remove-button', function(e) { + var group = $(e.target).parents('.form-group') + group.find('input, select').prop('disabled', true); + $optionalInputs.append(group); + var $label = group.find('label'); + var $option = $(''); + $select.append($option); + + return false; + }) + }); +} \ No newline at end of file diff --git a/htdocs/lib/settings/SchedulerInput.js b/htdocs/lib/settings/SchedulerInput.js new file mode 100644 index 000000000..1dd8156a7 --- /dev/null +++ b/htdocs/lib/settings/SchedulerInput.js @@ -0,0 +1,33 @@ +$.fn.schedulerInput = function() { + this.each(function() { + var $container = $(this); + var $template = $container.find('.template'); + $template.find('input, select').prop('disabled', true); + + var update = function(value){ + $container.find('.option').hide(); + $container.find('.option.' + value).show(); + } + + var $select = $container.find('select.mode'); + $select.on('change', function(e) { + var value = $(e.target).val(); + update(value); + }); + update($select.val()); + + $container.find('.add-button').on('click', function() { + var row = $template.clone(); + row.removeClass('template').show(); + row.find('input, select').prop('disabled', false); + $template.before(row); + + return false; + }); + + $container.on('click', '.remove-button', function(e) { + var row = $(e.target).parents('.scheduler-static-time-inputs'); + row.remove(); + }); + }); +} \ No newline at end of file diff --git a/htdocs/lib/settings/WaterfallDropdown.js b/htdocs/lib/settings/WaterfallDropdown.js new file mode 100644 index 000000000..ea0599235 --- /dev/null +++ b/htdocs/lib/settings/WaterfallDropdown.js @@ -0,0 +1,11 @@ +$.fn.waterfallDropdown = function(){ + this.each(function(){ + var $select = $(this); + var setVisibility = function() { + var show = $select.val() === 'CUSTOM'; + $('#waterfall_colors').parents('.form-group')[show ? 'show' : 'hide'](); + } + $select.on('change', setVisibility); + setVisibility(); + }) +} \ No newline at end of file diff --git a/htdocs/lib/settings/WsjtDecodingDepthsInput.js b/htdocs/lib/settings/WsjtDecodingDepthsInput.js new file mode 100644 index 000000000..f06f59c8a --- /dev/null +++ b/htdocs/lib/settings/WsjtDecodingDepthsInput.js @@ -0,0 +1,68 @@ +$.fn.wsjtDecodingDepthsInput = function() { + function WsjtDecodingDepthRow(inputs, mode, value) { + this.el = $(''); + this.modeInput = $(inputs.get(0)).clone(); + this.modeInput.val(mode); + this.valueInput = $(inputs.get(1)).clone(); + this.valueInput.val(value); + this.removeButton = $(''); + this.removeButton.data('row', this); + this.el.append([this.modeInput, this.valueInput, this.removeButton].map(function(i) { + return $(' + + + + + + """.format( + id=id(bookmark), + name=bookmark.getName(), + # TODO render frequency in si units + frequency=bookmark.getFrequency(), + rendered_frequency=render_frequency(bookmark.getFrequency()), + modulation=bookmark.getModulation() if mode is None else mode.modulation, + modulation_name=bookmark.getModulation() if mode is None else mode.name, + ) + + def _findBookmark(self, bookmark_id): + bookmarks = Bookmarks.getSharedInstance() + try: + return next(b for b in bookmarks.getBookmarks() if id(b) == bookmark_id) + except StopIteration: + return None + + def update(self): + bookmark_id = int(self.request.matches.group(1)) + bookmark = self._findBookmark(bookmark_id) + if bookmark is None: + self.send_response("{}", content_type="application/json", code=404) + return + try: + data = json.loads(self.get_body().decode("utf-8")) + for key in ["name", "frequency", "modulation"]: + if key in data: + value = data[key] + if key == "frequency": + value = int(value) + setattr(bookmark, key, value) + Bookmarks.getSharedInstance().store() + # TODO this should not be called explicitly... bookmarks don't have any event capability right now, though + Bookmarks.getSharedInstance().notifySubscriptions(bookmark) + self.send_response("{}", content_type="application/json", code=200) + except json.JSONDecodeError: + self.send_response("{}", content_type="application/json", code=400) + + def new(self): + bookmarks = Bookmarks.getSharedInstance() + + def create(bookmark_data): + # sanitize + data = { + "name": bookmark_data["name"], + "frequency": int(bookmark_data["frequency"]), + "modulation": bookmark_data["modulation"], + } + bookmark = Bookmark(data) + bookmarks.addBookmark(bookmark) + return {"bookmark_id": id(bookmark)} + + try: + data = json.loads(self.get_body().decode("utf-8")) + result = [create(b) for b in data] + bookmarks.store() + self.send_response(json.dumps(result), content_type="application/json", code=200) + except json.JSONDecodeError: + self.send_response("{}", content_type="application/json", code=400) + + def delete(self): + bookmark_id = int(self.request.matches.group(1)) + bookmark = self._findBookmark(bookmark_id) + if bookmark is None: + self.send_response("{}", content_type="application/json", code=404) + return + bookmarks = Bookmarks.getSharedInstance() + bookmarks.removeBookmark(bookmark) + bookmarks.store() + self.send_response("{}", content_type="application/json", code=200) + + def indexAction(self): + self.serve_template("settings/bookmarks.html", **self.template_variables()) diff --git a/owrx/controllers/settings/decoding.py b/owrx/controllers/settings/decoding.py new file mode 100644 index 000000000..480b0e9bf --- /dev/null +++ b/owrx/controllers/settings/decoding.py @@ -0,0 +1,95 @@ +from owrx.controllers.settings import SettingsFormController, SettingsBreadcrumb +from owrx.form.section import Section +from owrx.form.input import CheckboxInput, NumberInput, DropdownInput, Js8ProfileCheckboxInput, MultiCheckboxInput, Option, TextInput +from owrx.form.input.wfm import WfmTauValues +from owrx.form.input.wsjt import Q65ModeMatrix, WsjtDecodingDepthsInput +from owrx.form.input.converter import OptionalConverter +from owrx.wsjt import Fst4Profile, Fst4wProfile +from owrx.breadcrumb import Breadcrumb, BreadcrumbItem + + +class DecodingSettingsController(SettingsFormController): + def getTitle(self): + return "Demodulation and decoding" + + def get_breadcrumb(self) -> Breadcrumb: + return SettingsBreadcrumb().append(BreadcrumbItem("Demodulation and decoding", "settings/decoding")) + + def getSections(self): + return [ + Section( + "Demodulator settings", + NumberInput( + "squelch_auto_margin", + "Auto-Squelch threshold", + infotext="Offset to be added to the current signal level when using the auto-squelch", + append="dB", + ), + DropdownInput( + "wfm_deemphasis_tau", + "Tau setting for WFM (broadcast FM) deemphasis", + WfmTauValues, + infotext='See this Wikipedia article for more information', + ), + CheckboxInput( + "wfm_rds_rbds", + "Enable RBDS decoding (US RDS standard)", + ), + ), + Section( + "Digital voice", + TextInput( + "digital_voice_codecserver", + "Codecserver address", + infotext="Address of a remote codecserver instance (name[:port]). Leave empty to use local" + + " codecserver", + converter=OptionalConverter(), + ), + CheckboxInput( + "digital_voice_dmr_id_lookup", + 'Enable lookup of DMR ids in the ' + + "radioid database to show callsigns and names", + ), + CheckboxInput( + "digital_voice_nxdn_id_lookup", + 'Enable lookup of NXDN ids in the ' + + "radioid database to show callsigns and names", + ), + ), + Section( + "Digimodes", + NumberInput("digimodes_fft_size", "Digimodes FFT size", append="bins"), + ), + Section( + "Decoding settings", + NumberInput("decoding_queue_workers", "Number of decoding workers"), + NumberInput("decoding_queue_length", "Maximum length of decoding job queue"), + NumberInput( + "wsjt_decoding_depth", + "Default WSJT decoding depth", + infotext="A higher decoding depth will allow more results, but will also consume more cpu", + ), + WsjtDecodingDepthsInput( + "wsjt_decoding_depths", + "Individual decoding depths", + ), + NumberInput( + "js8_decoding_depth", + "Js8Call decoding depth", + infotext="A higher decoding depth will allow more results, but will also consume more cpu", + ), + Js8ProfileCheckboxInput("js8_enabled_profiles", "Js8Call enabled modes"), + MultiCheckboxInput( + "fst4_enabled_intervals", + "Enabled FST4 intervals", + [Option(v, "{}s".format(v)) for v in Fst4Profile.availableIntervals], + ), + MultiCheckboxInput( + "fst4w_enabled_intervals", + "Enabled FST4W intervals", + [Option(v, "{}s".format(v)) for v in Fst4wProfile.availableIntervals], + ), + Q65ModeMatrix("q65_enabled_combinations", "Enabled Q65 Mode combinations"), + ), + ] diff --git a/owrx/controllers/settings/general.py b/owrx/controllers/settings/general.py new file mode 100644 index 000000000..21ac82840 --- /dev/null +++ b/owrx/controllers/settings/general.py @@ -0,0 +1,244 @@ +from owrx.controllers.settings import SettingsFormController +from owrx.form.section import Section +from owrx.config.core import CoreConfig +from owrx.form.input import ( + CheckboxInput, + TextInput, + NumberInput, + FloatInput, + TextAreaInput, + DropdownInput, + Option, +) +from owrx.form.input.converter import WaterfallColorsConverter, IntConverter +from owrx.form.input.receiverid import ReceiverKeysInput +from owrx.form.input.gfx import AvatarInput, TopPhotoInput +from owrx.form.input.device import WaterfallLevelsInput, WaterfallAutoLevelsInput +from owrx.form.input.location import LocationInput +from owrx.waterfall import WaterfallOptions +from owrx.breadcrumb import Breadcrumb, BreadcrumbItem +from owrx.controllers.settings import SettingsBreadcrumb +import shutil +import os +import re +from glob import glob + +import logging + +logger = logging.getLogger(__name__) + + +class GeneralSettingsController(SettingsFormController): + def getTitle(self): + return "General Settings" + + def get_breadcrumb(self) -> Breadcrumb: + return SettingsBreadcrumb().append(BreadcrumbItem("General Settings", "settings/general")) + + def getSections(self): + return [ + Section( + "Receiver information", + TextInput("receiver_name", "Receiver name"), + TextInput("receiver_location", "Receiver location"), + NumberInput( + "receiver_asl", + "Receiver elevation", + append="meters above mean sea level", + ), + TextInput("receiver_admin", "Receiver admin"), + LocationInput("receiver_gps", "Receiver coordinates"), + TextInput("photo_title", "Photo title"), + TextAreaInput("photo_desc", "Photo description", infotext="HTML supported "), + ), + Section( + "Receiver images", + AvatarInput( + "receiver_avatar", + "Receiver Avatar", + infotext="For performance reasons, images are cached. " + + "It can take a few hours until they appear on the site.", + ), + TopPhotoInput( + "receiver_top_photo", + "Receiver Panorama", + infotext="For performance reasons, images are cached. " + + "It can take a few hours until they appear on the site.", + ), + ), + Section( + "Receiver limits", + NumberInput( + "max_clients", + "Maximum number of clients", + ), + ), + Section( + "Receiver listings", + ReceiverKeysInput( + "receiver_keys", + "Receiver keys", + ), + ), + Section( + "Waterfall settings", + DropdownInput( + "waterfall_scheme", + "Waterfall color scheme", + options=WaterfallOptions, + ), + TextAreaInput( + "waterfall_colors", + "Custom waterfall colors", + infotext="Please provide 6-digit hexadecimal RGB colors in HTML notation (#RRGGBB)" + + " or HEX notation (0xRRGGBB), one per line", + converter=WaterfallColorsConverter(), + ), + NumberInput( + "fft_fps", + "FFT speed", + infotext="This setting specifies how many lines are being added to the waterfall per second. " + + "Higher values will give you a faster waterfall, but will also use more CPU.", + append="frames per second", + ), + NumberInput("fft_size", "FFT size", append="bins"), + FloatInput( + "fft_voverlap_factor", + "FFT vertical overlap factor", + infotext="If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the " + + "diagram.", + ), + WaterfallLevelsInput("waterfall_levels", "Waterfall levels"), + WaterfallAutoLevelsInput( + "waterfall_auto_levels", + "Automatic adjustment margins", + infotext="Specifies the upper and lower dynamic headroom that should be added when automatically " + + "adjusting waterfall colors", + ), + CheckboxInput( + "waterfall_auto_level_default_mode", + "Automatically adjust waterfall level by default", + infotext="Enable this to automatically enable auto adjusting waterfall levels on page load.", + ), + NumberInput( + "waterfall_auto_min_range", + "Automatic adjustment minimum range", + append="dB", + infotext="Minimum dynamic range the waterfall should cover after automatically adjusting " + + "waterfall colors", + ), + ), + Section( + "Compression", + DropdownInput( + "audio_compression", + "Audio compression", + options=[ + Option("adpcm", "ADPCM"), + Option("none", "None"), + ], + ), + DropdownInput( + "fft_compression", + "Waterfall compression", + options=[ + Option("adpcm", "ADPCM"), + Option("none", "None"), + ], + ), + ), + Section( + "Display settings", + DropdownInput( + "tuning_precision", + "Tuning precision", + options=[Option(str(i), "{} Hz".format(10 ** i)) for i in range(0, 6)], + converter=IntConverter(), + ), + ), + Section( + "Map settings", + TextInput( + "google_maps_api_key", + "Google Maps API key", + infotext="Google Maps requires an API key, check out " + + '' + + "their documentation on how to obtain one.", + ), + NumberInput( + "map_position_retention_time", + "Map retention time", + infotext="Specifies for how long markers / grids will remain visible on the map", + append="s", + ), + DropdownInput( + "callsign_service", + "Callsign database service", + infotext="Allows users to navigate to an external callsign database service by clicking on " + + "callsigns", + options=[ + Option(None, "disabled"), + Option("qrzcq", "qrzcq.com"), + Option("qrz", "qrz.com"), + Option("aprsfi", "aprs.fi"), + ], + ), + DropdownInput( + "aircraft_tracking_service", + "Aircraft tracking service", + infotext="Allows users to navigate to an external flight tracking service by clicking on flight " + + "numbers", + options=[ + Option(None, "disabled"), + Option("flightaware", "FlightAware"), + Option("planefinder", "planefinder"), + ] + ) + ), + ] + + def remove_existing_image(self, image_id): + config = CoreConfig() + # remove all possible file extensions + for ext in ["png", "jpg", "webp"]: + try: + os.unlink("{}/{}.{}".format(config.get_data_directory(), image_id, ext)) + except FileNotFoundError: + pass + + def handle_image(self, data, image_id): + if image_id in data: + config = CoreConfig() + if data[image_id] == "restore": + self.remove_existing_image(image_id) + elif data[image_id]: + if not data[image_id].startswith(image_id): + logger.warning("invalid file name: %s", data[image_id]) + else: + # get file extension (at least 3 characters) + # should be all lowercase since they are set by the upload script + pattern = re.compile(".*\\.([a-z]{3,})$") + matches = pattern.match(data[image_id]) + if matches is None: + logger.warning("could not determine file extension for %s", image_id) + else: + self.remove_existing_image(image_id) + ext = matches.group(1) + data_file = "{}/{}.{}".format(config.get_data_directory(), image_id, ext) + temporary_file = "{}/{}".format(config.get_temporary_directory(), data[image_id]) + shutil.copy(temporary_file, data_file) + del data[image_id] + # remove any accumulated temporary files on save + for file in glob("{}/{}*".format(config.get_temporary_directory(), image_id)): + os.unlink(file) + + def processData(self, data): + # Image handling + for img in ["receiver_avatar", "receiver_top_photo"]: + self.handle_image(data, img) + # special handling for waterfall colors: custom colors only stay in config if custom color scheme is selected + if "waterfall_scheme" in data: + scheme = WaterfallOptions(data["waterfall_scheme"]) + if scheme is not WaterfallOptions.CUSTOM and "waterfall_colors" in data: + data["waterfall_colors"] = None + super().processData(data) diff --git a/owrx/controllers/settings/reporting.py b/owrx/controllers/settings/reporting.py new file mode 100644 index 000000000..5fb5a3ecd --- /dev/null +++ b/owrx/controllers/settings/reporting.py @@ -0,0 +1,132 @@ +from owrx.controllers.settings import SettingsFormController, SettingsBreadcrumb +from owrx.form.section import Section +from owrx.form.input.converter import OptionalConverter +from owrx.form.input.aprs import AprsBeaconSymbols, AprsAntennaDirections +from owrx.form.input import TextInput, CheckboxInput, DropdownInput, NumberInput, PasswordInput +from owrx.form.input.validator import AddressAndOptionalPortValidator +from owrx.breadcrumb import Breadcrumb, BreadcrumbItem + + +class ReportingController(SettingsFormController): + def getTitle(self): + return "Spotting and reporting" + + def get_breadcrumb(self) -> Breadcrumb: + return SettingsBreadcrumb().append(BreadcrumbItem("Spotting and reporting", "settings/reporting")) + + def getSections(self): + return [ + Section( + "APRS-IS reporting", + CheckboxInput( + "aprs_igate_enabled", + "Send received APRS data to APRS-IS", + infotext="Due to limits of the APRS-IS network, reporting will only work for background decoders" + ), + TextInput( + "aprs_callsign", + "APRS callsign", + infotext="This callsign will be used to send data to the APRS-IS network", + ), + TextInput("aprs_igate_server", "APRS-IS server"), + PasswordInput("aprs_igate_password", "APRS-IS network password"), + CheckboxInput( + "aprs_igate_beacon", + "Send the receiver position to the APRS-IS network", + infotext="Please check that your receiver location is setup correctly before enabling the beacon", + ), + DropdownInput( + "aprs_igate_symbol", + "APRS beacon symbol", + AprsBeaconSymbols, + ), + TextInput( + "aprs_igate_comment", + "APRS beacon text", + infotext="This text will be sent as APRS comment along with your beacon", + converter=OptionalConverter(), + ), + NumberInput( + "aprs_igate_height", + "Antenna height", + infotext="Antenna height above average terrain (HAAT)", + append="m", + converter=OptionalConverter(), + ), + NumberInput( + "aprs_igate_gain", + "Antenna gain", + append="dBi", + converter=OptionalConverter(), + ), + DropdownInput("aprs_igate_dir", "Antenna direction", AprsAntennaDirections), + ), + Section( + "pskreporter settings", + CheckboxInput( + "pskreporter_enabled", + "Enable sending spots to pskreporter.info", + ), + TextInput( + "pskreporter_callsign", + "pskreporter callsign", + infotext="This callsign will be used to send spots to pskreporter.info", + ), + TextInput( + "pskreporter_antenna_information", + "Antenna information", + infotext="Antenna description to be sent along with spots to pskreporter", + converter=OptionalConverter(), + ), + ), + Section( + "WSPRnet settings", + CheckboxInput( + "wsprnet_enabled", + "Enable sending spots to wsprnet.org", + ), + TextInput( + "wsprnet_callsign", + "wsprnet callsign", + infotext="This callsign will be used to send spots to wsprnet.org", + ), + ), + Section( + "MQTT settings", + CheckboxInput( + "mqtt_enabled", + "Enable publishing decodes to MQTT", + ), + TextInput( + "mqtt_host", + "Broker address", + infotext="Addresss of the MQTT broker to send decodes to (address[:port])", + validator=AddressAndOptionalPortValidator(), + ), + TextInput( + "mqtt_client_id", + "Client ID", + converter=OptionalConverter(), + ), + TextInput( + "mqtt_user", + "Username", + converter=OptionalConverter(), + ), + PasswordInput( + "mqtt_password", + "Password", + converter=OptionalConverter(), + ), + CheckboxInput( + "mqtt_use_ssl", + "Use SSL", + ), + TextInput( + "mqtt_topic", + "MQTT topic", + infotext="MQTT topic to publish decodes to (default: openwebrx/decodes)", + converter=OptionalConverter(), + ), + ) + ] diff --git a/owrx/controllers/settings/sdr.py b/owrx/controllers/settings/sdr.py new file mode 100644 index 000000000..1f17b4641 --- /dev/null +++ b/owrx/controllers/settings/sdr.py @@ -0,0 +1,449 @@ +from owrx.controllers.admin import AuthorizationMixin +from owrx.controllers.template import WebpageController +from owrx.controllers.settings import SettingsFormController +from owrx.source import SdrDeviceDescription, SdrDeviceDescriptionMissing, SdrClientClass +from owrx.config import Config +from owrx.connection import OpenWebRxReceiverClient +from owrx.controllers.settings import SettingsBreadcrumb +from owrx.form.section import Section +from urllib.parse import quote, unquote +from owrx.sdr import SdrService +from owrx.form.input import TextInput, DropdownInput, Option +from owrx.form.input.validator import RequiredValidator +from owrx.property import PropertyLayer +from owrx.breadcrumb import BreadcrumbMixin, Breadcrumb, BreadcrumbItem +from owrx.log import HistoryHandler +from abc import ABCMeta, abstractmethod +from uuid import uuid4 + + +class SdrDeviceBreadcrumb(SettingsBreadcrumb): + def __init__(self): + super().__init__() + self.append(BreadcrumbItem("SDR device settings", "settings/sdr")) + + +class SdrDeviceListController(AuthorizationMixin, BreadcrumbMixin, WebpageController): + def template_variables(self): + variables = super().template_variables() + variables["content"] = self.render_devices() + variables["title"] = "SDR device settings" + variables["modal"] = "" + variables["error"] = "" + return variables + + def get_breadcrumb(self): + return SdrDeviceBreadcrumb() + + def render_devices(self): + def render_device(device_id, config): + sources = SdrService.getAllSources() + source = sources[device_id] if device_id in sources else None + + additional_info = "" + state_info = "Unknown" + + if source is not None: + profiles = source.getProfiles() + currentProfile = profiles[source.getProfileId()] + clients = {c: len(source.getClients(c)) for c in SdrClientClass} + clients = {c: v for c, v in clients.items() if v} + connections = len([c for c in source.getClients() if isinstance(c, OpenWebRxReceiverClient)]) + additional_info = """ +
{num_profiles} profile(s)
+
Current profile: {current_profile}
+
Clients: {clients}
+
Connections: {connections}
+ """.format( + num_profiles=len(config["profiles"]), + current_profile=currentProfile["name"], + clients=", ".join("{cls}: {count}".format(cls=c.name, count=v) for c, v in clients.items()), + connections=connections, + ) + + state_info = ", ".join( + s + for s in [ + str(source.getState()), + None if source.isEnabled() else "Disabled", + "Failed" if source.isFailed() else None, + ] + if s is not None + ) + + return """ +
  • +
    +
    + +

    {device_name}

    +
    +
    State: {state}
    +
    +
    + {additional_info} +
    +
    +
  • + """.format( + device_name=config["name"] if config["name"] else "[Unnamed device]", + device_link="{}settings/sdr/{}".format(self.get_document_root(), quote(device_id)), + state=state_info, + additional_info=additional_info, + ) + + return """ +
      + {devices} +
    + + """.format( + devices="".join(render_device(key, value) for key, value in Config.get()["sdrs"].items()) + ) + + def indexAction(self): + self.serve_template("settings/general.html", **self.template_variables()) + + +class SdrFormController(SettingsFormController, metaclass=ABCMeta): + def __init__(self, handler, request, options): + super().__init__(handler, request, options) + self.device_id, self.device = self._get_device() + + def getTitle(self): + return self.device["name"] + + def render_sections(self): + return """ + {tabs} +
    + {sections} +
    + """.format( + tabs=self.render_tabs(), + sections=super().render_sections(), + ) + + def render_tabs(self): + return """ + + """.format( + device_link="{}settings/sdr/{}".format(self.get_document_root(), quote(self.device_id)), + device_name=self.device["name"] if self.device["name"] else "[Unnamed device]", + device_active="active" if self.isDeviceActive() else "", + new_profile_active="active" if self.isNewProfileActive() else "", + new_profile_link="{}settings/sdr/{}/newprofile".format(self.get_document_root(), quote(self.device_id)), + profile_tabs="".join( + """ + + """.format( + profile_link="{}settings/sdr/{}/profile/{}".format( + self.get_document_root(), quote(self.device_id), quote(profile_id) + ), + profile_name=profile["name"] if profile["name"] else "[Unnamed profile]", + profile_active="active" if self.isProfileActive(profile_id) else "", + ) + for profile_id, profile in self.device["profiles"].items() + ), + ) + + def isDeviceActive(self) -> bool: + return False + + def isProfileActive(self, profile_id) -> bool: + return False + + def isNewProfileActive(self) -> bool: + return False + + def store(self): + # need to overwrite the existing key in the config since the layering won't capture the changes otherwise + config = Config.get() + sdrs = config["sdrs"] + sdrs[self.device_id] = self.device + config["sdrs"] = sdrs + super().store() + + def _get_device(self): + config = Config.get() + device_id = unquote(self.request.matches.group(1)) + if device_id not in config["sdrs"]: + return None, None + return device_id, config["sdrs"][device_id] + + +class SdrFormControllerWithModal(SdrFormController, metaclass=ABCMeta): + def render_remove_button(self): + return "" + + def render_buttons(self): + return self.render_remove_button() + super().render_buttons() + + def buildModal(self): + return """ + + """.format( + object_type=self.getModalObjectType(), + confirm_url=self.getModalConfirmUrl(), + ) + + @abstractmethod + def getModalObjectType(self): + pass + + @abstractmethod + def getModalConfirmUrl(self): + pass + + +class SdrDeviceController(SdrFormControllerWithModal): + def get_breadcrumb(self) -> Breadcrumb: + return SdrDeviceBreadcrumb().append( + BreadcrumbItem(self.device["name"], "settings/sdr/{}".format(self.device_id)) + ) + + def getData(self): + return self.device + + def getSections(self): + try: + description = SdrDeviceDescription.getByType(self.device["type"]) + return [description.getDeviceSection()] + except SdrDeviceDescriptionMissing: + # TODO provide a generic interface that allows to switch the type + return [] + + def render_remove_button(self): + return """ + + """ + + def isDeviceActive(self) -> bool: + return True + + def indexAction(self): + if self.device is None: + self.send_response("device not found", code=404) + return + super().indexAction() + + def processFormData(self): + if self.device is None: + self.send_response("device not found", code=404) + return + return super().processFormData() + + def getModalObjectType(self): + return "SDR device" + + def getModalConfirmUrl(self): + return "{}settings/deletesdr/{}".format(self.get_document_root(), quote(self.device_id)) + + def deleteDevice(self): + if self.device_id is None: + return self.send_response("device not found", code=404) + config = Config.get() + sdrs = config["sdrs"] + del sdrs[self.device_id] + # need to overwrite the existing key in the config since the layering won't capture the changes otherwise + config["sdrs"] = sdrs + config.store() + return self.send_redirect("{}settings/sdr".format(self.get_document_root())) + + def render_sections(self): + handler = HistoryHandler.getHandler("owrx.source.{id}".format(id=self.device_id)) + return """ + {sections} +
    +
    Recent device log messages
    +
    +
    {messages}
    +
    +
    + """.format( + sections=super().render_sections(), + messages=handler.getFormattedHistory(), + ) + + +class NewSdrDeviceController(SettingsFormController): + def __init__(self, handler, request, options): + super().__init__(handler, request, options) + self.data_layer = PropertyLayer(name="", type="", profiles=PropertyLayer()) + self.device_id = str(uuid4()) + + def get_breadcrumb(self) -> Breadcrumb: + return SdrDeviceBreadcrumb().append(BreadcrumbItem("New device", "settings/sdr/newsdr")) + + def getSections(self): + return [ + Section( + "New device settings", + TextInput("name", "Device name", validator=RequiredValidator()), + DropdownInput( + "type", + "Device type", + [Option(sdr_type, name) for sdr_type, name in SdrDeviceDescription.getTypes().items()], + infotext="Note: Switching the type will not be possible after creation since the set of available " + + "options is different for each type.
    Note: This dropdown only shows device types that have " + + "their requirements met. If a type is missing from the list, please check the feature report.", + ), + ) + ] + + def getTitle(self): + return "New device" + + def getData(self): + return self.data_layer + + def store(self): + # need to overwrite the existing key in the config since the layering won't capture the changes otherwise + config = Config.get() + sdrs = config["sdrs"] + # a uuid should be unique, so i'm not sure if there's a point in this check + if self.device_id in sdrs: + raise ValueError("device {} already exists!".format(self.device_id)) + sdrs[self.device_id] = self.data_layer + config["sdrs"] = sdrs + super().store() + + def getSuccessfulRedirect(self): + return "{}settings/sdr/{}".format(self.get_document_root(), quote(self.device_id)) + + +class SdrProfileController(SdrFormControllerWithModal): + def __init__(self, handler, request, options): + super().__init__(handler, request, options) + self.profile_id, self.profile = self._get_profile() + + def get_breadcrumb(self) -> Breadcrumb: + return ( + SdrDeviceBreadcrumb() + .append(BreadcrumbItem(self.device["name"], "settings/sdr/{}".format(self.device_id))) + .append( + BreadcrumbItem( + self.profile["name"], "settings/sdr/{}/profile/{}".format(self.device_id, self.profile_id) + ) + ) + ) + + def getData(self): + return self.profile + + def _get_profile(self): + if self.device is None: + return None + profile_id = unquote(self.request.matches.group(2)) + if profile_id not in self.device["profiles"]: + return None + return profile_id, self.device["profiles"][profile_id] + + def isProfileActive(self, profile_id) -> bool: + return profile_id == self.profile_id + + def getSections(self): + try: + description = SdrDeviceDescription.getByType(self.device["type"]) + return [description.getProfileSection()] + except SdrDeviceDescriptionMissing: + # TODO provide a generic interface that allows to switch the type + return [] + + def indexAction(self): + if self.profile is None: + self.send_response("profile not found", code=404) + return + super().indexAction() + + def processFormData(self): + if self.profile is None: + self.send_response("profile not found", code=404) + return + return super().processFormData() + + def render_remove_button(self): + return """ + + """ + + def getModalObjectType(self): + return "profile" + + def getModalConfirmUrl(self): + return "{}settings/sdr/{}/deleteprofile/{}".format( + self.get_document_root(), quote(self.device_id), quote(self.profile_id) + ) + + def deleteProfile(self): + if self.profile_id is None: + return self.send_response("profile not found", code=404) + config = Config.get() + del self.device["profiles"][self.profile_id] + config.store() + return self.send_redirect("{}settings/sdr/{}".format(self.get_document_root(), quote(self.device_id))) + + +class NewProfileController(SdrProfileController): + def __init__(self, handler, request, options): + self.data_layer = PropertyLayer(name="") + super().__init__(handler, request, options) + + def get_breadcrumb(self) -> Breadcrumb: + return ( + SdrDeviceBreadcrumb() + .append(BreadcrumbItem(self.device["name"], "settings/sdr/{}".format(self.device_id))) + .append(BreadcrumbItem("New profile", "settings/sdr/{}/newprofile".format(self.device_id))) + ) + + def _get_profile(self): + return str(uuid4()), self.data_layer + + def isNewProfileActive(self) -> bool: + return True + + def store(self): + # a uuid should be unique, so i'm not sure if there's a point in this check + if self.profile_id in self.device["profiles"]: + raise ValueError("Profile {} already exists!".format(self.profile_id)) + self.device["profiles"][self.profile_id] = self.data_layer + super().store() + + def getSuccessfulRedirect(self): + return "{}settings/sdr/{}/profile/{}".format( + self.get_document_root(), quote(self.device_id), quote(self.profile_id) + ) + + def render_remove_button(self): + # new profile doesn't have a remove button + return "" diff --git a/owrx/controllers/status.py b/owrx/controllers/status.py new file mode 100644 index 000000000..b45292aec --- /dev/null +++ b/owrx/controllers/status.py @@ -0,0 +1,44 @@ +from .receiverid import ReceiverIdController +from owrx.version import openwebrx_version +from owrx.sdr import SdrService +from owrx.config import Config +from owrx.jsons import Encoder +import json + +import logging + +logger = logging.getLogger(__name__) + + +class StatusController(ReceiverIdController): + def getProfileStats(self, profile): + return { + "name": profile["name"], + "center_freq": profile["center_freq"], + "sample_rate": profile["samp_rate"], + } + + def getReceiverStats(self, receiver): + stats = { + "name": receiver.getName(), + # TODO would be better to have types from the config here + "type": type(receiver).__name__, + "profiles": [self.getProfileStats(p) for p in receiver.getProfiles().values()], + } + return stats + + def indexAction(self): + pm = Config.get() + status = { + "receiver": { + "name": pm["receiver_name"], + "admin": pm["receiver_admin"], + "gps": pm["receiver_gps"], + "asl": pm["receiver_asl"], + "location": pm["receiver_location"], + }, + "max_clients": pm["max_clients"], + "version": openwebrx_version, + "sdrs": [self.getReceiverStats(r) for r in SdrService.getActiveSources().values()], + } + self.send_response(json.dumps(status, cls=Encoder), content_type="application/json") diff --git a/owrx/controllers/template.py b/owrx/controllers/template.py new file mode 100644 index 000000000..f7e1a530d --- /dev/null +++ b/owrx/controllers/template.py @@ -0,0 +1,45 @@ +from owrx.controllers import Controller +from owrx.details import ReceiverDetails +from string import Template +import pkg_resources + + +class TemplateController(Controller): + def render_template(self, file, **vars): + file_content = pkg_resources.resource_string("htdocs", file).decode("utf-8") + template = Template(file_content) + + return template.safe_substitute(**vars) + + def serve_template(self, file, **vars): + self.send_response(self.render_template(file, **vars), content_type="text/html") + + def default_variables(self): + return {} + + +class WebpageController(TemplateController): + def get_document_root(self): + path_parts = [part for part in self.request.path[1:].split("/")] + levels = max(0, len(path_parts) - 1) + return "../" * levels + + def header_variables(self): + variables = {"document_root": self.get_document_root()} + variables.update(ReceiverDetails().__dict__()) + return variables + + def template_variables(self): + header = self.render_template("include/header.include.html", **self.header_variables()) + return {"header": header, "document_root": self.get_document_root()} + + +class IndexController(WebpageController): + def indexAction(self): + self.serve_template("index.html", **self.template_variables()) + + +class MapController(WebpageController): + def indexAction(self): + # TODO check if we have a google maps api key first? + self.serve_template("map.html", **self.template_variables()) diff --git a/owrx/controllers/websocket.py b/owrx/controllers/websocket.py new file mode 100644 index 000000000..3363abf0e --- /dev/null +++ b/owrx/controllers/websocket.py @@ -0,0 +1,10 @@ +from . import Controller +from owrx.websocket import WebSocketConnection +from owrx.connection import HandshakeMessageHandler + + +class WebSocketController(Controller): + def indexAction(self): + conn = WebSocketConnection(self.handler, HandshakeMessageHandler()) + # enter read loop + conn.handle() diff --git a/owrx/cpu.py b/owrx/cpu.py new file mode 100644 index 000000000..e296426a2 --- /dev/null +++ b/owrx/cpu.py @@ -0,0 +1,79 @@ +import threading + +import logging + +logger = logging.getLogger(__name__) + + +class CpuUsageThread(threading.Thread): + sharedInstance = None + creationLock = threading.Lock() + + @staticmethod + def getSharedInstance(): + with CpuUsageThread.creationLock: + if CpuUsageThread.sharedInstance is None: + CpuUsageThread.sharedInstance = CpuUsageThread() + return CpuUsageThread.sharedInstance + + def __init__(self): + self.clients = [] + self.doRun = True + self.last_worktime = 0 + self.last_idletime = 0 + self.endEvent = threading.Event() + self.startLock = threading.Lock() + super().__init__() + + def run(self): + logger.debug("cpu usage thread starting up") + while self.doRun: + try: + cpu_usage = self.get_cpu_usage() + except: + cpu_usage = 0 + for c in self.clients: + c.write_cpu_usage(cpu_usage) + self.endEvent.wait(timeout=3) + logger.debug("cpu usage thread shut down") + + def get_cpu_usage(self): + try: + f = open("/proc/stat", "r") + except: + return 0 # Workaround, possibly we're on a Mac + line = "" + while not "cpu " in line: + line = f.readline() + f.close() + spl = line.split(" ") + worktime = int(spl[2]) + int(spl[3]) + int(spl[4]) + idletime = int(spl[5]) + dworktime = worktime - self.last_worktime + didletime = idletime - self.last_idletime + rate = float(dworktime) / (didletime + dworktime) + self.last_worktime = worktime + self.last_idletime = idletime + if self.last_worktime == 0: + return 0 + return rate + + def add_client(self, c): + self.clients.append(c) + with self.startLock: + if not self.is_alive(): + self.start() + + def remove_client(self, c): + try: + self.clients.remove(c) + except ValueError: + pass + if not self.clients: + self.shutdown() + + def shutdown(self): + with CpuUsageThread.creationLock: + CpuUsageThread.sharedInstance = None + self.doRun = False + self.endEvent.set() diff --git a/owrx/dab/dablin.py b/owrx/dab/dablin.py new file mode 100644 index 000000000..ad7d2641b --- /dev/null +++ b/owrx/dab/dablin.py @@ -0,0 +1,20 @@ +from pycsdr.modules import ExecModule +from pycsdr.types import Format + + +class DablinModule(ExecModule): + def __init__(self): + self.serviceId = 0 + super().__init__( + Format.CHAR, + Format.FLOAT, + self._buildArgs() + ) + + def _buildArgs(self): + return ["dablin", "-p", "-s", "{:#06x}".format(self.serviceId)] + + def setDabServiceId(self, serviceId: int) -> None: + self.serviceId = serviceId + self.setArgs(self._buildArgs()) + self.restart() diff --git a/owrx/details.py b/owrx/details.py new file mode 100644 index 000000000..4d853cb63 --- /dev/null +++ b/owrx/details.py @@ -0,0 +1,30 @@ +from owrx.config import Config +from owrx.locator import Locator +from owrx.property import PropertyFilter +from owrx.property.filter import ByPropertyName +import logging + +logger = logging.getLogger(__name__) + + +class ReceiverDetails(PropertyFilter): + def __init__(self): + super().__init__( + Config.get(), + ByPropertyName( + "receiver_name", + "receiver_location", + "receiver_asl", + "receiver_gps", + "photo_title", + "photo_desc", + ) + ) + + def __dict__(self): + receiver_info = super().__dict__() + try: + receiver_info["locator"] = Locator.fromCoordinates(receiver_info["receiver_gps"]) + except ValueError as e: + logger.error("invalid receiver location, check in settings: %s", str(e)) + return receiver_info diff --git a/owrx/dsp.py b/owrx/dsp.py new file mode 100644 index 000000000..625254ea6 --- /dev/null +++ b/owrx/dsp.py @@ -0,0 +1,787 @@ +from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass +from owrx.property import PropertyStack, PropertyLayer, PropertyValidator, PropertyDeleted, PropertyDeletion +from owrx.property.validators import OrValidator, RegexValidator, BoolValidator +from owrx.modes import Modes, DigitalMode +from csdr.chain import Chain +from csdr.chain.demodulator import BaseDemodulatorChain, FixedIfSampleRateChain, FixedAudioRateChain, HdAudio, \ + SecondaryDemodulator, DialFrequencyReceiver, MetaProvider, SlotFilterChain, SecondarySelectorChain, \ + DeemphasisTauChain, DemodulatorError, RdsChain, DabServiceSelector +from csdr.chain.selector import Selector, SecondarySelector +from csdr.chain.clientaudio import ClientAudioChain +from csdr.chain.fft import FftChain +from csdr.chain.dummy import DummyDemodulator +from pycsdr.modules import Buffer, Writer +from pycsdr.types import Format +from typing import Union, Optional +from io import BytesIO +from abc import ABC, abstractmethod +import threading +import re +import pickle + +import logging + +logger = logging.getLogger(__name__) + + +# now that's a name. help, i've reached enterprise level OOP here +class ClientDemodulatorSecondaryDspEventClient(ABC): + @abstractmethod + def onSecondaryDspRateChange(self, rate): + pass + + @abstractmethod + def onSecondaryDspBandwidthChange(self, bw): + pass + + +class ClientDemodulatorChain(Chain): + def __init__(self, demod: BaseDemodulatorChain, sampleRate: int, outputRate: int, hdOutputRate: int, audioCompression: str, secondaryDspEventReceiver: ClientDemodulatorSecondaryDspEventClient): + self.sampleRate = sampleRate + self.outputRate = outputRate + self.hdOutputRate = hdOutputRate + self.secondaryDspEventReceiver = secondaryDspEventReceiver + self.selector = Selector(sampleRate, outputRate) + self.selectorBuffer = Buffer(Format.COMPLEX_FLOAT) + self.audioBuffer = None + self.demodulator = demod + self.secondaryDemodulator = None + self.centerFrequency = None + self.frequencyOffset = None + self.wfmDeemphasisTau = 50e-6 + self.rdsRbds = False + inputRate = demod.getFixedAudioRate() if isinstance(demod, FixedAudioRateChain) else outputRate + oRate = hdOutputRate if isinstance(demod, HdAudio) else outputRate + self.clientAudioChain = ClientAudioChain(demod.getOutputFormat(), inputRate, oRate, audioCompression) + self.secondaryFftSize = 2048 + self.secondaryFftOverlapFactor = 0.3 + self.secondaryFftFps = 9 + self.secondaryFftCompression = "adpcm" + self.secondaryFftChain = None + self.metaWriter = None + self.secondaryFftWriter = None + self.secondaryWriter = None + self.squelchLevel = -150 + self.secondarySelector = None + self.secondaryFrequencyOffset = None + super().__init__([self.selector, self.demodulator, self.clientAudioChain]) + + def stop(self): + super().stop() + if self.secondaryFftChain is not None: + self.secondaryFftChain.stop() + self.secondaryFftChain = None + if self.secondaryDemodulator is not None: + self.secondaryDemodulator.stop() + self.secondaryDemodulator = None + + def _connect(self, w1, w2, buffer: Optional[Buffer] = None) -> None: + if w1 is self.selector: + super()._connect(w1, w2, self.selectorBuffer) + elif w2 is self.clientAudioChain: + format = w1.getOutputFormat() + if self.audioBuffer is None or self.audioBuffer.getFormat() != format: + self.audioBuffer = Buffer(format) + if self.secondaryDemodulator is not None and self.secondaryDemodulator.getInputFormat() is not Format.COMPLEX_FLOAT: + self.secondaryDemodulator.setReader(self.audioBuffer.getReader()) + super()._connect(w1, w2, self.audioBuffer) + else: + super()._connect(w1, w2) + + def setDemodulator(self, demodulator: BaseDemodulatorChain): + if demodulator is self.demodulator: + return + + try: + self.clientAudioChain.setFormat(demodulator.getOutputFormat()) + except ValueError: + # this will happen if the new format does not match the current demodulator. + # it's expected and should be mended when swapping out the demodulator in the next step + pass + + if self.demodulator is not None: + self.demodulator.stop() + + self.demodulator = demodulator + + self.selector.setOutputRate(self._getSelectorOutputRate()) + + clientRate = self._getClientAudioInputRate() + self.demodulator.setSampleRate(clientRate) + + if isinstance(self.demodulator, DeemphasisTauChain): + self.demodulator.setDeemphasisTau(self.wfmDeemphasisTau) + + if isinstance(self.demodulator, RdsChain): + self.demodulator.setRdsRbds(self.rdsRbds) + + self._updateDialFrequency() + self._syncSquelch() + + if self.metaWriter is not None and isinstance(demodulator, MetaProvider): + demodulator.setMetaWriter(self.metaWriter) + + self.replace(1, demodulator) + + self.clientAudioChain.setInputRate(clientRate) + outputRate = self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate + self.clientAudioChain.setClientRate(outputRate) + + def stopDemodulator(self): + if self.demodulator is None: + return + + # we need to get the currrent demodulator out of the chain so that it can be deallocated properly + # so we just replace it with a dummy here + # in order to avoid any client audio chain hassle, the dummy simply imitates the output format of the current + # demodulator + self.replace(1, DummyDemodulator(self.demodulator.getOutputFormat())) + + self.demodulator.stop() + self.demodulator = None + + self.setSecondaryDemodulator(None) + + def _getSelectorOutputRate(self): + if isinstance(self.demodulator, FixedIfSampleRateChain): + return self.demodulator.getFixedIfSampleRate() + elif isinstance(self.secondaryDemodulator, FixedAudioRateChain): + if isinstance(self.demodulator, FixedAudioRateChain) and self.demodulator.getFixedAudioRate() != self.secondaryDemodulator.getFixedAudioRate(): + raise ValueError("secondary and primary demodulator chain audio rates do not match!") + return self.secondaryDemodulator.getFixedAudioRate() + else: + return self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate + + def _getClientAudioInputRate(self): + if isinstance(self.demodulator, FixedAudioRateChain): + return self.demodulator.getFixedAudioRate() + elif isinstance(self.secondaryDemodulator, FixedAudioRateChain): + return self.secondaryDemodulator.getFixedAudioRate() + else: + return self.hdOutputRate if isinstance(self.demodulator, HdAudio) else self.outputRate + + def setSecondaryDemodulator(self, demod: Optional[SecondaryDemodulator]): + if demod is self.secondaryDemodulator: + return + + if self.secondaryDemodulator is not None: + self.secondaryDemodulator.stop() + + self.secondaryDemodulator = demod + + rate = self._getSelectorOutputRate() + self.selector.setOutputRate(rate) + + clientRate = self._getClientAudioInputRate() + self.clientAudioChain.setInputRate(clientRate) + if self.demodulator is not None: + self.demodulator.setSampleRate(clientRate) + + self._updateDialFrequency() + self._syncSquelch() + + if isinstance(self.secondaryDemodulator, SecondarySelectorChain): + bandwidth = self.secondaryDemodulator.getBandwidth() + self.secondarySelector = SecondarySelector(rate, bandwidth) + self.secondarySelector.setReader(self.selectorBuffer.getReader()) + self.secondarySelector.setFrequencyOffset(self.secondaryFrequencyOffset) + self.secondaryDspEventReceiver.onSecondaryDspBandwidthChange(bandwidth) + else: + self.secondarySelector = None + + if self.secondaryDemodulator is not None: + self.secondaryDemodulator.setSampleRate(rate) + if self.secondarySelector is not None: + buffer = Buffer(Format.COMPLEX_FLOAT) + self.secondarySelector.setWriter(buffer) + self.secondaryDemodulator.setReader(buffer.getReader()) + elif self.secondaryDemodulator.getInputFormat() is Format.COMPLEX_FLOAT: + self.secondaryDemodulator.setReader(self.selectorBuffer.getReader()) + else: + self.secondaryDemodulator.setReader(self.audioBuffer.getReader()) + self.secondaryDemodulator.setWriter(self.secondaryWriter) + + if (self.secondaryDemodulator is None or not self.secondaryDemodulator.isSecondaryFftShown()) and self.secondaryFftChain is not None: + self.secondaryFftChain.stop() + self.secondaryFftChain = None + + if (self.secondaryDemodulator is not None and self.secondaryDemodulator.isSecondaryFftShown()) and self.secondaryFftChain is None: + self._createSecondaryFftChain() + + if self.secondaryFftChain is not None: + self.secondaryFftChain.setSampleRate(rate) + self.secondaryDspEventReceiver.onSecondaryDspRateChange(rate) + + def _createSecondaryFftChain(self): + if self.secondaryFftChain is not None: + self.secondaryFftChain.stop() + self.secondaryFftChain = FftChain(self._getSelectorOutputRate(), self.secondaryFftSize, self.secondaryFftOverlapFactor, self.secondaryFftFps, self.secondaryFftCompression) + self.secondaryFftChain.setReader(self.selectorBuffer.getReader()) + self.secondaryFftChain.setWriter(self.secondaryFftWriter) + + def _syncSquelch(self): + if self.demodulator is not None and not self.demodulator.supportsSquelch() or (self.secondaryDemodulator is not None and not self.secondaryDemodulator.supportsSquelch()): + self.selector.setSquelchLevel(-150) + else: + self.selector.setSquelchLevel(self.squelchLevel) + + def setLowCut(self, lowCut: Union[float, None]): + self.selector.setLowCut(lowCut) + + def setHighCut(self, highCut: Union[float, None]): + self.selector.setHighCut(highCut) + + def setBandpass(self, lowCut, highCut): + self.selector.setBandpass(lowCut, highCut) + + def setFrequencyOffset(self, offset: int) -> None: + if offset == self.frequencyOffset: + return + self.frequencyOffset = offset + self.selector.setFrequencyOffset(offset) + self._updateDialFrequency() + + def setCenterFrequency(self, frequency: int) -> None: + if frequency == self.centerFrequency: + return + self.centerFrequency = frequency + self._updateDialFrequency() + + def _updateDialFrequency(self): + if self.centerFrequency is None or self.frequencyOffset is None: + return + dialFrequency = self.centerFrequency + self.frequencyOffset + if isinstance(self.demodulator, DialFrequencyReceiver): + self.demodulator.setDialFrequency(dialFrequency) + if isinstance(self.secondaryDemodulator, DialFrequencyReceiver): + self.secondaryDemodulator.setDialFrequency(dialFrequency) + + def setAudioCompression(self, compression: str) -> None: + self.clientAudioChain.setAudioCompression(compression) + + def setSquelchLevel(self, level: float) -> None: + if level == self.squelchLevel: + return + self.squelchLevel = level + self._syncSquelch() + + def setOutputRate(self, outputRate) -> None: + if outputRate == self.outputRate: + return + + self.outputRate = outputRate + + if isinstance(self.demodulator, HdAudio): + return + self._updateDemodulatorOutputRate(outputRate) + + def setHdOutputRate(self, outputRate) -> None: + if outputRate == self.hdOutputRate: + return + + self.hdOutputRate = outputRate + + if not isinstance(self.demodulator, HdAudio): + return + self._updateDemodulatorOutputRate(outputRate) + + def _updateDemodulatorOutputRate(self, outputRate): + if not isinstance(self.demodulator, FixedIfSampleRateChain): + self.selector.setOutputRate(outputRate) + self.demodulator.setSampleRate(outputRate) + if self.secondaryDemodulator is not None: + self.secondaryDemodulator.setSampleRate(outputRate) + if not isinstance(self.demodulator, FixedAudioRateChain): + self.clientAudioChain.setClientRate(outputRate) + + def setSampleRate(self, sampleRate: int) -> None: + if sampleRate == self.sampleRate: + return + self.sampleRate = sampleRate + self.selector.setInputRate(sampleRate) + + def setPowerWriter(self, writer: Writer) -> None: + self.selector.setPowerWriter(writer) + + def setMetaWriter(self, writer: Writer) -> None: + if writer is self.metaWriter: + return + self.metaWriter = writer + if isinstance(self.demodulator, MetaProvider): + self.demodulator.setMetaWriter(self.metaWriter) + + def setSecondaryFftWriter(self, writer: Writer) -> None: + if writer is self.secondaryFftWriter: + return + self.secondaryFftWriter = writer + + if self.secondaryFftChain is not None: + self.secondaryFftChain.setWriter(writer) + + def setSecondaryWriter(self, writer: Writer) -> None: + if writer is self.secondaryWriter: + return + self.secondaryWriter = writer + if self.secondaryDemodulator is not None: + self.secondaryDemodulator.setWriter(writer) + + def setSlotFilter(self, filter: int) -> None: + if not isinstance(self.demodulator, SlotFilterChain): + return + self.demodulator.setSlotFilter(filter) + + def setDabServiceId(self, serviceId: int) -> None: + if not isinstance(self.demodulator, DabServiceSelector): + return + self.demodulator.setDabServiceId(serviceId) + + def setSecondaryFftSize(self, size: int) -> None: + if size == self.secondaryFftSize: + return + self.secondaryFftSize = size + if not self.secondaryFftChain: + return + self._createSecondaryFftChain() + + def setSecondaryFrequencyOffset(self, freq: int) -> None: + if self.secondaryFrequencyOffset == freq: + return + self.secondaryFrequencyOffset = freq + + if self.secondarySelector is None: + return + self.secondarySelector.setFrequencyOffset(self.secondaryFrequencyOffset) + + def setSecondaryFftCompression(self, compression: str) -> None: + if compression == self.secondaryFftCompression: + return + self.secondaryFftCompression = compression + if not self.secondaryFftChain: + return + self.secondaryFftChain.setCompression(self.secondaryFftCompression) + + def setSecondaryFftOverlapFactor(self, overlap: float) -> None: + if overlap == self.secondaryFftOverlapFactor: + return + self.secondaryFftOverlapFactor = overlap + if not self.secondaryFftChain: + return + self.secondaryFftChain.setVOverlapFactor(self.secondaryFftOverlapFactor) + + def setSecondaryFftFps(self, fps: int) -> None: + if fps == self.secondaryFftFps: + return + self.secondaryFftFps = fps + if not self.secondaryFftChain: + return + self.secondaryFftChain.setFps(self.secondaryFftFps) + + def getSecondaryFftOutputFormat(self) -> Format: + if self.secondaryFftCompression == "adpcm": + return Format.CHAR + return Format.FLOAT + + def setWfmDeemphasisTau(self, tau: float) -> None: + if tau == self.wfmDeemphasisTau: + return + self.wfmDeemphasisTau = tau + if isinstance(self.demodulator, DeemphasisTauChain): + self.demodulator.setDeemphasisTau(self.wfmDeemphasisTau) + + def setRdsRbds(self, rdsRbds: bool) -> None: + if rdsRbds == self.rdsRbds: + return + self.rdsRbds = rdsRbds + if isinstance(self.demodulator, RdsChain): + self.demodulator.setRdsRbds(self.rdsRbds) + + +class ModulationValidator(OrValidator): + """ + This validator only allows alphanumeric characters and numbers, but no spaces or special characters + """ + + def __init__(self): + super().__init__(BoolValidator(), RegexValidator(re.compile("^[a-z0-9]+$"))) + + +class DspManager(SdrSourceEventClient, ClientDemodulatorSecondaryDspEventClient): + def __init__(self, handler, sdrSource): + self.handler = handler + self.sdrSource = sdrSource + + self.props = PropertyStack() + + # current audio mode. should be "audio" or "hd_audio" depending on what demodulatur is in use. + self.audioOutput = None + + # local demodulator properties not forwarded to the sdr + # ensure strict validation since these can be set from the client + # and are used to build executable commands + validators = { + "output_rate": "int", + "hd_output_rate": "int", + "squelch_level": "num", + "secondary_mod": ModulationValidator(), + "low_cut": "num", + "high_cut": "num", + "offset_freq": "int", + "mod": ModulationValidator(), + "secondary_offset_freq": "int", + "dmr_filter": "int", + "dab_service_id": "int", + } + self.localProps = PropertyValidator(PropertyLayer().filter(*validators.keys()), validators) + + self.props.addLayer(0, self.localProps) + # properties that we inherit from the sdr + self.props.addLayer( + 1, + self.sdrSource.getProps().filter( + "audio_compression", + "fft_compression", + "digimodes_fft_size", + "samp_rate", + "center_freq", + "start_mod", + "start_freq", + "wfm_deemphasis_tau", + "wfm_rds_rbds", + "digital_voice_codecserver", + ), + ) + + # defaults for values that may not be set + self.props.addLayer( + 2, + PropertyLayer( + output_rate=12000, + hd_output_rate=48000, + digital_voice_codecserver="", + ).readonly() + ) + + self.chain = ClientDemodulatorChain( + self._getDemodulator("nfm"), + self.props["samp_rate"], + self.props["output_rate"], + self.props["hd_output_rate"], + self.props["audio_compression"], + self + ) + + self.readers = {} + + if "start_mod" in self.props: + mode = Modes.findByModulation(self.props["start_mod"]) + if mode: + self.setDemodulator(mode.get_modulation()) + if isinstance(mode, DigitalMode): + self.setSecondaryDemodulator(mode.modulation) + if mode.bandpass: + bpf = [mode.bandpass.low_cut, mode.bandpass.high_cut] + self.chain.setBandpass(*bpf) + self.props["low_cut"] = mode.bandpass.low_cut + self.props["high_cut"] = mode.bandpass.high_cut + else: + self.chain.setBandpass(None, None) + else: + # TODO modes should be mandatory + self.setDemodulator(self.props["start_mod"]) + + if "start_freq" in self.props and "center_freq" in self.props: + self.chain.setFrequencyOffset(self.props["start_freq"] - self.props["center_freq"]) + else: + self.chain.setFrequencyOffset(0) + + self.subscriptions = [ + self.props.wireProperty("audio_compression", self.setAudioCompression), + self.props.wireProperty("fft_compression", self.setSecondaryFftCompression), + self.props.wireProperty("fft_voverlap_factor", self.chain.setSecondaryFftOverlapFactor), + self.props.wireProperty("fft_fps", self.chain.setSecondaryFftFps), + self.props.wireProperty("digimodes_fft_size", self.setSecondaryFftSize), + self.props.wireProperty("samp_rate", self.chain.setSampleRate), + self.props.wireProperty("output_rate", self.chain.setOutputRate), + self.props.wireProperty("hd_output_rate", self.chain.setHdOutputRate), + self.props.wireProperty("offset_freq", self.chain.setFrequencyOffset), + self.props.wireProperty("center_freq", self.chain.setCenterFrequency), + self.props.wireProperty("squelch_level", self.chain.setSquelchLevel), + self.props.wireProperty("low_cut", self.setLowCut), + self.props.wireProperty("high_cut", self.setHighCut), + self.props.wireProperty("mod", self.setDemodulator), + self.props.wireProperty("dmr_filter", self.chain.setSlotFilter), + self.props.wireProperty("dab_service_id", self.chain.setDabServiceId), + self.props.wireProperty("wfm_deemphasis_tau", self.chain.setWfmDeemphasisTau), + self.props.wireProperty("wfm_rds_rbds", self.chain.setRdsRbds), + self.props.wireProperty("secondary_mod", self.setSecondaryDemodulator), + self.props.wireProperty("secondary_offset_freq", self.chain.setSecondaryFrequencyOffset), + ] + + # wire power level output + buffer = Buffer(Format.FLOAT) + self.chain.setPowerWriter(buffer) + self.wireOutput("smeter", buffer) + + # wire meta output + buffer = Buffer(Format.CHAR) + self.chain.setMetaWriter(buffer) + self.wireOutput("meta", buffer) + + # wire secondary FFT + buffer = Buffer(self.chain.getSecondaryFftOutputFormat()) + self.chain.setSecondaryFftWriter(buffer) + self.wireOutput("secondary_fft", buffer) + + # wire secondary demodulator + buffer = Buffer(Format.CHAR) + self.chain.setSecondaryWriter(buffer) + self.wireOutput("secondary_demod", buffer) + + self.startOnAvailable = False + + self.sdrSource.addClient(self) + + def setSecondaryFftSize(self, size): + self.chain.setSecondaryFftSize(size) + self.handler.write_secondary_dsp_config({"secondary_fft_size": size}) + + def _getDemodulator(self, demod: Union[str, BaseDemodulatorChain]) -> Optional[BaseDemodulatorChain]: + if isinstance(demod, BaseDemodulatorChain): + return demod + # TODO: move this to Modes + if demod == "nfm": + from csdr.chain.analog import NFm + return NFm(self.props["output_rate"]) + elif demod == "wfm": + from csdr.chain.analog import WFm + return WFm(self.props["hd_output_rate"], self.props["wfm_deemphasis_tau"], self.props["wfm_rds_rbds"]) + elif demod == "am": + from csdr.chain.analog import Am + return Am() + elif demod in ["usb", "lsb", "cw"]: + from csdr.chain.analog import Ssb + return Ssb() + elif demod == "dmr": + from csdr.chain.digiham import Dmr + return Dmr(self.props["digital_voice_codecserver"]) + elif demod == "dstar": + from csdr.chain.digiham import Dstar + return Dstar(self.props["digital_voice_codecserver"]) + elif demod == "ysf": + from csdr.chain.digiham import Ysf + return Ysf(self.props["digital_voice_codecserver"]) + elif demod == "nxdn": + from csdr.chain.digiham import Nxdn + return Nxdn(self.props["digital_voice_codecserver"]) + elif demod == "m17": + from csdr.chain.m17 import M17 + return M17() + elif demod == "drm": + from csdr.chain.drm import Drm + return Drm() + elif demod == "freedv": + from csdr.chain.freedv import FreeDV + return FreeDV() + elif demod == "dab": + from csdr.chain.dablin import Dablin + return Dablin() + elif demod == "empty": + from csdr.chain.analog import Empty + return Empty() + + def setDemodulator(self, mod): + self.chain.stopDemodulator() + try: + demodulator = self._getDemodulator(mod) + if demodulator is None: + raise ValueError("unsupported demodulator: {}".format(mod)) + self.chain.setDemodulator(demodulator) + + output = "hd_audio" if isinstance(demodulator, HdAudio) else "audio" + + if output != self.audioOutput: + self.audioOutput = output + # re-wire the audio to the correct client API + buffer = Buffer(self.chain.getOutputFormat()) + self.chain.setWriter(buffer) + self.wireOutput(self.audioOutput, buffer) + except DemodulatorError as de: + self.handler.write_demodulator_error(str(de)) + + def _getSecondaryDemodulator(self, mod) -> Optional[SecondaryDemodulator]: + if isinstance(mod, SecondaryDemodulator): + return mod + if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]: + from csdr.chain.digimodes import AudioChopperDemodulator + from owrx.wsjt import WsjtParser + return AudioChopperDemodulator(mod, WsjtParser()) + elif mod == "msk144": + from csdr.chain.digimodes import Msk144Demodulator + return Msk144Demodulator() + elif mod == "js8": + from csdr.chain.digimodes import AudioChopperDemodulator + from owrx.js8 import Js8Parser + return AudioChopperDemodulator(mod, Js8Parser()) + elif mod == "packet": + from csdr.chain.digimodes import PacketDemodulator + return PacketDemodulator() + elif mod == "pocsag": + from csdr.chain.digiham import PocsagDemodulator + return PocsagDemodulator() + elif mod == "bpsk31": + from csdr.chain.digimodes import PskDemodulator + return PskDemodulator(31.25) + elif mod == "bpsk63": + from csdr.chain.digimodes import PskDemodulator + return PskDemodulator(62.5) + elif mod == "rtty170": + from csdr.chain.digimodes import RttyDemodulator + return RttyDemodulator(45.45, 170) + elif mod == "rtty450": + from csdr.chain.digimodes import RttyDemodulator + return RttyDemodulator(50, 450, invert=True) + elif mod == "rtty85": + from csdr.chain.digimodes import RttyDemodulator + return RttyDemodulator(50, 85, invert=True) + elif mod == "adsb": + from csdr.chain.dump1090 import Dump1090 + return Dump1090() + elif mod == "ism": + from csdr.chain.rtl433 import Rtl433 + return Rtl433() + elif mod == "hfdl": + from csdr.chain.dumphfdl import DumpHFDL + return DumpHFDL() + elif mod == "vdl2": + from csdr.chain.dumpvdl2 import DumpVDL2 + return DumpVDL2() + + def setSecondaryDemodulator(self, mod): + demodulator = self._getSecondaryDemodulator(mod) + if not demodulator: + self.chain.setSecondaryDemodulator(None) + else: + self.chain.setSecondaryDemodulator(demodulator) + + def setAudioCompression(self, comp): + try: + self.chain.setAudioCompression(comp) + except ValueError: + # wrong output format... need to re-wire + buffer = Buffer(self.chain.getOutputFormat()) + self.chain.setWriter(buffer) + self.wireOutput(self.audioOutput, buffer) + + def setSecondaryFftCompression(self, compression): + try: + self.chain.setSecondaryFftCompression(compression) + except ValueError: + # wrong output format... need to re-wire + pass + + buffer = Buffer(self.chain.getSecondaryFftOutputFormat()) + self.chain.setSecondaryFftWriter(buffer) + self.wireOutput("secondary_fft", buffer) + + def setLowCut(self, lowCut: Union[float, PropertyDeletion]): + self.chain.setLowCut(None if lowCut is PropertyDeleted else lowCut) + + def setHighCut(self, highCut: Union[float, PropertyDeletion]): + self.chain.setHighCut(None if highCut is PropertyDeleted else highCut) + + def start(self): + if self.sdrSource.isAvailable(): + self.chain.setReader(self.sdrSource.getBuffer().getReader()) + else: + self.startOnAvailable = True + + def unwireOutput(self, t: str): + if t in self.readers: + self.readers[t].stop() + del self.readers[t] + + def wireOutput(self, t: str, buffer: Buffer): + logger.debug("wiring new output of type %s", t) + writers = { + "audio": self.handler.write_dsp_data, + "hd_audio": self.handler.write_hd_audio, + "smeter": self.handler.write_s_meter_level, + "secondary_fft": self.handler.write_secondary_fft, + "secondary_demod": self._unpickle(self.handler.write_secondary_demod), + "meta": self._unpickle(self.handler.write_metadata), + } + + write = writers[t] + + self.unwireOutput(t) + + reader = buffer.getReader() + self.readers[t] = reader + threading.Thread(target=self.chain.pump(reader.read, write), name="dsp_pump_{}".format(t)).start() + + def _unpickle(self, callback): + def unpickler(data): + b = data.tobytes() + # If we know it's not pickled, let us not unpickle + if len(b) < 2 or b[0] != 0x80 or not 3 <= b[1] <= pickle.HIGHEST_PROTOCOL: + callback(b.decode("ascii", errors="replace")) + return + + io = BytesIO(b) + try: + while True: + callback(pickle.load(io)) + except EOFError: + pass + except pickle.UnpicklingError: + callback(b.decode("ascii", errors="replace")) + + return unpickler + + def stop(self): + if self.chain: + self.chain.stop() + self.chain = None + for reader in self.readers.values(): + reader.stop() + self.readers = {} + + self.startOnAvailable = False + self.sdrSource.removeClient(self) + for sub in self.subscriptions: + sub.cancel() + self.subscriptions = [] + + def setProperties(self, props): + for k, v in props.items(): + self.setProperty(k, v) + + def setProperty(self, prop, value): + if value is None: + if prop in self.localProps: + del self.localProps[prop] + else: + self.localProps[prop] = value + + def getClientClass(self) -> SdrClientClass: + return SdrClientClass.USER + + def onStateChange(self, state: SdrSourceState): + if state is SdrSourceState.RUNNING: + logger.debug("received STATE_RUNNING, attempting DspSource restart") + if self.startOnAvailable: + self.chain.setReader(self.sdrSource.getBuffer().getReader()) + self.startOnAvailable = False + + def onFail(self): + logger.debug("received onFail(), shutting down DspSource") + self.stop() + + def onShutdown(self): + self.stop() + + def onSecondaryDspBandwidthChange(self, bw): + self.handler.write_secondary_dsp_config({"secondary_bw": bw}) + + def onSecondaryDspRateChange(self, rate): + self.handler.write_secondary_dsp_config({"if_samp_rate": rate}) diff --git a/owrx/feature.py b/owrx/feature.py new file mode 100644 index 000000000..e458a9622 --- /dev/null +++ b/owrx/feature.py @@ -0,0 +1,717 @@ +import subprocess +from functools import reduce +from operator import and_ +import re +from distutils.version import LooseVersion, StrictVersion +import inspect +from owrx.config.core import CoreConfig +from owrx.config import Config +import shlex +import os +from datetime import datetime, timedelta + +import logging + +logger = logging.getLogger(__name__) + + +class UnknownFeatureException(Exception): + pass + + +class FeatureCache(object): + sharedInstance = None + + @staticmethod + def getSharedInstance(): + if FeatureCache.sharedInstance is None: + FeatureCache.sharedInstance = FeatureCache() + return FeatureCache.sharedInstance + + def __init__(self): + self.cache = {} + self.cachetime = timedelta(hours=2) + + def has(self, feature): + if feature not in self.cache: + return False + now = datetime.now() + if self.cache[feature]["valid_to"] < now: + return False + return True + + def get(self, feature): + return self.cache[feature]["value"] + + def set(self, feature, value): + valid_to = datetime.now() + self.cachetime + self.cache[feature] = {"value": value, "valid_to": valid_to} + + +class FeatureDetector(object): + features = { + # core features; we won't start without these + "core": ["csdr"], + # different types of sdrs and their requirements + "rtl_sdr": ["rtl_connector"], + "rtl_sdr_soapy": ["soapy_connector", "soapy_rtl_sdr"], + "rtl_tcp": ["rtl_tcp_connector"], + "sdrplay": ["soapy_connector", "soapy_sdrplay"], + "hackrf": ["soapy_connector", "soapy_hackrf"], + "perseussdr": ["perseustest", "nmux"], + "airspy": ["soapy_connector", "soapy_airspy"], + "airspyhf": ["soapy_connector", "soapy_airspyhf"], + "afedri": ["soapy_connector", "soapy_afedri"], + "lime_sdr": ["soapy_connector", "soapy_lime_sdr"], + "fifi_sdr": ["alsa", "rockprog", "nmux"], + "pluto_sdr": ["soapy_connector", "soapy_pluto_sdr"], + "soapy_remote": ["soapy_connector", "soapy_remote"], + "uhd": ["soapy_connector", "soapy_uhd"], + "radioberry": ["soapy_connector", "soapy_radioberry"], + "fcdpp": ["soapy_connector", "soapy_fcdpp"], + "bladerf": ["soapy_connector", "soapy_bladerf"], + "sddc": ["sddc_connector"], + "hpsdr": ["hpsdr_connector"], + "runds": ["runds_connector"], + # optional features and their requirements + "digital_voice_digiham": ["digiham", "codecserver_ambe"], + "digital_voice_freedv": ["freedv_rx"], + "digital_voice_m17": ["m17_demod"], + "wsjt-x": ["wsjtx"], + "wsjt-x-2-3": ["wsjtx_2_3"], + "wsjt-x-2-4": ["wsjtx_2_4"], + "msk144": ["msk144decoder"], + "packet": ["direwolf"], + "pocsag": ["digiham"], + "js8call": ["js8", "js8py"], + "drm": ["dream"], + "dump1090": ["dump1090"], + "ism": ["rtl_433"], + "dumphfdl": ["dumphfdl"], + "dumpvdl2": ["dumpvdl2"], + "redsea": ["redsea"], + "dab": ["csdreti", "dablin"], + "mqtt": ["paho_mqtt"], + } + + def feature_availability(self): + return {name: self.is_available(name) for name in FeatureDetector.features} + + def feature_report(self): + def requirement_details(name): + available = self.has_requirement(name) + return { + "available": available, + # as of now, features are always enabled as soon as they are available. this may change in the future. + "enabled": available, + "description": self.get_requirement_description(name), + } + + def feature_details(name): + return { + "available": self.is_available(name), + "requirements": {name: requirement_details(name) for name in self.get_requirements(name)}, + } + + return {name: feature_details(name) for name in FeatureDetector.features} + + def is_available(self, feature): + return self.has_requirements(self.get_requirements(feature)) + + def get_failed_requirements(self, feature): + return [req for req in self.get_requirements(feature) if not self.has_requirement(req)] + + def get_requirements(self, feature): + try: + return FeatureDetector.features[feature] + except KeyError: + raise UnknownFeatureException('Feature "{0}" is not known.'.format(feature)) + + def has_requirements(self, requirements): + passed = True + for requirement in requirements: + passed = passed and self.has_requirement(requirement) + return passed + + def _get_requirement_method(self, requirement): + methodname = "has_" + requirement + if hasattr(self, methodname) and callable(getattr(self, methodname)): + return getattr(self, methodname) + return None + + def has_requirement(self, requirement): + cache = FeatureCache.getSharedInstance() + if cache.has(requirement): + return cache.get(requirement) + + method = self._get_requirement_method(requirement) + result = False + if method is not None: + result = method() + else: + logger.error("detection of requirement {0} not implement. please fix in code!".format(requirement)) + + cache.set(requirement, result) + return result + + def get_requirement_description(self, requirement): + return inspect.getdoc(self._get_requirement_method(requirement)) + + def command_is_runnable(self, command, expected_result=None): + tmp_dir = CoreConfig().get_temporary_directory() + cmd = shlex.split(command) + env = os.environ.copy() + # prevent X11 programs from opening windows if called from a GUI shell + env.pop("DISPLAY", None) + try: + process = subprocess.Popen( + cmd, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + cwd=tmp_dir, + env=env, + ) + while True: + try: + rc = process.wait(10) + break + except subprocess.TimeoutExpired: + logger.warning("feature check command \"%s\" did not return after 10 seconds!", command) + process.kill() + + if expected_result is None: + return rc != 32512 + else: + return rc == expected_result + except FileNotFoundError: + return False + + def has_csdr(self): + """ + OpenWebRX uses the demodulator and pipeline tools provided by the + [csdr project](https://github.com/jketterl/csdr). + + In addition, [pycsdr](https://github.com/jketterl/pycsdr) must be installed to provide python bindings for the + csdr library. + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `python3-csdr`. + """ + required_version = LooseVersion("0.19.0") + + try: + from pycsdr.modules import csdr_version + from pycsdr.modules import version as pycsdr_version + + return ( + LooseVersion(csdr_version) >= required_version and + LooseVersion(pycsdr_version) >= required_version + ) + except ImportError: + return False + + def has_nmux(self): + """ + Nmux is a tool provided by the csdr project. It is used for internal multiplexing of the IQ data streams. + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `nmux`. + """ + return self.command_is_runnable("nmux --help") + + def has_perseustest(self): + """ + To use a Microtelecom Perseus HF receiver, you need the `perseustest` utility from + [libperseus-sdr](https://github.com/Microtelecom/libperseus-sdr). + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `perseus-tools`. + """ + return self.command_is_runnable("perseustest -h") + + def has_digiham(self): + """ + To use digital voice modes, [digiham](https://github.com/jketterl/digiham) is required. + + In addition, [pydigiham](https://github.com/jketterl/pydigiham) must be installed to provide python bindings + for the digiham library. + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `python3-digiham`. + """ + required_version = LooseVersion("0.6") + + try: + from digiham.modules import digiham_version as digiham_version + from digiham.modules import version as pydigiham_version + + return ( + LooseVersion(digiham_version) >= required_version + and LooseVersion(pydigiham_version) >= required_version + ) + except ImportError: + return False + + def _check_connector(self, command, required_version): + owrx_connector_version_regex = re.compile("^{} version (.*)$".format(re.escape(command))) + + try: + process = subprocess.Popen([command, "--version"], stdout=subprocess.PIPE) + matches = owrx_connector_version_regex.match(process.stdout.readline().decode()) + if matches is None: + return False + version = LooseVersion(matches.group(1)) + process.wait(1) + return version >= required_version + except FileNotFoundError: + return False + + def _check_owrx_connector(self, command): + return self._check_connector(command, LooseVersion("0.7")) + + def has_rtl_connector(self): + """ + The [owrx_connector](https://github.com/jketterl/owrx_connector) offers direct interfacing between your + hardware and OpenWebRX. + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `owrx-connector`. + """ + return self._check_owrx_connector("rtl_connector") + + def has_rtl_tcp_connector(self): + """ + The [owrx_connector](https://github.com/jketterl/owrx_connector) offers direct interfacing between your + hardware and OpenWebRX. + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `owrx-connector`. + """ + return self._check_owrx_connector("rtl_tcp_connector") + + def has_soapy_connector(self): + """ + The [owrx_connector](https://github.com/jketterl/owrx_connector) offers direct interfacing between your + hardware and OpenWebRX. + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `owrx-connector`. + """ + return self._check_owrx_connector("soapy_connector") + + def _has_soapy_driver(self, driver): + try: + process = subprocess.Popen(["soapy_connector", "--listdrivers"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + + drivers = [line.decode().strip() for line in process.stdout] + process.wait(1) + + return driver in drivers + except FileNotFoundError: + return False + + def has_soapy_rtl_sdr(self): + """ + The [SoapyRTLSDR](https://github.com/pothosware/SoapyRTLSDR/wiki) module can be used as an alternative to + rtl_connector. + + Debian and Ubuntu users should be able to install the package `soapysdr-module-rtlsdr` from their distribution. + """ + return self._has_soapy_driver("rtlsdr") + + def has_soapy_sdrplay(self): + """ + The [SoapySDRPlay3](https://github.com/pothosware/SoapySDRPlay3) module is required for interfacing with + SDRPlay devices (RSP1\\*, RSP2\\*, RSPDuo) + """ + return self._has_soapy_driver("sdrplay") + + def has_soapy_airspy(self): + """ + The [SoapyAirspy](https://github.com/pothosware/SoapyAirspy/wiki) module is required for interfacing with + Airspy devices (Airspy R2, Airspy Mini). + + Debian and Ubuntu users should be able to install the package `soapysdr-module-airspy` from their distribution. + """ + return self._has_soapy_driver("airspy") + + def has_soapy_airspyhf(self): + """ + The [SoapyAirspyHF](https://github.com/pothosware/SoapyAirspyHF/wiki) module is required for interfacing with + Airspy HF devices (Airspy HF+, Airspy HF discovery). + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `soapysdr-module-airspyhf`. + """ + return self._has_soapy_driver("airspyhf") + + def has_soapy_afedri(self): + """ + The [SoapyAfedri](https://github.com/alexander-sholohov/SoapyAfedri) module allows using Afedri SDR-Net devices + with SoapySDR. + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `soapysdr-module-afedri`. + """ + return self._has_soapy_driver("afedri") + + def has_soapy_lime_sdr(self): + """ + The [LimeSuite](https://github.com/myriadrf/LimeSuite) installs - amongst other software - a Soapy driver for + the LimeSDR device series. + + Debian and Ubuntu users should be able to install the package `soapysdr-module-lms7` from their distribution. + """ + return self._has_soapy_driver("lime") + + def has_soapy_pluto_sdr(self): + """ + The [SoapyPlutoSDR](https://github.com/pothosware/SoapyPlutoSDR) module is required for interfacing with + PlutoSDR devices. + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `soapysdr-module-plutosdr`. + """ + return self._has_soapy_driver("plutosdr") + + def has_soapy_remote(self): + """ + SoapyRemote allows the usage of remote SDR devices over the network using SoapySDRServer. + + You can get the code and find additional information [here](https://github.com/pothosware/SoapyRemote/wiki). + + Debian and Ubuntu users should be able to install the package `soapysdr-module-remote` from their distribution. + """ + return self._has_soapy_driver("remote") + + def has_soapy_uhd(self): + """ + The [SoapyUHD](https://github.com/pothosware/SoapyUHD/wiki) module allows using UHD / USRP devices with + SoapySDR. + + Debian and Ubuntu users should be able to install the package `soapysdr-module-uhd` from their distribution. + """ + return self._has_soapy_driver("uhd") + + def has_soapy_radioberry(self): + """ + The Radioberry is a SDR hat for the Raspberry Pi. + + You can find more information, along with its SoapySDR module [here](https://github.com/pa3gsb/Radioberry-2.x). + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `soapysdr-module-radioberry`. + """ + return self._has_soapy_driver("radioberry") + + def has_soapy_hackrf(self): + """ + [SoapyHackRF](https://github.com/pothosware/SoapyHackRF/wiki) allows HackRF devices to be used with SoapySDR. + + Debian and Ubuntu users should be able to install the package `soapysdr-module-hackrf` from their distribution. + """ + return self._has_soapy_driver("hackrf") + + def has_soapy_fcdpp(self): + """ + The [SoapyFCDPP](https://github.com/pothosware/SoapyFCDPP) module allows the use of the Funcube Dongle Pro+. + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `soapysdr-module-fcdpp`. + """ + return self._has_soapy_driver("fcdpp") + + def has_soapy_bladerf(self): + """ + The [SoapyBladeRF](https://github.com/pothosware/SoapyBladeRF) module allows the use of Blade RF devices. + + Debian and Ubuntu users should be able to install the package `soapysdr-module-bladerf` from their distribution. + """ + return self._has_soapy_driver("bladerf") + + def has_m17_demod(self): + """ + OpenWebRX uses the [M17 Demodulator](https://github.com/mobilinkd/m17-cxx-demod) to demodulate M17 digital + voice signals. + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `m17-demod`. + """ + return self.command_is_runnable("m17-demod", 0) + + def has_direwolf(self): + """ + OpenWebRX uses the [direwolf](https://github.com/wb2osz/direwolf) software modem to decode Packet Radio and + report data back to APRS-IS. + + Debian and Ubuntu users should be able to install the package `direwolf` from their distribution. + """ + return self.command_is_runnable("direwolf --help") + + def has_wsjtx(self): + """ + To decode FT8 and other digimodes, you need to install the WSJT-X software suite. Please check the + [WSJT-X homepage](https://wsjt.sourceforge.io/) for ready-made packages or instructions + on how to build from source. + + Debian and Ubuntu users can also install the `wsjtx` package provided by the distribution. + """ + return reduce(and_, map(self.command_is_runnable, ["jt9", "wsprd"]), True) + + def _has_wsjtx_version(self, required_version): + wsjt_version_regex = re.compile("^WSJT-X (.*)$") + + try: + process = subprocess.Popen(["wsjtx_app_version", "--version"], stdout=subprocess.PIPE) + matches = wsjt_version_regex.match(process.stdout.readline().decode()) + if matches is None: + return False + version = LooseVersion(matches.group(1)) + process.wait(1) + return version >= required_version + except FileNotFoundError: + return False + + def has_wsjtx_2_3(self): + """ + Newer digital modes (e.g. FST4, FST4) require WSJT-X in at least version 2.3. + """ + return self.has_wsjtx() and self._has_wsjtx_version(LooseVersion("2.3")) + + def has_wsjtx_2_4(self): + """ + WSJT-X version 2.4 introduced the Q65 mode. + """ + return self.has_wsjtx() and self._has_wsjtx_version(LooseVersion("2.4")) + + def has_msk144decoder(self): + """ + To decode the MSK144 digimode please install + [msk144decoder](https://github.com/alexander-sholohov/msk144decoder). + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `msk144decoder`. + """ + return self.command_is_runnable("msk144decoder") + + def has_js8(self): + """ + To decode JS8, you will need to install [JS8Call](http://js8call.com/). + + Debian and Ubuntu users should be able to install the package `js8call` from their distribution. + """ + return self.command_is_runnable("js8") + + def has_js8py(self): + """ + OpenWebRX uses [js8py](https://github.com/jketterl/js8py) to decode binary JS8 messages into readable text. + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `python3-js8py`. + """ + required_version = StrictVersion("0.2") + try: + from js8py.version import strictversion + + return strictversion >= required_version + except ImportError: + return False + + def has_alsa(self): + """ + Some SDR receivers are identifying themselves as a soundcard. In order to read their data, OpenWebRX relies + on the Alsa library. + + Debian and Ubuntu users should be able to install the package `alsa-utils` from their distribution. + """ + return self.command_is_runnable("arecord --help") + + def has_rockprog(self): + """ + The `rockprog` executable is required to send commands to your FiFiSDR. It needs to be installed separately. + + You can find instructions and downloads [here](https://o28.sischa.net/fifisdr/trac/wiki/De%3Arockprog). + """ + return self.command_is_runnable("rockprog") + + def has_freedv_rx(self): + """ + The `freedv_rx` executable is required to demodulate FreeDV digital transmissions. It comes together with the + codec2 library, but it's only a supplemental part and not installed by default or contained in its packages. + To install it, you will need to compile codec2 from source and manually install freedv\\_rx. + + Detailed installation instructions are available on the + [OpenWebRX wiki](https://github.com/jketterl/openwebrx/wiki/FreeDV-demodulator-notes). + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `codec2`. + """ + return self.command_is_runnable("freedv_rx") + + def has_dream(self): + """ + In order to be able to decode DRM broadcasts, OpenWebRX needs the "dream" DRM decoder. + + A custom set of commands is recommended when compiling from source. Detailed installation instructions are + available on the [OpenWebRX wiki](https://github.com/jketterl/openwebrx/wiki/DRM-demodulator-notes). + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `dream-headless`. + """ + return self.command_is_runnable("dream --help", 0) + + def has_sddc_connector(self): + """ + The sddc_connector allows connectivity with SDR devices powered by libsddc, e.g. RX666, RX888, HF103. + + You can find more information [here](https://github.com/jketterl/sddc_connector). + """ + return self._check_connector("sddc_connector", LooseVersion("0.1")) + + def has_hpsdr_connector(self): + """ + The [HPSDR Connector](https://github.com/jancona/hpsdrconnector) is required to interface OpenWebRX with + Hermes Lite 2, Red Pitaya, and similar networked SDR devices. + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `hpsdrconnector`. + """ + return self.command_is_runnable("hpsdrconnector -h") + + def has_runds_connector(self): + """ + To use radios supporting R&S radios via EB200 or Ammos, you need to install + [runds_connector](https://github.com/jketterl/runds_connector). + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `runds-connector`. + """ + return self._check_connector("runds_connector", LooseVersion("0.2")) + + def has_codecserver_ambe(self): + """ + [Codecserver](https://github.com/jketterl/codecserver) is used to decode audio data from digital voice modes using the AMBE codec. + + NOTE: this feature flag checks both the availability of codecserver as well as the availability of the AMBE + codec in the configured codecserer instance. + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `codecserver`. + """ + + config = Config.get() + server = "" + if "digital_voice_codecserver" in config: + server = config["digital_voice_codecserver"] + try: + from digiham.modules import MbeSynthesizer + + return MbeSynthesizer.hasAmbe(server) + except ImportError: + return False + except ConnectionError: + return False + except RuntimeError as e: + logger.exception("Codecserver error while checking for AMBE support:") + return False + + def has_dump1090(self): + """ + To be able to decode Mode-S and ADS-B traffic originating from airplanes, you need to install the dump1090 + decoder. There is a number of forks available, any version that supports the `--ifile` and `--iformat` arguments + should work. + + Recommended fork: [dump1090 by Flightaware](https://github.com/flightaware/dump1090) + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `dump1090-fa-minimal`. + + If you are running a different fork, please make sure that the command `dump1090` (without suffixes) runs the + version you would like to use. You can use symbolic links or the + [Debian alternatives system](https://wiki.debian.org/DebianAlternatives) to achieve this. + """ + return self.command_is_runnable("dump1090 --version") + + def has_rtl_433(self): + """ + OpenWebRX can make use of [`rtl_433`](https://github.com/merbanan/rtl_433) to decode various signals in the + ISM bands. + + Debian and Ubuntu users should be able to install the package `rtl-433` from their distribution. + """ + return self.command_is_runnable("rtl_433 -h") + + def has_dumphfdl(self): + """ + OpenWebRX supports decoding HFDL airplane communications using + [`dumphfdl`](https://github.com/szpajder/dumphfdl). + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `dumphfdl`. + """ + return self.command_is_runnable("dumphfdl --version") + + def has_dumpvdl2(self): + """ + OpenWebRX supports decoding VDL Mode 2 airplane communications using + [`dumpvdl2`](https://github.com/szpajder/dumpvdl2). + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `dumpvdl2`. + """ + return self.command_is_runnable("dumpvdl2 --version") + + def has_redsea(self): + """ + OpenWebRX can decode RDS data on WFM broadcast station if the [`redsea`](https://github.com/windytan/redsea) + decoder is available. + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `redsea`. + """ + return self.command_is_runnable("redsea --version") + + def has_csdreti(self): + """ + To decode DAB broadcast signals, OpenWebRX needs the ETI decoder from the + [`csdr-eti`](https://github.com/jketterl/csdr-eti) project, together with the + associated python bindings from [`pycsdr-eti`](https://github.com/jketterl/pycsdr-eti). + + If you are using the OpenWebRX Debian or Ubuntu repository, you should be able to install the package + `python3-csdr-eti`. + """ + required_version = LooseVersion("0.1") + + try: + from csdreti.modules import csdreti_version + from csdreti.modules import version as pycsdreti_version + + return ( + LooseVersion(csdreti_version) >= required_version + and LooseVersion(pycsdreti_version) >= required_version + ) + except ImportError: + return False + + def has_dablin(self): + """ + To decode DAB broadcast signals, OpenWebRX needs the [`dablin`](https://github.com/Opendigitalradio/dablin) + decoding software. + + Debian and Ubuntu users should be able to install the package `dablin` from their distribution. + """ + return self.command_is_runnable("dablin -h") + + def has_paho_mqtt(self): + """ + OpenWebRX can pass decoded signal data to an MQTT broker for processing in third-party applications. To be able + to do this, the [paho-mqtt](https://pypi.org/project/paho-mqtt/) library is required. + + Debian and Ubuntu users should be able to install the package `python3-paho-mqtt` from their distribution. + """ + try: + from paho.mqtt import __version__ + return True + except ImportError: + return False diff --git a/owrx/fft.py b/owrx/fft.py new file mode 100644 index 000000000..4afd9222e --- /dev/null +++ b/owrx/fft.py @@ -0,0 +1,109 @@ +from owrx.config import Config +from csdr.chain.fft import FftChain +from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass +from owrx.property import PropertyStack +from pycsdr.modules import Buffer +import threading + +import logging + +logger = logging.getLogger(__name__) + + +class SpectrumThread(SdrSourceEventClient): + def __init__(self, sdrSource): + self.sdrSource = sdrSource + super().__init__() + + stack = PropertyStack() + stack.addLayer(0, self.sdrSource.props) + stack.addLayer(1, Config.get()) + self.props = stack.filter( + "samp_rate", + "fft_size", + "fft_fps", + "fft_voverlap_factor", + "fft_compression", + ) + + self.dsp = None + self.reader = None + + self.subscriptions = [] + + logger.debug("Spectrum thread initialized successfully.") + + def start(self): + if self.dsp is not None: + return + + self.dsp = FftChain( + self.props['samp_rate'], + self.props['fft_size'], + self.props['fft_voverlap_factor'], + self.props['fft_fps'], + self.props['fft_compression'] + ) + self.sdrSource.addClient(self) + + self.subscriptions += [ + self.props.filter("fft_size").wire(self.restart), + # these props can be set on the fly + self.props.wireProperty("samp_rate", self.dsp.setSampleRate), + self.props.wireProperty("fft_fps", self.dsp.setFps), + self.props.wireProperty("fft_voverlap_factor", self.dsp.setVOverlapFactor), + self.props.wireProperty("fft_compression", self._setCompression), + ] + + if self.sdrSource.isAvailable(): + self.dsp.setReader(self.sdrSource.getBuffer().getReader()) + + def _setCompression(self, compression): + if self.reader: + self.reader.stop() + try: + self.dsp.setCompression(compression) + except ValueError: + # expected since the compressions have different formats + pass + + buffer = Buffer(self.dsp.getOutputFormat()) + self.dsp.setWriter(buffer) + self.reader = buffer.getReader() + threading.Thread(target=self.dsp.pump(self.reader.read, self.sdrSource.writeSpectrumData)).start() + + def stopDsp(self): + if self.dsp is not None: + self.dsp.stop() + self.dsp = None + if self.reader is not None: + self.reader.stop() + self.reader = None + + def stop(self): + self.stopDsp() + self.sdrSource.removeClient(self) + while self.subscriptions: + self.subscriptions.pop().cancel() + + def restart(self, *args, **kwargs): + self.stop() + self.start() + + def getClientClass(self) -> SdrClientClass: + return SdrClientClass.USER + + def onStateChange(self, state: SdrSourceState): + if state is SdrSourceState.STOPPING: + self.stopDsp() + elif state == SdrSourceState.RUNNING: + if self.dsp is None: + self.start() + else: + self.dsp.setReader(self.sdrSource.getBuffer().getReader()) + + def onFail(self): + self.stopDsp() + + def onShutdown(self): + self.stopDsp() diff --git a/owrx/form/__init__.py b/owrx/form/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/owrx/form/error.py b/owrx/form/error.py new file mode 100644 index 000000000..60eef1011 --- /dev/null +++ b/owrx/form/error.py @@ -0,0 +1,15 @@ +class FormError(Exception): + def __init__(self, key, message): + super().__init__("Error processing form data for {}: {}".format(key, message)) + self.key = key + self.message = message + + def getKey(self): + return self.key + + def getMessage(self): + return self.message + + +class ValidationError(FormError): + pass diff --git a/owrx/form/input/__init__.py b/owrx/form/input/__init__.py new file mode 100644 index 000000000..eb9588dcb --- /dev/null +++ b/owrx/form/input/__init__.py @@ -0,0 +1,388 @@ +from abc import ABC +from owrx.modes import Modes +from owrx.form.input.validator import Validator +from owrx.form.input.converter import Converter, NullConverter, IntConverter, FloatConverter, EnumConverter, TextConverter +from enum import Enum + + +class Input(ABC): + def __init__(self, id, label, infotext=None, converter: Converter = None, validator: Validator = None, disabled=False, removable=False): + self.id = id + self.label = label + self.infotext = infotext + self.converter = self.defaultConverter() if converter is None else converter + self.validator = validator + self.disabled = disabled + self.removable = removable + + def setDisabled(self, disabled=True): + self.disabled = disabled + + def setRemovable(self, removable=True): + self.removable = removable + + def defaultConverter(self): + return NullConverter() + + def bootstrap_decorate(self, input): + return """ +
    + +
    +
    + {input} + {infotext} +
    + {removebutton} +
    +
    + """.format( + id=self.id, + label=self.label, + input=input, + infotext="{text}".format(text=self.infotext) if self.infotext else "", + removable="removable" if self.removable else "", + removebutton='' + if self.removable + else "", + ) + + def input_classes(self, errors): + classes = ["form-control", "form-control-sm"] + if errors: + classes.append("is-invalid") + return " ".join(classes) + + def input_properties(self, value, errors): + props = { + "class": self.input_classes(errors), + "id": self.id, + "name": self.id, + "placeholder": self.label, + "value": value, + } + if self.disabled: + props["disabled"] = "disabled" + return props + + def render_input_properties(self, value, error): + return " ".join('{}="{}"'.format(prop, value) for prop, value in self.input_properties(value, error).items()) + + def render_errors(self, errors): + return "".join("""
    {msg}
    """.format(msg=e) for e in errors) + + def render_input_group(self, value, errors): + return """ + {input} + {errors} + """.format( + input=self.render_input(value, errors), + errors=self.render_errors(errors) + ) + + def render_input(self, value, errors): + return "".format(properties=self.render_input_properties(value, errors)) + + def render(self, config, errors): + value = config[self.id] if self.id in config else None + error = errors[self.id] if self.id in errors else [] + return self.bootstrap_decorate(self.render_input_group(self.converter.convert_to_form(value), error)) + + def parse(self, data): + if self.id in data: + value = self.converter.convert_from_form(data[self.id][0]) + return {self.id: value} + return {} + + def validate(self, data): + if self.id in data and self.validator is not None: + self.validator.validate(self.id, data[self.id]) + + def getLabel(self): + return self.label + + +class TextInput(Input): + def input_properties(self, value, errors): + props = super().input_properties(value, errors) + props["type"] = "text" + return props + + def defaultConverter(self): + return TextConverter() + + +class PasswordInput(TextInput): + def input_properties(self, value, errors): + props = super().input_properties(value, errors) + props["type"] = "password" + return props + + +class NumberInput(Input): + def __init__(self, id, label, infotext=None, append="", converter: Converter = None, validator: Validator = None): + super().__init__(id, label, infotext, converter=converter, validator=validator) + self.step = None + self.append = append + + def defaultConverter(self): + return IntConverter() + + def input_properties(self, value, errors): + props = super().input_properties(value, errors) + props["type"] = "number" + if self.step: + props["step"] = self.step + return props + + def render_input_group(self, value, errors): + if self.append: + append = """ +
    + {append} +
    + """.format( + append=self.append + ) + else: + append = "" + + return """ +
    + {input} + {append} + {errors} +
    + """.format( + input=self.render_input(value, errors), + append=append, + errors=self.render_errors(errors) + ) + + +class FloatInput(NumberInput): + def __init__(self, id, label, infotext=None, converter: Converter = None): + super().__init__(id, label, infotext, converter=converter) + self.step = "any" + + def defaultConverter(self): + return FloatConverter() + + +class TextAreaInput(Input): + def render_input(self, value, errors): + return """ + + """.format( + properties=self.render_input_properties(value, errors), + value=value, + ) + + def input_properties(self, value, errors): + props = super().input_properties(value, errors) + props["style"] = "height:200px;" + # value works differently on textareas + del props["value"] + return props + + +class CheckboxInput(Input): + def __init__(self, id, checkboxText, infotext=None, converter: Converter = None): + super().__init__(id, "", infotext=infotext, converter=converter) + self.checkboxText = checkboxText + + def render_input(self, value, errors): + return """ +
    + + + +
    + """.format( + id=self.id, + classes=self.input_classes(errors), + checked="checked" if value else "", + disabled="disabled" if self.disabled else "", + checkboxText=self.checkboxText, + ) + + def input_classes(self, error): + classes = ["form-check", "form-control-sm"] + if error: + classes.append("is-invalid") + return " ".join(classes) + + def parse(self, data): + if self.id in data: + return {self.id: self.converter.convert_from_form("1" in data[self.id])} + return {} + + def getLabel(self): + return self.checkboxText + + +class Option(object): + # used for both MultiCheckboxInput and DropdownInput + def __init__(self, value, text): + self.value = value + self.text = text + + +class MultiCheckboxInput(Input): + def __init__(self, id, label, options, infotext=None): + super().__init__(id, label, infotext=infotext) + self.options = options + + def render_input(self, value, errors): + return "".join(self.render_checkbox(o, value, errors) for o in self.options) + + def checkbox_id(self, option): + return "{0}-{1}".format(self.id, option.value) + + def render_checkbox(self, option, value, errors): + return """ +
    + + +
    + """.format( + id=self.checkbox_id(option), + classes=self.input_classes(errors), + checked="checked" if option.value in value else "", + checkboxText=option.text, + disabled="disabled" if self.disabled else "", + ) + + def parse(self, data): + def in_response(option): + boxid = self.checkbox_id(option) + return boxid in data and data[boxid][0] == "on" + + return {self.id: [o.value for o in self.options if in_response(o)]} + + def input_classes(self, error): + classes = ["form-check", "form-control-sm"] + if error: + classes.append("is-invalid") + return " ".join(classes) + + +class ServicesCheckboxInput(MultiCheckboxInput): + def __init__(self, id, label, infotext=None): + services = [Option(s.modulation, s.name) for s in Modes.getAvailableServices()] + super().__init__(id, label, services, infotext) + + +class Js8ProfileCheckboxInput(MultiCheckboxInput): + def __init__(self, id, label, infotext=None): + profiles = [ + Option("normal", "Normal (15s, 50Hz, ~16WPM)"), + Option("slow", "Slow (30s, 25Hz, ~8WPM"), + Option("fast", "Fast (10s, 80Hz, ~24WPM"), + Option("turbo", "Turbo (6s, 160Hz, ~40WPM"), + ] + super().__init__(id, label, profiles, infotext) + + +class DropdownInput(Input): + def __init__(self, id, label, options, infotext=None, converter: Converter = None): + try: + isEnum = issubclass(options, DropdownEnum) + except TypeError: + isEnum = False + if isEnum: + self.options = [o.toOption() for o in options] + if converter is None: + converter = EnumConverter(options) + else: + self.options = options + super().__init__(id, label, infotext=infotext, converter=converter) + + def render_input(self, value, errors): + return """ + + """.format( + classes=self.input_classes(errors), + id=self.id, + options=self.render_options(value), + disabled="disabled" if self.disabled else "", + ) + + def render_options(self, value): + options = [ + """ + + """.format( + text=o.text, + value=o.value, + selected="selected" if o.value == value else "", + ) + for o in self.options + ] + return "".join(options) + + +class DropdownEnum(Enum): + def toOption(self): + return Option(self.name, str(self)) + + +class ModesInput(DropdownInput): + def __init__(self, id, label): + options = [Option(m.modulation, m.name) for m in Modes.getAvailableModes()] + super().__init__(id, label, options) + + +class ExponentialInput(Input): + def __init__(self, id, label, unit, infotext=None, validator: Validator = None): + super().__init__(id, label, infotext=infotext, validator=validator) + self.unit = unit + + def defaultConverter(self): + return IntConverter() + + def input_properties(self, value, errors): + props = super().input_properties(value, errors) + props["type"] = "number" + props["step"] = "any" + return props + + def render_input_group(self, value, errors): + append = """ +
    + +
    + """.format( + id=self.id, + disabled="disabled" if self.disabled else "", + unit=self.unit, + ) + + return """ +
    + {input} + {append} + {errors} +
    + """.format( + input=self.render_input(value, errors), + append=append, + errors=self.render_errors(errors) + ) + + def parse(self, data): + exponent_id = "{}-exponent".format(self.id) + if self.id in data and exponent_id in data: + value = int(float(data[self.id][0]) * 10 ** int(data[exponent_id][0])) + return {self.id: value} + return {} diff --git a/owrx/form/input/aprs.py b/owrx/form/input/aprs.py new file mode 100644 index 000000000..a81eab777 --- /dev/null +++ b/owrx/form/input/aprs.py @@ -0,0 +1,36 @@ +from owrx.form.input import DropdownEnum + + +class AprsBeaconSymbols(DropdownEnum): + BEACON_RECEIVE_ONLY = ("R&", "Receive only IGate") + BEACON_HF_GATEWAY = ("/&", "HF Gateway") + BEACON_IGATE_GENERIC = ("I&", "Igate Generic (please use more specific overlay)") + BEACON_PSKMAIL = ("P&", "PSKmail node") + BEACON_TX_1 = ("T&", "TX IGate with path set to 1 hop") + BEACON_WIRES_X = ("W&", "Wires-X") + BEACON_TX_2 = ("2&", "TX IGate with path set to 2 hops") + + def __new__(cls, *args, **kwargs): + value, description = args + obj = object.__new__(cls) + obj._value_ = value + obj.description = description + return obj + + def __str__(self): + return "{description} ({symbol})".format(description=self.description, symbol=self.value) + + +class AprsAntennaDirections(DropdownEnum): + DIRECTION_OMNI = None + DIRECTION_N = "N" + DIRECTION_NE = "NE" + DIRECTION_E = "E" + DIRECTION_SE = "SE" + DIRECTION_S = "S" + DIRECTION_SW = "SW" + DIRECTION_W = "W" + DIRECTION_NW = "NW" + + def __str__(self): + return "omnidirectional" if self.value is None else self.value diff --git a/owrx/form/input/converter.py b/owrx/form/input/converter.py new file mode 100644 index 000000000..ddb2d0e44 --- /dev/null +++ b/owrx/form/input/converter.py @@ -0,0 +1,122 @@ +from abc import ABC, abstractmethod +from owrx.jsons import Encoder +import json + + +class Converter(ABC): + @abstractmethod + def convert_to_form(self, value): + pass + + @abstractmethod + def convert_from_form(self, value): + pass + + +class NullConverter(Converter): + """ + The default converter class + Does not change the value in any way, just passes them through + """ + def convert_to_form(self, value): + return value + + def convert_from_form(self, value): + return value + + +class TextConverter(Converter): + """ + Converter class for text inputs + Does nothing more than to prevent the special python value "None" from appearing in the form + The string "None" should pass + """ + def convert_to_form(self, value): + if value is None: + return "" + return value + + def convert_from_form(self, value): + return value + + +class OptionalConverter(Converter): + """ + Transforms a special form value to None + The default is to look for an empty string, but this can be used to adopt to other types. + If the default is not found, the actual value is passed to the sub_converter for further transformation. + useful for optional fields since None is not stored in the configuration + """ + + def __init__(self, sub_converter: Converter = None, defaultFormValue=""): + self.sub_converter = NullConverter() if sub_converter is None else sub_converter + self.defaultFormValue = defaultFormValue + + def convert_to_form(self, value): + return self.defaultFormValue if value is None else self.sub_converter.convert_to_form(value) + + def convert_from_form(self, value): + return None if value == self.defaultFormValue else self.sub_converter.convert_from_form(value) + + +class IntConverter(Converter): + def convert_to_form(self, value): + return str(value) + + def convert_from_form(self, value): + return int(value) + + +class FloatConverter(Converter): + def convert_to_form(self, value): + return str(value) + + def convert_from_form(self, value): + return float(value) + + +class EnumConverter(Converter): + def __init__(self, enumCls): + self.enumCls = enumCls + + def convert_to_form(self, value): + if value is None: + return None + try: + return self.enumCls(value).name + # if the current value is not part of the enum, this will happen: + except ValueError: + # and this will restore the default + return None + + def convert_from_form(self, value): + return self.enumCls[value].value + + +class JsonConverter(Converter): + def convert_to_form(self, value): + return json.dumps(value, cls=Encoder) + + def convert_from_form(self, value): + return json.loads(value) + + +class WaterfallColorsConverter(Converter): + def convert_to_form(self, value): + if value is None: + return "" + return "\n".join("#{:06x}".format(v) for v in value) + + def convert_from_form(self, value): + def parseString(s): + try: + if s.startswith("#"): + return int(s[1:], 16) + # int() with base 0 can accept "0x" prefixed hex strings, or int numbers + return int(s, 0) + except ValueError: + return None + + # \r\n or \n? this should work with both. + values = [parseString(v.strip("\r ")) for v in value.split("\n")] + return [v for v in values if v is not None] diff --git a/owrx/form/input/device.py b/owrx/form/input/device.py new file mode 100644 index 000000000..2217b49ed --- /dev/null +++ b/owrx/form/input/device.py @@ -0,0 +1,434 @@ +from owrx.form.input import Input, CheckboxInput, DropdownInput, DropdownEnum, TextInput +from owrx.form.input.converter import OptionalConverter +from owrx.form.input.validator import RequiredValidator +from owrx.soapy import SoapySettings + + +class GainInput(Input): + def __init__(self, id, label, has_agc, gain_stages=None): + super().__init__(id, label) + self.has_agc = has_agc + self.gain_stages = gain_stages + + def render_input(self, value, errors): + try: + display_value = float(value) + except (ValueError, TypeError): + display_value = "0.0" + + return """ + + + {stageoption} + """.format( + id=self.id, + classes=self.input_classes(errors), + value=display_value, + label=self.label, + options=self.render_options(value), + stageoption="" if self.gain_stages is None else self.render_stage_option(value, errors), + disabled="disabled" if self.disabled else "", + ) + + def render_input_group(self, value, errors): + return """ +
    + {input} + {errors} +
    + """.format( + id=self.id, input=self.render_input(value, errors), errors=self.render_errors(errors) + ) + + def render_options(self, value): + options = [] + if self.has_agc: + options.append(("auto", "Enable hardware AGC")) + options.append(("manual", "Specify manual gain")), + if self.gain_stages: + options.append(("stages", "Specify gain stages individually")) + + mode = self.getMode(value) + + return "".join( + """ + + """.format( + value=v[0], text=v[1], selected="selected" if mode == v[0] else "" + ) + for v in options + ) + + def getMode(self, value): + if value is None: + return "auto" if self.has_agc else "manual" + + if value == "auto": + return "auto" + + try: + float(value) + return "manual" + except (ValueError, TypeError): + pass + + return "stages" + + def render_stage_option(self, value, errors): + try: + value_dict = {k: v for item in SoapySettings.parse(value) for k, v in item.items()} + except (AttributeError, ValueError): + value_dict = {} + + return """ + + """.format( + inputs="".join( + """ +
    + + +
    + """.format( + id=self.id, + stage=stage, + value=value_dict[stage] if stage in value_dict else "", + classes=self.input_classes(errors), + disabled="disabled" if self.disabled else "", + ) + for stage in self.gain_stages + ) + ) + + def parse(self, data): + def getStageValue(stage): + input_id = "{id}-{stage}".format(id=self.id, stage=stage) + if input_id in data: + return data[input_id][0] + else: + return None + + select_id = "{id}-select".format(id=self.id) + if select_id in data: + if self.has_agc and data[select_id][0] == "auto": + return {self.id: "auto"} + if data[select_id][0] == "manual": + input_id = "{id}-manual".format(id=self.id) + value = 0.0 + if input_id in data: + try: + value = float(data[input_id][0]) + except ValueError: + pass + return {self.id: value} + if self.gain_stages is not None and data[select_id][0] == "stages": + settings_dict = [{s: getStageValue(s)} for s in self.gain_stages] + # filter out empty ones + settings_dict = [s for s in settings_dict if next(iter(s.values()))] + return {self.id: SoapySettings.encode(settings_dict)} + + return {} + + +class BiasTeeInput(CheckboxInput): + def __init__(self): + super().__init__("bias_tee", "Enable Bias-Tee power supply") + + +class DirectSamplingOptions(DropdownEnum): + DIRECT_SAMPLING_OFF = (0, "Off") + DIRECT_SAMPLING_I = (1, "Direct Sampling (I branch)") + DIRECT_SAMPLING_Q = (2, "Direct Sampling (Q branch)") + + def __new__(cls, *args, **kwargs): + value, description = args + obj = object.__new__(cls) + obj._value_ = value + obj.description = description + return obj + + def __str__(self): + return self.description + + +class DirectSamplingInput(DropdownInput): + def __init__(self): + super().__init__( + "direct_sampling", + "Direct Sampling", + DirectSamplingOptions, + ) + + +class RemoteInput(TextInput): + def __init__(self): + super().__init__( + "remote", + "Remote IP and Port", + infotext="Remote hostname or IP and port to connect to. Format = IP:Port", + converter=OptionalConverter(), + validator=RequiredValidator(), + ) + + +class SchedulerInput(Input): + def __init__(self, id, label): + super().__init__(id, label) + self.profiles = {} + + def render(self, config, errors): + if "profiles" in config: + self.profiles = config["profiles"] + return super().render(config, errors) + + def render_profiles_select(self, value, errors, config_key, stage, extra_classes="", allow_empty=False): + stage_value = "" + if value and "schedule" in value and config_key in value["schedule"]: + stage_value = value["schedule"][config_key] + + options = "".join( + """ + + """.format( + id=p_id, + name=p["name"], + selected="selected" if stage_value == p_id else "", + ) + for p_id, p in self.profiles.items() + ) + + if allow_empty: + # prepend a special "off" option to allow a schedule slot to go unused (daylight scheduler) + options = """""".format( + selected="selected" if value is None else "" + ) + options + + return """ + + """.format( + id="{}-{}".format(self.id, stage), + classes=self.input_classes(errors), + extra_classes=extra_classes, + disabled="disabled" if self.disabled else "", + options=options, + ) + + def render_static_entires(self, value, errors): + def render_time_inputs(v): + values = ["{}:{}".format(x[0:2], x[2:4]) for x in [v[0:4], v[5:9]]] + return '
    -
    '.join( + """ + + """.format( + id="{}-{}-{}".format(self.id, "time", "start" if i == 0 else "end"), + classes=self.input_classes(errors), + disabled="disabled" if self.disabled else "", + value=v, + ) + for i, v in enumerate(values) + ) + + schedule = {"0000-0000": ""} + if value is not None and value and "schedule" in value and "type" in value and value["type"] == "static": + schedule = value["schedule"] + + rows = "".join( + """ +
    + {time_inputs} + {select} + +
    + """.format( + time_inputs=render_time_inputs(slot), + select=self.render_profiles_select(value, errors, slot, "profile"), + ) + for slot, entry in schedule.items() + ) + + return """ + {rows} + +
    + +
    + """.format( + rows=rows, + time_inputs=render_time_inputs("0000-0000"), + select=self.render_profiles_select("", errors, "0000-0000", "profile"), + ) + + def render_daylight_entries(self, value, errors): + return "".join( + """ +
    + + {select} +
    + """.format( + name=name, + select=self.render_profiles_select( + value, errors, stage, stage, extra_classes="col-9", allow_empty=True + ), + ) + for stage, name in [("day", "Day"), ("night", "Night"), ("greyline", "Greyline")] + ) + + def render_input(self, value, errors): + return """ +
    + + + +
    + """.format( + id=self.id, + classes=self.input_classes(errors), + disabled="disabled" if self.disabled else "", + options=self.render_options(value), + entries=self.render_static_entires(value, errors), + stages=self.render_daylight_entries(value, errors), + ) + + def _get_mode(self, value): + if value is not None and "type" in value: + return value["type"] + return "" + + def render_options(self, value): + options = [ + ("static", "Static scheduler"), + ("daylight", "Daylight scheduler"), + ] + + mode = self._get_mode(value) + + return "".join( + """ + + """.format( + value=value, name=name, selected="selected" if mode == value else "" + ) + for value, name in options + ) + + def parse(self, data): + def getStageValue(stage): + input_id = "{id}-{stage}".format(id=self.id, stage=stage) + if input_id in data: + # special treatment for the "off" option + if data[input_id][0] == "None": + return None + return data[input_id][0] + else: + return None + + select_id = "{id}-select".format(id=self.id) + if select_id in data: + if data[select_id][0] == "static": + keys = ["{}-{}".format(self.id, x) for x in ["time-start", "time-end", "profile"]] + lists = [data[key] for key in keys if key in data] + settings_dict = { + "{}{}-{}{}".format(start[0:2], start[3:5], end[0:2], end[3:5]): profile + for start, end, profile in zip(*lists) + } + # only apply scheduler if any slots are available + if settings_dict: + return {self.id: {"type": "static", "schedule": settings_dict}} + elif data[select_id][0] == "daylight": + settings_dict = {s: getStageValue(s) for s in ["day", "night", "greyline"]} + # filter out empty ones + settings_dict = {s: v for s, v in settings_dict.items() if v} + # only apply scheduler if any of the slots are in use + if settings_dict: + return {self.id: {"type": "daylight", "schedule": settings_dict}} + + return {} + + +class WaterfallLevelsInput(Input): + def __init__(self, id, label, infotext=None): + super().__init__(id, label, infotext=infotext) + + def render_input_group(self, value, errors): + return """ +
    + {input} +
    + {errors} + """.format( + rowclass="is-invalid" if errors else "", + id=self.id, + input=self.render_input(value, errors), + errors=self.render_errors(errors), + ) + + def getUnit(self): + return "dBFS" + + def getFields(self): + return {"min": "Minimum", "max": "Maximum"} + + def render_input(self, value, errors): + return "".join( + """ +
    + +
    + +
    + {unit} +
    +
    +
    + """.format( + id=self.id, + name=name, + label=label, + value=value[name] if value and name in value else "0", + classes=self.input_classes(errors), + disabled="disabled" if self.disabled else "", + unit=self.getUnit(), + ) + for name, label in self.getFields().items() + ) + + def parse(self, data): + def getValue(name): + key = "{}-{}".format(self.id, name) + if key in data: + return {name: float(data[key][0])} + raise KeyError("waterfall key not found") + + try: + return {self.id: {k: v for name in ["min", "max"] for k, v in getValue(name).items()}} + except KeyError: + return {} + + +class WaterfallAutoLevelsInput(WaterfallLevelsInput): + def getUnit(self): + return "dB" + + def getFields(self): + return {"min": "Lower", "max": "Upper"} diff --git a/owrx/form/input/gfx.py b/owrx/form/input/gfx.py new file mode 100644 index 000000000..24516b433 --- /dev/null +++ b/owrx/form/input/gfx.py @@ -0,0 +1,67 @@ +from abc import ABCMeta, abstractmethod +from owrx.form.input import Input +from datetime import datetime + + +class ImageInput(Input, metaclass=ABCMeta): + def render_input(self, value, errors): + # TODO display errors + return """ +
    + +
    + {label} +
    + + +
    + """.format( + id=self.id, + label=self.label, + url=self.cachebuster(self.getUrl()), + classes=" ".join(self.getImgClasses()), + maxsize=self.getMaxSize(), + ) + + def cachebuster(self, url: str): + return "{url}{separator}cb={cachebuster}".format( + url=url, + cachebuster=datetime.now().timestamp(), + separator="&" if "?" in url else "?", + ) + + @abstractmethod + def getUrl(self) -> str: + pass + + @abstractmethod + def getImgClasses(self) -> list: + pass + + @abstractmethod + def getMaxSize(self) -> int: + pass + + +class AvatarInput(ImageInput): + def getUrl(self) -> str: + return "../static/gfx/openwebrx-avatar.png" + + def getImgClasses(self) -> list: + return ["webrx-rx-avatar"] + + def getMaxSize(self) -> int: + # 256 kB + return 250 * 1024 + + +class TopPhotoInput(ImageInput): + def getUrl(self) -> str: + return "../static/gfx/openwebrx-top-photo.jpg" + + def getImgClasses(self) -> list: + return ["webrx-top-photo"] + + def getMaxSize(self) -> int: + # 2 MB + return 2 * 1024 * 1024 diff --git a/owrx/form/input/location.py b/owrx/form/input/location.py new file mode 100644 index 000000000..362455968 --- /dev/null +++ b/owrx/form/input/location.py @@ -0,0 +1,62 @@ +from owrx.form.input import Input +from owrx.form.input.validator import Validator +from owrx.form.error import ValidationError +from owrx.config import Config + +import logging + +logger = logging.getLogger(__name__) + + +class LocationValidator(Validator): + def validate(self, key, value): + if "lat" in value and not -90 < value["lat"] < 90: + raise ValidationError(key, "Latitude out of range (-90 to 90)") + if "lon" in value and not -180 < value["lon"] < 180: + raise ValidationError(key, "Longitude out of range (-180 to 180)") + pass + + +class LocationInput(Input): + def __init__(self, id, label, validator: Validator = None): + if validator is None: + validator = LocationValidator() + super().__init__(id, label, validator=validator) + + def render_input_group(self, value, errors): + return """ +
    + {inputs} +
    + {errors} +
    +
    +
    + """.format( + id=self.id, + rowclass="is-invalid" if errors else "", + inputs=self.render_input(value, errors), + errors=self.render_errors(errors), + key=Config.get()["google_maps_api_key"], + ) + + def render_input(self, value, errors): + return "".join(self.render_sub_input(value, id, errors) for id in ["lat", "lon"]) + + def render_sub_input(self, value, id, errors): + return """ +
    + +
    + """.format( + id="{0}-{1}".format(self.id, id), + label=self.label, + classes=self.input_classes(errors), + value=value[id], + disabled="disabled" if self.disabled else "", + ) + + def parse(self, data): + value = {k: float(data["{0}-{1}".format(self.id, k)][0]) for k in ["lat", "lon"]} + return {self.id: value} diff --git a/owrx/form/input/receiverid.py b/owrx/form/input/receiverid.py new file mode 100644 index 000000000..43c0a5ded --- /dev/null +++ b/owrx/form/input/receiverid.py @@ -0,0 +1,33 @@ +from owrx.form.input import TextAreaInput +from owrx.form.input.converter import Converter + + +class ReceiverKeysConverter(Converter): + def convert_to_form(self, value): + return "" if value is None else "\n".join(value) + + def convert_from_form(self, value): + # \r\n or \n? this should work with both. + stripped = [v.strip("\r ") for v in value.split("\n")] + # omit empty lines + return [v for v in stripped if v] + + +class ReceiverKeysInput(TextAreaInput): + def __init__(self, id, label): + super().__init__( + id, + label, + infotext="Put the keys you receive on listing sites (e.g. " + + 'Receiverbook) here, one per line', + ) + + def input_properties(self, value, errors): + props = super().input_properties(value, errors) + # disable word wrap on the textarea. + # why? keys are longer than the input, and word wrap makes the "one per line" instruction confusing. + props["wrap"] = "off" + return props + + def defaultConverter(self): + return ReceiverKeysConverter() diff --git a/owrx/form/input/validator.py b/owrx/form/input/validator.py new file mode 100644 index 000000000..bda4304df --- /dev/null +++ b/owrx/form/input/validator.py @@ -0,0 +1,71 @@ +from abc import ABC, abstractmethod +from owrx.form.error import ValidationError +from typing import List + + +class Validator(ABC): + @abstractmethod + def validate(self, key, value) -> None: + pass + + +class RequiredValidator(Validator): + def validate(self, key, value) -> None: + if value is None or value == "": + raise ValidationError(key, "Field is required") + + +class Range(object): + def __init__(self, start: int, end: int = None): + self.start = start + self.end = end if end is not None else start + + def isInRange(self, value): + return self.start <= value <= self.end + + def __str__(self): + if self.start == self.end: + return str(self.start) + return "{start}...{end}".format(**vars(self)) + + +class RangeValidator(Validator): + def __init__(self, minValue, maxValue): + self.range = Range(minValue, maxValue) + + def validate(self, key, value) -> None: + if value is None or value == "": + return # Ignore empty values + if not self.range.isInRange(float(value)): + raise ValidationError( + key, "Value must be between {min} and {max}".format(min=self.range.start, max=self.range.end) + ) + + +class RangeListValidator(Validator): + def __init__(self, rangeList: List[Range]): + self.rangeList = rangeList + + def validate(self, key, value) -> None: + if not any(range for range in self.rangeList if range.isInRange(value)): + raise ValidationError( + key, "Value is outside of the allowed range(s) {}".format(self._rangeStr()) + ) + + def _rangeStr(self): + return "[{}]".format(", ".join(str(r) for r in self.rangeList)) + + +class AddressAndOptionalPortValidator(Validator): + def validate(self, key, value) -> None: + parts = value.split(":") + if len(parts) > 2: + raise ValidationError(key, "Value contains too many colons") + + if len(parts) > 1: + try: + port = int(parts[1]) + except ValueError: + raise ValidationError(key, "Port number must be numeric") + if not 0 <= port <= 65535: + raise ValidationError(key, "Port number out of range") diff --git a/owrx/form/input/wfm.py b/owrx/form/input/wfm.py new file mode 100644 index 000000000..544754b0e --- /dev/null +++ b/owrx/form/input/wfm.py @@ -0,0 +1,16 @@ +from owrx.form.input import DropdownEnum + + +class WfmTauValues(DropdownEnum): + TAU_50_MICRO = (50e-6, "most regions") + TAU_75_MICRO = (75e-6, "Americas and South Korea") + + def __new__(cls, *args, **kwargs): + value, description = args + obj = object.__new__(cls) + obj._value_ = value + obj.description = description + return obj + + def __str__(self): + return "{}µs ({})".format(int(self.value * 1e6), self.description) diff --git a/owrx/form/input/wsjt.py b/owrx/form/input/wsjt.py new file mode 100644 index 000000000..1410599be --- /dev/null +++ b/owrx/form/input/wsjt.py @@ -0,0 +1,93 @@ +from owrx.form.input import Input +from owrx.form.input.converter import JsonConverter +from owrx.wsjt import Q65Mode, Q65Interval +from owrx.modes import Modes, WsjtMode +import html + + +class Q65ModeMatrix(Input): + def checkbox_id(self, mode, interval): + return "{0}-{1}-{2}".format(self.id, mode.value, interval.value) + + def render_checkbox(self, mode: Q65Mode, interval: Q65Interval, value, errors): + return """ +
    + + +
    + """.format( + classes=self.input_classes(errors), + id=self.checkbox_id(mode, interval), + checked="checked" if "{}{}".format(mode.name, interval.value) in value else "", + checkboxText="Mode {} interval {}s".format(mode.name, interval.value), + disabled="" if interval.is_available(mode) and not self.disabled else "disabled", + ) + + def render_input_group(self, value, errors): + return """ +
    + {checkboxes} + {errors} +
    + """.format( + checkboxes=self.render_input(value, errors), + errors=self.render_errors(errors), + ) + + def render_input(self, value, errors): + return "".join( + self.render_checkbox(mode, interval, value, errors) for interval in Q65Interval for mode in Q65Mode + ) + + def input_classes(self, error): + classes = ["form-check", "form-control-sm"] + if error: + classes.append("is-invalid") + return " ".join(classes) + + def parse(self, data): + def in_response(mode, interval): + boxid = self.checkbox_id(mode, interval) + return boxid in data and data[boxid][0] == "on" + + return { + self.id: [ + "{}{}".format(mode.name, interval.value) + for interval in Q65Interval + for mode in Q65Mode + if in_response(mode, interval) + ], + } + + +class WsjtDecodingDepthsInput(Input): + def defaultConverter(self): + return JsonConverter() + + def render_input(self, value, errors): + def render_mode(m): + return """ + + """.format( + mode=m.modulation, + name=m.name, + ) + + return """ + + + """.format( + id=self.id, + classes=self.input_classes(errors), + value=html.escape(value), + options="".join(render_mode(m) for m in Modes.getAvailableModes() if isinstance(m, WsjtMode)), + disabled="disabled" if self.disabled else "" + ) + + def input_classes(self, error): + return super().input_classes(error) + " wsjt-decoding-depths" diff --git a/owrx/form/section.py b/owrx/form/section.py new file mode 100644 index 000000000..f50c65f9c --- /dev/null +++ b/owrx/form/section.py @@ -0,0 +1,126 @@ +from owrx.form.error import FormError +from owrx.form.input import Input +from typing import List + + +class Section(object): + def __init__(self, title, *inputs): + self.title = title + self.inputs = inputs + + def render_input(self, input, data, errors): + return input.render(data, errors) + + def render_inputs(self, data, errors): + return "".join([self.render_input(i, data, errors) for i in self.inputs]) + + def classes(self): + return ["col-12", "settings-section"] + + def render(self, data, errors): + return """ +
    +

    + {title} +

    + {inputs} +
    + """.format( + classes=" ".join(self.classes()), title=self.title, inputs=self.render_inputs(data, errors) + ) + + def parse(self, data): + parsed_data = {} + errors = [] + for i in self.inputs: + try: + res = i.parse(data) + parsed_data.update(res) + i.validate(res) + except FormError as e: + errors.append(e) + except Exception as e: + errors.append(FormError(i.id, "{}: {}".format(type(e).__name__, e))) + return parsed_data, errors + + +class OptionalSection(Section): + def __init__(self, title, inputs: List[Input], mandatory, optional): + super().__init__(title, *inputs) + self.mandatory = mandatory + self.optional = optional + self.optional_inputs = [] + + def classes(self): + classes = super().classes() + classes.append("optional-section") + return classes + + def _is_optional(self, input): + return input.id in self.optional + + def render_optional_select(self): + return """ +
    +
    + +
    +
    + +
    + +
    +
    + """.format( + options="".join( + """ + + """.format( + value=input.id, + name=input.getLabel(), + ) + for input in self.optional_inputs + ) + ) + + def render_optional_inputs(self, data, errors): + return """ + + """.format( + inputs="".join(self.render_input(input, data, errors) for input in self.optional_inputs) + ) + + def render_inputs(self, data, errors): + return ( + super().render_inputs(data, errors) + + self.render_optional_select() + + self.render_optional_inputs(data, errors) + ) + + def render(self, data, errors): + indexed_inputs = {input.id: input for input in self.inputs} + visible_keys = set(self.mandatory + [k for k in self.optional if k in data or k in errors]) + optional_keys = set(k for k in self.optional if k not in data and k not in errors) + self.inputs = [input for k, input in indexed_inputs.items() if k in visible_keys] + for input in self.inputs: + if self._is_optional(input): + input.setRemovable() + self.optional_inputs = [input for k, input in indexed_inputs.items() if k in optional_keys] + for input in self.optional_inputs: + input.setRemovable() + input.setDisabled() + return super().render(data, errors) + + def parse(self, data): + data, errors = super().parse(data) + # remove optional keys if they have been removed from the form by setting them to None + for k in self.optional: + if k not in data: + data[k] = None + return data, errors diff --git a/owrx/hfdl/dumphfdl.py b/owrx/hfdl/dumphfdl.py new file mode 100644 index 000000000..1ec920e92 --- /dev/null +++ b/owrx/hfdl/dumphfdl.py @@ -0,0 +1,103 @@ +from pycsdr.modules import ExecModule +from pycsdr.types import Format +from owrx.aeronautical import AirplaneLocation, AcarsProcessor, IcaoSource, FlightSource +from owrx.map import Map +from owrx.metrics import Metrics, CounterMetric +from owrx.reporting import ReportingEngine +from datetime import datetime, timezone, timedelta + +import logging + +logger = logging.getLogger(__name__) + + +class DumpHFDLModule(ExecModule): + def __init__(self): + super().__init__( + Format.COMPLEX_FLOAT, + Format.CHAR, + [ + "dumphfdl", + "--iq-file", "-", + "--sample-format", "CF32", + "--sample-rate", "12000", + "--output", "decoded:json:file:path=-", + "0", + ], + flushSize=50000, + ) + + +class HFDLMessageParser(AcarsProcessor): + def __init__(self): + name = "dumphfdl.decodes.hfdl" + self.metrics = Metrics.getSharedInstance().getMetric(name) + if self.metrics is None: + self.metrics = CounterMetric() + Metrics.getSharedInstance().addMetric(name, self.metrics) + super().__init__("HFDL") + + def process(self, line): + msg = super().process(line) + if msg is not None: + try: + payload = msg["hfdl"] + if "lpdu" in payload: + lpdu = payload["lpdu"] + icao = lpdu["src"]["ac_info"]["icao"] if "ac_info" in lpdu["src"] else None + if lpdu["type"]["id"] in [13, 29]: + hfnpdu = lpdu["hfnpdu"] + if hfnpdu["type"]["id"] == 209: + # performance data + self.processPosition(hfnpdu, icao) + elif hfnpdu["type"]["id"] == 255: + # enveloped data + if "acars" in hfnpdu: + self.processAcars(hfnpdu["acars"], icao) + elif lpdu["type"]["id"] in [79, 143, 191]: + if "ac_info" in lpdu: + icao = lpdu["ac_info"]["icao"] + self.processPosition(lpdu["hfnpdu"], icao) + except Exception: + logger.exception("error processing HFDL data") + + self.metrics.inc() + + ReportingEngine.getSharedInstance().spot(msg) + return msg + + def processPosition(self, hfnpdu, icao=None): + if "pos" in hfnpdu: + pos = hfnpdu["pos"] + if abs(pos['lat']) <= 90 and abs(pos['lon']) <= 180: + flight = self.processFlight(hfnpdu["flight_id"]) + + if icao is not None: + source = IcaoSource(icao, flight=flight) + elif flight: + source = FlightSource(flight) + else: + source = None + + if source: + msg = { + "lat": pos["lat"], + "lon": pos["lon"], + "flight": flight + } + if "utc_time" in hfnpdu: + ts = self.processTimestamp(**hfnpdu["utc_time"]) + elif "time" in hfnpdu: + ts = self.processTimestamp(**hfnpdu["time"]) + else: + ts = None + Map.getSharedInstance().updateLocation(source, AirplaneLocation(msg), "HFDL", timestamp=ts) + + def processTimestamp(self, hour, min, sec) -> datetime: + now = datetime.now(timezone.utc) + t = now.replace(hour=hour, minute=min, second=sec, microsecond=0) + # if we have moved the time to the future, it's most likely that we're close to midnight and the time + # we received actually refers to yesterday + if t > now: + t -= timedelta(days=1) + return t diff --git a/owrx/http.py b/owrx/http.py new file mode 100644 index 000000000..050e5ef65 --- /dev/null +++ b/owrx/http.py @@ -0,0 +1,196 @@ +from owrx.controllers.status import StatusController +from owrx.controllers.template import IndexController, MapController +from owrx.controllers.feature import FeatureController +from owrx.controllers.assets import OwrxAssetsController, AprsSymbolsController, CompiledAssetsController +from owrx.controllers.websocket import WebSocketController +from owrx.controllers.api import ApiController +from owrx.controllers.metrics import MetricsController +from owrx.controllers.settings import SettingsController +from owrx.controllers.settings.general import GeneralSettingsController +from owrx.controllers.settings.sdr import ( + SdrDeviceListController, + SdrDeviceController, + SdrProfileController, + NewSdrDeviceController, + NewProfileController, +) +from owrx.controllers.settings.reporting import ReportingController +from owrx.controllers.settings.backgrounddecoding import BackgroundDecodingController +from owrx.controllers.settings.decoding import DecodingSettingsController +from owrx.controllers.settings.bookmarks import BookmarksController +from owrx.controllers.session import SessionController +from owrx.controllers.profile import ProfileController +from owrx.controllers.imageupload import ImageUploadController +from owrx.controllers.robots import RobotsController +from http.server import BaseHTTPRequestHandler +from urllib.parse import urlparse, parse_qs +import re +from abc import ABC, abstractmethod +from http.cookies import SimpleCookie + +import logging + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class Request(object): + def __init__(self, url, method, headers): + parsed_url = urlparse(url) + self.path = parsed_url.path + self.query = parse_qs(parsed_url.query) + self.matches = None + self.method = method + self.headers = headers + self.cookies = SimpleCookie() + if "Cookie" in headers: + self.cookies.load(headers["Cookie"]) + + def setMatches(self, matches): + self.matches = matches + + +class Route(ABC): + def __init__(self, controller, method="GET", options=None): + self.controller = controller + self.controllerOptions = options if options is not None else {} + self.method = method + + @abstractmethod + def matches(self, request): + pass + + +class StaticRoute(Route): + def __init__(self, route, controller, method="GET", options=None): + self.route = route + super().__init__(controller, method, options) + + def matches(self, request): + return request.path == self.route and self.method == request.method + + +class RegexRoute(Route): + def __init__(self, regex, controller, method="GET", options=None): + self.regex = re.compile(regex) + super().__init__(controller, method, options) + + def matches(self, request): + matches = self.regex.match(request.path) + # this is probably not the cleanest way to do it... + request.setMatches(matches) + return matches is not None and self.method == request.method + + +class Router(object): + def __init__(self): + self.routes = [ + StaticRoute("/", IndexController), + StaticRoute("/robots.txt", RobotsController), + StaticRoute("/status.json", StatusController), + RegexRoute("^/static/(.+)$", OwrxAssetsController), + RegexRoute("^/compiled/(.+)$", CompiledAssetsController), + RegexRoute("^/aprs-symbols/(.+)$", AprsSymbolsController), + StaticRoute("/ws/", WebSocketController), + RegexRoute("^(/favicon.ico)$", OwrxAssetsController), + StaticRoute("/map", MapController), + StaticRoute("/features", FeatureController), + StaticRoute("/api/features", ApiController), + StaticRoute("/metrics", MetricsController, options={"action": "prometheusAction"}), + StaticRoute("/metrics.json", MetricsController), + StaticRoute("/settings", SettingsController), + StaticRoute("/settings/general", GeneralSettingsController), + StaticRoute( + "/settings/general", GeneralSettingsController, method="POST", options={"action": "processFormData"} + ), + StaticRoute("/settings/sdr", SdrDeviceListController), + StaticRoute("/settings/newsdr", NewSdrDeviceController), + StaticRoute( + "/settings/newsdr", NewSdrDeviceController, method="POST", options={"action": "processFormData"} + ), + RegexRoute("^/settings/sdr/([^/]+)$", SdrDeviceController), + RegexRoute( + "^/settings/sdr/([^/]+)$", SdrDeviceController, method="POST", options={"action": "processFormData"} + ), + RegexRoute("^/settings/deletesdr/([^/]+)$", SdrDeviceController, options={"action": "deleteDevice"}), + RegexRoute("^/settings/sdr/([^/]+)/newprofile$", NewProfileController), + RegexRoute( + "^/settings/sdr/([^/]+)/newprofile$", + NewProfileController, + method="POST", + options={"action": "processFormData"}, + ), + RegexRoute("^/settings/sdr/([^/]+)/profile/([^/]+)$", SdrProfileController), + RegexRoute( + "^/settings/sdr/([^/]+)/profile/([^/]+)$", + SdrProfileController, + method="POST", + options={"action": "processFormData"}, + ), + RegexRoute( + "^/settings/sdr/([^/]+)/deleteprofile/([^/]+)$", + SdrProfileController, + options={"action": "deleteProfile"}, + ), + StaticRoute("/settings/bookmarks", BookmarksController), + StaticRoute("/settings/bookmarks", BookmarksController, method="POST", options={"action": "new"}), + RegexRoute("^/settings/bookmarks/(.+)$", BookmarksController, method="POST", options={"action": "update"}), + RegexRoute( + "^/settings/bookmarks/(.+)$", BookmarksController, method="DELETE", options={"action": "delete"} + ), + StaticRoute("/settings/reporting", ReportingController), + StaticRoute( + "/settings/reporting", ReportingController, method="POST", options={"action": "processFormData"} + ), + StaticRoute("/settings/backgrounddecoding", BackgroundDecodingController), + StaticRoute( + "/settings/backgrounddecoding", + BackgroundDecodingController, + method="POST", + options={"action": "processFormData"}, + ), + StaticRoute("/settings/decoding", DecodingSettingsController), + StaticRoute( + "/settings/decoding", DecodingSettingsController, method="POST", options={"action": "processFormData"} + ), + StaticRoute("/login", SessionController, options={"action": "loginAction"}), + StaticRoute("/login", SessionController, method="POST", options={"action": "processLoginAction"}), + StaticRoute("/logout", SessionController, options={"action": "logoutAction"}), + StaticRoute("/pwchange", ProfileController), + StaticRoute("/pwchange", ProfileController, method="POST", options={"action": "processPwChange"}), + StaticRoute("/imageupload", ImageUploadController), + StaticRoute("/imageupload", ImageUploadController, method="POST", options={"action": "processImage"}), + ] + + def find_route(self, request): + for r in self.routes: + if r.matches(request): + return r + + def route(self, handler, request): + route = self.find_route(request) + if route is not None: + controller = route.controller + controller(handler, request, route.controllerOptions).handle_request() + else: + handler.send_error(404, "Not Found", "The page you requested could not be found.") + + +class RequestHandler(BaseHTTPRequestHandler): + timeout = 30 + router = Router() + + def log_message(self, format, *args): + logger.debug("%s - - [%s] %s", self.address_string(), self.log_date_time_string(), format % args) + + def do_GET(self): + self.router.route(self, self._build_request("GET")) + + def do_POST(self): + self.router.route(self, self._build_request("POST")) + + def do_DELETE(self): + self.router.route(self, self._build_request("DELETE")) + + def _build_request(self, method): + return Request(self.path, method, self.headers) diff --git a/owrx/ism/rtl433.py b/owrx/ism/rtl433.py new file mode 100644 index 000000000..af2e8f02b --- /dev/null +++ b/owrx/ism/rtl433.py @@ -0,0 +1,23 @@ +from pycsdr.modules import ExecModule +from pycsdr.types import Format +from csdr.module import JsonParser +from owrx.reporting import ReportingEngine + + +class Rtl433Module(ExecModule): + def __init__(self): + super().__init__( + Format.COMPLEX_FLOAT, + Format.CHAR, + ["rtl_433", "-r", "cf32:-", "-F", "json", "-M", "time:unix", "-C", "si", "-s", "1200000"] + ) + + +class IsmParser(JsonParser): + def __init__(self): + super().__init__("ISM") + + def process(self, line): + data = super().process(line) + ReportingEngine.getSharedInstance().spot(data) + return data diff --git a/owrx/js8.py b/owrx/js8.py new file mode 100644 index 000000000..e358a760d --- /dev/null +++ b/owrx/js8.py @@ -0,0 +1,148 @@ +from owrx.audio import AudioChopperProfile, ConfigWiredProfileSource +from owrx.audio.chopper import AudioChopperParser +import re +from js8py import Js8 +from js8py.frames import Js8FrameHeartbeat, Js8FrameCompound +from owrx.map import Map, LocatorLocation, CallsignSource +from owrx.metrics import Metrics, CounterMetric +from owrx.config import Config +from abc import ABCMeta, abstractmethod +from owrx.reporting import ReportingEngine +from owrx.bands import Bandplan +from typing import List + +import logging + +logger = logging.getLogger(__name__) + + +class Js8Profile(AudioChopperProfile, metaclass=ABCMeta): + def decoding_depth(self): + pm = Config.get() + # return global default + if "js8_decoding_depth" in pm: + return pm["js8_decoding_depth"] + # default when no setting is provided + return 3 + + def getFileTimestampFormat(self): + return "%y%m%d_%H%M%S" + + def decoder_commandline(self, file): + return ["js8", "--js8", "-b", self.get_sub_mode(), "-d", str(self.decoding_depth()), file] + + @abstractmethod + def get_sub_mode(self): + pass + + +class Js8ProfileSource(ConfigWiredProfileSource): + def getPropertiesToWire(self) -> List[str]: + return ["js8_enabled_profiles"] + + def getProfiles(self) -> List[AudioChopperProfile]: + config = Config.get() + profiles = config["js8_enabled_profiles"] if "js8_enabled_profiles" in config else [] + return [self._loadProfile(p) for p in profiles] + + def _loadProfile(self, profileName): + className = "Js8{0}Profile".format(profileName[0].upper() + profileName[1:].lower()) + return globals()[className]() + + +class Js8NormalProfile(Js8Profile): + def getInterval(self): + return 15 + + def get_sub_mode(self): + return "A" + + +class Js8SlowProfile(Js8Profile): + def getInterval(self): + return 30 + + def get_sub_mode(self): + return "E" + + +class Js8FastProfile(Js8Profile): + def getInterval(self): + return 10 + + def get_sub_mode(self): + return "B" + + +class Js8TurboProfile(Js8Profile): + def getInterval(self): + return 6 + + def get_sub_mode(self): + return "C" + + +class Js8Parser(AudioChopperParser): + decoderRegex = re.compile(" ?") + + def parse(self, profile: AudioChopperProfile, freq: int, raw_msg: bytes): + try: + band = None + if freq is not None: + band = Bandplan.getSharedInstance().findBand(freq) + + msg = raw_msg.decode().rstrip() + if Js8Parser.decoderRegex.match(msg): + return + if msg.startswith(" EOF on input file"): + return + + frame = Js8().parse_message(msg) + + self.pushDecode(band) + + if (isinstance(frame, Js8FrameHeartbeat) or isinstance(frame, Js8FrameCompound)) and frame.grid: + Map.getSharedInstance().updateLocation( + CallsignSource(**frame.source), LocatorLocation(frame.grid), "JS8", band + ) + ReportingEngine.getSharedInstance().spot( + { + "source": frame.source, + "mode": "JS8", + "locator": frame.grid, + "freq": freq + frame.freq, + "db": frame.db, + "timestamp": frame.timestamp, + "msg": str(frame), + } + ) + + out = { + "mode": "JS8", + "msg": str(frame), + "timestamp": frame.timestamp, + "db": frame.db, + "dt": frame.dt, + "freq": freq + frame.freq, + "thread_type": frame.thread_type, + "js8mode": frame.mode, + } + + return out + + except Exception: + logger.exception("error while parsing js8 message") + + def pushDecode(self, band): + metrics = Metrics.getSharedInstance() + bandName = "unknown" + if band is not None: + bandName = band.getName() + + name = "js8call.decodes.{band}.JS8".format(band=bandName) + metric = metrics.getMetric(name) + if metric is None: + metric = CounterMetric() + metrics.addMetric(name, metric) + + metric.inc() diff --git a/owrx/jsons.py b/owrx/jsons.py new file mode 100644 index 000000000..4b2b97787 --- /dev/null +++ b/owrx/jsons.py @@ -0,0 +1,9 @@ +from owrx.property import PropertyManager +import json + + +class Encoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, PropertyManager): + return o.__dict__() + return super().default(o) diff --git a/owrx/locator.py b/owrx/locator.py new file mode 100644 index 000000000..bc173fee0 --- /dev/null +++ b/owrx/locator.py @@ -0,0 +1,30 @@ +class Locator(object): + @staticmethod + def fromCoordinates(coordinates, depth=3): + + lat = coordinates["lat"] + lon = coordinates["lon"] + + if not -90 < lat < 90: + raise ValueError("invalid latitude: {}".format(lat)) + if not -180 < lon < 180: + raise ValueError("invalid longitude: {}".format(lon)) + + lon = lon + 180 + lat = lat + 90 + + res = "" + res += chr(65 + int(lon / 20)) + res += chr(65 + int(lat / 10)) + if depth >= 2: + lon = lon % 20 + lat = lat % 10 + res += str(int(lon / 2)) + res += str(int(lat)) + if depth >= 3: + lon = lon % 2 + lat = lat % 1 + res += chr(97 + int(lon * 12)) + res += chr(97 + int(lat * 24)) + + return res diff --git a/owrx/log/__init__.py b/owrx/log/__init__.py new file mode 100644 index 000000000..745a079b6 --- /dev/null +++ b/owrx/log/__init__.py @@ -0,0 +1,52 @@ +import threading +import os +from logging import Logger, Handler, LogRecord, Formatter + + +class LogPipe(threading.Thread): + + def __init__(self, level: int, logger: Logger, prefix: str = ""): + threading.Thread.__init__(self) + self.daemon = False + self.level = level + self.logger = logger + self.prefix = prefix + self.fdRead, self.fdWrite = os.pipe() + self.pipeReader = os.fdopen(self.fdRead) + self.start() + + def fileno(self): + return self.fdWrite + + def run(self): + for line in iter(self.pipeReader.readline, ''): + self.logger.log(self.level, "{}: {}".format(self.prefix, line.strip('\n'))) + + self.pipeReader.close() + + def close(self): + os.close(self.fdWrite) + + +class HistoryHandler(Handler): + handlers = {} + + @staticmethod + def getHandler(name: str): + if name not in HistoryHandler.handlers: + HistoryHandler.handlers[name] = HistoryHandler() + return HistoryHandler.handlers[name] + + def __init__(self, maxRecords: int = 200): + super().__init__() + self.history = [] + self.maxRecords = maxRecords + self.setFormatter(Formatter(fmt="%(asctime)s - %(name)s - %(levelname)s - %(message)s")) + + def emit(self, record: LogRecord) -> None: + self.history.append(record) + # truncate + self.history = self.history[-self.maxRecords:] + + def getFormattedHistory(self) -> str: + return "\n".join([self.format(r) for r in self.history]) diff --git a/owrx/map.py b/owrx/map.py new file mode 100644 index 000000000..785c4909e --- /dev/null +++ b/owrx/map.py @@ -0,0 +1,190 @@ +from datetime import datetime, timedelta, timezone +from owrx.config import Config +from owrx.bands import Band +from abc import abstractmethod, ABC, ABCMeta +import threading +import time +import sys + +import logging + +logger = logging.getLogger(__name__) + + +class Location(object): + def getTTL(self) -> timedelta: + pm = Config.get() + return timedelta(seconds=pm["map_position_retention_time"]) + + def __dict__(self): + return { + "ttl": self.getTTL().total_seconds() * 1000 + } + + +class Source(ABC): + @abstractmethod + def getKey(self) -> str: + pass + + def __dict__(self): + return {} + + +class Map(object): + sharedInstance = None + creationLock = threading.Lock() + + @staticmethod + def getSharedInstance(): + with Map.creationLock: + if Map.sharedInstance is None: + Map.sharedInstance = Map() + return Map.sharedInstance + + def __init__(self): + self.clients = [] + self.positions = {} + self.positionsLock = threading.Lock() + + def removeLoop(): + loops = 0 + while True: + try: + self.removeOldPositions() + except Exception: + logger.exception("error while removing old map positions") + loops += 1 + # rebuild the positions dictionary every once in a while, it consumes lots of memory otherwise + if loops == 60: + try: + self.rebuildPositions() + except Exception: + logger.exception("error while rebuilding positions") + loops = 0 + time.sleep(60) + + threading.Thread(target=removeLoop, daemon=True, name="map_removeloop").start() + super().__init__() + + def broadcast(self, update): + for c in self.clients: + c.write_update(update) + + def addClient(self, client): + self.clients.append(client) + client.write_update( + [ + { + "source": record["source"].__dict__(), + "location": record["location"].__dict__(), + "lastseen": record["updated"].timestamp() * 1000, + "mode": record["mode"], + "band": record["band"].getName() if record["band"] is not None else None, + } + for record in self.positions.values() + ] + ) + + def removeClient(self, client): + try: + self.clients.remove(client) + except ValueError: + pass + + def updateLocation(self, source: Source, loc: Location, mode: str, band: Band = None, timestamp: datetime = None): + if timestamp is None: + timestamp = datetime.now(timezone.utc) + else: + # if we get an external timestamp, make sure it's not already expired + if datetime.now(timezone.utc) - loc.getTTL() > timestamp: + return + key = source.getKey() + with self.positionsLock: + if isinstance(loc, IncrementalUpdate) and key in self.positions: + loc.update(self.positions[key]["location"]) + self.positions[key] = {"source": source, "location": loc, "updated": timestamp, "mode": mode, "band": band} + self.broadcast( + [ + { + "source": source.__dict__(), + "location": loc.__dict__(), + "lastseen": timestamp.timestamp() * 1000, + "mode": mode, + "band": band.getName() if band is not None else None, + } + ] + ) + + def touchLocation(self, source: Source): + # not implemented on the client side yet, so do not use! + ts = datetime.now(timezone.utc) + key = source.getKey() + with self.positionsLock: + if key in self.positions: + self.positions[key]["updated"] = ts + self.broadcast([{"source": source.__dict__(), "lastseen": ts.timestamp() * 1000}]) + + def removeLocation(self, key): + with self.positionsLock: + del self.positions[key] + # TODO broadcast removal to clients + + def removeOldPositions(self): + now = datetime.now(timezone.utc) + + with self.positionsLock: + to_be_removed = [ + key for (key, pos) in self.positions.items() if now - pos["location"].getTTL() > pos["updated"] + ] + for key in to_be_removed: + self.removeLocation(key) + + def rebuildPositions(self): + logger.debug("rebuilding map storage; size before: %i", sys.getsizeof(self.positions)) + with self.positionsLock: + p = {key: value for key, value in self.positions.items()} + self.positions = p + logger.debug("rebuild complete; size after: %i", sys.getsizeof(self.positions)) + + +class LatLngLocation(Location): + def __init__(self, lat: float, lon: float): + self.lat = lat + self.lon = lon + + def __dict__(self): + res = super().__dict__() + res.update( + {"type": "latlon", "lat": self.lat, "lon": self.lon} + ) + return res + + +class LocatorLocation(Location): + def __init__(self, locator: str): + self.locator = locator + + def __dict__(self): + res = super().__dict__() + res.update( + {"type": "locator", "locator": self.locator} + ) + return res + + +class IncrementalUpdate(Location, metaclass=ABCMeta): + @abstractmethod + def update(self, previousLocation: Location): + pass + + +class CallsignSource(Source): + def __init__(self, callsign: str): + self.callsign = callsign + + def getKey(self) -> str: + return "callsign:{}".format(self.callsign) + + def __dict__(self): + return {"callsign": self.callsign} diff --git a/owrx/meta.py b/owrx/meta.py new file mode 100644 index 000000000..a69f547e0 --- /dev/null +++ b/owrx/meta.py @@ -0,0 +1,242 @@ +import json +import logging +import threading +import pickle +import re +from abc import ABC, ABCMeta, abstractmethod +from datetime import datetime, timedelta +from urllib import request +from urllib.error import HTTPError + +from csdr.module import PickleModule +from owrx.aprs import AprsParser, AprsLocation +from owrx.config import Config +from owrx.map import Map, LatLngLocation, CallsignSource +from owrx.bands import Bandplan + +logger = logging.getLogger(__name__) + + +class Enricher(ABC): + def __init__(self, parser): + self.parser = parser + + @abstractmethod + def enrich(self, meta, callback): + pass + + +class RadioIDCache(object): + sharedInstance = None + + @staticmethod + def getSharedInstance(): + if RadioIDCache.sharedInstance is None: + RadioIDCache.sharedInstance = RadioIDCache() + return RadioIDCache.sharedInstance + + def __init__(self): + self.cache = {} + self.cacheTimeout = timedelta(seconds=86400) + + def isValid(self, mode, radio_id): + key = self.__key(mode, radio_id) + if key not in self.cache: + return False + entry = self.cache[key] + return entry["timestamp"] + self.cacheTimeout > datetime.now() + + def __key(self, mode, radio_id): + return "{}-{}".format(mode, radio_id) + + def put(self, mode, radio_id, value): + self.cache[self.__key(mode, radio_id)] = {"timestamp": datetime.now(), "data": value} + + def get(self, mode, radio_id): + if not self.isValid(mode, radio_id): + return None + return self.cache[self.__key(mode, radio_id)]["data"] + + +class RadioIDEnricher(Enricher): + def __init__(self, mode, parser): + super().__init__(parser) + self.mode = mode + self.threads = {} + self.callbacks = {} + + def _fillCache(self, id): + data = self._downloadRadioIdData(id) + RadioIDCache.getSharedInstance().put(self.mode, id, data) + if id in self.callbacks: + while self.callbacks[id]: + self.callbacks[id].pop()(data) + del self.callbacks[id] + del self.threads[id] + + def _downloadRadioIdData(self, id): + try: + logger.debug("requesting radioid metadata for mode=%s and id=%s", self.mode, id) + res = request.urlopen("https://www.radioid.net/api/{0}/user/?id={1}".format(self.mode, id), timeout=30) + if res.status != 200: + logger.warning("radioid API returned error %i for mode=%s and id=%s", res.status, self.mode, id) + return None + data = json.loads(res.read().decode("utf-8")) + if "count" in data and data["count"] > 0 and "results" in data: + for item in data["results"]: + if "id" in item and item["id"] == id: + return item + except json.JSONDecodeError: + logger.warning("unable to parse radioid response JSON") + except HTTPError as e: + logger.warning("radioid responded with error: %s", str(e)) + + return None + + def enrich(self, meta, callback): + config_key = "digital_voice_{}_id_lookup".format(self.mode) + if not Config.get()[config_key]: + return meta + if "source" not in meta: + return meta + id = int(meta["source"]) + cache = RadioIDCache.getSharedInstance() + if not cache.isValid(self.mode, id): + if id not in self.threads: + self.threads[id] = threading.Thread(target=self._fillCache, args=[id], daemon=True) + self.threads[id].start() + if id not in self.callbacks: + self.callbacks[id] = [] + + def onFinish(data): + if data is not None: + meta["additional"] = data + callback(meta) + + self.callbacks[id].append(onFinish) + return meta + data = cache.get(self.mode, id) + if data is not None: + meta["additional"] = data + return meta + + +class DigihamEnricher(Enricher, metaclass=ABCMeta): + def parseCoordinate(self, meta, mode): + for key in ["lat", "lon"]: + if key in meta: + meta[key] = float(meta[key]) + callsign = self.getCallsign(meta) + if callsign is not None and "lat" in meta and "lon" in meta: + loc = LatLngLocation(meta["lat"], meta["lon"]) + Map.getSharedInstance().updateLocation(CallsignSource(callsign), loc, mode, self.parser.getBand()) + return meta + + @abstractmethod + def getCallsign(self, meta): + pass + + +class DmrEnricher(DigihamEnricher, RadioIDEnricher): + # callsign must be uppercase alphanumeric and at the beginning + # if there's anything after the callsign, it must be separated by a whitespace + talkerAliasRegex = re.compile("^([A-Z0-9]+)(\\s.*)?$") + + def __init__(self, parser): + super().__init__("dmr", parser) + + def getCallsign(self, meta): + # there's no explicit callsign data in dmr, so we can only rely on one of the following: + # a) a callsign provided by a radioid lookup + if "additional" in meta and "callsign" in meta["additional"]: + return meta["additional"]["callsign"] + # b) a callsign in the talker alias + if "talkeralias" in meta: + matches = DmrEnricher.talkerAliasRegex.match(meta["talkeralias"]) + if matches: + return matches.group(1) + + def enrich(self, meta, callback): + def asyncParse(meta): + self.parseCoordinate(meta, "DMR") + callback(meta) + meta = super().enrich(meta, asyncParse) + meta = self.parseCoordinate(meta, "DMR") + return meta + + +class YsfMetaEnricher(DigihamEnricher): + def getCallsign(self, meta): + if "source" in meta: + return meta["source"] + + def enrich(self, meta, callback): + meta = self.parseCoordinate(meta, "YSF") + return meta + + +class DStarEnricher(DigihamEnricher): + def getCallsign(self, meta): + if "ourcall" in meta: + return meta["ourcall"] + + def enrich(self, meta, callback): + meta = self.parseCoordinate(meta, "D-Star") + meta = self.parseDprs(meta) + return meta + + def parseDprs(self, meta): + if "dprs" in meta: + try: + # we can send the DPRS stuff through our APRS parser to extract the information + # TODO: only third-party parsing accepts this format right now + parser = AprsParser() + dprsData = parser.parseThirdpartyAprsData(meta["dprs"]) + if "data" in dprsData: + data = dprsData["data"] + if "lat" in data and "lon" in data: + # TODO: we could actually get the symbols from the parsed APRS data and show that on the meta panel + meta["lat"] = data["lat"] + meta["lon"] = data["lon"] + + if "ourcall" in meta: + # send location info to map as well (it will show up with the correct symbol there!) + loc = AprsLocation(data) + Map.getSharedInstance().updateLocation(CallsignSource(meta["ourcall"]), loc, "DPRS", self.parser.getBand()) + except Exception: + logger.exception("Error while parsing DPRS data") + + return meta + + +class MetaParser(PickleModule): + def __init__(self): + self.enrichers = { + "DMR": DmrEnricher(self), + "YSF": YsfMetaEnricher(self), + "DSTAR": DStarEnricher(self), + "NXDN": RadioIDEnricher("nxdn", self), + } + self.currentMetaData = None + self.band = None + super().__init__() + + def process(self, meta): + self.currentMetaData = None + if "protocol" in meta: + protocol = meta["protocol"] + if protocol in self.enrichers: + self.currentMetaData = meta = self.enrichers[protocol].enrich(meta, self.receive) + return meta + + def receive(self, meta): + # we may have moved on in the meantime + if meta is not self.currentMetaData: + return + self.writer.write(pickle.dumps(meta)) + + def setDialFrequency(self, freq): + self.band = Bandplan.getSharedInstance().findBand(freq) + + def getBand(self): + return self.band diff --git a/owrx/metrics.py b/owrx/metrics.py new file mode 100644 index 000000000..6600e8518 --- /dev/null +++ b/owrx/metrics.py @@ -0,0 +1,70 @@ +import threading +from owrx.client import ClientRegistry + + +class Metric(object): + def getValue(self): + return 0 + + +class CounterMetric(Metric): + def __init__(self): + self.counter = 0 + + def inc(self, increment=1): + self.counter += increment + + def getValue(self): + return {"count": self.counter} + + +class DirectMetric(Metric): + def __init__(self, getter): + self.getter = getter + + def getValue(self): + return self.getter() + + +class Metrics(object): + sharedInstance = None + creationLock = threading.Lock() + + @staticmethod + def getSharedInstance(): + with Metrics.creationLock: + if Metrics.sharedInstance is None: + Metrics.sharedInstance = Metrics() + return Metrics.sharedInstance + + def __init__(self): + self.metrics = {} + self.addMetric("openwebrx.users", DirectMetric(ClientRegistry.getSharedInstance().clientCount)) + + def addMetric(self, name, metric): + self.metrics[name] = metric + + def hasMetric(self, name): + return name in self.metrics + + def getMetric(self, name): + if not self.hasMetric(name): + return None + return self.metrics[name] + + def getFlatMetrics(self): + return self.metrics + + def getHierarchicalMetrics(self): + result = {} + + for (key, metric) in self.metrics.items(): + partial = result + keys = key.split(".") + for keypart in keys[0:-1]: + if not keypart in partial: + partial[keypart] = {} + partial = partial[keypart] + partial[keys[-1]] = metric.getValue() + + return result diff --git a/owrx/modes.py b/owrx/modes.py new file mode 100644 index 000000000..f5ea047ef --- /dev/null +++ b/owrx/modes.py @@ -0,0 +1,227 @@ +from owrx.feature import FeatureDetector +from owrx.audio import ProfileSource +from functools import reduce +from abc import ABCMeta, abstractmethod + + +class Bandpass(object): + def __init__(self, low_cut, high_cut): + self.low_cut = low_cut + self.high_cut = high_cut + + +class Mode: + def __init__(self, modulation: str, name: str, bandpass: Bandpass = None, ifRate=None, requirements=None, service=False, squelch=True): + self.modulation = modulation + self.name = name + self.requirements = requirements if requirements is not None else [] + self.service = service + self.bandpass = bandpass + self.ifRate = ifRate + self.squelch = squelch + + def is_available(self): + fd = FeatureDetector() + return reduce(lambda a, b: a and b, [fd.is_available(r) for r in self.requirements], True) + + def is_service(self): + return self.service + + def get_bandpass(self): + return self.bandpass + + def get_modulation(self): + return self.modulation + + +EmptyMode = Mode("empty", "Empty") + + +class AnalogMode(Mode): + pass + + +class DigitalMode(Mode): + def __init__( + self, + modulation, + name, + underlying, + bandpass: Bandpass = None, + ifRate = None, + requirements=None, + service=False, + squelch=True, + secondaryFft=True + ): + super().__init__(modulation, name, bandpass, ifRate, requirements, service, squelch) + self.underlying = underlying + self.secondaryFft = secondaryFft + + def get_underlying_mode(self): + mode = Modes.findByModulation(self.underlying[0]) + if mode is None: + mode = EmptyMode + return mode + + def get_bandpass(self): + if self.bandpass is not None: + return self.bandpass + return self.get_underlying_mode().get_bandpass() + + def get_modulation(self): + return self.get_underlying_mode().get_modulation() + + def for_underlying(self, underlying: str): + if underlying not in self.underlying: + raise ValueError("{} is not a valid underlying mode for {}".format(underlying, self.modulation)) + return DigitalMode( + self.modulation, self.name, [underlying], self.bandpass, self.requirements, self.service, self.squelch + ) + + +class AudioChopperMode(DigitalMode, metaclass=ABCMeta): + def __init__(self, modulation, name, bandpass=None, requirements=None): + if bandpass is None: + bandpass = Bandpass(0, 3000) + super().__init__(modulation, name, ["usb"], bandpass=bandpass, requirements=requirements, service=True) + + @abstractmethod + def get_profile_source(self) -> ProfileSource: + pass + + +class WsjtMode(AudioChopperMode): + def __init__(self, modulation, name, bandpass=None, requirements=None): + if requirements is None: + requirements = ["wsjt-x"] + super().__init__(modulation, name, bandpass=bandpass, requirements=requirements) + + def get_profile_source(self) -> ProfileSource: + # inline import due to circular dependencies + from owrx.wsjt import WsjtProfiles + return WsjtProfiles.getSource(self.modulation) + + +class Js8Mode(AudioChopperMode): + def __init__(self, modulation, name, bandpass=None, requirements=None): + if requirements is None: + requirements = ["js8call"] + super().__init__(modulation, name, bandpass, requirements) + + def get_profile_source(self) -> ProfileSource: + # inline import due to circular dependencies + from owrx.js8 import Js8ProfileSource + return Js8ProfileSource() + + +class Modes(object): + mappings = [ + AnalogMode("nfm", "FM", bandpass=Bandpass(-4000, 4000)), + AnalogMode("wfm", "WFM", bandpass=Bandpass(-75000, 75000)), + AnalogMode("am", "AM", bandpass=Bandpass(-4000, 4000)), + AnalogMode("lsb", "LSB", bandpass=Bandpass(-3000, -300)), + AnalogMode("usb", "USB", bandpass=Bandpass(300, 3000)), + AnalogMode("cw", "CW", bandpass=Bandpass(700, 900)), + AnalogMode("dmr", "DMR", bandpass=Bandpass(-6250, 6250), requirements=["digital_voice_digiham"], squelch=False), + AnalogMode( + "dstar", "D-Star", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_digiham"], squelch=False + ), + AnalogMode("nxdn", "NXDN", bandpass=Bandpass(-3250, 3250), requirements=["digital_voice_digiham"], squelch=False), + AnalogMode("ysf", "YSF", bandpass=Bandpass(-6250, 6250), requirements=["digital_voice_digiham"], squelch=False), + AnalogMode("m17", "M17", bandpass=Bandpass(-6250, 6250), requirements=["digital_voice_m17"], squelch=False), + AnalogMode( + "freedv", "FreeDV", bandpass=Bandpass(300, 3000), requirements=["digital_voice_freedv"], squelch=False + ), + AnalogMode("drm", "DRM", bandpass=Bandpass(-5000, 5000), requirements=["drm"], squelch=False), + AnalogMode("dab", "DAB", bandpass=None, ifRate=2.048e6, requirements=["dab"], squelch=False), + DigitalMode("bpsk31", "BPSK31", underlying=["usb"]), + DigitalMode("bpsk63", "BPSK63", underlying=["usb"]), + DigitalMode("rtty170", "RTTY 45/170", underlying=["usb", "lsb"]), + DigitalMode("rtty450", "RTTY 50N/450", underlying=["lsb", "usb"]), + DigitalMode("rtty85", "RTTY 50N/85", underlying=["lsb", "usb"]), + WsjtMode("ft8", "FT8"), + WsjtMode("ft4", "FT4"), + WsjtMode("jt65", "JT65"), + WsjtMode("jt9", "JT9"), + WsjtMode("wspr", "WSPR", bandpass=Bandpass(1350, 1650)), + WsjtMode("fst4", "FST4", requirements=["wsjt-x-2-3"]), + WsjtMode("fst4w", "FST4W", bandpass=Bandpass(1350, 1650), requirements=["wsjt-x-2-3"]), + WsjtMode("q65", "Q65", requirements=["wsjt-x-2-4"]), + DigitalMode("msk144", "MSK144", requirements=["msk144"], underlying=["usb"], service=True), + Js8Mode("js8", "JS8Call"), + DigitalMode( + "packet", + "Packet", + underlying=["nfm", "usb", "lsb"], + bandpass=Bandpass(-6250, 6250), + requirements=["packet"], + service=True, + squelch=False, + ), + DigitalMode( + "pocsag", + "Pocsag", + underlying=["nfm"], + bandpass=Bandpass(-6250, 6250), + requirements=["pocsag"], + service=True, + squelch=False, + ), + DigitalMode( + "adsb", + "ADS-B", + underlying=["empty"], + bandpass=None, + ifRate=2.4e6, + requirements=["dump1090"], + service=True, + squelch=False, + secondaryFft=False, + ), + DigitalMode( + "ism", + "ISM", + underlying=["empty"], + bandpass=Bandpass(-125000, 125000), + requirements=["ism"], + service=True, + squelch=False, + ), + DigitalMode( + "hfdl", + "HFDL", + underlying=["empty"], + bandpass=Bandpass(0, 3000), + requirements=["dumphfdl"], + service=True, + squelch=False, + ), + DigitalMode( + "vdl2", + "VDL2", + underlying=["empty"], + bandpass=Bandpass(-12500, 12500), + requirements=["dumpvdl2"], + service=True, + squelch=False, + ) + ] + + @staticmethod + def getModes(): + return Modes.mappings + + @staticmethod + def getAvailableModes(): + return [m for m in Modes.getModes() if m.is_available()] + + @staticmethod + def getAvailableServices(): + return [m for m in Modes.getAvailableModes() if m.is_service()] + + @staticmethod + def findByModulation(modulation): + modes = [m for m in Modes.getAvailableModes() if m.modulation == modulation] + if modes: + return modes[0] diff --git a/owrx/pocsag.py b/owrx/pocsag.py new file mode 100644 index 000000000..944df61d6 --- /dev/null +++ b/owrx/pocsag.py @@ -0,0 +1,39 @@ +from csdr.module import PickleModule +from owrx.bands import Bandplan +from owrx.metrics import Metrics, CounterMetric +from owrx.reporting import ReportingEngine +import logging + +logger = logging.getLogger(__name__) + + +class PocsagParser(PickleModule): + def __init__(self): + self.band = None + super().__init__() + + def process(self, meta): + try: + if "address" in meta: + meta["address"] = int(meta["address"]) + meta["mode"] = "Pocsag" + self.pushDecode() + ReportingEngine.getSharedInstance().spot(meta) + return meta + except Exception: + logger.exception("Exception while parsing Pocsag message") + + def setDialFrequency(self, freq: int) -> None: + self.band = Bandplan.getSharedInstance().findBand(freq) + + def pushDecode(self): + band = "unknown" + if self.band is not None: + band = self.band.getName() + name = "digiham.decodes.{band}.pocsag".format(band=band) + metrics = Metrics.getSharedInstance() + metric = metrics.getMetric(name) + if metric is None: + metric = CounterMetric() + metrics.addMetric(name, metric) + metric.inc() diff --git a/owrx/property/__init__.py b/owrx/property/__init__.py new file mode 100644 index 000000000..7cc84f649 --- /dev/null +++ b/owrx/property/__init__.py @@ -0,0 +1,421 @@ +from abc import ABC, abstractmethod +from owrx.property.validators import Validator +from owrx.property.filter import Filter, ByPropertyName +import logging + +logger = logging.getLogger(__name__) + + +class PropertyError(Exception): + pass + + +class PropertyDeletion(object): + def __bool__(self): + return False + + +# a special object that will be sent in events when a deletion occured +# it can also represent deletion of a key in internal storage, but should not be return from standard dict apis +PropertyDeleted = PropertyDeletion() + + +class Subscription(object): + def __init__(self, subscriptee, name, subscriber): + self.subscriptee = subscriptee + self.name = name + self.subscriber = subscriber + + def getName(self): + return self.name + + def call(self, *args, **kwargs): + self.subscriber(*args, **kwargs) + + def cancel(self): + self.subscriptee.unwire(self) + + +class PropertyManager(ABC): + def __init__(self): + self.subscribers = [] + + @abstractmethod + def __getitem__(self, item): + pass + + @abstractmethod + def __setitem__(self, key, value): + pass + + @abstractmethod + def __contains__(self, item): + pass + + @abstractmethod + def __dict__(self): + pass + + @abstractmethod + def __delitem__(self, key): + pass + + @abstractmethod + def keys(self): + pass + + @abstractmethod + def values(self): + pass + + @abstractmethod + def items(self): + pass + + def __len__(self): + return self.__dict__().__len__() + + def filter(self, *props): + return PropertyFilter(self, ByPropertyName(*props)) + + def readonly(self): + return PropertyReadOnly(self) + + def wire(self, callback): + sub = Subscription(self, None, callback) + self.subscribers.append(sub) + return sub + + def wireProperty(self, name, callback): + sub = Subscription(self, name, callback) + self.subscribers.append(sub) + if name in self: + sub.call(self[name]) + return sub + + def unwire(self, sub): + try: + self.subscribers.remove(sub) + except ValueError: + # happens when already removed before + pass + return self + + def _fireCallbacks(self, changes): + if not changes: + return + subscribers = self.subscribers.copy() + for c in subscribers: + try: + if c.getName() is None: + c.call(changes) + except Exception: + logger.exception("exception while firing changes") + for name in changes: + for c in subscribers: + try: + if c.getName() == name: + c.call(changes[name]) + except Exception: + logger.exception("exception while firing changes") + + +class PropertyLayer(PropertyManager): + def __init__(self, **kwargs): + super().__init__() + # copy, don't re-use + self.properties = {k: v for k, v in kwargs.items()} + + def __contains__(self, name): + return name in self.properties + + def __getitem__(self, name): + return self.properties[name] + + def __setitem__(self, name, value): + if name in self.properties and self.properties[name] == value: + return + self.properties[name] = value + self._fireCallbacks({name: value}) + + def __dict__(self): + return {k: v for k, v in self.properties.items()} + + def __delitem__(self, key): + self.properties.__delitem__(key) + self._fireCallbacks({key: PropertyDeleted}) + + def keys(self): + return self.properties.keys() + + def values(self): + return self.properties.values() + + def items(self): + return self.properties.items() + + +class PropertyFilter(PropertyManager): + def __init__(self, pm: PropertyManager, filter: Filter): + super().__init__() + self.pm = pm + self._filter = filter + self.pm.wire(self.receiveEvent) + + def receiveEvent(self, changes): + changesToForward = {name: value for name, value in changes.items() if self._filter.apply(name)} + self._fireCallbacks(changesToForward) + + def __getitem__(self, item): + if not self._filter.apply(item): + raise KeyError(item) + return self.pm.__getitem__(item) + + def __setitem__(self, key, value): + if not self._filter.apply(key): + raise KeyError(key) + return self.pm.__setitem__(key, value) + + def __contains__(self, item): + if not self._filter.apply(item): + return False + return self.pm.__contains__(item) + + def __dict__(self): + return {k: v for k, v in self.pm.__dict__().items() if self._filter.apply(k)} + + def __delitem__(self, key): + if not self._filter.apply(key): + raise KeyError(key) + return self.pm.__delitem__(key) + + def keys(self): + return [k for k in self.pm.keys() if self._filter.apply(k)] + + def values(self): + return [v for k, v in self.pm.items() if self._filter.apply(k)] + + def items(self): + return self.__dict__().items() + + +class PropertyDelegator(PropertyManager): + def __init__(self, pm: PropertyManager): + self.pm = pm + self.subscription = self.pm.wire(self._fireCallbacks) + super().__init__() + + def __getitem__(self, item): + return self.pm.__getitem__(item) + + def __setitem__(self, key, value): + return self.pm.__setitem__(key, value) + + def __contains__(self, item): + return self.pm.__contains__(item) + + def __dict__(self): + return self.pm.__dict__() + + def __delitem__(self, key): + return self.pm.__delitem__(key) + + def keys(self): + return self.pm.keys() + + def values(self): + return self.pm.values() + + def items(self): + return self.pm.items() + + +class PropertyValidationError(PropertyError): + def __init__(self, key, value): + super().__init__('Invalid value for property "{key}": "{value}"'.format(key=key, value=str(value))) + + +class PropertyValidator(PropertyDelegator): + def __init__(self, pm: PropertyManager, validators=None): + super().__init__(pm) + if validators is None: + self.validators = {} + else: + self.validators = {k: Validator.of(v) for k, v in validators.items()} + + def validate(self, key, value): + if key not in self.validators: + return + if not self.validators[key].isValid(value): + raise PropertyValidationError(key, value) + + def setValidator(self, key, validator): + self.validators[key] = Validator.of(validator) + + def __setitem__(self, key, value): + self.validate(key, value) + return self.pm.__setitem__(key, value) + + +class PropertyWriteError(PropertyError): + def __init__(self, key): + super().__init__('Key "{key}" is not writeable'.format(key=key)) + + +class PropertyReadOnly(PropertyDelegator): + def __setitem__(self, key, value): + raise PropertyWriteError(key) + + def __delitem__(self, key): + raise PropertyWriteError(key) + + +class PropertyStack(PropertyManager): + def __init__(self): + super().__init__() + self.layers = [] + + def addLayer(self, priority: int, pm: PropertyManager): + """ + highest priority = 0 + """ + self._fireCallbacks(self._addLayer(priority, pm)) + + def _addLayer(self, priority: int, pm: PropertyManager): + changes = {} + for key in pm.keys(): + if key not in self or self[key] != pm[key]: + changes[key] = pm[key] + + def eventClosure(changes): + self.receiveEvent(pm, changes) + + sub = pm.wire(eventClosure) + + self.layers.append({"priority": priority, "props": pm, "sub": sub}) + + return changes + + def removeLayerByPriority(self, priority): + for layer in self.layers: + if layer["priority"] == priority: + self.removeLayer(layer["props"]) + + def removeLayer(self, pm: PropertyManager): + for layer in self.layers: + if layer["props"] == pm: + self._fireCallbacks(self._removeLayer(layer)) + + def _removeLayer(self, layer): + layer["sub"].cancel() + self.layers.remove(layer) + changes = {} + pm = layer["props"] + for key in pm.keys(): + if key in self: + if self[key] != pm[key]: + changes[key] = self[key] + else: + changes[key] = PropertyDeleted + return changes + + def replaceLayer(self, priority: int, pm: PropertyManager): + layers = [x for x in self.layers if x["priority"] == priority] + + originalState = self.__dict__() + + changes = self._removeLayer(layers[0]) if layers else {} + changes = {**changes, **self._addLayer(priority, pm)} + changes = {k: v for k, v in changes.items() if k not in originalState or originalState[k] != v} + + self._fireCallbacks(changes) + + def receiveEvent(self, layer, changes): + changesToForward = {name: value for name, value in changes.items() if layer == self._getTopLayer(name)} + # deletions need to be handled separately: + # * send a deletion if the key was deleted in all layers + # * send lower value if the key is still present in a lower layer + deletionsToForward = { + name: PropertyDeleted if self._getTopLayer(name, False) is None else self[name] + for name, value in changes.items() + if value is PropertyDeleted + } + self._fireCallbacks({**changesToForward, **deletionsToForward}) + + def _getTopLayer(self, item, fallback=True): + layers = [la["props"] for la in sorted(self.layers, key=lambda l: l["priority"])] + for m in layers: + if item in m: + return m + # return top layer as fallback + if fallback and layers: + return layers[0] + + def __getitem__(self, item): + layer = self._getTopLayer(item) + return layer.__getitem__(item) + + def __setitem__(self, key, value): + layer = self._getTopLayer(key) + return layer.__setitem__(key, value) + + def __contains__(self, item): + layer = self._getTopLayer(item) + if layer: + return layer.__contains__(item) + return False + + def __dict__(self): + return {k: self.__getitem__(k) for k in self.keys()} + + def __delitem__(self, key): + for layer in self.layers: + if layer["props"].__contains__(key): + layer["props"].__delitem__(key) + + def keys(self): + return set([key for l in self.layers for key in l["props"].keys()]) + + def values(self): + return [self.__getitem__(k) for k in self.keys()] + + def items(self): + return self.__dict__().items() + + +class PropertyCarousel(PropertyDelegator): + def __init__(self): + # start with an empty dummy layer + self.emptyLayer = PropertyLayer().readonly() + super().__init__(self.emptyLayer) + self.layers = {} + + def _getDefaultLayer(self): + return self.emptyLayer + + def addLayer(self, key, value): + if key in self.layers and self.layers[key] is self.pm: + self.layers[key] = value + # switch after introducing the new value + self.switch(key) + else: + self.layers[key] = value + + def removeLayer(self, key): + if key in self.layers and self.layers[key] is self.pm: + self.switch() + del self.layers[key] + + def switch(self, key=None): + before = self.pm + self.subscription.cancel() + self.pm = self._getDefaultLayer() if key is None else self.layers[key] + self.subscription = self.pm.wire(self._fireCallbacks) + changes = {} + for key in set(list(before.keys()) + list(self.keys())): + if key not in self: + changes[key] = PropertyDeleted + else: + if key not in before or before[key] != self[key]: + changes[key] = self[key] + self._fireCallbacks(changes) diff --git a/owrx/property/filter.py b/owrx/property/filter.py new file mode 100644 index 000000000..7e870d05b --- /dev/null +++ b/owrx/property/filter.py @@ -0,0 +1,23 @@ +from abc import ABC, abstractmethod + + +class Filter(ABC): + @abstractmethod + def apply(self, prop) -> bool: + pass + + +class ByPropertyName(Filter): + def __init__(self, *props): + self.props = props + + def apply(self, prop) -> bool: + return prop in self.props + + +class ByLambda(Filter): + def __init__(self, func): + self.func = func + + def apply(self, prop) -> bool: + return self.func(prop) diff --git a/owrx/property/validators.py b/owrx/property/validators.py new file mode 100644 index 000000000..f1c197e2a --- /dev/null +++ b/owrx/property/validators.py @@ -0,0 +1,97 @@ +from abc import ABC, abstractmethod +from functools import reduce +from operator import or_ + + +class ValidatorException(Exception): + pass + + +class Validator(ABC): + @staticmethod + def of(x): + if isinstance(x, Validator): + return x + if callable(x): + return LambdaValidator(x) + if x in validator_types: + return validator_types[x]() + raise ValidatorException("Cannot create validator") + + @abstractmethod + def isValid(self, value): + pass + + +class LambdaValidator(Validator): + def __init__(self, c): + self.callable = c + + def isValid(self, value): + return self.callable(value) + + +class TypeValidator(Validator): + def __init__(self, type): + self.type = type + super().__init__() + + def isValid(self, value): + return isinstance(value, self.type) + + +class IntegerValidator(TypeValidator): + def __init__(self): + super().__init__(int) + + +class FloatValidator(TypeValidator): + def __init__(self): + super().__init__(float) + + +class StringValidator(TypeValidator): + def __init__(self): + super().__init__(str) + + +class BoolValidator(TypeValidator): + def __init__(self): + super().__init__(bool) + + +class OrValidator(Validator): + def __init__(self, *validators): + self.validators = validators + super().__init__() + + def isValid(self, value): + return reduce( + or_, + [v.isValid(value) for v in self.validators], + False + ) + + +class NumberValidator(OrValidator): + def __init__(self): + super().__init__(IntegerValidator(), FloatValidator()) + + +class RegexValidator(StringValidator): + def __init__(self, regex): + self.regex = regex + super().__init__() + + def isValid(self, value): + return super().isValid(value) and self.regex.match(value) is not None + + +validator_types = { + "string": StringValidator, + "str": StringValidator, + "integer": IntegerValidator, + "int": IntegerValidator, + "number": NumberValidator, + "num": NumberValidator, +} diff --git a/owrx/rds/redsea.py b/owrx/rds/redsea.py new file mode 100644 index 000000000..e52d1c96c --- /dev/null +++ b/owrx/rds/redsea.py @@ -0,0 +1,14 @@ +from pycsdr.modules import ExecModule +from pycsdr.types import Format + + +class RedseaModule(ExecModule): + def __init__(self, sampleRate: int, rbds: bool): + args = ["redsea", "--samplerate", str(sampleRate)] + if rbds: + args += ["--rbds"] + super().__init__( + Format.SHORT, + Format.CHAR, + args + ) diff --git a/owrx/receiverid.py b/owrx/receiverid.py new file mode 100644 index 000000000..e21760af2 --- /dev/null +++ b/owrx/receiverid.py @@ -0,0 +1,98 @@ +import re +import logging +import hashlib +import hmac +from datetime import datetime, timezone +from owrx.config import Config + +logger = logging.getLogger(__name__) + + +keyRegex = re.compile("^([a-zA-Z]+)-([0-9a-f]{32})-([0-9a-f]{64})$") +keyChallengeRegex = re.compile("^([a-zA-Z]+)-([0-9a-f]{32})-([0-9a-f]{32})$") +headerRegex = re.compile("^ReceiverId (.*)$") + + +class KeyException(Exception): + pass + + +class Key(object): + def __init__(self, keyString): + matches = keyRegex.match(keyString) + if not matches: + raise KeyException("invalid key format") + self.source = matches.group(1) + self.id = matches.group(2) + self.secret = matches.group(3) + + +class KeyChallenge(object): + def __init__(self, challengeString): + matches = keyChallengeRegex.match(challengeString) + if not matches: + raise KeyException("invalid key challenge format") + self.source = matches.group(1) + self.id = matches.group(2) + self.challenge = matches.group(3) + + +class KeyResponse(object): + def __init__(self, source, id, time, signature): + self.source = source + self.id = id + self.time = time + self.signature = signature + + def __str__(self): + return "{source}-{id}-{time}-{signature}".format( + source=self.source, + id=self.id, + time=self.time, + signature=self.signature, + ) + + +class ReceiverId(object): + @staticmethod + def getResponseHeader(requestHeader): + matches = headerRegex.match(requestHeader) + if not matches: + raise KeyException("invalid authorization header") + challenges = [KeyChallenge(i) for i in matches.group(1).split(",")] + + def signChallenge(challenge): + key = ReceiverId.findKey(challenge) + if key is None: + return + return ReceiverId.signChallenge(challenge, key) + + responses = [signChallenge(c) for c in challenges] + return ",".join(str(r) for r in responses if r is not None) + + @staticmethod + def findKey(challenge): + def parseKey(keyString): + try: + return Key(keyString) + except KeyException as e: + logger.error(e) + + config = Config.get() + if "receiver_keys" not in config or config["receiver_keys"] is None: + return None + keys = [parseKey(keyString) for keyString in config["receiver_keys"]] + keys = [key for key in keys if key is not None] + matching_keys = [key for key in keys if key.source == challenge.source and key.id == challenge.id] + if matching_keys: + return matching_keys[0] + return None + + @staticmethod + def signChallenge(challenge, key): + now = datetime.utcnow().replace(microsecond=0, tzinfo=timezone.utc) + now_bytes = int(now.timestamp()).to_bytes(4, byteorder="big") + m = hmac.new(bytes.fromhex(key.secret), digestmod=hashlib.sha256) + m.update(bytes.fromhex(challenge.challenge)) + m.update(now_bytes) + return KeyResponse(challenge.source, challenge.id, now_bytes.hex(), m.hexdigest()) diff --git a/owrx/reporting/__init__.py b/owrx/reporting/__init__.py new file mode 100644 index 000000000..36a655edd --- /dev/null +++ b/owrx/reporting/__init__.py @@ -0,0 +1,74 @@ +import threading +from owrx.config import Config +from owrx.reporting.reporter import Reporter, FilteredReporter +from owrx.reporting.pskreporter import PskReporter +from owrx.reporting.wsprnet import WsprnetReporter +from owrx.feature import FeatureDetector +import logging + +logger = logging.getLogger(__name__) + + +class ReportingEngine(object): + creationLock = threading.Lock() + sharedInstance = None + + # concrete classes if they can be imported without the risk of optional dependencies + # tuples if the import needs to be detected by a feature check + reporterClasses = { + "pskreporter": PskReporter, + "wsprnet": WsprnetReporter, + "mqtt": ("owrx.reporting.mqtt", "MqttReporter") + } + + @staticmethod + def getSharedInstance(): + with ReportingEngine.creationLock: + if ReportingEngine.sharedInstance is None: + ReportingEngine.sharedInstance = ReportingEngine() + return ReportingEngine.sharedInstance + + @staticmethod + def stopAll(): + with ReportingEngine.creationLock: + if ReportingEngine.sharedInstance is not None: + ReportingEngine.sharedInstance.stop() + + def __init__(self): + self.reporters = [] + configKeys = ["{}_enabled".format(n) for n in self.reporterClasses.keys()] + self.configSub = Config.get().filter(*configKeys).wire(self.setupReporters) + self.setupReporters() + + def setupReporters(self, *args): + config = Config.get() + for typeStr, reporterClass in self.reporterClasses.items(): + configKey = "{}_enabled".format(typeStr) + if isinstance(reporterClass, tuple): + # feature check + if FeatureDetector().is_available(typeStr): + package, className = reporterClass + module = __import__(package, fromlist=[className]) + reporterClass = getattr(module, className) + else: + continue + if configKey in config and config[configKey]: + if not any(isinstance(r, reporterClass) for r in self.reporters): + self.reporters += [reporterClass()] + else: + for reporter in [r for r in self.reporters if isinstance(r, reporterClass)]: + reporter.stop() + self.reporters.remove(reporter) + + def stop(self): + for r in self.reporters: + r.stop() + self.configSub.cancel() + + def spot(self, spot): + for r in self.reporters: + if not isinstance(r, FilteredReporter) or spot["mode"] in r.getSupportedModes(): + try: + r.spot(spot) + except Exception: + logger.exception("error sending spot to reporter") diff --git a/owrx/reporting/mqtt.py b/owrx/reporting/mqtt.py new file mode 100644 index 000000000..16ca75b0b --- /dev/null +++ b/owrx/reporting/mqtt.py @@ -0,0 +1,71 @@ +from paho.mqtt.client import Client +from owrx.reporting.reporter import Reporter +from owrx.config import Config +from owrx.property import PropertyDeleted +import json +import threading +import time + +import logging + +logger = logging.getLogger(__name__) + + +class MqttReporter(Reporter): + DEFAULT_TOPIC = "openwebrx/decodes" + + def __init__(self): + pm = Config.get() + self.topic = self.DEFAULT_TOPIC + self.client = self._getClient() + self.subscriptions = [ + pm.wireProperty("mqtt_topic", self._setTopic), + pm.filter("mqtt_host", "mqtt_user", "mqtt_password", "mqtt_client_id", "mqtt_use_ssl").wire(self._reconnect) + ] + + def _getClient(self): + pm = Config.get() + clientId = pm["mqtt_client_id"] if "mqtt_client_id" in pm else "" + client = Client(clientId) + + if "mqtt_user" in pm and "mqtt_password" in pm: + client.username_pw_set(pm["mqtt_user"], pm["mqtt_password"]) + + port = 1883 + if pm["mqtt_use_ssl"]: + client.tls_set() + port = 8883 + + parts = pm["mqtt_host"].split(":") + host = parts[0] + if len(parts) > 1: + port = int(parts[1]) + + try: + client.connect(host=host, port=port) + except: + logger.exception("Exception connecting to MQTT server") + + threading.Thread(target=client.loop_forever).start() + + return client + + def _setTopic(self, topic): + if topic is PropertyDeleted: + self.topic = self.DEFAULT_TOPIC + else: + self.topic = topic + + def _reconnect(self, *args, **kwargs): + logger.debug("Reconnecting...") + old = self.client + self.client = self._getClient() + old.disconnect() + + def stop(self): + self.client.disconnect() + while self.subscriptions: + self.subscriptions.pop().cancel() + + def spot(self, spot): + self.client.publish(self.topic, payload=json.dumps(spot)) diff --git a/owrx/reporting/pskreporter.py b/owrx/reporting/pskreporter.py new file mode 100644 index 000000000..6ba8f74bb --- /dev/null +++ b/owrx/reporting/pskreporter.py @@ -0,0 +1,240 @@ +import logging +import threading +import time +import random +import socket +from functools import reduce +from operator import and_ +from owrx.config import Config +from owrx.version import openwebrx_version +from owrx.locator import Locator +from owrx.metrics import Metrics, CounterMetric +from owrx.reporting.reporter import FilteredReporter + +logger = logging.getLogger(__name__) + + +class PskReporter(FilteredReporter): + """ + This class implements the reporting interface to send received signals to pskreporter.info. + + It interfaces with pskreporter as documented here: https://pskreporter.info/pskdev.html + """ + interval = 300 + + def getSupportedModes(self): + """ + Supports all valid MODE and SUBMODE values from the ADIF standard. + + Current version at the time of the last change: + https://www.adif.org/314/ADIF_314.htm#Mode_Enumeration + """ + return ["FT8", "FT4", "JT9", "JT65", "FST4", "JS8", "Q65", "WSPR", "FST4W", "MSK144"] + + def stop(self): + self.cancelTimer() + with self.spotLock: + self.spots = [] + + def __init__(self): + self.spots = [] + self.spotLock = threading.Lock() + self.uploader = Uploader() + self.timer = None + metrics = Metrics.getSharedInstance() + self.dupeCounter = CounterMetric() + metrics.addMetric("pskreporter.duplicates", self.dupeCounter) + self.spotCounter = CounterMetric() + metrics.addMetric("pskreporter.spots", self.spotCounter) + + def scheduleNextUpload(self): + if self.timer: + return + delay = PskReporter.interval + random.uniform(0, 30) + logger.debug("scheduling next pskreporter upload in %f seconds", delay) + self.timer = threading.Timer(delay, self.upload) + self.timer.start() + + def spotEquals(self, s1, s2): + keys = ["source", "timestamp", "locator", "mode", "msg"] + + return reduce(and_, map(lambda key: s1[key] == s2[key], keys)) + + def spot(self, spot): + with self.spotLock: + if any(x for x in self.spots if self.spotEquals(spot, x)): + # dupe + self.dupeCounter.inc() + else: + self.spotCounter.inc() + self.spots.append(spot) + self.scheduleNextUpload() + + def upload(self): + try: + with self.spotLock: + self.timer = None + spots = self.spots + self.spots = [] + + if spots: + self.uploader.upload(spots) + except Exception: + logger.exception("Failed to upload spots") + + def cancelTimer(self): + if self.timer: + self.timer.cancel() + + +class Uploader(object): + receieverDelimiter = [0x99, 0x92] + senderDelimiter = [0x99, 0x93] + + def __init__(self): + self.sequence = 0 + self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + def upload(self, spots): + logger.debug("uploading %i spots", len(spots)) + for packet in self.getPackets(spots): + self.socket.sendto(packet, ("report.pskreporter.info", 4739)) + + def getPackets(self, spots): + encoded = [self.encodeSpot(spot) for spot in spots] + # filter out any erroneous encodes + encoded = [e for e in encoded if e is not None] + + def chunks(block, max_size): + size = 0 + current = [] + for r in block: + if size + len(r) > max_size: + yield current + current = [] + size = 0 + size += len(r) + current.append(r) + yield current + + rHeader = self.getReceiverInformationHeader() + rInfo = self.getReceiverInformation() + sHeader = self.getSenderInformationHeader() + + packets = [] + # 1200 bytes of sender data should keep the packet size below MTU for most cases + for chunk in chunks(encoded, 1200): + sInfo = self.getSenderInformation(chunk) + length = 16 + len(rHeader) + len(sHeader) + len(rInfo) + len(sInfo) + header = self.getHeader(length) + packets.append(header + rHeader + sHeader + rInfo + sInfo) + self.sequence = (self.sequence + len(chunk)) % (1 << 32) + + return packets + + def getHeader(self, length): + return bytes( + # protocol version + [0x00, 0x0A] + + list(length.to_bytes(2, "big")) + + list(int(time.time()).to_bytes(4, "big")) + + list(self.sequence.to_bytes(4, "big")) + + list((id(self) & 0xFFFFFFFF).to_bytes(4, "big")) + ) + + def encodeString(self, s): + return [len(s)] + list(s.encode("utf-8")) + + def encodeSpot(self, spot): + try: + return bytes( + self.encodeString(spot["source"]["callsign"]) + + list(int(spot["freq"]).to_bytes(5, "big")) + + list(int(spot["db"]).to_bytes(1, "big", signed=True)) + + self.encodeString(spot["mode"]) + + self.encodeString(spot["locator"]) + # informationsource. 1 means "automatically extracted + + [0x01] + + list(int(spot["timestamp"] / 1000).to_bytes(4, "big")) + ) + except Exception: + logger.exception("Error while encoding spot for pskreporter") + return None + + def getReceiverInformationHeader(self): + pm = Config.get() + with_antenna = "pskreporter_antenna_information" in pm and pm["pskreporter_antenna_information"] is not None + num_fields = 4 if with_antenna else 3 + length = 12 + num_fields * 8 + return bytes( + # id + [0x00, 0x03] + # length + + list(length.to_bytes(2, "big")) + + Uploader.receieverDelimiter + # number of fields + + list(num_fields.to_bytes(2, "big")) + # scoped field count + + [0x00, 0x01] + # receiverCallsign + + [0x80, 0x02, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # receiverLocator + + [0x80, 0x04, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # decodingSoftware + + [0x80, 0x08, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # antennaInformation + + ([0x80, 0x09, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] if with_antenna else []) + # padding + + [0x00, 0x00] + ) + + def getReceiverInformation(self): + pm = Config.get() + bodyFields = [ + # callsign + pm["pskreporter_callsign"], + # locator + Locator.fromCoordinates(pm["receiver_gps"]), + # decodingSoftware + "OpenWebRX " + openwebrx_version, + ] + if "pskreporter_antenna_information" in pm and pm["pskreporter_antenna_information"] is not None: + bodyFields += [pm["pskreporter_antenna_information"]] + body = [b for s in bodyFields for b in self.encodeString(s)] + body = self.pad(body, 4) + body = bytes(Uploader.receieverDelimiter + list((len(body) + 4).to_bytes(2, "big")) + body) + return body + + def getSenderInformationHeader(self): + return bytes( + # id, length + [0x00, 0x02, 0x00, 0x3C] + + Uploader.senderDelimiter + # number of fields + + [0x00, 0x07] + # senderCallsign + + [0x80, 0x01, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # frequency + + [0x80, 0x05, 0x00, 0x05, 0x00, 0x00, 0x76, 0x8F] + # sNR + + [0x80, 0x06, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F] + # mode + + [0x80, 0x0A, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # senderLocator + + [0x80, 0x03, 0xFF, 0xFF, 0x00, 0x00, 0x76, 0x8F] + # informationSource + + [0x80, 0x0B, 0x00, 0x01, 0x00, 0x00, 0x76, 0x8F] + # flowStartSeconds + + [0x00, 0x96, 0x00, 0x04] + ) + + def getSenderInformation(self, chunk): + sInfo = self.padBytes(b"".join(chunk), 4) + sInfoLength = len(sInfo) + 4 + return bytes(Uploader.senderDelimiter) + sInfoLength.to_bytes(2, "big") + sInfo + + def pad(self, b, l): + return b + [0x00 for _ in range(0, -1 * len(b) % l)] + + def padBytes(self, b, l): + return b + bytes([0x00 for _ in range(0, -1 * len(b) % l)]) diff --git a/owrx/reporting/reporter.py b/owrx/reporting/reporter.py new file mode 100644 index 000000000..7c5c19771 --- /dev/null +++ b/owrx/reporting/reporter.py @@ -0,0 +1,17 @@ +from abc import ABC, abstractmethod + + +class Reporter(ABC): + @abstractmethod + def stop(self): + pass + + @abstractmethod + def spot(self, spot): + pass + + +class FilteredReporter(Reporter): + @abstractmethod + def getSupportedModes(self): + return [] diff --git a/owrx/reporting/wsprnet.py b/owrx/reporting/wsprnet.py new file mode 100644 index 000000000..805a029d4 --- /dev/null +++ b/owrx/reporting/wsprnet.py @@ -0,0 +1,97 @@ +from owrx.reporting.reporter import FilteredReporter +from owrx.version import openwebrx_version +from owrx.config import Config +from owrx.locator import Locator +from owrx.metrics import Metrics, CounterMetric +from queue import Queue, Full +from urllib import request, parse +import threading +import logging +from datetime import datetime, timezone + +logger = logging.getLogger(__name__) + + +PoisonPill = object() + + +class Worker(threading.Thread): + def __init__(self, queue: Queue): + self.queue = queue + self.doRun = True + + super().__init__(daemon=True) + + def run(self): + while self.doRun: + try: + spot = self.queue.get() + if spot is PoisonPill: + self.doRun = False + else: + self.uploadSpot(spot) + self.queue.task_done() + except Exception: + logger.exception("Exception while uploading WSPRNet spot") + + def _getMode(self, spot): + interval = round(spot["interval"] / 60) + # FST4W modes are mapped not to conflict with WSPR modes 2 and 15: + if spot["mode"] != "WSPR" and interval in [2, 15]: + return interval + 1 + return interval + + def uploadSpot(self, spot): + config = Config.get() + # function=wspr&date=210114&time=1732&sig=-15&dt=0.5&drift=0&tqrg=7.040019&tcall=DF2UU&tgrid=JN48&dbm=37&version=2.3.0-rc3&rcall=DD5JFK&rgrid=JN58SC&rqrg=7.040047&mode=2 + # {'timestamp': 1610655960000, 'db': -23.0, 'dt': 0.3, 'freq': 7040048, 'drift': -1, 'msg': 'LA3JJ JO59 37', 'callsign': 'LA3JJ', 'locator': 'JO59', 'mode': 'WSPR'} + date = datetime.fromtimestamp(spot["timestamp"] / 1000, tz=timezone.utc) + data = parse.urlencode( + { + "function": "wspr", + "date": date.strftime("%y%m%d"), + "time": date.strftime("%H%M"), + "sig": spot["db"], + "dt": spot["dt"], + # FST4W does not have drift + "drift": spot["drift"] if "drift" in spot else 0, + "tqrg": spot["freq"] / 1e6, + "tcall": spot["source"]["callsign"], + "tgrid": spot["locator"], + "dbm": spot["dbm"], + "version": openwebrx_version, + "rcall": config["wsprnet_callsign"], + "rgrid": Locator.fromCoordinates(config["receiver_gps"]), + "mode": self._getMode(spot), + } + ).encode() + request.urlopen("http://wsprnet.org/post/", data, timeout=60) + + +class WsprnetReporter(FilteredReporter): + def __init__(self): + # max 100 entries + self.queue = Queue(100) + # single worker + Worker(self.queue).start() + + # metrics + metrics = Metrics.getSharedInstance() + self.spotCounter = CounterMetric() + metrics.addMetric("wsprnet.spots", self.spotCounter) + + def stop(self): + while not self.queue.empty(): + self.queue.get(timeout=1) + self.queue.task_done() + self.queue.put(PoisonPill) + + def spot(self, spot): + try: + self.queue.put(spot, block=False) + self.spotCounter.inc() + except Full: + logger.warning("WSPRNet Queue overflow, one spot lost") + + def getSupportedModes(self): + return ["WSPR", "FST4W"] diff --git a/owrx/sdr.py b/owrx/sdr.py new file mode 100644 index 000000000..da51e9b81 --- /dev/null +++ b/owrx/sdr.py @@ -0,0 +1,266 @@ +from owrx.config import Config +from owrx.property import PropertyManager, PropertyDeleted, PropertyDelegator, PropertyLayer, PropertyReadOnly +from owrx.feature import FeatureDetector, UnknownFeatureException +from owrx.source import SdrSource, SdrSourceEventClient +from functools import partial + +import logging + +logger = logging.getLogger(__name__) + + +class MappedSdrSources(PropertyDelegator): + def __init__(self, pm: PropertyManager): + self.subscriptions = {} + super().__init__(PropertyLayer()) + for key, value in pm.items(): + self._addSource(key, value) + pm.wire(self.handleSdrDeviceChange) + + def handleSdrDeviceChange(self, changes): + for key, value in changes.items(): + if value is PropertyDeleted: + if key in self: + del self[key] + else: + if key not in self: + self._addSource(key, value) + + def handleDeviceUpdate(self, key, value, *args): + if key not in self and self.isDeviceValid(value): + self[key] = self.buildNewSource(key, value) + elif key in self and not self.isDeviceValid(value): + del self[key] + + def _addSource(self, key, value): + self.handleDeviceUpdate(key, value) + updateMethod = partial(self.handleDeviceUpdate, key, value) + self.subscriptions[key] = [ + value.filter("type", "profiles").wire(updateMethod), + value["profiles"].wire(updateMethod) + ] + + def isDeviceValid(self, device): + return self._sdrTypeAvailable(device) and self._hasProfiles(device) + + def _hasProfiles(self, device): + return "profiles" in device and device["profiles"] and len(device["profiles"]) > 0 + + def _sdrTypeAvailable(self, value): + featureDetector = FeatureDetector() + try: + if not featureDetector.is_available(value["type"]): + logger.error( + 'The SDR source type "{0}" is not available. please check the feature report for details.'.format( + value["type"] + ) + ) + return False + return True + except UnknownFeatureException: + logger.error( + 'The SDR source type "{0}" is invalid. Please check your configuration'.format(value["type"]) + ) + return False + + def buildNewSource(self, id, props): + sdrType = props["type"] + className = "".join(x for x in sdrType.title() if x.isalnum()) + "Source" + module = __import__("owrx.source.{0}".format(sdrType), fromlist=[className]) + cls = getattr(module, className) + return cls(id, props) + + def _removeSource(self, key, source): + source.shutdown() + for sub in self.subscriptions[key]: + sub.cancel() + del self.subscriptions[key] + + def __setitem__(self, key, value): + source = self[key] if key in self else None + if source is value: + return + super().__setitem__(key, value) + if source is not None: + self._removeSource(key, source) + + def __delitem__(self, key): + source = self[key] if key in self else None + super().__delitem__(key) + if source is not None: + self._removeSource(key, source) + + +class SourceStateHandler(SdrSourceEventClient): + def __init__(self, pm, key, source: SdrSource): + self.pm = pm + self.key = key + self.source = source + + def selfDestruct(self): + self.source.removeClient(self) + + def onFail(self): + del self.pm[self.key] + + def onDisable(self): + del self.pm[self.key] + + def onEnable(self): + self.pm[self.key] = self.source + + def onShutdown(self): + del self.pm[self.key] + + +class ActiveSdrSources(PropertyReadOnly): + def __init__(self, pm: PropertyManager): + self.handlers = {} + self._layer = PropertyLayer() + super().__init__(self._layer) + for key, value in pm.items(): + self._addSource(key, value) + pm.wire(self.handleSdrDeviceChange) + + def handleSdrDeviceChange(self, changes): + for key, value in changes.items(): + if value is PropertyDeleted: + self._removeSource(key) + else: + self._addSource(key, value) + + def isAvailable(self, source: SdrSource): + return source.isEnabled() and not source.isFailed() + + def _addSource(self, key, source: SdrSource): + if self.isAvailable(source): + self._layer[key] = source + self.handlers[key] = SourceStateHandler(self._layer, key, source) + source.addClient(self.handlers[key]) + + def _removeSource(self, key): + self.handlers[key].selfDestruct() + del self.handlers[key] + if key in self._layer: + del self._layer[key] + + +class AvailableProfiles(PropertyReadOnly): + def __init__(self, pm: PropertyManager): + self.subscriptions = {} + self.profileSubscriptions = {} + self._layer = PropertyLayer() + super().__init__(self._layer) + for key, value in pm.items(): + self._addSource(key, value) + pm.wire(self.handleSdrDeviceChange) + + def handleSdrDeviceChange(self, changes): + for key, value in changes.items(): + if value is PropertyDeleted: + self._removeSource(key) + else: + self._addSource(key, value) + + def handleSdrNameChange(self, s_id, source, name): + profiles = source.getProfiles() + for p_id in list(self._layer.keys()): + source_id, profile_id = p_id.split("|") + if source_id == s_id: + profile = profiles[profile_id] + self._layer[p_id] = "{} {}".format(name, profile["name"]) + + def handleProfileChange(self, source_id, source: SdrSource, changes): + for key, value in changes.items(): + if value is PropertyDeleted: + self._removeProfile(source_id, key) + else: + self._addProfile(source_id, source, key, value) + + def handleProfileNameChange(self, s_id, source: SdrSource, p_id, name): + for concat_p_id in list(self._layer.keys()): + source_id, profile_id = concat_p_id.split("|") + if source_id == s_id and profile_id == p_id: + self._layer[concat_p_id] = "{} {}".format(source.getName(), name) + + def _addSource(self, key, source: SdrSource): + for p_id, p in source.getProfiles().items(): + self._addProfile(key, source, p_id, p) + self.subscriptions[key] = [ + source.getProps().wireProperty("name", partial(self.handleSdrNameChange, key, source)), + source.getProfiles().wire(partial(self.handleProfileChange, key, source)), + ] + + def _addProfile(self, s_id, source: SdrSource, p_id, profile): + self._layer["{}|{}".format(s_id, p_id)] = "{} {}".format(source.getName(), profile["name"]) + if s_id not in self.profileSubscriptions: + self.profileSubscriptions[s_id] = {} + self.profileSubscriptions[s_id][p_id] = profile.wireProperty("name", partial(self.handleProfileNameChange, s_id, source, p_id)) + + def _removeSource(self, key): + for profile_id in list(self._layer.keys()): + if profile_id.startswith("{}|".format(key)): + del self._layer[profile_id] + if key in self.subscriptions: + while self.subscriptions[key]: + self.subscriptions[key].pop().cancel() + del self.subscriptions[key] + if key in self.profileSubscriptions: + for p_id in self.profileSubscriptions[key].keys(): + self.profileSubscriptions[key][p_id].cancel() + del self.profileSubscriptions[key] + + def _removeProfile(self, s_id, p_id): + for concat_p_id in list(self._layer.keys()): + source_id, profile_id = concat_p_id.split("|") + if source_id == s_id and profile_id == p_id: + del self._layer[concat_p_id] + if s_id in self.profileSubscriptions and p_id in self.profileSubscriptions[s_id]: + self.profileSubscriptions[s_id][p_id].cancel() + del self.profileSubscriptions[s_id][p_id] + + +class SdrService(object): + sources = None + activeSources = None + availableProfiles = None + + @staticmethod + def getFirstSource(): + sources = SdrService.getActiveSources() + if not sources: + return None + # TODO: configure default sdr in config? right now it will pick the first one off the list. + return sources[list(sources.keys())[0]] + + @staticmethod + def getSource(id): + sources = SdrService.getActiveSources() + if not sources: + return None + if id not in sources: + return None + return sources[id] + + @staticmethod + def getAllSources(): + if SdrService.sources is None: + SdrService.sources = MappedSdrSources(Config.get()["sdrs"]) + return SdrService.sources + + @staticmethod + def getActiveSources(): + if SdrService.activeSources is None: + SdrService.activeSources = ActiveSdrSources(SdrService.getAllSources()) + return SdrService.activeSources + + @staticmethod + def getAvailableProfiles(): + if SdrService.availableProfiles is None: + SdrService.availableProfiles = AvailableProfiles(SdrService.getActiveSources()) + return SdrService.availableProfiles + + @staticmethod + def stopAllSources(): + for source in SdrService.getAllSources().values(): + source.stop() diff --git a/owrx/service/__init__.py b/owrx/service/__init__.py new file mode 100644 index 000000000..d78b03e3d --- /dev/null +++ b/owrx/service/__init__.py @@ -0,0 +1,379 @@ +import threading +from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass +from owrx.sdr import SdrService +from owrx.bands import Bandplan +from owrx.config import Config +from owrx.source.resampler import Resampler +from owrx.property import PropertyLayer, PropertyDeleted +from owrx.service.schedule import ServiceScheduler +from owrx.service.chain import ServiceDemodulatorChain +from owrx.modes import Modes, DigitalMode +from typing import Union, Optional +from csdr.chain.demodulator import BaseDemodulatorChain, ServiceDemodulator, DialFrequencyReceiver +from pycsdr.modules import Buffer + +import logging + +logger = logging.getLogger(__name__) + + +class ServiceHandler(SdrSourceEventClient): + def __init__(self, source): + self.lock = threading.RLock() + self.services = [] + self.source = source + self.startupTimer = None + self.activitySub = None + self.running = False + props = self.source.getProps() + self.enabledSub = props.wireProperty("services", self._receiveEvent) + self.decodersSub = None + # need to call _start() manually if property is not set since the default is True, but the initial call is only + # made if the property is present + if "services" not in props: + self._start() + + def _receiveEvent(self, state): + # deletion means fall back to default, which is True + if state is PropertyDeleted: + state = True + if self.running == state: + return + if state: + self._start() + else: + self._stop() + + def _start(self): + self.running = True + self.source.addClient(self) + props = self.source.getProps() + self.activitySub = props.filter("center_freq", "samp_rate").wire(self.onFrequencyChange) + self.decodersSub = Config.get().wireProperty("services_decoders", self.onFrequencyChange) + if self.source.isAvailable(): + self._scheduleServiceStartup() + + def _stop(self): + if self.activitySub is not None: + self.activitySub.cancel() + self.activitySub = None + if self.decodersSub is not None: + self.decodersSub.cancel() + self.decodersSub = None + self._cancelStartupTimer() + self.source.removeClient(self) + self.stopServices() + self.running = False + + def getClientClass(self) -> SdrClientClass: + return SdrClientClass.INACTIVE + + def onStateChange(self, state: SdrSourceState): + if state is SdrSourceState.RUNNING: + self._scheduleServiceStartup() + elif state is SdrSourceState.STOPPING: + logger.debug("sdr source becoming unavailable; stopping services.") + self.stopServices() + + def onFail(self): + logger.debug("sdr source failed; stopping services.") + self.stopServices() + + def onShutdown(self): + logger.debug("sdr source is shutting down; shutting down service handler, too.") + self.shutdown() + + def onEnable(self): + self._scheduleServiceStartup() + + def isSupported(self, mode): + configured = Config.get()["services_decoders"] + available = [m.modulation for m in Modes.getAvailableServices()] + return mode in configured and mode in available + + def shutdown(self): + self._stop() + if self.enabledSub is not None: + self.enabledSub.cancel() + self.enabledSub = None + + def stopServices(self): + with self.lock: + services = self.services + self.services = [] + + for service in services: + service.stop() + + def onFrequencyChange(self, changes): + self.stopServices() + if not self.source.isAvailable(): + return + self._scheduleServiceStartup() + + def _cancelStartupTimer(self): + if self.startupTimer: + self.startupTimer.cancel() + self.startupTimer = None + + def _scheduleServiceStartup(self): + self._cancelStartupTimer() + self.startupTimer = threading.Timer(10, self.updateServices) + self.startupTimer.start() + + def updateServices(self): + def addService(dial, source): + try: + service = self.setupService(dial, source) + self.services.append(service) + except Exception: + logger.exception("Error setting up service {mode} on frequency {frequency}".format(**dial)) + + with self.lock: + logger.debug("re-scheduling services due to sdr changes") + self.stopServices() + if not self.source.isAvailable(): + logger.debug("sdr source is unavailable") + return + cf = self.source.getProps()["center_freq"] + sr = self.source.getProps()["samp_rate"] + srh = sr / 2 + frequency_range = (cf - srh, cf + srh) + + dials = [ + dial + for dial in Bandplan.getSharedInstance().collectDialFrequencies(frequency_range) + if self.isSupported(dial["mode"]) + ] + + if not dials: + logger.debug("no services available") + return + + groups = self.optimizeResampling(dials, sr) + if groups is None: + for dial in dials: + addService(dial, self.source) + else: + for group in groups: + if len(group) > 1: + cf = self.get_center_frequency(group) + bw = self.get_bandwidth(group) + logger.debug("setting up resampler on center frequency: {0}, bandwidth: {1}".format(cf, bw)) + resampler_props = PropertyLayer(center_freq=cf, samp_rate=bw) + resampler = Resampler(resampler_props, self.source) + + for dial in group: + addService(dial, resampler) + + # resampler goes in after the services since it must not be shutdown as long as the services are + # still running + self.services.append(resampler) + else: + dial = group[0] + addService(dial, self.source) + + def get_min_max(self, group): + def find_bandpass(dial): + mode = Modes.findByModulation(dial["mode"]) + if "underlying" in dial: + mode = mode.for_underlying(dial["underlying"]) + return mode.get_bandpass() + + frequencies = sorted(group, key=lambda f: f["frequency"]) + lowest = frequencies[0] + min = lowest["frequency"] + find_bandpass(lowest).low_cut + highest = frequencies[-1] + max = highest["frequency"] + find_bandpass(highest).high_cut + return min, max + + def get_center_frequency(self, group): + min, max = self.get_min_max(group) + return (min + max) / 2 + + def get_bandwidth(self, group): + minFreq, maxFreq = self.get_min_max(group) + # minimum bandwidth for a resampler: 25kHz + return max((maxFreq - minFreq) * 1.15, 25000) + + def optimizeResampling(self, freqs, bandwidth): + freqs = sorted(freqs, key=lambda f: f["frequency"]) + distances = [ + { + "frequency": freqs[i]["frequency"], + "distance": freqs[i + 1]["frequency"] - freqs[i]["frequency"], + } + for i in range(0, len(freqs) - 1) + ] + + distances = [d for d in distances if d["distance"] > 0] + + distances = sorted(distances, key=lambda f: f["distance"], reverse=True) + + def calculate_usage(num_splits): + splits = sorted([f["frequency"] for f in distances[0:num_splits]]) + previous = 0 + groups = [] + for split in splits: + groups.append([f for f in freqs if previous < f["frequency"] <= split]) + previous = split + groups.append([f for f in freqs if previous < f["frequency"]]) + + def get_total_bandwidth(group): + if len(group) > 1: + return bandwidth + len(group) * self.get_bandwidth(group) + else: + return bandwidth + + total_bandwidth = sum([get_total_bandwidth(group) for group in groups]) + return { + "num_splits": num_splits, + "total_bandwidth": total_bandwidth, + "groups": groups, + } + + usages = [calculate_usage(i) for i in range(0, len(freqs))] + # another possible outcome might be that it's best not to resample at all. this is a special case. + usages += [ + { + "num_splits": None, + "total_bandwidth": bandwidth * len(freqs), + "groups": [freqs], + } + ] + results = sorted(usages, key=lambda f: f["total_bandwidth"]) + + for r in results: + logger.debug("splits: {0}, total: {1}".format(r["num_splits"], r["total_bandwidth"])) + + best = results[0] + if best["num_splits"] is None: + return None + return best["groups"] + + def setupService(self, dial, source): + logger.debug("setting up service {mode} on frequency {frequency}".format(**dial)) + + modeObject = Modes.findByModulation(dial["mode"]) + if not isinstance(modeObject, DigitalMode): + logger.warning("mode is not a digimode: %s", dial["mode"]) + return None + + if "underlying" in dial: + modeObject = modeObject.for_underlying(dial["underlying"]) + + demod = self._getDemodulator(modeObject.get_modulation()) + secondaryDemod = self._getSecondaryDemodulator(modeObject.modulation) + center_freq = source.getProps()["center_freq"] + sampleRate = source.getProps()["samp_rate"] + if isinstance(secondaryDemod, DialFrequencyReceiver): + secondaryDemod.setDialFrequency(dial["frequency"]) + + chain = ServiceDemodulatorChain(demod, secondaryDemod, sampleRate, dial["frequency"] - center_freq) + bandpass = modeObject.get_bandpass() + if bandpass: + chain.setBandPass(bandpass.low_cut, bandpass.high_cut) + else: + chain.setBandPass(None, None) + chain.setReader(source.getBuffer().getReader()) + + # dummy buffer, we don't use the output right now + buffer = Buffer(chain.getOutputFormat()) + chain.setWriter(buffer) + return chain + + # TODO move this elsewhere + def _getDemodulator(self, demod: Union[str, BaseDemodulatorChain]): + if isinstance(demod, BaseDemodulatorChain): + return demod + # TODO: move this to Modes + if demod == "nfm": + from csdr.chain.analog import NFm + return NFm(48000) + elif demod in ["usb", "lsb", "cw"]: + from csdr.chain.analog import Ssb + return Ssb() + + # TODO move this elsewhere + def _getSecondaryDemodulator(self, mod) -> Optional[ServiceDemodulator]: + if isinstance(mod, ServiceDemodulatorChain): + return mod + if mod in ["ft8", "wspr", "jt65", "jt9", "ft4", "fst4", "fst4w", "q65"]: + from csdr.chain.digimodes import AudioChopperDemodulator + from owrx.wsjt import WsjtParser + return AudioChopperDemodulator(mod, WsjtParser()) + elif mod == "msk144": + from csdr.chain.digimodes import Msk144Demodulator + return Msk144Demodulator() + elif mod == "js8": + from csdr.chain.digimodes import AudioChopperDemodulator + from owrx.js8 import Js8Parser + return AudioChopperDemodulator(mod, Js8Parser()) + elif mod == "packet": + from csdr.chain.digimodes import PacketDemodulator + return PacketDemodulator(service=True) + elif mod == "adsb": + from csdr.chain.dump1090 import Dump1090 + return Dump1090() + elif mod == "hfdl": + from csdr.chain.dumphfdl import DumpHFDL + return DumpHFDL() + elif mod == "vdl2": + from csdr.chain.dumpvdl2 import DumpVDL2 + return DumpVDL2() + elif mod == "pocsag": + from csdr.chain.digiham import PocsagDemodulator + return PocsagDemodulator() + elif mod == "ism": + from csdr.chain.rtl433 import Rtl433 + return Rtl433() + + raise ValueError("unsupported service modulation: {}".format(mod)) + + +class Services(object): + handlers = {} + schedulers = {} + + @staticmethod + def start(): + config = Config.get() + config.wireProperty("services_enabled", Services._receiveEnabledEvent) + activeSources = SdrService.getActiveSources() + activeSources.wire(Services._receiveDeviceEvent) + for key, source in activeSources.items(): + Services.schedulers[key] = ServiceScheduler(source) + + @staticmethod + def _receiveEnabledEvent(state): + if state: + for key, source in SdrService.getActiveSources().__dict__().items(): + Services.handlers[key] = ServiceHandler(source) + else: + for handler in list(Services.handlers.values()): + handler.shutdown() + Services.handlers = {} + + @staticmethod + def _receiveDeviceEvent(changes): + for key, source in changes.items(): + if source is PropertyDeleted: + if key in Services.handlers: + Services.handlers[key].shutdown() + del Services.handlers[key] + if key in Services.schedulers: + Services.schedulers[key].shutdown() + del Services.schedulers[key] + else: + Services.schedulers[key] = ServiceScheduler(source) + if Config.get()["services_enabled"]: + Services.handlers[key] = ServiceHandler(source) + + @staticmethod + def stop(): + for handler in list(Services.handlers.values()): + handler.shutdown() + Services.handlers = {} + for scheduler in list(Services.schedulers.values()): + scheduler.shutdown() + Services.schedulers = {} diff --git a/owrx/service/chain.py b/owrx/service/chain.py new file mode 100644 index 000000000..f71cf2bcc --- /dev/null +++ b/owrx/service/chain.py @@ -0,0 +1,23 @@ +from csdr.chain import Chain +from csdr.chain.selector import Selector +from csdr.chain.demodulator import BaseDemodulatorChain, ServiceDemodulator +from pycsdr.types import Format + + +class ServiceDemodulatorChain(Chain): + def __init__(self, demod: BaseDemodulatorChain, secondaryDemod: ServiceDemodulator, sampleRate: int, frequencyOffset: int): + self.selector = Selector(sampleRate, secondaryDemod.getFixedAudioRate(), withSquelch=False) + self.selector.setFrequencyOffset(frequencyOffset) + + workers = [self.selector] + + # primary demodulator is only necessary if the secondary does not accept IQ input + if secondaryDemod.getInputFormat() is not Format.COMPLEX_FLOAT: + workers += [demod] + + workers += [secondaryDemod] + + super().__init__(workers) + + def setBandPass(self, lowCut, highCut): + self.selector.setBandpass(lowCut, highCut) diff --git a/owrx/service/schedule.py b/owrx/service/schedule.py new file mode 100644 index 000000000..3e552f810 --- /dev/null +++ b/owrx/service/schedule.py @@ -0,0 +1,315 @@ +from datetime import datetime, timezone, timedelta +from owrx.source import SdrSourceEventClient, SdrSourceState, SdrClientClass, SdrBusyState +from owrx.config import Config +import threading +import math +from abc import ABC, ABCMeta, abstractmethod + +import logging + +logger = logging.getLogger(__name__) + + +class ScheduleEntry(ABC): + def __init__(self, startTime, endTime, profile): + self.startTime = startTime + self.endTime = endTime + self.profile = profile + + def getProfile(self): + return self.profile + + def __str__(self): + return "{0} - {1}: {2}".format(self.startTime, self.endTime, self.profile) + + @abstractmethod + def isCurrent(self, dt): + pass + + @abstractmethod + def getScheduledEnd(self): + pass + + @abstractmethod + def getNextActivation(self): + pass + + +class TimeScheduleEntry(ScheduleEntry): + def isCurrent(self, dt): + time = dt.time() + if self.startTime < self.endTime: + return self.startTime <= time < self.endTime + else: + return self.startTime <= time or time < self.endTime + + def getScheduledEnd(self): + now = datetime.utcnow() + end = now.combine(date=now.date(), time=self.endTime) + while end < now: + end += timedelta(days=1) + return end + + def getNextActivation(self): + now = datetime.utcnow() + start = now.combine(date=now.date(), time=self.startTime) + while start < now: + start += timedelta(days=1) + return start + + +class DatetimeScheduleEntry(ScheduleEntry): + def isCurrent(self, dt): + return self.startTime <= dt < self.endTime + + def getScheduledEnd(self): + return self.endTime + + def getNextActivation(self): + return self.startTime + + +class Schedule(ABC): + @staticmethod + def parse(props): + if "scheduler" in props: + sc = props["scheduler"] + t = sc["type"] if "type" in sc else "static" + if t == "static": + return StaticSchedule(sc["schedule"]) + elif t == "daylight": + return DaylightSchedule(sc["schedule"]) + else: + logger.warning("Invalid scheduler type: %s", t) + # downwards compatibility + elif "schedule" in props: + return StaticSchedule(props["schedule"]) + + @abstractmethod + def getCurrentEntry(self): + pass + + @abstractmethod + def getNextEntry(self): + pass + + +class TimerangeSchedule(Schedule, metaclass=ABCMeta): + @abstractmethod + def getEntries(self): + pass + + def getCurrentEntry(self): + current = [p for p in self.getEntries() if p.isCurrent(datetime.utcnow())] + if current: + return current[0] + return None + + def getNextEntry(self): + s = sorted(self.getEntries(), key=lambda e: e.getNextActivation()) + if s: + return s[0] + return None + + +class StaticSchedule(TimerangeSchedule): + def __init__(self, scheduleDict): + self.entries = [] + for time, profile in scheduleDict.items(): + if len(time) != 9: + logger.warning("invalid schedule spec: %s", time) + continue + + startTime = datetime.strptime(time[0:4], "%H%M").replace(tzinfo=timezone.utc).time() + endTime = datetime.strptime(time[5:9], "%H%M").replace(tzinfo=timezone.utc).time() + self.entries.append(TimeScheduleEntry(startTime, endTime, profile)) + + def getEntries(self): + return self.entries + + +class DaylightSchedule(TimerangeSchedule): + greyLineTime = timedelta(hours=1) + + def __init__(self, scheduleDict): + self.schedule = scheduleDict + + def getSunTimes(self, date): + pm = Config.get() + lat = pm["receiver_gps"]["lat"] + lng = pm["receiver_gps"]["lon"] + degtorad = math.pi / 180 + radtodeg = 180 / math.pi + + # Number of days since 01/01 + days = date.timetuple().tm_yday + + # Longitudinal correction + longCorr = 4 * lng + + # calibrate for solstice + b = 2 * math.pi * (days - 81) / 365 + + # Equation of Time Correction + eoTCorr = 9.87 * math.sin(2 * b) - 7.53 * math.cos(b) - 1.5 * math.sin(b) + + # Solar correction + solarCorr = longCorr + eoTCorr + + # Solar declination + declination = math.asin(math.sin(23.45 * degtorad) * math.sin(b)) + + sunrise = 12 - math.acos(-math.tan(lat * degtorad) * math.tan(declination)) * radtodeg / 15 - solarCorr / 60 + sunset = 12 + math.acos(-math.tan(lat * degtorad) * math.tan(declination)) * radtodeg / 15 - solarCorr / 60 + + midnight = datetime.combine(date, datetime.min.time()) + sunrise = midnight + timedelta(hours=sunrise) + sunset = midnight + timedelta(hours=sunset) + logger.debug("for {date} sunrise: {sunrise} sunset {sunset}".format(date=date, sunrise=sunrise, sunset=sunset)) + + return sunrise, sunset + + def getEntries(self): + now = datetime.utcnow() + date = now.date() + # greyline is optional, it its set it will shorten the other profiles + useGreyline = "greyline" in self.schedule + entries = [] + + delta = DaylightSchedule.greyLineTime if useGreyline else timedelta() + events = [] + # we need to start yesterday for longitudes close to the date line + offset = -1 + while len(events) < 1: + sunrise, sunset = self.getSunTimes(date + timedelta(days=offset)) + offset += 1 + events += [{"type": "sunrise", "time": sunrise}, {"type": "sunset", "time": sunset}] + # keep only events in the future + events = [v for v in events if v["time"] + delta > now] + events.sort(key=lambda e: e["time"]) + + previousEvent = None + for event in events: + # night profile _until_ sunrise, day profile _until_ sunset + stype = "night" if event["type"] == "sunrise" else "day" + if stype in self.schedule and (previousEvent is not None or event["time"] - delta > now): + start = now if previousEvent is None else previousEvent + entries.append(DatetimeScheduleEntry(start, event["time"] - delta, self.schedule[stype])) + if useGreyline: + entries.append( + DatetimeScheduleEntry(event["time"] - delta, event["time"] + delta, self.schedule["greyline"]) + ) + previousEvent = event["time"] + delta + + logger.debug([str(e) for e in entries]) + return entries + + +class ServiceScheduler(SdrSourceEventClient): + def __init__(self, source): + self.source = source + self.selectionTimer = None + self.currentEntry = None + self.source.addClient(self) + self.schedule = None + props = self.source.getProps() + self.subscriptions = [] + self.subscriptions.append(props.filter("center_freq", "samp_rate").wire(self.onFrequencyChange)) + self.subscriptions.append(props.wireProperty("scheduler", self.parseSchedule)) + # wireProperty calls parseSchedule with the initial value + # self.parseSchedule() + + def parseSchedule(self, *args): + props = self.source.getProps() + self.schedule = Schedule.parse(props) + self.scheduleSelection() + + def shutdown(self): + while self.subscriptions: + self.subscriptions.pop().cancel() + self.cancelTimer() + self.source.removeClient(self) + + def scheduleSelection(self, time=None): + if not self.source.isEnabled() or self.source.isFailed(): + return + seconds = 10 + if time is not None: + delta = time - datetime.utcnow() + seconds = delta.total_seconds() + self.cancelTimer() + self.selectionTimer = threading.Timer(seconds, self.selectProfile) + self.selectionTimer.start() + + def cancelTimer(self): + if self.selectionTimer: + self.selectionTimer.cancel() + + def getClientClass(self) -> SdrClientClass: + if self.currentEntry is None: + return SdrClientClass.INACTIVE + else: + return SdrClientClass.BACKGROUND + + def onStateChange(self, state: SdrSourceState): + if state is SdrSourceState.STOPPING: + self.scheduleSelection() + + def onFail(self): + self.shutdown() + + def onShutdown(self): + self.shutdown() + + def onDisable(self): + self.cancelTimer() + + def onEnable(self): + self.scheduleSelection() + + def onBusyStateChange(self, state: SdrBusyState): + if state is SdrBusyState.IDLE: + self.scheduleSelection() + + def onFrequencyChange(self, changes): + self.scheduleSelection() + + def _setCurrentEntry(self, entry): + self.currentEntry = entry + + if entry is not None: + logger.debug("selected profile %s until %s", entry.getProfile(), entry.getScheduledEnd()) + self.scheduleSelection(entry.getScheduledEnd()) + + try: + self.source.activateProfile(entry.getProfile()) + self.source.start() + except KeyError: + pass + + # tell the source to re-evaluate its current status + # this should make it shut down if there's no other activity + # TODO this is an improvised solution, should probably be integrated / improved in SdrSourceEventClient + self.source.checkStatus() + + def selectProfile(self): + if self.source.hasClients(SdrClientClass.USER): + logger.debug("source has active users; not touching") + return + + if self.schedule is None: + self._setCurrentEntry(None) + logger.debug("no active schedule, scheduler standing by for external events.") + return + + logger.debug("source seems to be idle, selecting profile for background services") + self._setCurrentEntry(self.schedule.getCurrentEntry()) + + if self.currentEntry is None: + logger.debug("schedule did not return a current profile. checking next (future) entry...") + nextEntry = self.schedule.getNextEntry() + if nextEntry is not None: + self.scheduleSelection(nextEntry.getNextActivation()) + else: + logger.debug("no next entry available, scheduler standing by for external events.") + return diff --git a/owrx/soapy.py b/owrx/soapy.py new file mode 100644 index 000000000..25b5f35e9 --- /dev/null +++ b/owrx/soapy.py @@ -0,0 +1,21 @@ +class SoapySettings(object): + @staticmethod + def parse(dstr): + def decodeComponent(c): + kv = c.split("=", 1) + if len(kv) < 2: + return c + else: + return {kv[0]: kv[1]} + + return [decodeComponent(c) for c in dstr.split(",")] + + @staticmethod + def encode(dobj): + def encodeComponent(c): + if isinstance(c, str): + return c + else: + return ",".join(["{0}={1}".format(key, value) for key, value in c.items()]) + + return ",".join([encodeComponent(c) for c in dobj]) diff --git a/owrx/socket.py b/owrx/socket.py new file mode 100644 index 000000000..069a5382e --- /dev/null +++ b/owrx/socket.py @@ -0,0 +1,10 @@ +import socket + + +def getAvailablePort(): + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(("", 0)) + s.listen(1) + port = s.getsockname()[1] + s.close() + return port diff --git a/owrx/source/__init__.py b/owrx/source/__init__.py new file mode 100644 index 000000000..1a5004713 --- /dev/null +++ b/owrx/source/__init__.py @@ -0,0 +1,692 @@ +from owrx.config import Config +import threading +import subprocess +import os +import socket +import shlex +import time +import signal +import pkgutil +from abc import ABC, abstractmethod +from owrx.command import CommandMapper +from owrx.socket import getAvailablePort +from owrx.property import PropertyStack, PropertyLayer, PropertyFilter, PropertyCarousel, PropertyDeleted +from owrx.property.filter import ByLambda +from owrx.form.input import Input, TextInput, NumberInput, CheckboxInput, ModesInput, ExponentialInput +from owrx.form.input.converter import OptionalConverter +from owrx.form.input.device import GainInput, SchedulerInput, WaterfallLevelsInput +from owrx.form.input.validator import RequiredValidator, Range, RangeListValidator +from owrx.form.input.converter import Converter +from owrx.form.section import OptionalSection +from owrx.feature import FeatureDetector +from owrx.log import LogPipe, HistoryHandler +from typing import List +from enum import Enum + +from pycsdr.modules import TcpSource, Buffer +from pycsdr.types import Format + +import logging + +logger = logging.getLogger(__name__) + + +class SdrSourceState(Enum): + STOPPED = "Stopped" + STARTING = "Starting" + RUNNING = "Running" + STOPPING = "Stopping" + TUNING = "Tuning" + + def __str__(self): + return self.value + + +class SdrBusyState(Enum): + IDLE = 1 + BUSY = 2 + + +class SdrClientClass(Enum): + INACTIVE = 1 + BACKGROUND = 2 + USER = 3 + + +class SdrSourceEventClient(object): + def onStateChange(self, state: SdrSourceState): + pass + + def onBusyStateChange(self, state: SdrBusyState): + pass + + def onFail(self): + pass + + def onShutdown(self): + pass + + def onDisable(self): + pass + + def onEnable(self): + pass + + def getClientClass(self) -> SdrClientClass: + return SdrClientClass.INACTIVE + + +class SdrProfileCarousel(PropertyCarousel): + def __init__(self, props): + super().__init__() + if "profiles" not in props: + return + + for profile_id, profile in props["profiles"].items(): + self.addLayer(profile_id, profile) + # activate first available profile + self.switch() + + props["profiles"].wire(self.handleProfileUpdate) + + def addLayer(self, profile_id, profile): + profile_stack = PropertyStack() + profile_stack.addLayer(0, PropertyLayer(profile_id=profile_id).readonly()) + profile_stack.addLayer(1, profile) + super().addLayer(profile_id, profile_stack) + + def handleProfileUpdate(self, changes): + for profile_id, profile in changes.items(): + if profile is PropertyDeleted: + self.removeLayer(profile_id) + else: + self.addLayer(profile_id, profile) + + def _getDefaultLayer(self): + # return the first available profile, or the default empty layer if we don't have any + if self.layers: + return next(iter(self.layers.values())) + return super()._getDefaultLayer() + + +class SdrSource(ABC): + def __init__(self, id, props): + self.id = id + + self.commandMapper = None + self.tcpSource = None + self.buffer = None + self.logger = logger.getChild(id) if id is not None else logger + self.logger.addHandler(HistoryHandler.getHandler(self.logger.name)) + self.stdoutPipe = None + self.stderrPipe = None + + self.props = PropertyStack() + + # layer 0 reserved for profile properties + self.profileCarousel = SdrProfileCarousel(props) + # prevent profile names from overriding the device name + self.props.addLayer(0, PropertyFilter(self.profileCarousel, ByLambda(lambda x: x != "name"))) + + # props from our device config + self.props.addLayer(1, props) + + # the sdr_id is constant, so we put it in a separate layer + # this is used to detect device changes, that are then sent to the client + self.props.addLayer(2, PropertyLayer(sdr_id=id).readonly()) + + # finally, accept global config properties from the top-level config + self.props.addLayer(3, Config.get()) + + self.sdrProps = self.props.filter(*self.getEventNames()) + + self.wireEvents() + + self.port = getAvailablePort() + self.monitor = None + self.clients = [] + self.spectrumClients = [] + self.spectrumThread = None + self.spectrumLock = threading.Lock() + self.process = None + self.modificationLock = threading.Lock() + self.state = SdrSourceState.STOPPED + self.enabled = "enabled" not in props or props["enabled"] + props.filter("enabled").wire(self._handleEnableChanged) + self.failed = False + self.busyState = SdrBusyState.IDLE + + self.validateProfiles() + + if self.isAlwaysOn() and self.isEnabled(): + self.start() + + props.filter("always-on").wire(self._handleAlwaysOnChanged) + + def isEnabled(self): + return self.enabled + + def _handleEnableChanged(self, changes): + if "enabled" in changes and changes["enabled"] is not PropertyDeleted: + self.enabled = changes["enabled"] + else: + self.enabled = True + if not self.enabled: + self.stop() + for c in self.clients.copy(): + if self.isEnabled(): + c.onEnable() + else: + c.onDisable() + + def _handleAlwaysOnChanged(self, changes): + if self.isAlwaysOn(): + self.start() + else: + self.checkStatus() + + def isFailed(self): + return self.failed + + def fail(self): + self.failed = True + for c in self.clients.copy(): + c.onFail() + + def validateProfiles(self): + props = PropertyStack() + props.addLayer(1, self.props) + for id, p in self.props["profiles"].items(): + props.replaceLayer(0, p) + if "center_freq" not in props: + self.logger.warning('Profile "%s" does not specify a center_freq', id) + continue + if "samp_rate" not in props: + self.logger.warning('Profile "%s" does not specify a samp_rate', id) + continue + if "start_freq" in props: + start_freq = props["start_freq"] + srh = props["samp_rate"] / 2 + center_freq = props["center_freq"] + if start_freq < center_freq - srh or start_freq > center_freq + srh: + self.logger.warning('start_freq for profile "%s" is out of range', id) + + def isAlwaysOn(self): + return "always-on" in self.props and self.props["always-on"] + + def getEventNames(self): + return [ + "samp_rate", + "center_freq", + "ppm", + "rf_gain", + "lfo_offset", + ] + list(self.getCommandMapper().keys()) + + def getCommandMapper(self): + if self.commandMapper is None: + self.commandMapper = CommandMapper() + return self.commandMapper + + @abstractmethod + def onPropertyChange(self, changes): + pass + + def wireEvents(self): + self.sdrProps.wire(self.onPropertyChange) + + def getCommand(self): + return [self.getCommandMapper().map(self.getCommandValues())] + + def activateProfile(self, profile_id): + try: + profile_name = self.getProfiles()[profile_id]["name"] + self.logger.debug("activating profile \"%s\" for \"%s\"", profile_name, self.getName()) + self.profileCarousel.switch(profile_id) + except KeyError: + self.logger.warning("invalid profile %s for sdr %s. ignoring", profile_id, self.getId()) + + def getId(self): + return self.id + + def getProfileId(self): + return self.props["profile_id"] + + def getProfiles(self): + return self.props["profiles"] + + def getName(self): + return self.props["name"] + + def getProps(self): + return self.props + + def getPort(self): + return self.port + + def _getTcpSourceFormat(self): + return Format.COMPLEX_FLOAT + + def _getTcpSource(self): + with self.modificationLock: + if self.tcpSource is None: + self.tcpSource = TcpSource(self.port, self._getTcpSourceFormat()) + return self.tcpSource + + def getBuffer(self): + if self.buffer is None: + self.buffer = Buffer(Format.COMPLEX_FLOAT) + self._getTcpSource().setWriter(self.buffer) + return self.buffer + + def getCommandValues(self): + dict = self.sdrProps.__dict__() + if "lfo_offset" in dict and dict["lfo_offset"] is not None: + dict["tuner_freq"] = dict["center_freq"] + dict["lfo_offset"] + else: + dict["tuner_freq"] = dict["center_freq"] + return dict + + def start(self): + with self.modificationLock: + if self.monitor: + return + + if self.isFailed(): + return + + try: + self.preStart() + except Exception: + self.logger.exception("Exception during preStart()") + + cmd = self.getCommand() + cmd = [c for c in cmd if c is not None] + + self.stdoutPipe = LogPipe(logging.INFO, self.logger, "STDOUT") + self.stderrPipe = LogPipe(logging.WARNING, self.logger, "STDERR") + + # don't use shell mode for commands without piping + if len(cmd) > 1: + # multiple commands with pipes + cmd = "|".join(cmd) + self.process = subprocess.Popen( + cmd, + shell=True, + start_new_session=True, + stdout=self.stdoutPipe, + stderr=self.stderrPipe + ) + else: + # single command + cmd = cmd[0] + # start_new_session can go as soon as there's no piped commands left + # the os.killpg call must be replaced with something more reasonable at the same time + self.process = subprocess.Popen( + shlex.split(cmd), + start_new_session=True, + stdout=self.stdoutPipe, + stderr=self.stderrPipe + ) + self.logger.info("Started sdr source: " + cmd) + + available = False + failed = False + + def wait_for_process_to_end(): + nonlocal failed + rc = self.process.wait() + self.logger.debug("shut down with RC={0}".format(rc)) + self.process = None + self.monitor = None + self.stdoutPipe.close() + self.stdoutPipe = None + self.stderrPipe.close() + self.stderrPipe = None + if self.getState() is SdrSourceState.RUNNING: + self.fail() + else: + failed = True + self.setState(SdrSourceState.STOPPED) + + self.monitor = threading.Thread(target=wait_for_process_to_end, name="source_monitor") + self.monitor.start() + + retries = 1000 + while retries > 0 and not failed: + retries -= 1 + if self.monitor is None: + break + testsock = socket.socket() + testsock.settimeout(1) + try: + testsock.connect(("127.0.0.1", self.getPort())) + testsock.close() + available = True + break + except: + time.sleep(0.1) + + if not available: + failed = True + + try: + self.postStart() + except Exception: + self.logger.exception("Exception during postStart()") + failed = True + + if failed: + self.fail() + else: + self.setState(SdrSourceState.RUNNING) + + def preStart(self): + """ + override this method in subclasses if there's anything to be done before starting up the actual SDR + """ + pass + + def postStart(self): + """ + override this method in subclasses if there's things to do after the actual SDR has started up + """ + pass + + def isAvailable(self): + return self.monitor is not None + + def stop(self): + with self.modificationLock: + if self.process is not None: + self.setState(SdrSourceState.STOPPING) + try: + os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) + if self.monitor: + # wait 10 seconds for a regular shutdown + self.monitor.join(10) + # if the monitor is still running, the process still hasn't ended, so kill it + if self.monitor: + self.logger.warning("source has not shut down normally within 10 seconds, sending SIGKILL") + os.killpg(os.getpgid(self.process.pid), signal.SIGKILL) + except ProcessLookupError: + # been killed by something else, ignore + pass + except AttributeError: + # self.process has been overwritten by the monitor since we checked it, which is fine + pass + if self.monitor: + self.monitor.join() + if self.tcpSource is not None: + self.tcpSource.stop() + self.tcpSource = None + self.buffer = None + + def shutdown(self): + self.stop() + for c in self.clients.copy(): + c.onShutdown() + + def getClients(self, *args): + if not args: + return self.clients + return [c for c in self.clients if c.getClientClass() in args] + + def hasClients(self, *args): + return len(self.getClients(*args)) > 0 + + def addClient(self, c: SdrSourceEventClient): + if c in self.clients: + return + self.clients.append(c) + c.onStateChange(self.getState()) + hasUsers = self.hasClients(SdrClientClass.USER) + hasBackgroundTasks = self.hasClients(SdrClientClass.BACKGROUND) + if hasUsers or hasBackgroundTasks: + self.start() + self.setBusyState(SdrBusyState.BUSY if hasUsers else SdrBusyState.IDLE) + + def removeClient(self, c: SdrSourceEventClient): + if c not in self.clients: + return + + self.clients.remove(c) + + self.checkStatus() + + def checkStatus(self): + hasUsers = self.hasClients(SdrClientClass.USER) + self.setBusyState(SdrBusyState.BUSY if hasUsers else SdrBusyState.IDLE) + + # no need to check for users if we are always-on + if self.isAlwaysOn(): + return + + hasBackgroundTasks = self.hasClients(SdrClientClass.BACKGROUND) + if not hasUsers and not hasBackgroundTasks: + self.stop() + + def addSpectrumClient(self, c): + if c in self.spectrumClients: + return + + # local import due to circular depencency + from owrx.fft import SpectrumThread + + self.spectrumClients.append(c) + with self.spectrumLock: + if self.spectrumThread is None: + self.spectrumThread = SpectrumThread(self) + self.spectrumThread.start() + + def removeSpectrumClient(self, c): + try: + self.spectrumClients.remove(c) + except ValueError: + pass + with self.spectrumLock: + if not self.spectrumClients and self.spectrumThread is not None: + self.spectrumThread.stop() + self.spectrumThread = None + + def writeSpectrumData(self, data): + for c in self.spectrumClients: + c.write_spectrum_data(data) + + def getState(self) -> SdrSourceState: + return self.state + + def setState(self, state: SdrSourceState): + if state == self.state: + return + self.state = state + for c in self.clients.copy(): + c.onStateChange(state) + + def setBusyState(self, state: SdrBusyState): + if state == self.busyState: + return + self.busyState = state + for c in self.clients.copy(): + c.onBusyStateChange(state) + + +class SdrDeviceDescriptionMissing(Exception): + pass + + +class SdrDeviceTypeConverter(Converter): + def convert_to_form(self, value): + # local import due to circular dependendies + types = SdrDeviceDescription.getTypes() + if value in types: + return types[value] + return value + + def convert_from_form(self, value): + return None + + +class SdrDeviceTypeDisplay(Input): + """ + Not an input per se, just an element that can display the SDR device type in the web config + """ + def __init__(self, id, label): + super().__init__(id, label, disabled=True) + + def defaultConverter(self): + return SdrDeviceTypeConverter() + + def parse(self, data): + return {} + + +class SdrDeviceDescription(object): + @staticmethod + def getByType(sdr_type: str) -> "SdrDeviceDescription": + try: + className = "".join(x for x in sdr_type.title() if x.isalnum()) + "DeviceDescription" + module = __import__("owrx.source.{0}".format(sdr_type), fromlist=[className]) + cls = getattr(module, className) + return cls() + except (ImportError, AttributeError): + raise SdrDeviceDescriptionMissing("Device description for type {} not available".format(sdr_type)) + + @staticmethod + def getTypes(): + def get_description(module_name): + try: + description = SdrDeviceDescription.getByType(module_name) + return description.getName() + except SdrDeviceDescriptionMissing: + return None + + descriptions = { + module_name: get_description(module_name) for _, module_name, _ in pkgutil.walk_packages(__path__) + } + # filter out empty names and unavailable types + fd = FeatureDetector() + return {k: v for k, v in descriptions.items() if v is not None and fd.is_available(k)} + + def getName(self): + """ + must be overridden with a textual representation of the device, to be used for device type selection + + :return: str + """ + return None + + def supportsPpm(self): + """ + can be overridden if the device does not support configuring PPM correction + + :return: bool + """ + return True + + def getDeviceInputs(self) -> List[Input]: + keys = self.getDeviceMandatoryKeys() + self.getDeviceOptionalKeys() + return [TextInput("name", "Device name", validator=RequiredValidator())] + [ + i for i in self.getInputs() if i.id in keys + ] + + def getProfileInputs(self) -> List[Input]: + keys = self.getProfileMandatoryKeys() + self.getProfileOptionalKeys() + return [TextInput("name", "Profile name", validator=RequiredValidator())] + [ + i for i in self.getInputs() if i.id in keys + ] + + def getInputs(self) -> List[Input]: + return [ + SdrDeviceTypeDisplay("type", "Device type"), + CheckboxInput("enabled", "Enable this device", converter=OptionalConverter(defaultFormValue=True)), + GainInput("rf_gain", "Device gain", self.hasAgc()), + NumberInput( + "ppm", + "Frequency correction", + append="ppm", + ), + CheckboxInput( + "always-on", + "Keep device running at all times", + infotext="Prevents shutdown of the device when idle. Useful for devices with unreliable startup.", + ), + CheckboxInput( + "services", + "Run background services on this device", + ), + ExponentialInput( + "lfo_offset", + "Oscillator offset", + "Hz", + infotext="Use this when the actual receiving frequency differs from the frequency to be tuned on the" + + " device.
    Formula: Center frequency + oscillator offset = sdr tune frequency", + ), + WaterfallLevelsInput("waterfall_levels", "Waterfall levels"), + CheckboxInput( + "waterfall_auto_level_default_mode", + "Automatically adjust waterfall level by default", + infotext="Enable this to automatically enable auto adjusting waterfall levels on page load.", + ), + SchedulerInput("scheduler", "Scheduler"), + ExponentialInput("center_freq", "Center frequency", "Hz"), + ExponentialInput( + "samp_rate", + "Sample rate", + "S/s", + validator=RangeListValidator(self.getSampleRateRanges()) + ), + ExponentialInput("start_freq", "Initial frequency", "Hz"), + ModesInput("start_mod", "Initial modulation"), + NumberInput("initial_squelch_level", "Initial squelch level", append="dBFS"), + ] + + def hasAgc(self): + # default is True since most devices have agc. override in subclasses if agc is not available + return True + + def getDeviceMandatoryKeys(self): + return ["name", "type", "enabled"] + + def getDeviceOptionalKeys(self): + keys = [ + "always-on", + "services", + "rf_gain", + "lfo_offset", + "waterfall_levels", + "waterfall_auto_level_default_mode", + "scheduler", + ] + if self.supportsPpm(): + keys += ["ppm"] + return keys + + def getProfileMandatoryKeys(self): + return ["name", "center_freq", "samp_rate", "start_freq", "start_mod"] + + def getProfileOptionalKeys(self): + return [ + "initial_squelch_level", + "rf_gain", + "lfo_offset", + "waterfall_levels", + "waterfall_auto_level_default_mode", + ] + + def getDeviceSection(self): + return OptionalSection( + "Device settings", self.getDeviceInputs(), self.getDeviceMandatoryKeys(), self.getDeviceOptionalKeys() + ) + + def getProfileSection(self): + return OptionalSection( + "Profile settings", + self.getProfileInputs(), + self.getProfileMandatoryKeys(), + self.getProfileOptionalKeys(), + ) + + def getSampleRateRanges(self) -> List[Range]: + # semi-sane default value. should be overridden with more specific values per device. + return [Range(500000, 10000000)] diff --git a/owrx/source/afedri.py b/owrx/source/afedri.py new file mode 100644 index 000000000..6b69895b9 --- /dev/null +++ b/owrx/source/afedri.py @@ -0,0 +1,127 @@ +from ipaddress import IPv4Address, AddressValueError +from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription +from owrx.form.input import Input, CheckboxInput, DropdownInput, Option +from owrx.form.input.device import TextInput +from owrx.form.input.validator import Validator, ValidationError, Range +from typing import List + + +AFEDRI_DEVICE_KEYS = ["rx_mode"] +AFEDRI_PROFILE_KEYS = ["r820t_lna_agc", "r820t_mixer_agc"] + + +class IPv4AndPortValidator(Validator): + def validate(self, key, value) -> None: + parts = value.split(":") + if len(parts) != 2: + raise ValidationError(key, "Wrong format. Expected IPv4:port") + + try: + IPv4Address(parts[0]) + except AddressValueError as e: + raise ValidationError(key, "IP address error: {}".format(str(e))) + + try: + port = int(parts[1]) + except ValueError: + raise ValidationError(key, "Port number invalid") + if not 0 <= port <= 65535: + raise ValidationError(key, "Port number out of range") + + +class AfedriAddressPortInput(TextInput): + def __init__(self): + super().__init__( + "afedri_adress_port", + "Afedri IP and Port", + infotext="Afedri IP and port to connect to. Format = IPv4:Port", + validator=IPv4AndPortValidator(), + ) + + +class AfedriSource(SoapyConnectorSource): + def getSoapySettingsMappings(self): + mappings = super().getSoapySettingsMappings() + mappings.update({x: x for x in AFEDRI_PROFILE_KEYS}) + return mappings + + def getEventNames(self): + return super().getEventNames() + ["afedri_adress_port"] + AFEDRI_DEVICE_KEYS + + def getDriver(self): + return "afedri" + + def buildSoapyDeviceParameters(self, parsed, values): + params = super().buildSoapyDeviceParameters(parsed, values) + + address, port = values["afedri_adress_port"].split(":") + params += [{"address": address, "port": port}] + + can_be_set_at_start = AFEDRI_DEVICE_KEYS + for elm in can_be_set_at_start: + if elm in values: + params += [{elm: values[elm]}] + + return params + + +class AfedriDeviceDescription(SoapyConnectorDeviceDescription): + def getName(self): + return "Afedri device" + + def supportsPpm(self): + # not currently mapped, and it's unclear how this should be sent to the device + return False + + def hasAgc(self): + # not currently mapped + return False + + def getInputs(self) -> List[Input]: + return super().getInputs() + [ + AfedriAddressPortInput(), + CheckboxInput( + "r820t_lna_agc", + "Enable R820T LNA AGC", + ), + CheckboxInput( + "r820t_mixer_agc", + "Enable R820T Mixer AGC", + ), + DropdownInput( + "rx_mode", + "Switch the device to a specific RX mode at start", + options=[ + Option("0", "Single"), + Option("1", "DualDiversity"), + Option("2", "Dual"), + Option("3", "DiversityInternal"), + Option("4", "QuadDiversity"), + Option("5", "Quad"), + ], + ), + ] + + def getDeviceMandatoryKeys(self): + return super().getDeviceMandatoryKeys() + ["afedri_adress_port"] + + def getDeviceOptionalKeys(self): + return super().getDeviceOptionalKeys() + AFEDRI_DEVICE_KEYS + + def getProfileOptionalKeys(self): + return super().getProfileOptionalKeys() + AFEDRI_PROFILE_KEYS + + def getGainStages(self): + return [ + "RF", + "FE", + "R820T_LNA_GAIN", + "R820T_MIXER_GAIN", + "R820T_VGA_GAIN", + ] + + def getNumberOfChannels(self) -> int: + return 4 + + def getSampleRateRanges(self) -> List[Range]: + return [Range(48000, 2400000)] diff --git a/owrx/source/airspy.py b/owrx/source/airspy.py new file mode 100644 index 000000000..e9815bbb8 --- /dev/null +++ b/owrx/source/airspy.py @@ -0,0 +1,62 @@ +from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription +from owrx.form.input import Input, CheckboxInput +from owrx.form.input.device import BiasTeeInput +from owrx.form.input.validator import Range +from typing import List + + +class AirspySource(SoapyConnectorSource): + def getSoapySettingsMappings(self): + mappings = super().getSoapySettingsMappings() + mappings.update( + { + "bias_tee": "biastee", + "bitpack": "bitpack", + } + ) + return mappings + + def getDriver(self): + return "airspy" + + +class AirspyDeviceDescription(SoapyConnectorDeviceDescription): + def getName(self): + return "Airspy R2 or Mini" + + def supportsPpm(self): + # not supported by the device API + # frequency calibration can be done with separate tools and will be persisted on the device. + # see discussion here: https://groups.io/g/openwebrx/topic/79360293 + return False + + def getInputs(self) -> List[Input]: + return super().getInputs() + [ + BiasTeeInput(), + CheckboxInput( + "bitpack", + "Enable bit-packing", + infotext="Packs two 12-bit samples into 3 bytes." + + " Lowers USB bandwidth consumption, increases CPU load", + ), + ] + + def getDeviceOptionalKeys(self): + return super().getDeviceOptionalKeys() + ["bias_tee", "bitpack"] + + def getProfileOptionalKeys(self): + return super().getProfileOptionalKeys() + ["bias_tee"] + + def getGainStages(self): + return ["LNA", "MIX", "VGA"] + + def getSampleRateRanges(self) -> List[Range]: + # Airspy R2 does 2.5 or 10 MS/s + # Airspy mini does 3 or 6 MS/s + # we don't know what device we're actually dealing with, but we can still clamp it down to a sum of the options. + return [ + Range(2500000), + Range(3000000), + Range(6000000), + Range(10000000), + ] diff --git a/owrx/source/airspyhf.py b/owrx/source/airspyhf.py new file mode 100644 index 000000000..8787628d8 --- /dev/null +++ b/owrx/source/airspyhf.py @@ -0,0 +1,27 @@ +from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription +from owrx.form.input.validator import Range +from typing import List + + +class AirspyhfSource(SoapyConnectorSource): + def getDriver(self): + return "airspyhf" + + +class AirspyhfDeviceDescription(SoapyConnectorDeviceDescription): + def getName(self): + return "Airspy HF+ or Discovery" + + def supportsPpm(self): + # not currently supported by the SoapySDR module. + return False + + def getSampleRateRanges(self) -> List[Range]: + return [ + Range(192000), + Range(256000), + Range(384000), + Range(456000), + Range(768000), + Range(912000), + ] diff --git a/owrx/source/bladerf.py b/owrx/source/bladerf.py new file mode 100644 index 000000000..af57aab14 --- /dev/null +++ b/owrx/source/bladerf.py @@ -0,0 +1,16 @@ +from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription +from owrx.form.input.validator import Range +from typing import List + + +class BladerfSource(SoapyConnectorSource): + def getDriver(self): + return "bladerf" + + +class BladerfDeviceDescription(SoapyConnectorDeviceDescription): + def getName(self): + return "Blade RF" + + def getSampleRateRanges(self) -> List[Range]: + return [Range(160000, 40000000)] diff --git a/owrx/source/connector.py b/owrx/source/connector.py new file mode 100644 index 000000000..7175d9b60 --- /dev/null +++ b/owrx/source/connector.py @@ -0,0 +1,96 @@ +from owrx.source import SdrSource, SdrDeviceDescription +from owrx.socket import getAvailablePort +from owrx.property import PropertyDeleted +import socket +from owrx.command import Flag, Option +from typing import List +from owrx.form.input import Input, NumberInput, CheckboxInput + + +class ConnectorSource(SdrSource): + def __init__(self, id, props): + self.controlSocket = None + self.controlPort = getAvailablePort() + super().__init__(id, props) + + def getCommandMapper(self): + return ( + super() + .getCommandMapper() + .setMappings( + { + "samp_rate": Option("-s"), + "tuner_freq": Option("-f"), + "port": Option("-p"), + "controlPort": Option("-c"), + "device": Option("-d"), + "iqswap": Flag("-i"), + "rtltcp_compat": Option("-r"), + "ppm": Option("-P"), + "rf_gain": Option("-g"), + } + ) + ) + + def sendControlMessage(self, changes): + for prop, value in changes.items(): + if value is PropertyDeleted: + value = None + self.logger.debug("sending property change over control socket: {0} changed to {1}".format(prop, value)) + self.controlSocket.sendall("{prop}:{value}\n".format(prop=prop, value=value).encode()) + + def onPropertyChange(self, changes): + if self.monitor is None: + return + if ( + ("center_freq" in changes or "lfo_offset" in changes) + and "lfo_offset" in self.sdrProps + and self.sdrProps["lfo_offset"] is not None + ): + changes["center_freq"] = self.sdrProps["center_freq"] + self.sdrProps["lfo_offset"] + changes.pop("lfo_offset", None) + self.sendControlMessage(changes) + + def postStart(self): + self.logger.debug("opening control socket...") + self.controlSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.controlSocket.connect(("localhost", self.controlPort)) + + def stop(self): + super().stop() + if self.controlSocket: + self.controlSocket.close() + self.controlSocket = None + + def getControlPort(self): + return self.controlPort + + def getCommandValues(self): + values = super().getCommandValues() + values["port"] = self.getPort() + values["controlPort"] = self.getControlPort() + return values + + +class ConnectorDeviceDescription(SdrDeviceDescription): + def getInputs(self) -> List[Input]: + return super().getInputs() + [ + NumberInput( + "rtltcp_compat", + "Port for rtl_tcp compatible data", + infotext="Activate an rtl_tcp compatible interface on the port number specified.
    " + + "Note: Port is only available on the local machine, not on the network.
    " + + "Note: IQ data may be degraded by the downsampling process to 8 bits.", + ), + CheckboxInput( + "iqswap", + "Swap I and Q channels", + infotext="Swapping inverts the spectrum, so this is useful in combination with an inverting mixer", + ), + ] + + def getDeviceOptionalKeys(self): + return super().getDeviceOptionalKeys() + ["rtltcp_compat", "iqswap"] + + def getProfileOptionalKeys(self): + return super().getProfileOptionalKeys() + ["iqswap"] diff --git a/owrx/source/direct.py b/owrx/source/direct.py new file mode 100644 index 000000000..4860dd156 --- /dev/null +++ b/owrx/source/direct.py @@ -0,0 +1,75 @@ +from abc import ABCMeta +from owrx.source import SdrSource, SdrDeviceDescription +from csdr.chain import Chain +from typing import Optional +from pycsdr.modules import Buffer +from pycsdr.types import Format + + +class DirectSource(SdrSource, metaclass=ABCMeta): + def __init__(self, id, props): + self._conversion = None + super().__init__(id, props) + + def onPropertyChange(self, changes): + self.logger.debug("restarting sdr source due to property changes: {0}".format(changes)) + self.stop() + self.sleepOnRestart() + self.start() + + def nmux_memory(self): + # in megabytes. This sets the approximate size of the circular buffer used by nmux. + return 50 + + def getNmuxCommand(self): + props = self.sdrProps + + nmux_bufcnt = nmux_bufsize = 0 + while nmux_bufsize < props["samp_rate"] / 4: + nmux_bufsize += 4096 + while nmux_bufsize * nmux_bufcnt < self.nmux_memory() * 1e6: + nmux_bufcnt += 1 + if nmux_bufcnt == 0 or nmux_bufsize == 0: + raise ValueError("Error: unable to calculate nmux buffer parameters.") + + return [ + "nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" + % ( + nmux_bufsize, + nmux_bufcnt, + self.port, + ) + ] + + def getCommand(self): + return super().getCommand() + self.getNmuxCommand() + + # override this in subclasses, if necessary + def getFormatConversion(self) -> Optional[Chain]: + return None + + def _getTcpSourceFormat(self): + conversion = self.getFormatConversion() + return Format.COMPLEX_FLOAT if conversion is None else conversion.getInputFormat() + + # override this in subclasses, if necessary + def sleepOnRestart(self): + pass + + def getBuffer(self): + if self.buffer is None: + source = self._getTcpSource() + buffer = Buffer(source.getOutputFormat()) + source.setWriter(buffer) + self._conversion = self.getFormatConversion() + if self._conversion is not None: + self._conversion.setReader(buffer.getReader()) + # this one must be COMPLEX_FLOAT + buffer = Buffer(Format.COMPLEX_FLOAT) + self._conversion.setWriter(buffer) + self.buffer = buffer + return self.buffer + + +class DirectSourceDeviceDescription(SdrDeviceDescription): + pass diff --git a/owrx/source/fcdpp.py b/owrx/source/fcdpp.py new file mode 100644 index 000000000..ca614c72a --- /dev/null +++ b/owrx/source/fcdpp.py @@ -0,0 +1,19 @@ +from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription +from owrx.form.input.validator import Range +from typing import List + + +class FcdppSource(SoapyConnectorSource): + def getDriver(self): + return "fcdpp" + + +class FcdppDeviceDescription(SoapyConnectorDeviceDescription): + def getName(self): + return "FunCube Dongle Pro+" + + def getSampleRateRanges(self) -> List[Range]: + return [ + Range(96000), + Range(192000), + ] diff --git a/owrx/source/fifi_sdr.py b/owrx/source/fifi_sdr.py new file mode 100644 index 000000000..027c0037a --- /dev/null +++ b/owrx/source/fifi_sdr.py @@ -0,0 +1,79 @@ +from owrx.command import Option +from owrx.source.direct import DirectSource, DirectSourceDeviceDescription +from owrx.log import LogPipe +from subprocess import Popen +from csdr.chain import Chain +from pycsdr.modules import Convert, Gain +from pycsdr.types import Format +from typing import List +from owrx.form.input import Input, TextInput +from owrx.form.input.validator import Range +import logging + + +class FifiSdrSource(DirectSource): + def getCommandMapper(self): + return ( + super() + .getCommandMapper() + .setBase("arecord") + .setMappings({"device": Option("-D"), "samp_rate": Option("-r")}) + .setStatic("-t raw -f S16_LE -c2 -") + ) + + def getEventNames(self): + return super().getEventNames() + ["device"] + + def getFormatConversion(self) -> Chain: + return Chain([Convert(Format.COMPLEX_SHORT, Format.COMPLEX_FLOAT), Gain(Format.COMPLEX_FLOAT, 5.0)]) + + def sendRockProgFrequency(self, frequency): + stdoutPipe = LogPipe(logging.DEBUG, self.logger, "STDOUT") + stderrPipe = LogPipe(logging.DEBUG, self.logger, "STDERR") + process = Popen( + ["rockprog", "--vco", "-w", "--freq={}".format(frequency / 1e6)], + stdout=stdoutPipe, + stderr=stderrPipe + ) + process.communicate() + rc = process.wait() + if rc != 0: + self.logger.warning("rockprog failed to set frequency; rc=%i", rc) + stdoutPipe.close() + stderrPipe.close() + + def preStart(self): + values = self.getCommandValues() + self.sendRockProgFrequency(values["tuner_freq"]) + + def onPropertyChange(self, changes): + if "center_freq" in changes: + self.sendRockProgFrequency(changes["center_freq"]) + + +class FifiSdrDeviceDescription(DirectSourceDeviceDescription): + def getName(self): + return "FiFi SDR" + + def supportsPpm(self): + # not currently mapped, and it's unclear how this should be sent to the device + return False + + def getInputs(self) -> List[Input]: + return super().getInputs() + [ + TextInput( + "device", + "Device identifier", + infotext="Alsa audio device identifier", + ), + ] + + def getDeviceOptionalKeys(self): + return super().getDeviceOptionalKeys() + ["device"] + + def getSampleRateRanges(self) -> List[Range]: + return [ + Range(48000), + Range(96000), + Range(192000), + ] diff --git a/owrx/source/hackrf.py b/owrx/source/hackrf.py new file mode 100644 index 000000000..5fbb96a3e --- /dev/null +++ b/owrx/source/hackrf.py @@ -0,0 +1,40 @@ +from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription +from owrx.form.input import Input +from owrx.form.input.device import BiasTeeInput +from owrx.form.input.validator import Range +from typing import List + + +class HackrfSource(SoapyConnectorSource): + def getSoapySettingsMappings(self): + mappings = super().getSoapySettingsMappings() + mappings.update({"bias_tee": "bias_tx"}) + return mappings + + def getDriver(self): + return "hackrf" + + +class HackrfDeviceDescription(SoapyConnectorDeviceDescription): + def getName(self): + return "HackRF" + + def supportsPpm(self): + # not implemented by the SoapySDR module. + # see discussion here: https://groups.io/g/openwebrx/topic/78339109 + return False + + def getInputs(self) -> List[Input]: + return super().getInputs() + [BiasTeeInput()] + + def getDeviceOptionalKeys(self): + return super().getDeviceOptionalKeys() + ["bias_tee"] + + def getProfileOptionalKeys(self): + return super().getProfileOptionalKeys() + ["bias_tee"] + + def getGainStages(self): + return ["LNA", "AMP", "VGA"] + + def getSampleRateRanges(self) -> List[Range]: + return [Range(1000000, 20000000)] diff --git a/owrx/source/hpsdr.py b/owrx/source/hpsdr.py new file mode 100644 index 000000000..612e4202d --- /dev/null +++ b/owrx/source/hpsdr.py @@ -0,0 +1,99 @@ +from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription +from owrx.command import Option, Flag +from owrx.form.input import Input, NumberInput, TextInput, CheckboxInput +from owrx.form.input.validator import RangeValidator, Range +from typing import List + +# These are the command line options available: +# --frequency uint +# Tune to specified frequency in Hz (default 7100000) +# --gain uint +# LNA gain between 0 (-12dB) and 60 (48dB) (default 20) +# --radio string +# IP address of radio (default use first radio discovered) +# --samplerate uint +# Use the specified samplerate: one of 48000, 96000, 192000, 384000 (default 96000) +# --debug +# Emit debug log messages on stdout +# --serverPort uint +# Server port for this radio (default 7300) +# +# If a remote IP address is not set, the connector will use the HPSDR discovery protocol +# to find radios on the local network and will connect to the first radio it discovers. +# If there is more than one HPSDR radio on the network, the IP address of the desired radio +# should always be specified. +# To use multiple HPSDR radios, each radio should have its IP address and a unique server port +# specfied. For example: +# Radio 1: (Remote IP: 192.168.1.11, Server port: 7300) +# Radio 2: (Remote IP: 192.168.1.22, Server port: 7301) + + +class HpsdrSource(ConnectorSource): + def getCommandMapper(self): + return ( + super() + .getCommandMapper() + .setBase("hpsdrconnector") + .setMappings( + { + "tuner_freq": Option("--frequency"), + "samp_rate": Option("--samplerate"), + "remote": Option("--radio"), + "rf_gain": Option("--gain"), + "server_port": Option("--serverPort"), + "debug": Flag("--debug"), + } + ) + ) + + +class RemoteInput(TextInput): + def __init__(self): + super().__init__( + "remote", + "Remote IP", + infotext=( + "HPSDR radio IP address. If it is not set, the connector will connect to the first radio it discovers. " + "If there is more than one HPSDR radio on the network, IP addresses of the desired radios should always be specified." + ) + ) + + +class HpsdrDeviceDescription(ConnectorDeviceDescription): + def getName(self): + return "HPSDR devices (Hermes / Hermes Lite 2 / Red Pitaya)" + + def getInputs(self) -> List[Input]: + return super().getInputs() + [ + RemoteInput(), + NumberInput( + "rf_gain", + "LNA Gain", + "LNA gain between 0 (-12dB) and 60 (48dB) (default 20)", + validator=RangeValidator(0, 60) + ), + CheckboxInput( + "debug", + "Show connector debugging messages in the log" + ), + NumberInput( + "server_port", + "Server port", + ("Radio server port (default 7300). When using multiple radios, each must be on a separate port, " + "e.g. 7300 for the first, 7301 for the second.") + ), + ] + + def getDeviceOptionalKeys(self): + return list(filter(lambda x : x not in ["rtltcp_compat", "iqswap"], super().getDeviceOptionalKeys())) + ["remote","debug","server_port"] + + def getProfileOptionalKeys(self): + return list(filter(lambda x : x != "iqswap", super().getProfileOptionalKeys())) + + def getSampleRateRanges(self) -> List[Range]: + return [ + Range(48000), + Range(96000), + Range(192000), + Range(384000), + ] diff --git a/owrx/source/lime_sdr.py b/owrx/source/lime_sdr.py new file mode 100644 index 000000000..186d25584 --- /dev/null +++ b/owrx/source/lime_sdr.py @@ -0,0 +1,16 @@ +from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription +from owrx.form.input.validator import Range +from typing import List + + +class LimeSdrSource(SoapyConnectorSource): + def getDriver(self): + return "lime" + + +class LimeSdrDeviceDescription(SoapyConnectorDeviceDescription): + def getName(self): + return "LimeSDR device" + + def getSampleRateRanges(self) -> List[Range]: + return [Range(100000, 65000000)] diff --git a/owrx/source/perseussdr.py b/owrx/source/perseussdr.py new file mode 100644 index 000000000..b3656d6a5 --- /dev/null +++ b/owrx/source/perseussdr.py @@ -0,0 +1,93 @@ +from owrx.source.direct import DirectSource, DirectSourceDeviceDescription +from owrx.command import Option, Flag +from owrx.form.input import Input, DropdownEnum, DropdownInput, CheckboxInput +from owrx.form.input.validator import Range +from typing import List + + +# +# In order to interface Perseus hardware, we resolve to use the +# perseustest utility that comes with libperseus-sdr support package. +# Below the base options used are shown: +# +# -p output I/Q samples as 32 bits floating point +# -d -1 suppress debug messages +# -a don't test attenuators on startup +# -t 0 runs indefinitely +# -o - output samples on stdout +# +# As we are already returning I/Q samples as pairs of 32 bits +# floating points (option -p),no need for further conversions, +# so the method getFormatConversion(self) is not implemented at all. + + +class PerseussdrSource(DirectSource): + def getCommandMapper(self): + return ( + super() + .getCommandMapper() + .setBase("perseustest -p -d -1 -a -t 0 -o - ") + .setMappings( + { + "samp_rate": Option("-s"), + "tuner_freq": Option("-f"), + "attenuator": Option("-u"), + "adc_preamp": Flag("-m"), + "adc_dither": Flag("-x"), + "wideband": Flag("-w"), + } + ) + ) + + +class AttenuatorOptions(DropdownEnum): + ATTENUATOR_0 = 0 + ATTENUATOR_10 = -10 + ATTENUATOR_20 = -20 + ATTENUATOR_30 = -30 + + def __str__(self): + return "{value} dB".format(value=self.value) + + +class PerseussdrDeviceDescription(DirectSourceDeviceDescription): + def getName(self): + return "Perseus SDR" + + def supportsPpm(self): + # not currently mapped, and not available as an option to "perseustest" + return False + + def getInputs(self) -> List[Input]: + return super().getInputs() + [ + DropdownInput("attenuator", "Attenuator", options=AttenuatorOptions), + CheckboxInput("adc_preamp", "Activate ADC preamp"), + CheckboxInput("adc_dither", "Enable ADC dithering"), + CheckboxInput("wideband", "Disable analog filters"), + ] + + def getDeviceOptionalKeys(self): + # no rf_gain + return [key for key in super().getDeviceOptionalKeys() if key != "rf_gain"] + [ + "attenuator", + "adc_preamp", + "adc_dither", + "wideband", + ] + + def getProfileOptionalKeys(self): + return [key for key in super().getProfileOptionalKeys() if key != "rf_gain"] + [ + "attenuator", + "adc_preamp", + "adc_dither", + "wideband", + ] + + def getSampleRateRanges(self) -> List[Range]: + return [ + Range(125000), + Range(250000), + Range(500000), + Range(1000000), + Range(2000000), + ] diff --git a/owrx/source/pluto_sdr.py b/owrx/source/pluto_sdr.py new file mode 100644 index 000000000..21d9e378e --- /dev/null +++ b/owrx/source/pluto_sdr.py @@ -0,0 +1,39 @@ +from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription +from owrx.form.input import Input, TextInput +from owrx.form.input.validator import Range +from typing import List + + +class PlutoSdrSource(SoapyConnectorSource): + def getDriver(self): + return "plutosdr" + + def getEventNames(self): + return super().getEventNames() + ["hostname"] + + def buildSoapyDeviceParameters(self, parsed, values): + params = super().buildSoapyDeviceParameters(parsed, values) + if "hostname" in values: + params = [p for p in params if "hostname" not in p] + params += [{"hostname": values["hostname"]}] + return params + + +class PlutoSdrDeviceDescription(SoapyConnectorDeviceDescription): + def getName(self): + return "PlutoSDR" + + def getInputs(self) -> List[Input]: + return super().getInputs() + [ + TextInput( + "hostname", + "Hostname", + infotext="Use this for PlutoSDR devices attached to the network" + ) + ] + + def getDeviceOptionalKeys(self): + return super().getDeviceOptionalKeys() + ["hostname"] + + def getSampleRateRanges(self) -> List[Range]: + return [Range(520833, 61440000)] diff --git a/owrx/source/radioberry.py b/owrx/source/radioberry.py new file mode 100644 index 000000000..5d2dbe4ad --- /dev/null +++ b/owrx/source/radioberry.py @@ -0,0 +1,21 @@ +from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription +from owrx.form.input.validator import Range +from typing import List + + +class RadioberrySource(SoapyConnectorSource): + def getDriver(self): + return "radioberry" + + +class RadioberryDeviceDescription(SoapyConnectorDeviceDescription): + def getName(self): + return "RadioBerry" + + def getSampleRateRanges(self) -> List[Range]: + return [ + Range(48000), + Range(96000), + Range(192000), + Range(384000), + ] diff --git a/owrx/source/resampler.py b/owrx/source/resampler.py new file mode 100644 index 000000000..fb1c2d09f --- /dev/null +++ b/owrx/source/resampler.py @@ -0,0 +1,45 @@ +from owrx.source import SdrSource +from pycsdr.modules import Buffer, FirDecimate, Shift +from pycsdr.types import Format +from csdr.chain import Chain + + +class Resampler(SdrSource): + def onPropertyChange(self, changes): + self.logger.warning("Resampler is unable to handle property changes: {0}".format(changes)) + + def __init__(self, props, sdr): + sdrProps = sdr.getProps() + shift = (sdrProps["center_freq"] - props["center_freq"]) / sdrProps["samp_rate"] + decimation = int(float(sdrProps["samp_rate"]) / props["samp_rate"]) + if_samp_rate = sdrProps["samp_rate"] / decimation + transition_bw = 0.15 * (if_samp_rate / float(sdrProps["samp_rate"])) + props["samp_rate"] = if_samp_rate + + self.chain = Chain([ + Shift(shift), + FirDecimate(decimation, transition_bw) + ]) + + self.chain.setReader(sdr.getBuffer().getReader()) + + super().__init__(None, props) + + def getBuffer(self): + if self.buffer is None: + self.buffer = Buffer(Format.COMPLEX_FLOAT) + self.chain.setWriter(self.buffer) + return self.buffer + + def stop(self): + self.chain.stop() + self.chain = None + super().stop() + + def activateProfile(self, profile_id=None): + self.logger.warning("Resampler does not support setting profiles") + pass + + def validateProfiles(self): + # resampler does not support profiles + pass diff --git a/owrx/source/rtl_sdr.py b/owrx/source/rtl_sdr.py new file mode 100644 index 000000000..1c23e9576 --- /dev/null +++ b/owrx/source/rtl_sdr.py @@ -0,0 +1,41 @@ +from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription +from owrx.command import Flag, Option +from typing import List +from owrx.form.input import Input, TextInput +from owrx.form.input.device import BiasTeeInput, DirectSamplingInput +from owrx.form.input.validator import Range + + +class RtlSdrSource(ConnectorSource): + def getCommandMapper(self): + return ( + super() + .getCommandMapper() + .setBase("rtl_connector") + .setMappings({"bias_tee": Flag("-b"), "direct_sampling": Option("-e")}) + ) + + +class RtlSdrDeviceDescription(ConnectorDeviceDescription): + def getName(self): + return "RTL-SDR device" + + def getInputs(self) -> List[Input]: + return super().getInputs() + [ + TextInput( + "device", + "Device identifier", + infotext="Device serial number or index", + ), + BiasTeeInput(), + DirectSamplingInput(), + ] + + def getDeviceOptionalKeys(self): + return super().getDeviceOptionalKeys() + ["device", "bias_tee", "direct_sampling"] + + def getProfileOptionalKeys(self): + return super().getProfileOptionalKeys() + ["bias_tee", "direct_sampling"] + + def getSampleRateRanges(self) -> List[Range]: + return [Range(250000, 3200000)] diff --git a/owrx/source/rtl_sdr_soapy.py b/owrx/source/rtl_sdr_soapy.py new file mode 100644 index 000000000..541a970b8 --- /dev/null +++ b/owrx/source/rtl_sdr_soapy.py @@ -0,0 +1,32 @@ +from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription +from owrx.form.input import Input +from owrx.form.input.device import BiasTeeInput, DirectSamplingInput +from owrx.form.input.validator import Range +from typing import List + + +class RtlSdrSoapySource(SoapyConnectorSource): + def getSoapySettingsMappings(self): + mappings = super().getSoapySettingsMappings() + mappings.update({"direct_sampling": "direct_samp", "bias_tee": "biastee"}) + return mappings + + def getDriver(self): + return "rtlsdr" + + +class RtlSdrSoapyDeviceDescription(SoapyConnectorDeviceDescription): + def getName(self): + return "RTL-SDR device (via SoapySDR)" + + def getInputs(self) -> List[Input]: + return super().getInputs() + [BiasTeeInput(), DirectSamplingInput()] + + def getDeviceOptionalKeys(self): + return super().getDeviceOptionalKeys() + ["bias_tee", "direct_sampling"] + + def getProfileOptionalKeys(self): + return super().getProfileOptionalKeys() + ["bias_tee", "direct_sampling"] + + def getSampleRateRanges(self) -> List[Range]: + return [Range(250000, 3200000)] diff --git a/owrx/source/rtl_tcp.py b/owrx/source/rtl_tcp.py new file mode 100644 index 000000000..3375fe75c --- /dev/null +++ b/owrx/source/rtl_tcp.py @@ -0,0 +1,42 @@ +from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription +from owrx.command import Flag, Option, Argument +from owrx.form.input import Input +from owrx.form.input.device import RemoteInput, DirectSamplingInput +from owrx.form.input.validator import Range +from typing import List + + +class RtlTcpSource(ConnectorSource): + def getCommandMapper(self): + return ( + super() + .getCommandMapper() + .setBase("rtl_tcp_connector") + .setMappings( + { + "bias_tee": Flag("-b"), + "direct_sampling": Option("-e"), + "remote": Argument(), + } + ) + ) + + +class RtlTcpDeviceDescription(ConnectorDeviceDescription): + def getName(self): + return "RTL-SDR device (via rtl_tcp)" + + def getInputs(self) -> List[Input]: + return super().getInputs() + [RemoteInput(), DirectSamplingInput()] + + def getDeviceMandatoryKeys(self): + return super().getDeviceMandatoryKeys() + ["remote"] + + def getDeviceOptionalKeys(self): + return super().getDeviceOptionalKeys() + ["direct_sampling"] + + def getProfileOptionalKeys(self): + return super().getProfileOptionalKeys() + ["direct_sampling"] + + def getSampleRateRanges(self) -> List[Range]: + return [Range(250000, 3200000)] diff --git a/owrx/source/runds.py b/owrx/source/runds.py new file mode 100644 index 000000000..b1a864162 --- /dev/null +++ b/owrx/source/runds.py @@ -0,0 +1,63 @@ +from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription +from owrx.command import Argument, Flag, Option +from owrx.form.input import Input, DropdownInput, DropdownEnum, CheckboxInput +from owrx.form.input.device import RemoteInput +from owrx.form.input.validator import Range +from typing import List + + +class RundsSource(ConnectorSource): + def getCommandMapper(self): + return ( + super() + .getCommandMapper() + .setBase("runds_connector") + .setMappings( + { + "long": Flag("-l"), + "remote": Argument(), + "protocol": Option("-m"), + } + ) + ) + + +class ProtocolOptions(DropdownEnum): + PROTOCOL_EB200 = ("eb200", "EB200 protocol") + PROTOCOL_AMMOS = ("ammos", "Ammos protocol") + + def __new__(cls, *args, **kwargs): + value, description = args + obj = object.__new__(cls) + obj._value_ = value + obj.description = description + return obj + + def __str__(self): + return self.description + + +class RundsDeviceDescription(ConnectorDeviceDescription): + def getName(self): + return "R&S device using EB200 or Ammos protocol" + + def supportsPpm(self): + # currently not implemented in the connector + return False + + def getInputs(self) -> List[Input]: + return super().getInputs() + [ + RemoteInput(), + DropdownInput("protocol", "Protocol", ProtocolOptions), + CheckboxInput("long", "Use 32-bit sample size (LONG)"), + ] + + def getDeviceMandatoryKeys(self): + return super().getDeviceMandatoryKeys() + ["remote"] + + def getDeviceOptionalKeys(self): + return super().getDeviceOptionalKeys() + ["protocol", "long"] + + def getSampleRateRanges(self) -> List[Range]: + # can't be very specific here due to the wide range of devices, so this is more of a sanity check. + return [Range(0, 20000000)] diff --git a/owrx/source/sddc.py b/owrx/source/sddc.py new file mode 100644 index 000000000..b30655a9d --- /dev/null +++ b/owrx/source/sddc.py @@ -0,0 +1,20 @@ +from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription +from owrx.form.input.validator import Range +from typing import List + + +class SddcSource(ConnectorSource): + def getCommandMapper(self): + return super().getCommandMapper().setBase("sddc_connector") + + +class SddcDeviceDescription(ConnectorDeviceDescription): + def getName(self): + return "BBRF103 / RX666 / RX888 device (libsddc)" + + def hasAgc(self): + return False + + def getSampleRateRanges(self) -> List[Range]: + # resampling is done in software... it can't cover the full range, but it's not finished either. + return [Range(0, 64000000)] diff --git a/owrx/source/sdrplay.py b/owrx/source/sdrplay.py new file mode 100644 index 000000000..d6bb2cbd8 --- /dev/null +++ b/owrx/source/sdrplay.py @@ -0,0 +1,79 @@ +from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription +from owrx.form.input import Input, CheckboxInput +from owrx.form.input.device import BiasTeeInput +from owrx.form.input.validator import Range +from typing import List + + +class SdrplaySource(SoapyConnectorSource): + def getSoapySettingsMappings(self): + mappings = super().getSoapySettingsMappings() + mappings.update( + { + "bias_tee": "biasT_ctrl", + "rf_notch": "rfnotch_ctrl", + "dab_notch": "dabnotch_ctrl", + "external_reference": "extref_ctrl", + "hdr_ctrl": "hdr_ctrl", + } + ) + return mappings + + def getDriver(self): + return "sdrplay" + + +class SdrplayDeviceDescription(SoapyConnectorDeviceDescription): + def getName(self): + return "SDRPlay device (RSP1, RSP2, RSPDuo, RSPDx)" + + def getGainStages(self): + return ["RFGR", "IFGR"] + + def getInputs(self) -> List[Input]: + return super().getInputs() + [ + BiasTeeInput(), + CheckboxInput( + "rf_notch", + "Enable RF notch filter", + ), + CheckboxInput( + "dab_notch", + "Enable DAB notch filter", + ), + CheckboxInput( + "external_reference", + "Enable external reference clock", + ), + CheckboxInput( + "hdr_ctrl", + "Enable RSPdx HDR mode", + ) + ] + + def getDeviceOptionalKeys(self): + return super().getDeviceOptionalKeys() + [ + "bias_tee", "rf_notch", "dab_notch", "external_reference", "hdr_ctrl" + ] + + def getProfileOptionalKeys(self): + return super().getProfileOptionalKeys() + [ + "bias_tee", "rf_notch", "dab_notch", "external_reference", "hdr_ctrl" + ] + + def getSampleRateRanges(self) -> List[Range]: + # this is from SoapySDRPlay3's implementation of listSampleRates(). + # i don't think it's accurate, but this is the limitation we'd be running into if we had proper soapy + # integration. + return [ + Range(62500), + Range(96000), + Range(125000), + Range(192000), + Range(250000), + Range(384000), + Range(500000), + Range(768000), + Range(1000000), + Range(2000000, 10660000), + ] diff --git a/owrx/source/soapy.py b/owrx/source/soapy.py new file mode 100644 index 000000000..59e9317a6 --- /dev/null +++ b/owrx/source/soapy.py @@ -0,0 +1,133 @@ +from abc import ABCMeta, abstractmethod +from owrx.command import Option +from owrx.source.connector import ConnectorSource, ConnectorDeviceDescription +from typing import List +from owrx.form.input import Input, NumberInput, TextInput +from owrx.form.input.validator import RangeValidator +from owrx.form.input.device import GainInput +from owrx.soapy import SoapySettings + + +class SoapyConnectorSource(ConnectorSource, metaclass=ABCMeta): + def getCommandMapper(self): + return ( + super() + .getCommandMapper() + .setBase("soapy_connector") + .setMappings( + { + "antenna": Option("-a"), + "soapy_settings": Option("-t"), + "channel": Option("-n"), + } + ) + ) + + """ + must be implemented by child classes to be able to build a driver-based device selector by default. + return value must be the corresponding soapy driver identifier. + """ + + @abstractmethod + def getDriver(self): + pass + + def getEventNames(self): + return super().getEventNames() + list(self.getSoapySettingsMappings().keys()) + + def buildSoapyDeviceParameters(self, parsed, values): + """ + this method always attempts to inject a driver= part into the soapysdr query, depending on what connector was used. + this prevents the soapy_connector from using the wrong device in scenarios where there's no same-type sdrs. + """ + parsed = [v for v in parsed if "driver" not in v] + parsed += [{"driver": self.getDriver()}] + return parsed + + def getSoapySettingsMappings(self): + return {} + + def buildSoapySettings(self, values): + settings = {} + for k, v in self.getSoapySettingsMappings().items(): + if k in values and values[k] is not None: + settings[v] = self.convertSoapySettingsValue(values[k]) + return settings + + def convertSoapySettingsValue(self, value): + if isinstance(value, bool): + return "true" if value else "false" + return value + + def getCommandValues(self): + values = super().getCommandValues() + if "device" in values and values["device"] is not None: + parsed = SoapySettings.parse(values["device"]) + else: + parsed = [] + modified = self.buildSoapyDeviceParameters(parsed, values) + values["device"] = SoapySettings.encode(modified) + settings = ",".join(["{0}={1}".format(k, v) for k, v in self.buildSoapySettings(values).items()]) + if len(settings): + values["soapy_settings"] = settings + return values + + def onPropertyChange(self, changes): + mappings = self.getSoapySettingsMappings() + affectsSettings = False + forward = {} + for prop, value in changes.items(): + if prop in mappings.keys(): + affectsSettings = True + else: + forward[prop] = value + if affectsSettings: + settings = {} + for owrx_key, soapy_key in mappings.items(): + if owrx_key in self.props: + settings[soapy_key] = self.convertSoapySettingsValue(self.props[owrx_key]) + forward["settings"] = ",".join("{0}={1}".format(k, v) for k, v in settings.items()) + super().onPropertyChange(forward) + + +class SoapyConnectorDeviceDescription(ConnectorDeviceDescription): + def getInputs(self) -> List[Input]: + inputs = super().getInputs() + [ + TextInput( + "device", + "Device identifier", + infotext='SoapySDR device identifier string (example: "serial=123456789")', + ), + GainInput( + "rf_gain", + "Device Gain", + gain_stages=self.getGainStages(), + has_agc=self.hasAgc(), + ), + TextInput("antenna", "Antenna"), + ] + if self.getNumberOfChannels() > 1: + inputs += [ + NumberInput( + "channel", + "Select SoapySDR Channel", + validator=RangeValidator(0, self.getNumberOfChannels() - 1) + ) + ] + return inputs + + def getNumberOfChannels(self) -> int: + """ + can be overridden for sdr devices that have multiple channels. will allow the user to select a channel from + the device selection screen if > 1 + """ + return 1 + + def getDeviceOptionalKeys(self): + return super().getDeviceOptionalKeys() + ["device", "rf_gain", "antenna", "channel"] + + def getProfileOptionalKeys(self): + return super().getProfileOptionalKeys() + ["antenna"] + + def getGainStages(self): + return None diff --git a/owrx/source/soapy_remote.py b/owrx/source/soapy_remote.py new file mode 100644 index 000000000..44feb883c --- /dev/null +++ b/owrx/source/soapy_remote.py @@ -0,0 +1,47 @@ +from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription +from owrx.form.input import Input, TextInput +from owrx.form.input.device import RemoteInput +from owrx.form.input.converter import OptionalConverter +from owrx.form.input.validator import Range +from typing import List + + +class SoapyRemoteSource(SoapyConnectorSource): + def getEventNames(self): + return super().getEventNames() + ["remote", "remote_driver"] + + def getDriver(self): + return "remote" + + def buildSoapyDeviceParameters(self, parsed, values): + params = super().buildSoapyDeviceParameters(parsed, values) + params = [v for v in params if "remote" not in params] + params += [{"remote": values["remote"]}] + if "remote_driver" in values and values["remote_driver"] is not None: + params += [{"remote:driver": values["remote_driver"]}] + return params + + +class SoapyRemoteDeviceDescription(SoapyConnectorDeviceDescription): + def getName(self): + return "Device connected to a SoapyRemote server" + + def getInputs(self) -> List[Input]: + return super().getInputs() + [ + RemoteInput(), + TextInput( + "remote_driver", + "Remote driver", + infotext="SoapySDR driver to be used on the remote SoapySDRServer", + converter=OptionalConverter(), + ), + ] + + def getDeviceMandatoryKeys(self): + return super().getDeviceMandatoryKeys() + ["remote"] + + def getDeviceOptionalKeys(self): + return super().getDeviceOptionalKeys() + ["remote_driver"] + + def getSampleRateRanges(self) -> List[Range]: + return [Range(500000, 20000000)] diff --git a/owrx/source/uhd.py b/owrx/source/uhd.py new file mode 100644 index 000000000..5c1809fd5 --- /dev/null +++ b/owrx/source/uhd.py @@ -0,0 +1,17 @@ +from owrx.source.soapy import SoapyConnectorSource, SoapyConnectorDeviceDescription +from owrx.form.input.validator import Range +from typing import List + + +class UhdSource(SoapyConnectorSource): + def getDriver(self): + return "uhd" + + +class UhdDeviceDescription(SoapyConnectorDeviceDescription): + def getName(self): + return "Ettus Research USRP device" + + def getSampleRateRanges(self) -> List[Range]: + # not sure since this depends of the specific model + return [Range(0, 64000000)] diff --git a/owrx/users.py b/owrx/users.py new file mode 100644 index 000000000..be103b166 --- /dev/null +++ b/owrx/users.py @@ -0,0 +1,237 @@ +from abc import ABC, abstractmethod +from owrx.config.core import CoreConfig +from datetime import datetime, timezone +import json +import hashlib +import os +import stat + +import logging + +logger = logging.getLogger(__name__) + + +class PasswordException(Exception): + pass + + +class Password(ABC): + @staticmethod + def from_dict(d: dict): + if "encoding" not in d: + raise PasswordException("password encoding not set") + if d["encoding"] == "string": + return CleartextPassword(d) + elif d["encoding"] == "hash": + return HashedPassword(d) + raise PasswordException("invalid passord encoding: {0}".format(d["type"])) + + @abstractmethod + def is_valid(self, inp: str) -> bool: + pass + + @abstractmethod + def toJson(self) -> dict: + pass + + +class CleartextPassword(Password): + def __init__(self, pwinfo): + if isinstance(pwinfo, str): + self._value = pwinfo + elif isinstance(pwinfo, dict): + self._value = pwinfo["value"] + else: + raise ValueError("invalid argument to ClearTextPassword()") + + def is_valid(self, inp: str) -> bool: + return self._value == inp + + def toJson(self) -> dict: + return { + "encoding": "string", + "value": self._value + } + + +class HashedPassword(Password): + def __init__(self, pwinfo, algorithm="sha256"): + self.iterations = 100000 + if isinstance(pwinfo, str): + self._createFromString(pwinfo, algorithm) + else: + self._loadFromDict(pwinfo) + + def _createFromString(self, pw: str, algorithm: str): + self._algorithm = algorithm + self._salt = os.urandom(32) + dk = hashlib.pbkdf2_hmac(self._algorithm, pw.encode(), self._salt, self.iterations) + self._hash = dk.hex() + pass + + def _loadFromDict(self, d: dict): + self._hash = d["value"] + self._algorithm = d["algorithm"] + self._salt = bytes.fromhex(d["salt"]) + pass + + def is_valid(self, inp: str) -> bool: + dk = hashlib.pbkdf2_hmac(self._algorithm, inp.encode(), self._salt, self.iterations) + return dk.hex() == self._hash + + def toJson(self) -> dict: + return { + "encoding": "hash", + "value": self._hash, + "algorithm": self._algorithm, + "salt": self._salt.hex(), + } + + +DefaultPasswordClass = HashedPassword + + +class User(object): + def __init__(self, name: str, enabled: bool, password: Password, must_change_password: bool = False): + self.name = name + self.enabled = enabled + self.password = password + self.must_change_password = must_change_password + + def toJson(self): + return { + "user": self.name, + "enabled": self.enabled, + "must_change_password": self.must_change_password, + "password": self.password.toJson() + } + + @staticmethod + def fromJson(d): + if "user" in d and "password" in d and "enabled" in d: + mcp = d["must_change_password"] if "must_change_password" in d else False + return User(d["user"], d["enabled"], Password.from_dict(d["password"]), mcp) + + def setPassword(self, password: Password, must_change_password: bool = None): + self.password = password + if must_change_password is not None: + self.must_change_password = must_change_password + + def is_enabled(self): + return self.enabled + + def enable(self): + self.enabled = True + + def disable(self): + self.enabled = False + + +class UserList(object): + sharedInstance = None + + @staticmethod + def getSharedInstance(): + if UserList.sharedInstance is None: + UserList.sharedInstance = UserList() + return UserList.sharedInstance + + def __init__(self): + self.file_modified = None + self.users = {} + + def refresh(self): + if self.file_modified is None or self._getUsersFileModifiedTimestamp() > self.file_modified: + logger.debug("reloading users from disk due to file modification") + self.users = self._loadUsers() + + def _getUsersFile(self): + config = CoreConfig() + return "{data_directory}/users.json".format(data_directory=config.get_data_directory()) + + def _getUsersFileModifiedTimestamp(self): + timestamp = 0 + try: + timestamp = os.path.getmtime(self._getUsersFile()) + except FileNotFoundError: + pass + return datetime.fromtimestamp(timestamp, timezone.utc) + + def _loadUsers(self): + usersFile = self._getUsersFile() + # to avoid concurrency issues and problems when parsing errors occur: + # get early, store late + modified = self._getUsersFileModifiedTimestamp() + try: + with open(usersFile, "r") as f: + users_json = json.load(f) + + users = {u.name: u for u in [User.fromJson(d) for d in users_json]} + self.file_modified = modified + return users + except FileNotFoundError: + self.file_modified = modified + return {} + except json.JSONDecodeError: + logger.exception("error while parsing users file %s", usersFile) + return {} + except Exception: + logger.exception("error while processing users from %s", usersFile) + return {} + + def _userToJson(self, u): + return u.toJson() + + def store(self): + usersFile = self._getUsersFile() + users = [u.toJson() for u in self.values()] + try: + # don't write directly to file to avoid corruption on exceptions + jsonContent = json.dumps(users, indent=4) + with open(usersFile, "w") as f: + f.write(jsonContent) + # file should be readable by us only + os.chmod(usersFile, stat.S_IWUSR + stat.S_IRUSR) + except Exception: + logger.exception("error while writing users file %s", usersFile) + self.refresh() + + def _getUsername(self, user): + if isinstance(user, User): + return user.name + elif isinstance(user, str): + return user + else: + raise ValueError("invalid user type") + + def addUser(self, user: User): + self[user.name] = user + + def deleteUser(self, user): + del self[self._getUsername(user)] + + def __delitem__(self, key): + self.refresh() + if key not in self.users: + raise KeyError("User {user} doesn't exist".format(user=key)) + del self.users[key] + self.store() + + def __getitem__(self, item): + self.refresh() + return self.users[item] + + def __contains__(self, item): + self.refresh() + return item in self.users + + def __setitem__(self, key, value): + self.refresh() + if key in self.users: + raise KeyError("User {user} already exists".format(user=key)) + self.users[key] = value + self.store() + + def values(self): + self.refresh() + return self.users.values() diff --git a/owrx/vdl2/dumpvdl2.py b/owrx/vdl2/dumpvdl2.py new file mode 100644 index 000000000..ab46947fd --- /dev/null +++ b/owrx/vdl2/dumpvdl2.py @@ -0,0 +1,118 @@ +from pycsdr.modules import ExecModule +from pycsdr.types import Format +from owrx.aeronautical import AcarsProcessor +from owrx.map import Map +from owrx.aeronautical import AirplaneLocation, IcaoSource +from owrx.metrics import Metrics, CounterMetric +from owrx.reporting import ReportingEngine +from datetime import datetime, date, time, timezone + +import logging + +logger = logging.getLogger(__name__) + + +class DumpVDL2Module(ExecModule): + def __init__(self): + super().__init__( + Format.COMPLEX_SHORT, + Format.CHAR, + [ + "dumpvdl2", + "--iq-file", "-", + "--oversample", "1", + "--sample-format", "S16_LE", + "--output", "decoded:json:file:path=-", + ] + ) + + +class VDL2MessageParser(AcarsProcessor): + def __init__(self): + name = "dumpvdl2.decodes.vdl2" + self.metrics = Metrics.getSharedInstance().getMetric(name) + if self.metrics is None: + self.metrics = CounterMetric() + Metrics.getSharedInstance().addMetric(name, self.metrics) + super().__init__("VDL2") + + def process(self, line): + msg = super().process(line) + if msg is not None: + try: + payload = msg["vdl2"] + if "avlc" in payload: + avlc = payload["avlc"] + src = avlc["src"]["addr"] + if avlc["frame_type"] == "I": + if "acars" in avlc: + self.processAcars(avlc["acars"], icao=src) + elif "x25" in avlc: + x25 = avlc["x25"] + if "clnp" in x25: + clnp = x25["clnp"] + if "cotp" in clnp: + cotp = clnp["cotp"] + if "adsc_v2" in cotp: + adsc_v2 = cotp["adsc_v2"] + if "adsc_report" in adsc_v2: + adsc_report = adsc_v2["adsc_report"] + data = adsc_report["data"] + if "periodic_report" in data: + report_data = data["periodic_report"]["report_data"] + self.processReport(report_data, src) + elif "event_report" in data: + report_data = data["event_report"]["report_data"] + self.processReport(report_data, src) + except Exception: + logger.exception("error processing VDL2 data") + self.metrics.inc() + ReportingEngine.getSharedInstance().spot(msg) + return msg + + def processReport(self, report, icao): + if "position" not in report: + return + msg = { + "lat": self.convertLatitude(**report["position"]["lat"]), + "lon": self.convertLongitude(**report["position"]["lon"]), + "altitude": report["position"]["alt"]["val"], + } + if "ground_vector" in report: + msg.update({ + "groundtrack": report["ground_vector"]["ground_track"]["val"], + "groundspeed": report["ground_vector"]["ground_speed"]["val"], + }) + if "air_vector" in report: + msg.update({ + "verticalspeed": report["air_vector"]["vertical_rate"]["val"], + }) + if "timestamp" in report: + timestamp = self.convertTimestamp(**report["timestamp"]) + else: + timestamp = None + Map.getSharedInstance().updateLocation(IcaoSource(icao), AirplaneLocation(msg), "VDL2", timestamp=timestamp) + + def convertLatitude(self, dir, **args) -> float: + coord = self.convertCoordinate(**args) + if dir == "south": + coord *= -1 + return coord + + def convertLongitude(self, dir, **args) -> float: + coord = self.convertCoordinate(**args) + if dir == "west": + coord *= -1 + return coord + + def convertCoordinate(self, deg, min, sec) -> float: + return deg + float(min) / 60 + float(sec) / 3600 + + def convertTimestamp(self, date, time): + return datetime.combine(self.convertDate(**date), self.convertTime(**time), tzinfo=timezone.utc) + + def convertDate(self, year, month, day): + return date(year=year, month=month, day=day) + + def convertTime(self, hour, min, sec): + return time(hour=hour, minute=min, second=sec, microsecond=0, tzinfo=timezone.utc) diff --git a/owrx/version.py b/owrx/version.py new file mode 100644 index 000000000..ace52b4f4 --- /dev/null +++ b/owrx/version.py @@ -0,0 +1,5 @@ +from distutils.version import LooseVersion + +_versionstring = "1.3.0-dev" +looseversion = LooseVersion(_versionstring) +openwebrx_version = "v{0}".format(looseversion) diff --git a/owrx/waterfall.py b/owrx/waterfall.py new file mode 100644 index 000000000..c88231a12 --- /dev/null +++ b/owrx/waterfall.py @@ -0,0 +1,326 @@ +from owrx.form.input import DropdownEnum +from owrx.config import Config + + +class Waterfall(object): + def __init__(self, colors): + self.colors = colors + + def getColors(self): + return self.colors + + +class GoogleTurboWaterfall(Waterfall): + def __init__(self): + super().__init__( + [ + 0x30123B, + 0x311542, + 0x33184A, + 0x341B51, + 0x351E58, + 0x36215F, + 0x372466, + 0x38266C, + 0x392973, + 0x3A2C79, + 0x3B2F80, + 0x3C3286, + 0x3D358B, + 0x3E3891, + 0x3E3A97, + 0x3F3D9C, + 0x4040A2, + 0x4043A7, + 0x4146AC, + 0x4248B1, + 0x424BB6, + 0x434EBA, + 0x4351BF, + 0x4453C3, + 0x4456C7, + 0x4559CB, + 0x455BCF, + 0x455ED3, + 0x4561D7, + 0x4663DA, + 0x4666DD, + 0x4669E1, + 0x466BE4, + 0x466EE7, + 0x4671E9, + 0x4673EC, + 0x4676EE, + 0x4678F1, + 0x467BF3, + 0x467DF5, + 0x4680F7, + 0x4682F9, + 0x4685FA, + 0x4587FC, + 0x458AFD, + 0x448CFE, + 0x448FFE, + 0x4391FF, + 0x4294FF, + 0x4196FF, + 0x3F99FF, + 0x3E9BFF, + 0x3D9EFE, + 0x3BA1FD, + 0x3AA3FD, + 0x38A6FB, + 0x36A8FA, + 0x35ABF9, + 0x33ADF7, + 0x31B0F6, + 0x2FB2F4, + 0x2DB5F2, + 0x2CB7F0, + 0x2AB9EE, + 0x28BCEC, + 0x26BEEA, + 0x25C0E7, + 0x23C3E5, + 0x21C5E2, + 0x20C7E0, + 0x1FC9DD, + 0x1DCCDB, + 0x1CCED8, + 0x1BD0D5, + 0x1AD2D3, + 0x19D4D0, + 0x18D6CD, + 0x18D8CB, + 0x18DAC8, + 0x17DBC5, + 0x17DDC3, + 0x17DFC0, + 0x18E0BE, + 0x18E2BB, + 0x19E3B9, + 0x1AE5B7, + 0x1BE6B4, + 0x1DE8B2, + 0x1EE9AF, + 0x20EAAD, + 0x22ECAA, + 0x24EDA7, + 0x27EEA4, + 0x29EFA1, + 0x2CF09E, + 0x2FF19B, + 0x32F298, + 0x35F394, + 0x38F491, + 0x3CF58E, + 0x3FF68B, + 0x43F787, + 0x46F884, + 0x4AF980, + 0x4EFA7D, + 0x51FA79, + 0x55FB76, + 0x59FC73, + 0x5DFC6F, + 0x61FD6C, + 0x65FD69, + 0x69FE65, + 0x6DFE62, + 0x71FE5F, + 0x75FF5C, + 0x79FF59, + 0x7DFF56, + 0x80FF53, + 0x84FF50, + 0x88FF4E, + 0x8BFF4B, + 0x8FFF49, + 0x92FF46, + 0x96FF44, + 0x99FF42, + 0x9CFE40, + 0x9FFE3E, + 0xA2FD3D, + 0xA4FD3B, + 0xA7FC3A, + 0xAAFC39, + 0xACFB38, + 0xAFFA37, + 0xB1F936, + 0xB4F835, + 0xB7F835, + 0xB9F634, + 0xBCF534, + 0xBFF434, + 0xC1F334, + 0xC4F233, + 0xC6F033, + 0xC9EF34, + 0xCBEE34, + 0xCEEC34, + 0xD0EB34, + 0xD2E934, + 0xD5E835, + 0xD7E635, + 0xD9E435, + 0xDBE236, + 0xDDE136, + 0xE0DF37, + 0xE2DD37, + 0xE4DB38, + 0xE6D938, + 0xE7D738, + 0xE9D539, + 0xEBD339, + 0xEDD139, + 0xEECF3A, + 0xF0CD3A, + 0xF1CB3A, + 0xF3C93A, + 0xF4C73A, + 0xF5C53A, + 0xF7C33A, + 0xF8C13A, + 0xF9BF39, + 0xFABD39, + 0xFABA38, + 0xFBB838, + 0xFCB637, + 0xFCB436, + 0xFDB135, + 0xFDAF35, + 0xFEAC34, + 0xFEA933, + 0xFEA732, + 0xFEA431, + 0xFFA12F, + 0xFF9E2E, + 0xFF9C2D, + 0xFF992C, + 0xFE962B, + 0xFE932A, + 0xFE9028, + 0xFE8D27, + 0xFD8A26, + 0xFD8724, + 0xFC8423, + 0xFC8122, + 0xFB7E20, + 0xFB7B1F, + 0xFA781E, + 0xF9751C, + 0xF8721B, + 0xF86F1A, + 0xF76C19, + 0xF66917, + 0xF56616, + 0xF46315, + 0xF36014, + 0xF25D13, + 0xF05B11, + 0xEF5810, + 0xEE550F, + 0xED530E, + 0xEB500E, + 0xEA4E0D, + 0xE94B0C, + 0xE7490B, + 0xE6470A, + 0xE4450A, + 0xE34209, + 0xE14009, + 0xDF3E08, + 0xDE3C07, + 0xDC3A07, + 0xDA3806, + 0xD83606, + 0xD63405, + 0xD43205, + 0xD23105, + 0xD02F04, + 0xCE2D04, + 0xCC2B03, + 0xCA2903, + 0xC82803, + 0xC62602, + 0xC32402, + 0xC12302, + 0xBF2102, + 0xBC1F01, + 0xBA1E01, + 0xB71C01, + 0xB41B01, + 0xB21901, + 0xAF1801, + 0xAC1601, + 0xAA1501, + 0xA71401, + 0xA41201, + 0xA11101, + 0x9E1001, + 0x9B0F01, + 0x980D01, + 0x950C01, + 0x920B01, + 0x8E0A01, + 0x8B0901, + 0x880801, + 0x850701, + 0x810602, + 0x7E0502, + 0x7A0402, + ] + ) + + +class TeejeezWaterfall(Waterfall): + def __init__(self): + super().__init__([0x000000, 0x0000FF, 0x00FFFF, 0x00FF00, 0xFFFF00, 0xFF0000, 0xFF00FF, 0xFFFFFF]) + + +class Ha7ilmWaterfall(Waterfall): + def __init__(self): + super().__init__([0x000000, 0x2E6893, 0x69A5D0, 0x214B69, 0x9DC4E0, 0xFFF775, 0xFF8A8A, 0xB20000]) + + +class CustomWaterfall(Waterfall): + def __init__(self): + config = Config.get() + if "waterfall_colors" in config and config["waterfall_colors"]: + colors = config["waterfall_colors"] + else: + # fallback: black and white + colors = [0x000000, 0xffffff] + super().__init__(colors) + + +class WaterfallOptions(DropdownEnum): + DEFAULT = ("Google Turbo (OpenWebRX default)", GoogleTurboWaterfall) + TEEJEEZ = ("Original colorscheme by teejeez (default in OpenWebRX < 0.20)", TeejeezWaterfall) + HA7ILM = ("Old theme by HA7ILM", Ha7ilmWaterfall) + CUSTOM = ("Custom", CustomWaterfall) + + def __new__(cls, *args, **kwargs): + description, waterfallClass = args + obj = object.__new__(cls) + obj._value_ = waterfallClass.__name__ + obj.waterfallClass = waterfallClass + obj.description = description + return obj + + def __str__(self): + return self.description + + def instantiate(self): + return self.waterfallClass() + + @staticmethod + def findByColors(colors): + for o in WaterfallOptions: + if o is WaterfallOptions.CUSTOM: + continue + waterfall = o.instantiate() + if waterfall.getColors() == colors: + return o + return WaterfallOptions.CUSTOM diff --git a/owrx/websocket.py b/owrx/websocket.py new file mode 100644 index 000000000..55badf637 --- /dev/null +++ b/owrx/websocket.py @@ -0,0 +1,298 @@ +from owrx.jsons import Encoder +import base64 +import hashlib +import json +from multiprocessing import Pipe +import select +import threading +from abc import ABC, abstractmethod + +import logging + +logger = logging.getLogger(__name__) + +OPCODE_TEXT_MESSAGE = 0x01 +OPCODE_BINARY_MESSAGE = 0x02 +OPCODE_CLOSE = 0x08 +OPCODE_PING = 0x09 +OPCODE_PONG = 0x0A + + +class WebSocketException(IOError): + pass + + +class IncompleteRead(WebSocketException): + pass + + +class Drained(WebSocketException): + pass + + +class Handler(ABC): + @abstractmethod + def handleTextMessage(self, connection, message: str): + pass + + @abstractmethod + def handleBinaryMessage(self, connection, data: bytes): + pass + + @abstractmethod + def handleClose(self): + pass + + +class WebSocketConnection(object): + connections = [] + + @staticmethod + def closeAll(): + for c in WebSocketConnection.connections: + try: + c.close() + except: + logger.exception("exception while shutting down websocket connections") + + def __init__(self, handler, messageHandler: Handler): + self.handler = handler + self.handler.connection.setblocking(0) + self.messageHandler = None + self.setMessageHandler(messageHandler) + (self.interruptPipeRecv, self.interruptPipeSend) = Pipe(duplex=False) + self.open = True + self.socketError = False + self.sendLock = threading.Lock() + + headers = {key.lower(): value for key, value in self.handler.headers.items()} + if "upgrade" not in headers: + raise WebSocketException("Upgrade header not found") + if headers["upgrade"].lower() != "websocket": + raise WebSocketException("Upgrade header does not contain expected value") + if "sec-websocket-key" not in headers: + raise WebSocketException("Websocket key not provided") + + ws_key = headers["sec-websocket-key"] + shakey = hashlib.sha1() + shakey.update("{ws_key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11".format(ws_key=ws_key).encode()) + ws_key_toreturn = base64.b64encode(shakey.digest()) + self.handler.wfile.write( + "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: {0}\r\nCQ-CQ-de: HA5KFU\r\n\r\n".format( + ws_key_toreturn.decode() + ).encode() + ) + self.pingTimer = None + self.resetPing() + + def setMessageHandler(self, messageHandler: Handler): + self.messageHandler = messageHandler + + def get_header(self, size, opcode): + ws_first_byte = 0b10000000 | (opcode & 0x0F) + if size > 2 ** 16 - 1: + # frame size can be increased up to 2^64 by setting the size to 127 + # anything beyond that would need to be segmented into frames. i don't really think we'll need more. + return bytes( + [ + ws_first_byte, + 127, + (size >> 56) & 0xFF, + (size >> 48) & 0xFF, + (size >> 40) & 0xFF, + (size >> 32) & 0xFF, + (size >> 24) & 0xFF, + (size >> 16) & 0xFF, + (size >> 8) & 0xFF, + size & 0xFF, + ] + ) + elif size > 125: + # up to 2^16 can be sent using the extended payload size field by putting the size to 126 + return bytes([ws_first_byte, 126, (size >> 8) & 0xFF, size & 0xFF]) + else: + # 125 bytes binary message in a single unmasked frame + return bytes([ws_first_byte, size]) + + def send(self, data): + # convenience + if type(data) == dict: + # allow_nan = False disallows NaN and Infinty to be encoded. Browser JSON will not parse them anyway. + data = json.dumps(data, allow_nan=False, cls=Encoder) + + # string-type messages are sent as text frames + if type(data) == str: + header = self.get_header(len(data), OPCODE_TEXT_MESSAGE) + data_to_send = header + data.encode("utf-8") + # anything else as binary + else: + header = self.get_header(len(data), OPCODE_BINARY_MESSAGE) + data_to_send = header + data + + self._sendBytes(data_to_send) + + def _sendBytes(self, data_to_send): + def chunks(input, n): + """Yield successive n-sized chunks from input.""" + for i in range(0, len(input), n): + yield input[i: i + n] + + with self.sendLock: + if self.socketError: + logger.warning("_sendBytes() after socket error, ignoring") + else: + try: + for chunk in chunks(data_to_send, 1024): + (_, write, _) = select.select([], [self.handler.wfile], [], 10) + if self.handler.wfile in write: + written = self.handler.wfile.write(chunk) + if written != len(chunk): + logger.error("incomplete write! closing socket!") + self.close(socketError=True) + break + else: + logger.debug("socket not returned from select; closing") + self.close(socketError=True) + break + # these exception happen when the socket is closed + except OSError: + logger.exception("OSError while writing data") + self.close(socketError=True) + except ValueError: + logger.exception("ValueError while writing data") + self.close(socketError=True) + + def interrupt(self): + if self.interruptPipeSend is None: + logger.debug("interrupt with closed pipe") + return + self.interruptPipeSend.send(bytes(0x00)) + + def handle(self): + WebSocketConnection.connections.append(self) + try: + self.read_loop() + finally: + logger.debug("websocket loop ended; shutting down") + + self.messageHandler.handleClose() + self.cancelPing() + + if self.socketError: + logger.debug("websocket closed in error, skipping close frame") + else: + logger.debug("websocket loop ended; sending close frame") + + header = self.get_header(0, OPCODE_CLOSE) + self._sendBytes(header) + + try: + WebSocketConnection.connections.remove(self) + except ValueError: + pass + + def read_loop(self): + def protected_read(num): + data = self.handler.rfile.read(num) + if data is None: + raise Drained() + if len(data) != num: + raise IncompleteRead() + return data + + self.open = True + while self.open: + (read, _, _) = select.select([self.interruptPipeRecv, self.handler.rfile], [], [], 15) + if self.handler.rfile in read: + available = True + self.resetPing() + while self.open and available: + try: + header = protected_read(2) + opcode = header[0] & 0x0F + length = header[1] & 0x7F + mask = (header[1] & 0x80) >> 7 + if length == 126: + header = protected_read(2) + length = (header[0] << 8) + header[1] + if mask: + masking_key = protected_read(4) + data = protected_read(length) + data = bytes([b ^ masking_key[index % 4] for (index, b) in enumerate(data)]) + else: + data = protected_read(length) + if opcode == OPCODE_TEXT_MESSAGE: + message = data.decode("utf-8") + try: + self.messageHandler.handleTextMessage(self, message) + except Exception: + logger.exception("Exception in websocket handler handleTextMessage()") + elif opcode == OPCODE_BINARY_MESSAGE: + try: + self.messageHandler.handleBinaryMessage(self, data) + except Exception: + logger.exception("Exception in websocket handler handleBinaryMessage()") + elif opcode == OPCODE_PING: + self.sendPong() + elif opcode == OPCODE_PONG: + # since every read resets the ping timer, there's nothing to do here. + pass + elif opcode == OPCODE_CLOSE: + logger.debug("websocket close frame received; closing connection") + self.open = False + else: + logger.warning("unsupported opcode: {0}".format(opcode)) + except Drained: + available = False + except IncompleteRead: + logger.warning("incomplete read on websocket; closing connection") + self.socketError = True + self.open = False + except OSError: + logger.exception("OSError while reading data; closing connection") + self.socketError = True + self.open = False + + self.interruptPipeSend.close() + self.interruptPipeSend = None + # drain messages left in the queue so that the queue can be successfully closed + # this is necessary since python keeps the file descriptors open otherwise + try: + while True: + self.interruptPipeRecv.recv() + except EOFError: + pass + self.interruptPipeRecv.close() + self.interruptPipeRecv = None + + def close(self, socketError: bool = False): + # only set flag if it is True + if socketError: + self.socketError = True + if not self.open: + return + self.open = False + self.interrupt() + + def cancelPing(self): + if self.pingTimer: + old = self.pingTimer + self.pingTimer = None + old.cancel() + + def resetPing(self): + self.cancelPing() + if not self.open: + logger.debug("resetPing() while closed. passing...") + return + self.pingTimer = threading.Timer(30, self.sendPing) + self.pingTimer.start() + + def sendPing(self): + header = self.get_header(0, OPCODE_PING) + self._sendBytes(header) + self.resetPing() + + def sendPong(self): + header = self.get_header(0, OPCODE_PONG) + self._sendBytes(header) diff --git a/owrx/wsjt.py b/owrx/wsjt.py new file mode 100644 index 000000000..b1fdd9c90 --- /dev/null +++ b/owrx/wsjt.py @@ -0,0 +1,412 @@ +from datetime import datetime, timezone +from typing import List +from owrx.map import Map, LocatorLocation, CallsignSource +from owrx.metrics import Metrics, CounterMetric +from owrx.reporting import ReportingEngine +from owrx.audio import AudioChopperProfile, StaticProfileSource, ConfigWiredProfileSource +from owrx.audio.chopper import AudioChopperParser +from abc import ABC, ABCMeta, abstractmethod +from owrx.config import Config +from enum import Enum +from owrx.bands import Bandplan +import re + +import logging + +logger = logging.getLogger(__name__) + + +class WsjtProfile(AudioChopperProfile, metaclass=ABCMeta): + def decoding_depth(self): + pm = Config.get() + mode = self.getMode().lower() + # mode-specific setting? + if "wsjt_decoding_depths" in pm and mode in pm["wsjt_decoding_depths"]: + return pm["wsjt_decoding_depths"][mode] + # return global default + if "wsjt_decoding_depth" in pm: + return pm["wsjt_decoding_depth"] + # default when no setting is provided + return 3 + + def getTimestampFormat(self): + if self.getInterval() < 60: + return "%H%M%S" + return "%H%M" + + def getFileTimestampFormat(self): + return "%y%m%d_" + self.getTimestampFormat() + + @abstractmethod + def getMode(self): + pass + + +class Fst4ProfileSource(ConfigWiredProfileSource): + def getPropertiesToWire(self) -> List[str]: + return ["fst4_enabled_intervals"] + + def getProfiles(self) -> List[AudioChopperProfile]: + config = Config.get() + profiles = config["fst4_enabled_intervals"] if "fst4_enabled_intervals" in config else [] + return [Fst4Profile(i) for i in profiles if i in Fst4Profile.availableIntervals] + + +class Fst4wProfileSource(ConfigWiredProfileSource): + def getPropertiesToWire(self) -> List[str]: + return ["fst4w_enabled_intervals"] + + def getProfiles(self) -> List[AudioChopperProfile]: + config = Config.get() + profiles = config["fst4w_enabled_intervals"] if "fst4w_enabled_intervals" in config else [] + return [Fst4wProfile(i) for i in profiles if i in Fst4wProfile.availableIntervals] + + +class Q65ProfileSource(ConfigWiredProfileSource): + def getPropertiesToWire(self) -> List[str]: + return ["q65_enabled_combinations"] + + def getProfiles(self) -> List[AudioChopperProfile]: + config = Config.get() + profiles = config["q65_enabled_combinations"] if "q65_enabled_combinations" in config else [] + + def buildProfile(modestring): + try: + mode = Q65Mode[modestring[0]] + interval = Q65Interval(int(modestring[1:])) + if interval.is_available(mode): + return Q65Profile(interval, mode) + except (ValueError, KeyError): + pass + logger.warning('"%s" is not a valid Q65 mode, or an invalid mode string, ignoring', modestring) + return None + + mapped = [buildProfile(m) for m in profiles] + return [p for p in mapped if p is not None] + + +class WsjtProfiles(object): + @staticmethod + def getSource(mode: str): + if mode == "ft8": + return StaticProfileSource([Ft8Profile()]) + elif mode == "wspr": + return StaticProfileSource([WsprProfile()]) + elif mode == "jt65": + return StaticProfileSource([Jt65Profile()]) + elif mode == "jt9": + return StaticProfileSource([Jt9Profile()]) + elif mode == "ft4": + return StaticProfileSource([Ft4Profile()]) + elif mode == "fst4": + return Fst4ProfileSource() + elif mode == "fst4w": + return Fst4wProfileSource() + elif mode == "q65": + return Q65ProfileSource() + + +class Ft8Profile(WsjtProfile): + def getInterval(self): + return 15 + + def decoder_commandline(self, file): + return ["jt9", "--ft8", "-d", str(self.decoding_depth()), file] + + def getMode(self): + return "FT8" + + +class WsprProfile(WsjtProfile): + def getInterval(self): + return 120 + + def decoder_commandline(self, file): + cmd = ["wsprd"] + if self.decoding_depth() > 1: + cmd += ["-d"] + cmd += [file] + return cmd + + def getMode(self): + return "WSPR" + + +class Jt65Profile(WsjtProfile): + def getInterval(self): + return 60 + + def decoder_commandline(self, file): + return ["jt9", "--jt65", "-d", str(self.decoding_depth()), file] + + def getMode(self): + return "JT65" + + +class Jt9Profile(WsjtProfile): + def getInterval(self): + return 60 + + def decoder_commandline(self, file): + return ["jt9", "--jt9", "-d", str(self.decoding_depth()), file] + + def getMode(self): + return "JT9" + + +class Ft4Profile(WsjtProfile): + def getInterval(self): + return 7.5 + + def decoder_commandline(self, file): + return ["jt9", "--ft4", "-d", str(self.decoding_depth()), file] + + def getMode(self): + return "FT4" + + +class Fst4Profile(WsjtProfile): + availableIntervals = [15, 30, 60, 120, 300, 900, 1800] + + def __init__(self, interval): + self.interval = interval + + def getInterval(self): + return self.interval + + def decoder_commandline(self, file): + return ["jt9", "--fst4", "-p", str(self.interval), "-d", str(self.decoding_depth()), file] + + def getMode(self): + return "FST4" + + +class Fst4wProfile(WsjtProfile): + availableIntervals = [120, 300, 900, 1800] + + def __init__(self, interval): + self.interval = interval + + def getInterval(self): + return self.interval + + def decoder_commandline(self, file): + return ["jt9", "--fst4w", "-p", str(self.interval), "-d", str(self.decoding_depth()), file] + + def getMode(self): + return "FST4W" + + +class Q65Mode(Enum): + # value is the bandwidth multiplier according to https://physics.princeton.edu/pulsar/k1jt/Q65_Quick_Start.pdf + A = 1 + B = 2 + C = 4 + D = 8 + E = 16 + + def is_available(self, interval: "Q65Interval"): + return interval.is_available(self) + + +class Q65Interval(Enum): + # (interval, occupied bandwidth in mode "A") + # according to https://physics.princeton.edu/pulsar/k1jt/Q65_Quick_Start.pdf + INTERVAL_15 = (15, 433) + INTERVAL_30 = (30, 217) + INTERVAL_60 = (60, 108) + INTERVAL_120 = (120, 49) + INTERVAL_300 = (300, 19) + + def __new__(cls, *args, **kwargs): + interval, occupied_bandwidth = args + obj = object.__new__(cls) + obj._value_ = interval + obj.occupied_bandwidth = occupied_bandwidth + return obj + + def is_available(self, mode: Q65Mode): + # total bandwidth must not exceed the typical SSB bandwidth + return self.occupied_bandwidth * mode.value < 2700 + + +class Q65Profile(WsjtProfile): + def __init__(self, interval: Q65Interval, mode: Q65Mode): + self.interval = interval.value + self.mode = mode + + def getMode(self): + return "Q65" + + def getInterval(self): + return self.interval + + def decoder_commandline(self, file): + return ["jt9", "--q65", "-p", str(self.interval), "-b", self.mode.name, "-d", str(self.decoding_depth()), file] + + +class Msk144Profile(WsjtProfile): + def getMode(self): + return "MSK144" + + def getInterval(self): + return 15 + + def decoder_commandline(self, file): + return None + + +class WsjtParser(AudioChopperParser): + def parse(self, profile: WsjtProfile, freq: int, raw_msg: bytes): + try: + band = None + if freq is not None: + band = Bandplan.getSharedInstance().findBand(freq) + + msg = raw_msg.decode().rstrip() + # known debug messages we know to skip + if msg.startswith(""): + return + if msg.startswith(" EOF on input file"): + return + + mode = profile.getMode() + if mode in ["WSPR", "FST4W"]: + messageParser = BeaconMessageParser() + else: + messageParser = QsoMessageParser() + if mode == "WSPR": + decoder = WsprDecoder(profile, messageParser) + else: + decoder = Jt9Decoder(profile, messageParser) + out = decoder.parse(msg, freq) + if isinstance(profile, Q65Profile) and not out["msg"]: + # all efforts in vain, it's just a potential signal indicator + return + out["mode"] = mode + out["interval"] = profile.getInterval() + + self.pushDecode(mode, band) + if "source" in out and "locator" in out: + Map.getSharedInstance().updateLocation( + CallsignSource(**out["source"]), LocatorLocation(out["locator"]), mode, band + ) + ReportingEngine.getSharedInstance().spot(out) + + return out + except Exception: + logger.exception("Exception while parsing wsjt message") + + def pushDecode(self, mode, band): + metrics = Metrics.getSharedInstance() + bandName = "unknown" + if band is not None: + bandName = band.getName() + + if mode is None: + mode = "unknown" + + name = "wsjt.decodes.{band}.{mode}".format(band=bandName, mode=mode) + metric = metrics.getMetric(name) + if metric is None: + metric = CounterMetric() + metrics.addMetric(name, metric) + + metric.inc() + + +class Decoder(ABC): + def __init__(self, profile, messageParser): + self.profile = profile + self.messageParser = messageParser + + def parse_timestamp(self, instring): + dateformat = self.profile.getTimestampFormat() + remain = instring[len(dateformat) + 1:] + try: + ts = datetime.strptime(instring[0: len(dateformat)], dateformat) + return remain, int( + datetime.combine(datetime.utcnow().date(), ts.time()).replace(tzinfo=timezone.utc).timestamp() * 1000 + ) + except ValueError: + return remain, None + + @abstractmethod + def parse(self, msg, dial_freq): + pass + + +class MessageParser(ABC): + @abstractmethod + def parse(self, msg): + pass + + +# Used in QSO-style modes (FT8, FT4, FST4) +class QsoMessageParser(MessageParser): + locator_pattern = re.compile(".*\\s([A-Z0-9/]{2,})(\\sR)?\\s([A-R]{2}[0-9]{2})$") + + def parse(self, msg): + m = QsoMessageParser.locator_pattern.match(msg) + if m is None: + return {} + # this is a valid locator in theory, but it's somewhere in the arctic ocean, near the north pole, so it's very + # likely this just means roger roger goodbye. + if m.group(3) == "RR73": + return {"source": {"callsign": m.group(1)}} + return {"source": {"callsign": m.group(1)}, "locator": m.group(3)} + + +# Used in propagation reporting / beacon modes (WSPR / FST4W) +class BeaconMessageParser(MessageParser): + wspr_splitter_pattern = re.compile("([A-Z0-9/]*)\\s([A-R]{2}[0-9]{2})\\s([0-9]+)") + + def parse(self, msg): + m = BeaconMessageParser.wspr_splitter_pattern.match(msg) + if m is None: + return {} + return {"source": {"callsign": m.group(1)}, "locator": m.group(2), "dbm": m.group(3)} + + +class Jt9Decoder(Decoder): + def parse(self, msg, dial_freq): + # ft8 sample + # '222100 -15 -0.0 508 ~ CQ EA7MJ IM66' + # jt65 sample + # '2352 -7 0.4 1801 # R0WAS R2ABM KO85' + # '0003 -4 0.4 1762 # CQ R2ABM KO85' + # fst4 sample + # '**** -23 0.6 3023 ` <...> <...> R 591631 BI53PV' + # MSK144 sample + # '221602 8 0.4 1488 & K1JT WA4CQG EM72' + msg, timestamp = self.parse_timestamp(msg) + wsjt_msg = msg[17:53].strip() + + result = { + "timestamp": timestamp, + "db": float(msg[0:3]), + "dt": float(msg[4:8]), + "freq": dial_freq + int(msg[9:13]), + "msg": wsjt_msg, + } + result.update(self.messageParser.parse(wsjt_msg)) + return result + + +class WsprDecoder(Decoder): + def parse(self, msg, dial_freq): + # wspr sample + # '2600 -24 0.4 0.001492 -1 G8AXA JO01 33' + # '0052 -29 2.6 0.001486 0 G02CWT IO92 23' + msg, timestamp = self.parse_timestamp(msg) + wsjt_msg = msg[24:].strip() + result = { + "timestamp": timestamp, + "db": float(msg[0:3]), + "dt": float(msg[4:8]), + "freq": dial_freq + int(float(msg[10:20]) * 1e6), + "drift": int(msg[20:23]), + "msg": wsjt_msg, + } + result.update(self.messageParser.parse(wsjt_msg)) + return result diff --git a/rxws.py b/rxws.py deleted file mode 100644 index a1f210cef..000000000 --- a/rxws.py +++ /dev/null @@ -1,171 +0,0 @@ -""" -rxws: WebSocket methods implemented for OpenWebRX - - This file is part of OpenWebRX, - an open-source SDR receiver software with a web UI. - Copyright (c) 2013-2015 by Andras Retzler - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation, either version 3 of the - License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -""" - -import base64 -import sha -import select -import code - -class WebSocketException(Exception): - pass - -def handshake(myself): - my_client_id=myself.path[4:] - my_headers=myself.headers.items() - my_header_keys=map(lambda x:x[0],my_headers) - h_key_exists=lambda x:my_header_keys.count(x) - h_value=lambda x:my_headers[my_header_keys.index(x)][1] - #print "The Lambdas(tm)" - #print h_key_exists("upgrade") - #print h_value("upgrade") - #print h_key_exists("sec-websocket-key") - if (not h_key_exists("upgrade")) or not (h_value("upgrade")=="websocket") or (not h_key_exists("sec-websocket-key")): - raise WebSocketException - ws_key=h_value("sec-websocket-key") - ws_key_toreturn=base64.b64encode(sha.new(ws_key+"258EAFA5-E914-47DA-95CA-C5AB0DC85B11").digest()) - #A sample list of keys we get: [('origin', 'http://localhost:8073'), ('upgrade', 'websocket'), ('sec-websocket-extensions', 'x-webkit-deflate-frame'), ('sec-websocket-version', '13'), ('host', 'localhost:8073'), ('sec-websocket-key', 't9J1rgy4fc9fg2Hshhnkmg=='), ('connection', 'Upgrade'), ('pragma', 'no-cache'), ('cache-control', 'no-cache')] - myself.wfile.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "+ws_key_toreturn+"\r\nCQ-CQ-de: HA5KFU\r\n\r\n") - -def get_header(size): - #this does something similar: https://github.com/lemmingzshadow/php-websocket/blob/master/server/lib/WebSocket/Connection.php - ws_first_byte=0b10000010 # FIN=1, OP=2 - if(size>125): - ws_second_byte=126 # The following two bytes will indicate frame size - extended_size=chr((size>>8)&0xff)+chr(size&0xff) #Okay, it uses reverse byte order (little-endian) compared to anything else sent on TCP - else: - ws_second_byte=size - #256 bytes binary message in a single unmasked frame | 0x82 0x7E 0x0100 [256 bytes of binary data] - extended_size="" - return chr(ws_first_byte)+chr(ws_second_byte)+extended_size - -def code_payload(data, masking_key=""): - # both encode or decode - if masking_key=="": - key = (61, 84, 35, 6) - else: - key = [ord(i) for i in masking_key] - encoded="" - for i in range(0,len(data)): - encoded+=chr(ord(data[i])^key[i%4]) - return encoded - -def xxdg(data): - output="" - for i in range(0,len(data)/8): - output+=xxd(data[i:i+8]) - if i%2: output+="\n" - else: output+=" " - return output - - -def xxd(data): - #diagnostic purposes only - output="" - for d in data: - output+=hex(ord(d))[2:].zfill(2)+" " - return output - -#for R/W the WebSocket, use recv/send -#for reading the TCP socket, use readsock -#for writing the TCP socket, use myself.wfile.write and flush - -def readsock(myself,size,blocking): - #http://thenestofheliopolis.blogspot.hu/2011/01/how-to-implement-non-blocking-two-way.html - if blocking: - return myself.rfile.read(size) - else: - poll = select.poll() - poll.register(myself.rfile.fileno(), select.POLLIN or select.POLLPRI) - fd = poll.poll(0) #timeout is 0 - if len(fd): - f = fd[0] - if f[1] > 0: - return myself.rfile.read(size) - return "" - - -def recv(myself, blocking=False, debug=False): - bufsize=70000 - #myself.connection.setblocking(blocking) #umm... we cannot do that with rfile - if debug: print "ws_recv begin" - try: - data=readsock(myself,6,blocking) - #print "rxws.recv bytes:",xxd(data) - except: - if debug: print "ws_recv error" - return "" - if debug: print "ws_recv recved" - if(len(data)==0): return "" - fin=ord(data[0])&128!=0 - is_text_frame=ord(data[0])&15==1 - length=ord(data[1])&0x7f - data+=readsock(myself,length,blocking) - #print "rxws.recv length is ",length," (multiple packets together?) len(data) =",len(data) - has_one_byte_length=length<125 - masked=ord(data[1])&0x80!=0 - #print "len=", length, len(data)-2 - #print "fin, is_text_frame, has_one_byte_length, masked = ", (fin, is_text_frame, has_one_byte_length, masked) - #print xxd(data) - if fin and is_text_frame and has_one_byte_length: - if masked: - return code_payload(data[6:], data[2:6]) - else: - return data[2:] - -#Useful links for ideas on WebSockets: -# http://stackoverflow.com/questions/8125507/how-can-i-send-and-receive-websocket-messages-on-the-server-side -# https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_server -# http://tools.ietf.org/html/rfc6455#section-5.2 - - -def flush(myself): - myself.wfile.flush() - #or the socket, not the rfile: - #lR,lW,lX = select.select([],[myself.connection,],[],60) - - -def send(myself, data, begin_id="", debug=0): - base_frame_size=35000 #could guess by MTU? - debug=0 - #try: - while True: - counter=0 - from_end=len(data)-counter - if from_end+len(begin_id)>base_frame_size: - data_to_send=begin_id+data[counter:counter+base_frame_size-len(begin_id)] - header=get_header(len(data_to_send)) - flush(myself) - myself.wfile.write(header+data_to_send) - flush(myself) - if debug: print "rxws.send ==================== #1 if branch :: from={0} to={1} dlen={2} hlen={3}".format(counter,counter+base_frame_size-len(begin_id),len(data_to_send),len(header)) - else: - data_to_send=begin_id+data[counter:] - header=get_header(len(data_to_send)) - flush(myself) - myself.wfile.write(header+data_to_send) - flush(myself) - if debug: print "rxws.send :: #2 else branch :: dlen={0} hlen={1}".format(len(data_to_send),len(header)) - #if debug: print "header:\n"+xxdg(header)+"\n\nws data:\n"+xxdg(data_to_send) - break - counter+=base_frame_size-len(begin_id) - #except: - # pass diff --git a/sdrhu.py b/sdrhu.py deleted file mode 100755 index d06ae058e..000000000 --- a/sdrhu.py +++ /dev/null @@ -1,50 +0,0 @@ -#!/usr/bin/python2 -""" - - This file is part of OpenWebRX, - an open-source SDR receiver software with a web UI. - Copyright (c) 2013-2015 by Andras Retzler - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation, either version 3 of the - License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -""" - -import config_webrx as cfg, time, subprocess - -def run(continuously=True): - if not cfg.sdrhu_key: return - firsttime="(Your receiver is soon getting listed on sdr.hu!)" - while True: - cmd = "wget --timeout=15 -4qO- https://sdr.hu/update --post-data \"url=http://"+cfg.server_hostname+":"+str(cfg.web_port)+"&apikey="+cfg.sdrhu_key+"\" 2>&1" - print "[openwebrx-sdrhu]", cmd - returned=subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate() - returned=returned[0] - #print returned - if "UPDATE:" in returned: - retrytime_mins = 20 - value=returned.split("UPDATE:")[1].split("\n",1)[0] - if value.startswith("SUCCESS"): - print "[openwebrx-sdrhu] Update succeeded! "+firsttime - firsttime="" - else: - print "[openwebrx-sdrhu] Update failed, your receiver cannot be listed on sdr.hu! Reason:", value - else: - retrytime_mins = 2 - print "[openwebrx-sdrhu] wget failed while updating, your receiver cannot be listed on sdr.hu!" - if not continuously: break - time.sleep(60*retrytime_mins) - -if __name__=="__main__": - run(False) - diff --git a/setup.py b/setup.py new file mode 100644 index 000000000..7075a587d --- /dev/null +++ b/setup.py @@ -0,0 +1,31 @@ +from glob import glob +from setuptools import setup +from owrx.version import looseversion + +try: + from setuptools import find_namespace_packages +except ImportError: + from setuptools import PEP420PackageFinder + + find_namespace_packages = PEP420PackageFinder.find + +setup( + name="OpenWebRX", + version=str(looseversion), + packages=find_namespace_packages( + include=[ + "owrx*", + "csdr*", + "htdocs", + ] + ), + package_data={"htdocs": [f[len("htdocs/") :] for f in glob("htdocs/**/*", recursive=True)]}, + entry_points={"console_scripts": ["openwebrx=owrx.__main__:main"]}, + url="https://www.openwebrx.de/", + author="Jakob Ketterl", + author_email="jakob.ketterl@gmx.de", + maintainer="Jakob Ketterl", + maintainer_email="jakob.ketterl@gmx.de", + license="GAGPL", + python_requires=">=3.5", +) diff --git a/systemd/openwebrx.service b/systemd/openwebrx.service new file mode 100644 index 000000000..8b878336b --- /dev/null +++ b/systemd/openwebrx.service @@ -0,0 +1,12 @@ +[Unit] +Description=OpenWebRX WebSDR receiver + +[Service] +Type=simple +User=openwebrx +Group=openwebrx +ExecStart=/usr/bin/openwebrx +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/property/__init__.py b/test/property/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/property/filter/__init__.py b/test/property/filter/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/property/filter/test_by_lambda.py b/test/property/filter/test_by_lambda.py new file mode 100644 index 000000000..14c6daf75 --- /dev/null +++ b/test/property/filter/test_by_lambda.py @@ -0,0 +1,17 @@ +from owrx.property.filter import ByLambda +from unittest import TestCase +from unittest.mock import Mock + + +class TestByLambda(TestCase): + def testPositive(self): + mock = Mock(return_value=True) + filter = ByLambda(mock) + self.assertTrue(filter.apply("test_key")) + mock.assert_called_with("test_key") + + def testNegateive(self): + mock = Mock(return_value=False) + filter = ByLambda(mock) + self.assertFalse(filter.apply("test_key")) + mock.assert_called_with("test_key") diff --git a/test/property/filter/test_by_property_name.py b/test/property/filter/test_by_property_name.py new file mode 100644 index 000000000..098f69b5a --- /dev/null +++ b/test/property/filter/test_by_property_name.py @@ -0,0 +1,12 @@ +from owrx.property.filter import ByPropertyName +from unittest import TestCase + + +class ByPropertyNameTest(TestCase): + def testNameIsInList(self): + filter = ByPropertyName("test_key") + self.assertTrue(filter.apply("test_key")) + + def testNameNotInList(self): + filter = ByPropertyName("test_key") + self.assertFalse(filter.apply("other_key")) diff --git a/test/property/test_property_carousel.py b/test/property/test_property_carousel.py new file mode 100644 index 000000000..bc0c837d7 --- /dev/null +++ b/test/property/test_property_carousel.py @@ -0,0 +1,125 @@ +from unittest import TestCase +from unittest.mock import Mock +from owrx.property import PropertyCarousel, PropertyLayer, PropertyDeleted, PropertyWriteError + + +class PropertyCarouselTest(TestCase): + def testInitiallyEmpty(self): + pc = PropertyCarousel() + with self.assertRaises(KeyError): + x = pc["testkey"] + + def testPropertyAccess(self): + pc = PropertyCarousel() + pl = PropertyLayer(testkey="testvalue") + pc.addLayer("test", pl) + pc.switch("test") + self.assertEqual(pc["testkey"], "testvalue") + + def testWriteAccess(self): + pc = PropertyCarousel() + pl = PropertyLayer(testkey="old_value") + pc.addLayer("test", pl) + pc.switch("test") + pc["testkey"] = "new_value" + self.assertEqual(pc["testkey"], "new_value") + self.assertEqual(pl["testkey"], "new_value") + + def testForwardsEvents(self): + pc = PropertyCarousel() + pl = PropertyLayer(testkey="old_value") + pc.addLayer("test", pl) + pc.switch("test") + mock = Mock() + pc.wire(mock.method) + pc["testkey"] = "new_value" + mock.method.assert_called_once_with({"testkey": "new_value"}) + + def testStopsForwardingAfterSwitch(self): + pc = PropertyCarousel() + pl_x = PropertyLayer(testkey="old_value") + pc.addLayer("x", pl_x) + pl_y = PropertyLayer(testkey="new_value") + pc.addLayer("y", pl_y) + pc.switch("x") + pc.switch("y") + mock = Mock() + pc.wire(mock.method) + pl_x["testkey"] = "new_value" + mock.method.assert_not_called() + + def testEventsOnSwitch(self): + pc = PropertyCarousel() + pl_x = PropertyLayer(old_key="old_value") + pc.addLayer("x", pl_x) + pl_y = PropertyLayer(new_key="new_value") + pc.addLayer("y", pl_y) + pc.switch("x") + mock = Mock() + pc.wire(mock.method) + pc.switch("y") + mock.method.assert_called_once_with({"old_key": PropertyDeleted, "new_key": "new_value"}) + + def testNoEventsIfKeysDontChange(self): + pc = PropertyCarousel() + pl_x = PropertyLayer(testkey="same_value") + pc.addLayer("x", pl_x) + pl_y = PropertyLayer(testkey="same_value") + pc.addLayer("y", pl_y) + pc.switch("x") + mock = Mock() + pc.wire(mock.method) + pc.switch("y") + mock.method.assert_not_called() + + def testKeyErrorOnInvalidSwitch(self): + pc = PropertyCarousel() + with self.assertRaises(KeyError): + pc.switch("doesntmatter") + + def testRemoveLayer(self): + pc = PropertyCarousel() + pl = PropertyLayer(testkey="testvalue") + pc.addLayer("x", pl) + pc.switch("x") + self.assertEqual(pc["testkey"], "testvalue") + pc.removeLayer("x") + with self.assertRaises(KeyError): + pc.switch("x") + + def testPropertyResetAfterRemoval(self): + pc = PropertyCarousel() + pl = PropertyLayer(testkey="testvalue") + pc.addLayer("x", pl) + pc.switch("x") + self.assertEqual(pc["testkey"], "testvalue") + pc.removeLayer("x") + with self.assertRaises(KeyError): + x = pc["testkey"] + + def testEmptySwitch(self): + pc = PropertyCarousel() + pl = PropertyLayer(testkey="testvalue") + pc.addLayer("x", pl) + pc.switch("x") + self.assertEqual(pc["testkey"], "testvalue") + pc.switch() + with self.assertRaises(KeyError): + x = pc["testkey"] + + def testErrorOnWriteOnDefaultLayer(self): + pc = PropertyCarousel() + with self.assertRaises(PropertyWriteError): + pc["testkey"] = "testvalue" + + def testSendsChangesIfActiveLayerIsReplaced(self): + pc = PropertyCarousel() + pl = PropertyLayer(testkey="testvalue") + pc.addLayer("x", pl) + pc.switch("x") + self.assertEqual(pc["testkey"], "testvalue") + mock = Mock() + pc.wire(mock.method) + pl = PropertyLayer(testkey="othervalue") + pc.addLayer("x", pl) + mock.method.assert_called_once_with({"testkey": "othervalue"}) diff --git a/test/property/test_property_deletion.py b/test/property/test_property_deletion.py new file mode 100644 index 000000000..b68cf08a7 --- /dev/null +++ b/test/property/test_property_deletion.py @@ -0,0 +1,8 @@ +from unittest import TestCase +from owrx.property import PropertyDeletion + + +class PropertyDeletionTest(TestCase): + def testDeletionEvaluatesToFalse(self): + deletion = PropertyDeletion() + self.assertFalse(deletion) diff --git a/test/property/test_property_filter.py b/test/property/test_property_filter.py new file mode 100644 index 000000000..82f596174 --- /dev/null +++ b/test/property/test_property_filter.py @@ -0,0 +1,82 @@ +from unittest import TestCase +from unittest.mock import Mock +from owrx.property import PropertyLayer, PropertyFilter, PropertyDeleted + + +class PropertyFilterTest(TestCase): + def testPassesProperty(self): + pm = PropertyLayer() + pm["testkey"] = "testvalue" + mock = Mock() + mock.apply.return_value = True + pf = PropertyFilter(pm, mock) + self.assertEqual(pf["testkey"], "testvalue") + + def testMissesProperty(self): + pm = PropertyLayer() + pm["testkey"] = "testvalue" + mock = Mock() + mock.apply.return_value = False + pf = PropertyFilter(pm, mock) + self.assertFalse("testkey" in pf) + with self.assertRaises(KeyError): + x = pf["testkey"] + + def testForwardsEvent(self): + pm = PropertyLayer() + mock = Mock() + mock.apply.return_value = True + pf = PropertyFilter(pm, mock) + mock = Mock() + pf.wire(mock.method) + pm["testkey"] = "testvalue" + mock.method.assert_called_once_with({"testkey": "testvalue"}) + + def testForwardsPropertyEvent(self): + pm = PropertyLayer() + mock = Mock() + mock.apply.return_value = True + pf = PropertyFilter(pm, mock) + mock = Mock() + pf.wireProperty("testkey", mock.method) + pm["testkey"] = "testvalue" + mock.method.assert_called_once_with("testvalue") + + def testForwardsWrite(self): + pm = PropertyLayer() + mock = Mock() + mock.apply.return_value = True + pf = PropertyFilter(pm, mock) + pf["testkey"] = "testvalue" + self.assertTrue("testkey" in pm) + self.assertEqual(pm["testkey"], "testvalue") + + def testOverwrite(self): + pm = PropertyLayer() + pm["testkey"] = "old value" + mock = Mock() + mock.apply.return_value = True + pf = PropertyFilter(pm, mock) + pf["testkey"] = "new value" + self.assertEqual(pm["testkey"], "new value") + self.assertEqual(pf["testkey"], "new value") + + def testRejectsWrite(self): + pm = PropertyLayer() + pm["testkey"] = "old value" + mock = Mock() + mock.apply.return_value = False + pf = PropertyFilter(pm, mock) + with self.assertRaises(KeyError): + pf["testkey"] = "new value" + self.assertEqual(pm["testkey"], "old value") + + def testPropagatesDeletion(self): + pm = PropertyLayer(testkey="somevalue") + filter_mock = Mock() + filter_mock.apply.return_value = True + pf = PropertyFilter(pm, filter_mock) + mock = Mock() + pf.wire(mock.method) + del pf["testkey"] + mock.method.assert_called_once_with({"testkey": PropertyDeleted}) diff --git a/test/property/test_property_layer.py b/test/property/test_property_layer.py new file mode 100644 index 000000000..f07ae1b0c --- /dev/null +++ b/test/property/test_property_layer.py @@ -0,0 +1,93 @@ +from owrx.property import PropertyLayer, PropertyDeleted +from unittest import TestCase +from unittest.mock import Mock + + +class PropertyLayerTest(TestCase): + def testCreationWithKwArgs(self): + pm = PropertyLayer(testkey="value") + self.assertEqual(pm["testkey"], "value") + + # this should be synonymous, so this is rather for illustration purposes + contents = {"testkey": "value"} + pm = PropertyLayer(**contents) + self.assertEqual(pm["testkey"], "value") + + def testKeyIsset(self): + pm = PropertyLayer() + self.assertFalse("some_key" in pm) + + def testKeyError(self): + pm = PropertyLayer() + with self.assertRaises(KeyError): + x = pm["some_key"] + + def testSubscription(self): + pm = PropertyLayer() + pm["testkey"] = "before" + mock = Mock() + pm.wire(mock.method) + pm["testkey"] = "after" + mock.method.assert_called_once_with({"testkey": "after"}) + + def testUnsubscribe(self): + pm = PropertyLayer() + pm["testkey"] = "before" + mock = Mock() + sub = pm.wire(mock.method) + pm["testkey"] = "between" + mock.method.assert_called_once_with({"testkey": "between"}) + + mock.reset_mock() + pm.unwire(sub) + pm["testkey"] = "after" + mock.method.assert_not_called() + + def testContains(self): + pm = PropertyLayer() + pm["testkey"] = "value" + self.assertTrue("testkey" in pm) + + def testDoesNotContain(self): + pm = PropertyLayer() + self.assertFalse("testkey" in pm) + + def testSubscribeBeforeSet(self): + pm = PropertyLayer() + mock = Mock() + pm.wireProperty("testkey", mock.method) + mock.method.assert_not_called() + pm["testkey"] = "newvalue" + mock.method.assert_called_once_with("newvalue") + + def testEventPreventedWhenValueUnchanged(self): + pm = PropertyLayer() + pm["testkey"] = "testvalue" + mock = Mock() + pm.wire(mock.method) + pm["testkey"] = "testvalue" + mock.method.assert_not_called() + + def testDeletionIsSent(self): + pm = PropertyLayer(testkey="somevalue") + mock = Mock() + pm.wireProperty("testkey", mock.method) + mock.method.reset_mock() + del pm["testkey"] + mock.method.assert_called_once_with(PropertyDeleted) + + def testDeletionInGeneralWiring(self): + pm = PropertyLayer(testkey="somevalue") + mock = Mock() + pm.wire(mock.method) + del pm["testkey"] + mock.method.assert_called_once_with({"testkey": PropertyDeleted}) + + def testNoDeletionEventWhenPropertyDoesntExist(self): + pm = PropertyLayer(otherkey="somevalue") + mock = Mock() + pm.wireProperty("testkey", mock.method) + mock.method.reset_mock() + with self.assertRaises(KeyError): + del pm["testkey"] + mock.method.assert_not_called() diff --git a/test/property/test_property_readonly.py b/test/property/test_property_readonly.py new file mode 100644 index 000000000..09d57ee18 --- /dev/null +++ b/test/property/test_property_readonly.py @@ -0,0 +1,23 @@ +from unittest import TestCase +from owrx.property import PropertyLayer, PropertyReadOnly, PropertyWriteError + + +class PropertyReadOnlyTest(TestCase): + def testPreventsWrites(self): + layer = PropertyLayer() + layer["testkey"] = "initial value" + ro = PropertyReadOnly(layer) + with self.assertRaises(PropertyWriteError): + ro["testkey"] = "new value" + with self.assertRaises(PropertyWriteError): + ro["otherkey"] = "testvalue" + self.assertEqual(ro["testkey"], "initial value") + self.assertNotIn("otherkey", ro) + + def testPreventsDeletes(self): + layer = PropertyLayer(testkey="some value") + ro = PropertyReadOnly(layer) + with self.assertRaises(PropertyWriteError): + del ro["testkey"] + self.assertEqual(ro["testkey"], "some value") + self.assertEqual(layer["testkey"], "some value") diff --git a/test/property/test_property_stack.py b/test/property/test_property_stack.py new file mode 100644 index 000000000..860f13f36 --- /dev/null +++ b/test/property/test_property_stack.py @@ -0,0 +1,240 @@ +from unittest import TestCase +from unittest.mock import Mock +from owrx.property import PropertyLayer, PropertyStack, PropertyDeleted + + +class PropertyStackTest(TestCase): + def testLayer(self): + om = PropertyStack() + pm = PropertyLayer() + pm["testkey"] = "testvalue" + om.addLayer(1, pm) + self.assertEqual(om["testkey"], "testvalue") + + def testHighPriority(self): + om = PropertyStack() + low_pm = PropertyLayer() + high_pm = PropertyLayer() + low_pm["testkey"] = "low value" + high_pm["testkey"] = "high value" + om.addLayer(1, low_pm) + om.addLayer(0, high_pm) + self.assertEqual(om["testkey"], "high value") + + def testPriorityFallback(self): + om = PropertyStack() + low_pm = PropertyLayer() + high_pm = PropertyLayer() + low_pm["testkey"] = "low value" + om.addLayer(1, low_pm) + om.addLayer(0, high_pm) + self.assertEqual(om["testkey"], "low value") + + def testLayerRemoval(self): + om = PropertyStack() + low_pm = PropertyLayer() + high_pm = PropertyLayer() + low_pm["testkey"] = "low value" + high_pm["testkey"] = "high value" + om.addLayer(1, low_pm) + om.addLayer(0, high_pm) + self.assertEqual(om["testkey"], "high value") + om.removeLayer(high_pm) + self.assertEqual(om["testkey"], "low value") + + def testLayerRemovalByPriority(self): + om = PropertyStack() + low_pm = PropertyLayer() + high_pm = PropertyLayer() + low_pm["testkey"] = "low value" + high_pm["testkey"] = "high value" + om.addLayer(1, low_pm) + om.addLayer(0, high_pm) + self.assertEqual(om["testkey"], "high value") + om.removeLayerByPriority(0) + self.assertEqual(om["testkey"], "low value") + + def testPropertyChange(self): + layer = PropertyLayer() + stack = PropertyStack() + stack.addLayer(0, layer) + mock = Mock() + stack.wire(mock.method) + layer["testkey"] = "testvalue" + mock.method.assert_called_once_with({"testkey": "testvalue"}) + + def testPropertyChangeEventPriority(self): + low_layer = PropertyLayer() + high_layer = PropertyLayer() + low_layer["testkey"] = "initial low value" + high_layer["testkey"] = "initial high value" + stack = PropertyStack() + stack.addLayer(1, low_layer) + stack.addLayer(0, high_layer) + mock = Mock() + stack.wire(mock.method) + low_layer["testkey"] = "modified low value" + mock.method.assert_not_called() + high_layer["testkey"] = "modified high value" + mock.method.assert_called_once_with({"testkey": "modified high value"}) + + def testPropertyEventOnLayerAdd(self): + low_layer = PropertyLayer() + low_layer["testkey"] = "low value" + stack = PropertyStack() + stack.addLayer(1, low_layer) + mock = Mock() + stack.wireProperty("testkey", mock.method) + mock.reset_mock() + high_layer = PropertyLayer() + high_layer["testkey"] = "high value" + stack.addLayer(0, high_layer) + mock.method.assert_called_once_with("high value") + + def testNoEventOnExistingValue(self): + low_layer = PropertyLayer() + low_layer["testkey"] = "same value" + stack = PropertyStack() + stack.addLayer(1, low_layer) + mock = Mock() + stack.wireProperty("testkey", mock.method) + mock.reset_mock() + high_layer = PropertyLayer() + high_layer["testkey"] = "same value" + stack.addLayer(0, high_layer) + mock.method.assert_not_called() + + def testEventOnLayerWithNewProperty(self): + low_layer = PropertyLayer() + low_layer["existingkey"] = "existing value" + stack = PropertyStack() + stack.addLayer(1, low_layer) + mock = Mock() + stack.wireProperty("newkey", mock.method) + high_layer = PropertyLayer() + high_layer["newkey"] = "new value" + stack.addLayer(0, high_layer) + mock.method.assert_called_once_with("new value") + + def testEventOnLayerRemoval(self): + low_layer = PropertyLayer() + high_layer = PropertyLayer() + stack = PropertyStack() + stack.addLayer(1, low_layer) + stack.addLayer(0, high_layer) + low_layer["testkey"] = "low value" + high_layer["testkey"] = "high value" + + mock = Mock() + stack.wireProperty("testkey", mock.method) + mock.method.assert_called_once_with("high value") + mock.reset_mock() + stack.removeLayer(high_layer) + mock.method.assert_called_once_with("low value") + + def testNoneOnKeyRemoval(self): + low_layer = PropertyLayer() + high_layer = PropertyLayer() + stack = PropertyStack() + stack.addLayer(1, low_layer) + stack.addLayer(0, high_layer) + low_layer["testkey"] = "low value" + high_layer["testkey"] = "high value" + high_layer["unique key"] = "unique value" + + mock = Mock() + stack.wireProperty("unique key", mock.method) + mock.method.assert_called_once_with("unique value") + mock.reset_mock() + stack.removeLayer(high_layer) + mock.method.assert_called_once_with(PropertyDeleted) + + def testReplaceLayer(self): + first_layer = PropertyLayer() + first_layer["testkey"] = "old value" + second_layer = PropertyLayer() + second_layer["testkey"] = "new value" + + stack = PropertyStack() + stack.addLayer(0, first_layer) + + mock = Mock() + stack.wireProperty("testkey", mock.method) + mock.method.assert_called_once_with("old value") + mock.reset_mock() + + stack.replaceLayer(0, second_layer) + mock.method.assert_called_once_with("new value") + + def testUnwiresEventsOnRemoval(self): + layer = PropertyLayer() + layer["testkey"] = "before" + stack = PropertyStack() + stack.addLayer(0, layer) + mock = Mock() + stack.wire(mock.method) + stack.removeLayer(layer) + mock.method.assert_called_once_with({"testkey": PropertyDeleted}) + mock.reset_mock() + + layer["testkey"] = "after" + mock.method.assert_not_called() + + def testReplaceLayerNoEventWhenValueUnchanged(self): + fixed = PropertyLayer() + fixed["testkey"] = "fixed value" + first_layer = PropertyLayer() + first_layer["testkey"] = "same value" + second_layer = PropertyLayer() + second_layer["testkey"] = "same value" + + stack = PropertyStack() + stack.addLayer(1, fixed) + stack.addLayer(0, first_layer) + mock = Mock() + stack.wire(mock.method) + mock.method.assert_not_called() + + stack.replaceLayer(0, second_layer) + mock.method.assert_not_called() + + def testWritesToExpectedLayer(self): + om = PropertyStack() + low_pm = PropertyLayer() + high_pm = PropertyLayer() + low_pm["testkey"] = "low value" + om.addLayer(1, low_pm) + om.addLayer(0, high_pm) + om["testkey"] = "new value" + self.assertEqual(low_pm["testkey"], "new value") + + def testDeletionEvent(self): + ps = PropertyStack() + pm = PropertyLayer(testkey="testvalue") + ps.addLayer(0, pm) + mock = Mock() + ps.wire(mock.method) + del ps["testkey"] + mock.method.assert_called_once_with({"testkey": PropertyDeleted}) + + def testDeletionWithSecondLayer(self): + ps = PropertyStack() + low_pm = PropertyLayer(testkey="testvalue") + high_pm = PropertyLayer() + ps.addLayer(0, high_pm) + ps.addLayer(1, low_pm) + mock = Mock() + ps.wire(mock.method) + del low_pm["testkey"] + mock.method.assert_called_once_with({"testkey": PropertyDeleted}) + + def testChangeEventWhenKeyDeleted(self): + ps = PropertyStack() + low_pm = PropertyLayer(testkey="lowvalue") + high_pm = PropertyLayer(testkey="highvalue") + ps.addLayer(0, high_pm) + ps.addLayer(1, low_pm) + mock = Mock() + ps.wire(mock.method) + del high_pm["testkey"] + mock.method.assert_called_once_with({"testkey": "lowvalue"}) diff --git a/test/property/test_property_validator.py b/test/property/test_property_validator.py new file mode 100644 index 000000000..a246031ae --- /dev/null +++ b/test/property/test_property_validator.py @@ -0,0 +1,37 @@ +from unittest import TestCase +from owrx.property import PropertyLayer, PropertyValidator, PropertyValidationError +from owrx.property.validators import NumberValidator, StringValidator + + +class PropertyValidatorTest(TestCase): + def testPassesUnvalidated(self): + pm = PropertyLayer() + pv = PropertyValidator(pm) + pv["testkey"] = "testvalue" + self.assertEqual(pv["testkey"], "testvalue") + self.assertEqual(pm["testkey"], "testvalue") + + def testPassesValidValue(self): + pv = PropertyValidator(PropertyLayer(), {"testkey": NumberValidator()}) + pv["testkey"] = 42 + self.assertEqual(pv["testkey"], 42) + + def testThrowsErrorOnInvalidValue(self): + pv = PropertyValidator(PropertyLayer(), {"testkey": NumberValidator()}) + with self.assertRaises(PropertyValidationError): + pv["testkey"] = "text" + + def testSetValidator(self): + pv = PropertyValidator(PropertyLayer()) + pv.setValidator("testkey", NumberValidator()) + with self.assertRaises(PropertyValidationError): + pv["testkey"] = "text" + + def testUpdateValidator(self): + pv = PropertyValidator(PropertyLayer(), {"testkey": StringValidator()}) + # this should pass + pv["testkey"] = "text" + pv.setValidator("testkey", NumberValidator()) + # this should raise + with self.assertRaises(PropertyValidationError): + pv["testkey"] = "text" diff --git a/test/property/validators/__init__.py b/test/property/validators/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/property/validators/test_bool_validator.py b/test/property/validators/test_bool_validator.py new file mode 100644 index 000000000..08cfea601 --- /dev/null +++ b/test/property/validators/test_bool_validator.py @@ -0,0 +1,17 @@ +from unittest import TestCase +from owrx.property.validators import BoolValidator + + +class NumberValidatorTest(TestCase): + def testPassesNumbers(self): + validator = BoolValidator() + self.assertTrue(validator.isValid(True)) + self.assertTrue(validator.isValid(False)) + + def testDoesntPassOthers(self): + validator = BoolValidator() + self.assertFalse(validator.isValid(123)) + self.assertFalse(validator.isValid(-2)) + self.assertFalse(validator.isValid(.5)) + self.assertFalse(validator.isValid("text")) + self.assertFalse(validator.isValid(object())) diff --git a/test/property/validators/test_float_validator.py b/test/property/validators/test_float_validator.py new file mode 100644 index 000000000..f4e43ecf1 --- /dev/null +++ b/test/property/validators/test_float_validator.py @@ -0,0 +1,15 @@ +from unittest import TestCase +from owrx.property.validators import FloatValidator + + +class FloatValidatorTest(TestCase): + def testPassesNumbers(self): + validator = FloatValidator() + self.assertTrue(validator.isValid(.5)) + + def testDoesntPassOthers(self): + validator = FloatValidator() + self.assertFalse(validator.isValid(123)) + self.assertFalse(validator.isValid(-2)) + self.assertFalse(validator.isValid("text")) + self.assertFalse(validator.isValid(object())) diff --git a/test/property/validators/test_integer_validator.py b/test/property/validators/test_integer_validator.py new file mode 100644 index 000000000..454918aee --- /dev/null +++ b/test/property/validators/test_integer_validator.py @@ -0,0 +1,15 @@ +from unittest import TestCase +from owrx.property.validators import IntegerValidator + + +class IntegerValidatorTest(TestCase): + def testPassesIntegers(self): + validator = IntegerValidator() + self.assertTrue(validator.isValid(123)) + self.assertTrue(validator.isValid(-2)) + + def testDoesntPassOthers(self): + validator = IntegerValidator() + self.assertFalse(validator.isValid(.5)) + self.assertFalse(validator.isValid("text")) + self.assertFalse(validator.isValid(object())) diff --git a/test/property/validators/test_lambda_validator.py b/test/property/validators/test_lambda_validator.py new file mode 100644 index 000000000..969dce649 --- /dev/null +++ b/test/property/validators/test_lambda_validator.py @@ -0,0 +1,21 @@ +from unittest import TestCase +from unittest.mock import Mock +from owrx.property.validators import LambdaValidator + + +class LambdaValidatorTest(TestCase): + def testPassesValue(self): + mock = Mock() + validator = LambdaValidator(mock.method) + validator.isValid("test") + mock.method.assert_called_once_with("test") + + def testReturnsTrue(self): + validator = LambdaValidator(lambda x: True) + self.assertTrue(validator.isValid("any value")) + self.assertTrue(validator.isValid(3.1415926)) + + def testReturnsFalse(self): + validator = LambdaValidator(lambda x: False) + self.assertFalse(validator.isValid("any value")) + self.assertFalse(validator.isValid(42)) diff --git a/test/property/validators/test_number_validator.py b/test/property/validators/test_number_validator.py new file mode 100644 index 000000000..3eff11b55 --- /dev/null +++ b/test/property/validators/test_number_validator.py @@ -0,0 +1,18 @@ +from unittest import TestCase +from owrx.property.validators import NumberValidator + + +class NumberValidatorTest(TestCase): + def testPassesNumbers(self): + validator = NumberValidator() + self.assertTrue(validator.isValid(123)) + self.assertTrue(validator.isValid(-2)) + self.assertTrue(validator.isValid(.5)) + + def testDoesntPassOthers(self): + validator = NumberValidator() + # bool is a subclass of int, so it passes this test. + # not sure if we need to be more specific or if this is alright. + # self.assertFalse(validator.isValid(True)) + self.assertFalse(validator.isValid("text")) + self.assertFalse(validator.isValid(object())) diff --git a/test/property/validators/test_or_validator.py b/test/property/validators/test_or_validator.py new file mode 100644 index 000000000..0f7f79d1d --- /dev/null +++ b/test/property/validators/test_or_validator.py @@ -0,0 +1,17 @@ +from unittest import TestCase +from owrx.property.validators import OrValidator, IntegerValidator, StringValidator + + +class OrValidatorTest(TestCase): + def testPassesAnyValidators(self): + validator = OrValidator(IntegerValidator(), StringValidator()) + self.assertTrue(validator.isValid(42)) + self.assertTrue(validator.isValid("text")) + + def testRejectsOtherTypes(self): + validator = OrValidator(IntegerValidator(), StringValidator()) + self.assertFalse(validator.isValid(.5)) + + def testRejectsIfNoValidator(self): + validator = OrValidator() + self.assertFalse(validator.isValid("any value")) diff --git a/test/property/validators/test_regex_validator.py b/test/property/validators/test_regex_validator.py new file mode 100644 index 000000000..2151d1b95 --- /dev/null +++ b/test/property/validators/test_regex_validator.py @@ -0,0 +1,17 @@ +from unittest import TestCase +from owrx.property.validators import RegexValidator +import re + + +class RegexValidatorTest(TestCase): + def testMatchesRegex(self): + validator = RegexValidator(re.compile("abc")) + self.assertTrue(validator.isValid("abc")) + + def testDoesntMatchRegex(self): + validator = RegexValidator(re.compile("abc")) + self.assertFalse(validator.isValid("xyz")) + + def testFailsIfValueIsNoString(self): + validator = RegexValidator(re.compile("abc")) + self.assertFalse(validator.isValid(42)) diff --git a/test/property/validators/test_string_validator.py b/test/property/validators/test_string_validator.py new file mode 100644 index 000000000..d285f1a26 --- /dev/null +++ b/test/property/validators/test_string_validator.py @@ -0,0 +1,14 @@ +from unittest import TestCase +from owrx.property.validators import StringValidator + +class StringValidatorTest(TestCase): + def testPassesStrings(self): + validator = StringValidator() + self.assertTrue(validator.isValid("text")) + + def testDoesntPassOthers(self): + validator = StringValidator() + self.assertFalse(validator.isValid(123)) + self.assertFalse(validator.isValid(-2)) + self.assertFalse(validator.isValid(.5)) + self.assertFalse(validator.isValid(object())) diff --git a/test/property/validators/test_validator.py b/test/property/validators/test_validator.py new file mode 100644 index 000000000..6fbbf785a --- /dev/null +++ b/test/property/validators/test_validator.py @@ -0,0 +1,20 @@ +from unittest import TestCase +from owrx.property.validators import Validator, NumberValidator, LambdaValidator, StringValidator + + +class ValidatorTest(TestCase): + + def testReturnsValidator(self): + validator = NumberValidator() + self.assertIs(validator, Validator.of(validator)) + + def testTransformsLambda(self): + def my_callable(v): + return True + validator = Validator.of(my_callable) + self.assertIsInstance(validator, LambdaValidator) + self.assertTrue(validator.isValid("test")) + + def testGetsValidatorByKey(self): + validator = Validator.of("str") + self.assertIsInstance(validator, StringValidator)
    ' + b.name + '' + renderFrequency(b.frequency) + '' + modulation + '
    ' + bookmark.name + '' + renderFrequency(bookmark.frequency) +'' + modulation_name + '' + + '' + + '
    ').append(i); + })); + } + + WsjtDecodingDepthRow.prototype.getEl = function() { + return this.el; + } + + WsjtDecodingDepthRow.prototype.getValue = function() { + var value = parseInt(this.valueInput.val()) + if (Number.isNaN(value)) { + return {}; + } + return Object.fromEntries([[this.modeInput.val(), value]]); + } + + this.each(function(){ + var $input = $(this); + var $el = $input.parent(); + var $inputs = $el.find('.inputs') + var inputs = $inputs.find('input, select'); + $inputs.remove(); + var rows = $.map(JSON.parse($input.val()), function(value, mode) { + return new WsjtDecodingDepthRow(inputs, mode, value); + }); + var $table = $(''); + $table.append(rows.map(function(r) { + return r.getEl(); + })); + + var updateValue = function(){ + $input.val(JSON.stringify($.extend.apply({}, rows.map(function(r) { + return r.getValue(); + })))); + }; + + $table.on('change', updateValue); + var $addButton = $(''); + + $addButton.on('click', function() { + var row = new WsjtDecodingDepthRow(inputs) + rows.push(row); + $table.append(row.getEl()); + return false; + }); + $el.on('click', '.btn.remove', function(e){ + var row = $(e.target).data('row'); + var index = rows.indexOf(row); + if (index < 0) return false; + rows.splice(index, 1); + row.getEl().remove(); + updateValue(); + return false; + }); + + $input.after($table, $addButton); + }); +}; \ No newline at end of file diff --git a/htdocs/lib/wheelDelta.js b/htdocs/lib/wheelDelta.js new file mode 100644 index 000000000..f3ff847a1 --- /dev/null +++ b/htdocs/lib/wheelDelta.js @@ -0,0 +1,22 @@ +/* + * Normalize scroll wheel events. + * + * It seems like there's no consent as to how mouse wheel events are presented in the javascript API. The standard + * states that a MouseEvent has a deltaY property that contains the scroll distances, together with a deltaMode + * property that state the "unit" that deltaY has been measured in. The deltaMode can be either pixels, lines or + * pages. The latter is seldomly used in practise. + * + * The troublesome part is that there is no standard on how to correlate the two at this point. + * + * The basic idea is that one tick of a mouse wheel results in a total return value of +/- 1 from this method. + * It's important to keep in mind that one tick of a wheel may result in multiple events in the browser. The aim + * of this method is to scale the sum of deltaY over + */ +function wheelDelta(evt) { + if ('deltaMode' in evt && evt.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { + // chrome and webkit-based browsers seem to correlate one tick of the wheel to 120 pixels. + return evt.deltaY / 120; + } + // firefox seems to scroll at an interval of 6 lines per wheel tick + return evt.deltaY / 6; +} diff --git a/htdocs/login.html b/htdocs/login.html new file mode 100644 index 000000000..6c05ef9ff --- /dev/null +++ b/htdocs/login.html @@ -0,0 +1,29 @@ + + + + OpenWebRX Login + + + + + + + + + ${header} + + \ No newline at end of file diff --git a/htdocs/map.html b/htdocs/map.html new file mode 100644 index 000000000..f93d2e24d --- /dev/null +++ b/htdocs/map.html @@ -0,0 +1,26 @@ + + + + OpenWebRX Map + + + + + + + + + + + ${header} +
    +
    +

    Colors

    + +
    +
    + + diff --git a/htdocs/map.js b/htdocs/map.js new file mode 100644 index 000000000..ed7cdcfff --- /dev/null +++ b/htdocs/map.js @@ -0,0 +1,605 @@ +$(function(){ + var query = new URLSearchParams(window.location.search); + + var expectedCallsign; + if (query.has('callsign')) { + expectedCallsign = Object.fromEntries(query.entries()); + } + var expectedLocator; + if (query.has('locator')) expectedLocator = query.get('locator'); + var expectedIcao; + if (query.has('icao')) expectedIcao = query.get('icao'); + + var protocol = window.location.protocol.match(/https/) ? 'wss' : 'ws'; + + var href = window.location.href; + var index = href.lastIndexOf('/'); + if (index > 0) { + href = href.substr(0, index + 1); + } + href = href.split("://")[1]; + href = protocol + "://" + href; + if (!href.endsWith('/')) { + href += '/'; + } + var ws_url = href + "ws/"; + + var map; + var markers = {}; + var rectangles = {}; + var receiverMarker; + var updateQueue = []; + + var strokeOpacity = 0.8; + var fillOpacity = 0.35; + var callsign_service; + var aircraft_tracking_service; + + var colorKeys = {}; + var colorScale = chroma.scale(['red', 'blue', 'green']).mode('hsl'); + var getColor = function(id){ + if (!id) return "#000000"; + if (!colorKeys[id]) { + var keys = Object.keys(colorKeys); + keys.push(id); + keys.sort(function(a, b) { + var pa = parseFloat(a); + var pb = parseFloat(b); + if (isNaN(pa) || isNaN(pb)) return a.localeCompare(b); + return pa - pb; + }); + var colors = colorScale.colors(keys.length); + colorKeys = {}; + keys.forEach(function(key, index) { + colorKeys[key] = colors[index]; + }); + reColor(); + updateLegend(); + } + return colorKeys[id]; + } + + // when the color palette changes, update all grid squares with new color + var reColor = function() { + $.each(rectangles, function(_, r) { + var color = getColor(colorAccessor(r)); + r.setOptions({ + strokeColor: color, + fillColor: color + }); + }); + } + + var colorMode = 'byband'; + var colorAccessor = function(r) { + switch (colorMode) { + case 'byband': + return r.band; + case 'bymode': + return r.mode; + } + }; + + $(function(){ + $('#openwebrx-map-colormode').on('change', function(){ + colorMode = $(this).val(); + colorKeys = {}; + filterRectangles(allRectangles); + reColor(); + updateLegend(); + }); + }); + + var updateLegend = function() { + var lis = $.map(colorKeys, function(value, key) { + // fake rectangle to test if the filter would match + var fakeRectangle = Object.fromEntries([[colorMode.slice(2), key]]); + var disabled = rectangleFilter(fakeRectangle) ? '' : ' disabled'; + return '
  • ' + key + '
  • '; + }); + $(".openwebrx-map-legend .content").html('
      ' + lis.join('') + '
    '); + }; + + var shallowEquals = function(obj1, obj2) { + // basic shallow object comparison + return Object.entries(obj1).sort().toString() === Object.entries(obj2).sort().toString(); + } + + var processUpdates = function(updates) { + if (typeof(AprsMarker) == 'undefined') { + updateQueue = updateQueue.concat(updates); + return; + } + updates.forEach(function(update){ + var key = sourceToKey(update.source); + + switch (update.location.type) { + case 'latlon': + var pos = false; + if (update.location.lat && update.location.lon) { + pos = new google.maps.LatLng(update.location.lat, update.location.lon); + } + var marker; + var markerClass = google.maps.Marker; + var aprsOptions = {} + if (update.location.symbol) { + markerClass = AprsMarker; + aprsOptions.symbol = update.location.symbol; + aprsOptions.course = update.location.course; + aprsOptions.speed = update.location.speed; + } else if (update.source.icao || update.source.flight) { + markerClass = PlaneMarker; + aprsOptions = update.location; + } + if (markers[key]) { + marker = markers[key]; + if (!pos) { + delete markers[key]; + marker.setMap(); + return; + } + } else { + if (pos) { + marker = new markerClass(); + marker.addListener('click', function () { + showMarkerInfoWindow(update.source, pos); + }); + marker.setMap(map); + markers[key] = marker; + } + } + if (!marker) return; + marker.setOptions($.extend({ + position: pos, + title: sourceToString(update.source) + }, aprsOptions, getMarkerOpacityOptions(update.lastseen, update.location.ttl) )); + marker.source = update.source; + marker.lastseen = update.lastseen; + marker.mode = update.mode; + marker.band = update.band; + marker.comment = update.location.comment; + marker.ttl = update.location.ttl; + + if (expectedCallsign && shallowEquals(expectedCallsign, update.source)) { + map.panTo(pos); + showMarkerInfoWindow(update.source, pos); + expectedCallsign = false; + } + + if (expectedIcao && expectedIcao === update.source.icao) { + map.panTo(pos); + showMarkerInfoWindow(update.source, pos); + expectedIcao = false; + } + + if (infowindow && infowindow.source && shallowEquals(infowindow.source, update.source)) { + showMarkerInfoWindow(infowindow.source, pos); + } + break; + case 'locator': + var loc = update.location.locator; + var lat = (loc.charCodeAt(1) - 65 - 9) * 10 + Number(loc[3]); + var lon = (loc.charCodeAt(0) - 65 - 9) * 20 + Number(loc[2]) * 2; + var center = new google.maps.LatLng({lat: lat + .5, lng: lon + 1}); + var rectangle; + // the accessor is designed to work on the rectangle... but it should work on the update object, too + var color = getColor(colorAccessor(update)); + if (rectangles[key]) { + rectangle = rectangles[key]; + } else { + rectangle = new google.maps.Rectangle(); + rectangle.addListener('click', function(){ + showLocatorInfoWindow(this.locator, this.center); + }); + rectangles[key] = rectangle; + } + rectangle.source = update.source; + rectangle.lastseen = update.lastseen; + rectangle.locator = update.location.locator; + rectangle.mode = update.mode; + rectangle.band = update.band; + rectangle.center = center; + rectangle.ttl = update.location.ttl; + + rectangle.setOptions($.extend({ + strokeColor: color, + strokeWeight: 2, + fillColor: color, + map: rectangleFilter(rectangle) ? map : undefined, + bounds:{ + north: lat, + south: lat + 1, + west: lon, + east: lon + 2 + } + }, getRectangleOpacityOptions(update.lastseen, update.location.ttl) )); + + if (expectedLocator && expectedLocator === update.location.locator) { + map.panTo(center); + showLocatorInfoWindow(expectedLocator, center); + expectedLocator = false; + } + + if (infowindow && infowindow.locator && infowindow.locator === update.location.locator) { + showLocatorInfoWindow(infowindow.locator, center); + } + break; + } + }); + }; + + var clearMap = function(){ + var reset = function(_, item) { item.setMap(); }; + $.each(markers, reset); + $.each(rectangles, reset); + receiverMarker.setMap(); + markers = {}; + rectangles = {}; + }; + + var reconnect_timeout = false; + + var config = {} + + var connect = function(){ + var ws = new WebSocket(ws_url); + ws.onopen = function(){ + ws.send("SERVER DE CLIENT client=map.js type=map"); + reconnect_timeout = false + }; + + ws.onmessage = function(e){ + if (typeof e.data != 'string') { + console.error("unsupported binary data on websocket; ignoring"); + return + } + if (e.data.substr(0, 16) == "CLIENT DE SERVER") { + return + } + try { + var json = JSON.parse(e.data); + switch (json.type) { + case "config": + Object.assign(config, json.value); + if ('receiver_gps' in config) { + var receiverPos = { + lat: config.receiver_gps.lat, + lng: config.receiver_gps.lon + }; + if (!map) $.getScript("https://maps.googleapis.com/maps/api/js?key=" + config.google_maps_api_key).done(function(){ + map = new google.maps.Map($('.openwebrx-map')[0], { + center: receiverPos, + zoom: 5, + }); + + $.getScript("static/lib/nite-overlay.js").done(function(){ + nite.init(map); + setInterval(function() { nite.refresh() }, 10000); // every 10s + }); + $.when( + $.getScript('static/lib/AprsMarker.js'), + $.getScript('static/lib/PlaneMarker.js') + ).done(function(){ + processUpdates(updateQueue); + updateQueue = []; + }); + + var $legend = $(".openwebrx-map-legend"); + setupLegendFilters($legend); + map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push($legend[0]); + + if (!receiverMarker) { + receiverMarker = new google.maps.Marker(); + receiverMarker.addListener('click', function() { + showReceiverInfoWindow(receiverMarker); + }); + } + receiverMarker.setOptions({ + map: map, + position: receiverPos, + title: config['receiver_name'], + config: config + }); + }); else { + receiverMarker.setOptions({ + map: map, + position: receiverPos, + config: config + }); + } + } + if ('receiver_name' in config && receiverMarker) { + receiverMarker.setOptions({ + title: config['receiver_name'] + }); + } + if ('callsign_service' in config) { + callsign_service = config['callsign_service']; + } + if ('aircraft_tracking_service' in config) { + aircraft_tracking_service = config['aircraft_tracking_service']; + } + break; + case "update": + processUpdates(json.value); + break; + case 'receiver_details': + $('.webrx-top-container').header().setDetails(json['value']); + break; + default: + console.warn('received message of unknown type: ' + json['type']); + } + } catch (e) { + // don't lose exception + console.error(e); + } + }; + ws.onclose = function(){ + clearMap(); + if (reconnect_timeout) { + // max value: roundabout 8 and a half minutes + reconnect_timeout = Math.min(reconnect_timeout * 2, 512000); + } else { + // initial value: 1s + reconnect_timeout = 1000; + } + setTimeout(connect, reconnect_timeout); + }; + + window.onbeforeunload = function() { //http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript + ws.onclose = function () {}; + ws.close(); + }; + + /* + ws.onerror = function(){ + console.info("websocket error"); + }; + */ + }; + + connect(); + + var getInfoWindow = function() { + if (!infowindow) { + infowindow = new google.maps.InfoWindow(); + google.maps.event.addListener(infowindow, 'closeclick', function() { + delete infowindow.locator; + delete infowindow.source; + }); + } + delete infowindow.locator; + delete infowindow.source; + return infowindow; + }; + + var sourceToKey = function(source) { + // special treatment for special entities + // not just for display but also in key treatment in order not to overlap with other locations sent by the same callsign + if ('item' in source) return source['item']; + if ('object' in source) return source['object']; + if ('icao' in source) return source['icao']; + if ('flight' in source) return source['flight']; + var key = source.callsign; + if ('ssid' in source) key += '-' + source.ssid; + return key; + }; + + // we can reuse the same logic for displaying and indexing + var sourceToString = sourceToKey; + + var linkifyCallsign = function(source) { + var callsignString = sourceToString(source); + switch (callsign_service) { + case "qrzcq": + return '' + callsignString + ''; + case "qrz": + return '' + callsignString + ''; + case 'aprsfi': + var callWithSsid = sourceToKey(source); + return '' + callsignString + ''; + default: + return callsignString; + } + }; + + var linkifyAircraft = function(source, identification) { + var aircraftString = identification || source.flight || source.icao; + var link = false; + switch (aircraft_tracking_service) { + case 'flightaware': + if (source.icao) { + link = 'https://flightaware.com/live/modes/' + source.icao; + if (identification) link += "/ident/" + identification + link += '/redirect'; + } else if (source.flight) { + link = 'https://flightaware.com/live/flight/' + source.flight; + } + break; + case 'planefinder': + if (identification) link = 'https://planefinder.net/flight/' + identification; + if (source.flight) link = 'https://planefinder.net/flight/' + source.flight; + break; + } + if (link) { + return '' + aircraftString + ''; + } + return aircraftString; + } + + var distanceKm = function(p1, p2) { + // Earth radius in km + var R = 6371.0; + // Convert degrees to radians + var rlat1 = p1.lat() * (Math.PI/180); + var rlat2 = p2.lat() * (Math.PI/180); + // Compute difference in radians + var difflat = rlat2-rlat1; + var difflon = (p2.lng()-p1.lng()) * (Math.PI/180); + // Compute distance + d = 2 * R * Math.asin(Math.sqrt( + Math.sin(difflat/2) * Math.sin(difflat/2) + + Math.cos(rlat1) * Math.cos(rlat2) * Math.sin(difflon/2) * Math.sin(difflon/2) + )); + return Math.round(d); + }; + + var infowindow; + var showLocatorInfoWindow = function(locator, pos) { + var infowindow = getInfoWindow(); + infowindow.locator = locator; + var inLocator = Object.values(rectangles).filter(rectangleFilter).filter(function(d) { + return d.locator === locator; + }).sort(function(a, b){ + return b.lastseen - a.lastseen; + }); + var distance = receiverMarker? + " at " + distanceKm(receiverMarker.position, pos) + " km" : ""; + infowindow.setContent( + '

    Locator: ' + locator + distance + '

    ' + + '
    Active Callsigns:
    ' + + '
      ' + + inLocator.map(function(i){ + var timestring = moment(i.lastseen).fromNow(); + var message = linkifyCallsign(i.source) + ' (' + timestring + ' using ' + i.mode; + if (i.band) message += ' on ' + i.band; + message += ')'; + return '
    • ' + message + '
    • ' + }).join("") + + '
    ' + ); + infowindow.setPosition(pos); + infowindow.open(map); + }; + + var showMarkerInfoWindow = function(source, pos) { + var infowindow = getInfoWindow(); + infowindow.source = source; + var marker = markers[sourceToKey(source)]; + var timestring = moment(marker.lastseen).fromNow(); + var commentString = ""; + var distance = ""; + if (marker.comment) { + commentString = '
    ' + marker.comment + '
    '; + } + if (receiverMarker) { + distance = " at " + distanceKm(receiverMarker.position, marker.position) + " km"; + } + var title; + if (marker.source.icao || marker.source.flight) { + title = linkifyAircraft(source, marker.identification); + if ('altitude' in marker) { + commentString += '
    Altitude: ' + marker.altitude + ' ft
    '; + } + if ('groundspeed' in marker) { + commentString += '
    Speed: ' + Math.round(marker.groundspeed) + ' kt
    '; + } + if ('verticalspeed' in marker) { + commentString += '
    V/S: ' + marker.verticalspeed + ' ft/min
    '; + } + if ('IAS' in marker) { + commentString += '
    IAS: ' + marker.IAS + ' kt
    '; + } + if ('TAS' in marker) { + commentString += '
    TAS: ' + marker.TAS + ' kt
    /'; + } + } else { + title = linkifyCallsign(source); + } + infowindow.setContent( + '

    ' + title + distance + '

    ' + + '
    ' + timestring + ' using ' + marker.mode + ( marker.band ? ' on ' + marker.band : '' ) + '
    ' + + commentString + ); + infowindow.open(map, marker); + }; + + var showReceiverInfoWindow = function(marker) { + var infowindow = getInfoWindow() + infowindow.setContent( + '

    ' + marker.config['receiver_name'] + '

    ' + + '
    Receiver location
    ' + ); + infowindow.open(map, marker); + }; + + var getScale = function(lastseen, ttl) { + var age = new Date().getTime() - lastseen; + var scale = 1; + if (age >= ttl / 2) { + scale = (ttl - age) / (ttl / 2); + } + return Math.max(0, Math.min(1, scale)); + }; + + var getRectangleOpacityOptions = function(lastseen, ttl) { + var scale = getScale(lastseen, ttl); + return { + strokeOpacity: strokeOpacity * scale, + fillOpacity: fillOpacity * scale + }; + }; + + var getMarkerOpacityOptions = function(lastseen, ttl) { + var scale = getScale(lastseen, ttl); + return { + opacity: scale + }; + }; + + // fade out / remove positions after time + setInterval(function(){ + var now = new Date().getTime(); + Object.values(rectangles).forEach(function(m){ + var age = now - m.lastseen; + if (age > m.ttl) { + delete rectangles[sourceToKey(m.source)]; + m.setMap(); + return; + } + m.setOptions(getRectangleOpacityOptions(m.lastseen, m.ttl)); + }); + Object.values(markers).forEach(function(m) { + var age = now - m.lastseen; + if (age > m.ttl) { + delete markers[sourceToKey(m.source)]; + m.setMap(); + return; + } + m.setOptions(getMarkerOpacityOptions(m.lastseen, m.ttl)); + }); + }, 1000); + + var rectangleFilter = allRectangles = function() { return true; }; + + var filterRectangles = function(filter) { + rectangleFilter = filter; + $.each(rectangles, function(_, r) { + r.setMap(rectangleFilter(r) ? map : undefined); + }); + }; + + var setupLegendFilters = function($legend) { + $content = $legend.find('.content'); + $content.on('click', 'li', function() { + var $el = $(this); + $lis = $content.find('li'); + if ($lis.hasClass('disabled') && !$el.hasClass('disabled')) { + $lis.removeClass('disabled'); + filterRectangles(allRectangles); + } else { + $el.removeClass('disabled'); + $lis.filter(function() { + return this != $el[0] + }).addClass('disabled'); + + var key = colorMode.slice(2); + var selector = $el.data('selector'); + filterRectangles(function(r) { + return r[key] === selector; + }); + } + }); + } + +}); diff --git a/htdocs/mathbox-bundle.min.js b/htdocs/mathbox-bundle.min.js deleted file mode 100644 index 4f61f9f7f..000000000 --- a/htdocs/mathbox-bundle.min.js +++ /dev/null @@ -1,33 +0,0 @@ -var THREE={REVISION:"71"};"object"==typeof module&&(module.exports=THREE),void 0===Math.sign&&(Math.sign=function(t){return 0>t?-1:t>0?1:+t}),THREE.log=function(){console.log.apply(console,arguments)},THREE.warn=function(){console.warn.apply(console,arguments)},THREE.error=function(){console.error.apply(console,arguments)},THREE.MOUSE={LEFT:0,MIDDLE:1,RIGHT:2},THREE.CullFaceNone=0,THREE.CullFaceBack=1,THREE.CullFaceFront=2,THREE.CullFaceFrontBack=3,THREE.FrontFaceDirectionCW=0,THREE.FrontFaceDirectionCCW=1,THREE.BasicShadowMap=0,THREE.PCFShadowMap=1,THREE.PCFSoftShadowMap=2,THREE.FrontSide=0,THREE.BackSide=1,THREE.DoubleSide=2,THREE.NoShading=0,THREE.FlatShading=1,THREE.SmoothShading=2,THREE.NoColors=0,THREE.FaceColors=1,THREE.VertexColors=2,THREE.NoBlending=0,THREE.NormalBlending=1,THREE.AdditiveBlending=2,THREE.SubtractiveBlending=3,THREE.MultiplyBlending=4,THREE.CustomBlending=5,THREE.AddEquation=100,THREE.SubtractEquation=101,THREE.ReverseSubtractEquation=102,THREE.MinEquation=103,THREE.MaxEquation=104,THREE.ZeroFactor=200,THREE.OneFactor=201,THREE.SrcColorFactor=202,THREE.OneMinusSrcColorFactor=203,THREE.SrcAlphaFactor=204,THREE.OneMinusSrcAlphaFactor=205,THREE.DstAlphaFactor=206,THREE.OneMinusDstAlphaFactor=207,THREE.DstColorFactor=208,THREE.OneMinusDstColorFactor=209,THREE.SrcAlphaSaturateFactor=210,THREE.MultiplyOperation=0,THREE.MixOperation=1,THREE.AddOperation=2,THREE.UVMapping=300,THREE.CubeReflectionMapping=301,THREE.CubeRefractionMapping=302,THREE.EquirectangularReflectionMapping=303,THREE.EquirectangularRefractionMapping=304,THREE.SphericalReflectionMapping=305,THREE.RepeatWrapping=1e3,THREE.ClampToEdgeWrapping=1001,THREE.MirroredRepeatWrapping=1002,THREE.NearestFilter=1003,THREE.NearestMipMapNearestFilter=1004,THREE.NearestMipMapLinearFilter=1005,THREE.LinearFilter=1006,THREE.LinearMipMapNearestFilter=1007,THREE.LinearMipMapLinearFilter=1008,THREE.UnsignedByteType=1009,THREE.ByteType=1010,THREE.ShortType=1011,THREE.UnsignedShortType=1012,THREE.IntType=1013,THREE.UnsignedIntType=1014,THREE.FloatType=1015,THREE.HalfFloatType=1025,THREE.UnsignedShort4444Type=1016,THREE.UnsignedShort5551Type=1017,THREE.UnsignedShort565Type=1018,THREE.AlphaFormat=1019,THREE.RGBFormat=1020,THREE.RGBAFormat=1021,THREE.LuminanceFormat=1022,THREE.LuminanceAlphaFormat=1023,THREE.RGBEFormat=THREE.RGBAFormat,THREE.RGB_S3TC_DXT1_Format=2001,THREE.RGBA_S3TC_DXT1_Format=2002,THREE.RGBA_S3TC_DXT3_Format=2003,THREE.RGBA_S3TC_DXT5_Format=2004,THREE.RGB_PVRTC_4BPPV1_Format=2100,THREE.RGB_PVRTC_2BPPV1_Format=2101,THREE.RGBA_PVRTC_4BPPV1_Format=2102,THREE.RGBA_PVRTC_2BPPV1_Format=2103,THREE.Projector=function(){THREE.error("THREE.Projector has been moved to /examples/js/renderers/Projector.js."),this.projectVector=function(t,e){THREE.warn("THREE.Projector: .projectVector() is now vector.project()."),t.project(e)},this.unprojectVector=function(t,e){THREE.warn("THREE.Projector: .unprojectVector() is now vector.unproject()."),t.unproject(e)},this.pickingRay=function(t,e){THREE.error("THREE.Projector: .pickingRay() is now raycaster.setFromCamera().")}},THREE.CanvasRenderer=function(){THREE.error("THREE.CanvasRenderer has been moved to /examples/js/renderers/CanvasRenderer.js"),this.domElement=document.createElement("canvas"),this.clear=function(){},this.render=function(){},this.setClearColor=function(){},this.setSize=function(){}},THREE.Color=function(t){return 3===arguments.length?this.setRGB(arguments[0],arguments[1],arguments[2]):this.set(t)},THREE.Color.prototype={constructor:THREE.Color,r:1,g:1,b:1,set:function(t){return t instanceof THREE.Color?this.copy(t):"number"==typeof t?this.setHex(t):"string"==typeof t&&this.setStyle(t),this},setHex:function(t){return t=Math.floor(t),this.r=(t>>16&255)/255,this.g=(t>>8&255)/255,this.b=(255&t)/255,this},setRGB:function(t,e,n){return this.r=t,this.g=e,this.b=n,this},setHSL:function(t,e,n){if(0===e)this.r=this.g=this.b=n;else{var r=function(t,e,n){return 0>n&&(n+=1),n>1&&(n-=1),1/6>n?t+6*(e-t)*n:.5>n?e:2/3>n?t+6*(e-t)*(2/3-n):t},i=.5>=n?n*(1+e):n+e-n*e,o=2*n-i;this.r=r(o,i,t+1/3),this.g=r(o,i,t),this.b=r(o,i,t-1/3)}return this},setStyle:function(t){if(/^rgb\((\d+), ?(\d+), ?(\d+)\)$/i.test(t)){var e=/^rgb\((\d+), ?(\d+), ?(\d+)\)$/i.exec(t);return this.r=Math.min(255,parseInt(e[1],10))/255,this.g=Math.min(255,parseInt(e[2],10))/255,this.b=Math.min(255,parseInt(e[3],10))/255,this}if(/^rgb\((\d+)\%, ?(\d+)\%, ?(\d+)\%\)$/i.test(t)){var e=/^rgb\((\d+)\%, ?(\d+)\%, ?(\d+)\%\)$/i.exec(t);return this.r=Math.min(100,parseInt(e[1],10))/100,this.g=Math.min(100,parseInt(e[2],10))/100,this.b=Math.min(100,parseInt(e[3],10))/100,this}if(/^\#([0-9a-f]{6})$/i.test(t)){var e=/^\#([0-9a-f]{6})$/i.exec(t);return this.setHex(parseInt(e[1],16)),this}if(/^\#([0-9a-f])([0-9a-f])([0-9a-f])$/i.test(t)){var e=/^\#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(t);return this.setHex(parseInt(e[1]+e[1]+e[2]+e[2]+e[3]+e[3],16)),this}return/^(\w+)$/i.test(t)?(this.setHex(THREE.ColorKeywords[t]),this):void 0},copy:function(t){return this.r=t.r,this.g=t.g,this.b=t.b,this},copyGammaToLinear:function(t,e){return void 0===e&&(e=2),this.r=Math.pow(t.r,e),this.g=Math.pow(t.g,e),this.b=Math.pow(t.b,e),this},copyLinearToGamma:function(t,e){void 0===e&&(e=2);var n=e>0?1/e:1;return this.r=Math.pow(t.r,n),this.g=Math.pow(t.g,n),this.b=Math.pow(t.b,n),this},convertGammaToLinear:function(){var t=this.r,e=this.g,n=this.b;return this.r=t*t,this.g=e*e,this.b=n*n,this},convertLinearToGamma:function(){return this.r=Math.sqrt(this.r),this.g=Math.sqrt(this.g),this.b=Math.sqrt(this.b),this},getHex:function(){return 255*this.r<<16^255*this.g<<8^255*this.b<<0},getHexString:function(){return("000000"+this.getHex().toString(16)).slice(-6)},getHSL:function(t){var e,n,r=t||{h:0,s:0,l:0},i=this.r,o=this.g,s=this.b,a=Math.max(i,o,s),u=Math.min(i,o,s),h=(u+a)/2;if(u===a)e=0,n=0;else{var l=a-u;switch(n=.5>=h?l/(a+u):l/(2-a-u),a){case i:e=(o-s)/l+(s>o?6:0);break;case o:e=(s-i)/l+2;break;case s:e=(i-o)/l+4}e/=6}return r.h=e,r.s=n,r.l=h,r},getStyle:function(){return"rgb("+(255*this.r|0)+","+(255*this.g|0)+","+(255*this.b|0)+")"},offsetHSL:function(t,e,n){var r=this.getHSL();return r.h+=t,r.s+=e,r.l+=n,this.setHSL(r.h,r.s,r.l),this},add:function(t){return this.r+=t.r,this.g+=t.g,this.b+=t.b,this},addColors:function(t,e){return this.r=t.r+e.r,this.g=t.g+e.g,this.b=t.b+e.b,this},addScalar:function(t){return this.r+=t,this.g+=t,this.b+=t,this},multiply:function(t){return this.r*=t.r,this.g*=t.g,this.b*=t.b,this},multiplyScalar:function(t){return this.r*=t,this.g*=t,this.b*=t,this},lerp:function(t,e){return this.r+=(t.r-this.r)*e,this.g+=(t.g-this.g)*e,this.b+=(t.b-this.b)*e,this},equals:function(t){return t.r===this.r&&t.g===this.g&&t.b===this.b},fromArray:function(t){return this.r=t[0],this.g=t[1],this.b=t[2],this},toArray:function(t,e){return void 0===t&&(t=[]),void 0===e&&(e=0),t[e]=this.r,t[e+1]=this.g,t[e+2]=this.b,t},clone:function(){return(new THREE.Color).setRGB(this.r,this.g,this.b)}},THREE.ColorKeywords={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074},THREE.Quaternion=function(t,e,n,r){this._x=t||0,this._y=e||0,this._z=n||0,this._w=void 0!==r?r:1},THREE.Quaternion.prototype={constructor:THREE.Quaternion,_x:0,_y:0,_z:0,_w:0,get x(){return this._x},set x(t){this._x=t,this.onChangeCallback()},get y(){return this._y},set y(t){this._y=t,this.onChangeCallback()},get z(){return this._z},set z(t){this._z=t,this.onChangeCallback()},get w(){return this._w},set w(t){this._w=t,this.onChangeCallback()},set:function(t,e,n,r){return this._x=t,this._y=e,this._z=n,this._w=r,this.onChangeCallback(),this},copy:function(t){return this._x=t.x,this._y=t.y,this._z=t.z,this._w=t.w,this.onChangeCallback(),this},setFromEuler:function(t,e){if(t instanceof THREE.Euler==!1)throw new Error("THREE.Quaternion: .setFromEuler() now expects a Euler rotation rather than a Vector3 and order.");var n=Math.cos(t._x/2),r=Math.cos(t._y/2),i=Math.cos(t._z/2),o=Math.sin(t._x/2),s=Math.sin(t._y/2),a=Math.sin(t._z/2);return"XYZ"===t.order?(this._x=o*r*i+n*s*a,this._y=n*s*i-o*r*a,this._z=n*r*a+o*s*i,this._w=n*r*i-o*s*a):"YXZ"===t.order?(this._x=o*r*i+n*s*a,this._y=n*s*i-o*r*a,this._z=n*r*a-o*s*i,this._w=n*r*i+o*s*a):"ZXY"===t.order?(this._x=o*r*i-n*s*a,this._y=n*s*i+o*r*a,this._z=n*r*a+o*s*i,this._w=n*r*i-o*s*a):"ZYX"===t.order?(this._x=o*r*i-n*s*a,this._y=n*s*i+o*r*a,this._z=n*r*a-o*s*i,this._w=n*r*i+o*s*a):"YZX"===t.order?(this._x=o*r*i+n*s*a,this._y=n*s*i+o*r*a,this._z=n*r*a-o*s*i,this._w=n*r*i-o*s*a):"XZY"===t.order&&(this._x=o*r*i-n*s*a,this._y=n*s*i-o*r*a,this._z=n*r*a+o*s*i,this._w=n*r*i+o*s*a),e!==!1&&this.onChangeCallback(),this},setFromAxisAngle:function(t,e){var n=e/2,r=Math.sin(n);return this._x=t.x*r,this._y=t.y*r,this._z=t.z*r,this._w=Math.cos(n),this.onChangeCallback(),this},setFromRotationMatrix:function(t){var e,n=t.elements,r=n[0],i=n[4],o=n[8],s=n[1],a=n[5],u=n[9],h=n[2],l=n[6],c=n[10],p=r+a+c;return p>0?(e=.5/Math.sqrt(p+1),this._w=.25/e,this._x=(l-u)*e,this._y=(o-h)*e,this._z=(s-i)*e):r>a&&r>c?(e=2*Math.sqrt(1+r-a-c),this._w=(l-u)/e,this._x=.25*e,this._y=(i+s)/e,this._z=(o+h)/e):a>c?(e=2*Math.sqrt(1+a-r-c),this._w=(o-h)/e,this._x=(i+s)/e,this._y=.25*e,this._z=(u+l)/e):(e=2*Math.sqrt(1+c-r-a),this._w=(s-i)/e,this._x=(o+h)/e,this._y=(u+l)/e,this._z=.25*e),this.onChangeCallback(),this},setFromUnitVectors:function(){var t,e,n=1e-6;return function(r,i){return void 0===t&&(t=new THREE.Vector3),e=r.dot(i)+1,n>e?(e=0,Math.abs(r.x)>Math.abs(r.z)?t.set(-r.y,r.x,0):t.set(0,-r.z,r.y)):t.crossVectors(r,i),this._x=t.x,this._y=t.y,this._z=t.z,this._w=e,this.normalize(),this}}(),inverse:function(){return this.conjugate().normalize(),this},conjugate:function(){return this._x*=-1,this._y*=-1,this._z*=-1,this.onChangeCallback(),this},dot:function(t){return this._x*t._x+this._y*t._y+this._z*t._z+this._w*t._w},lengthSq:function(){return this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w},length:function(){return Math.sqrt(this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w)},normalize:function(){var t=this.length();return 0===t?(this._x=0,this._y=0,this._z=0,this._w=1):(t=1/t,this._x=this._x*t,this._y=this._y*t,this._z=this._z*t,this._w=this._w*t),this.onChangeCallback(),this},multiply:function(t,e){return void 0!==e?(THREE.warn("THREE.Quaternion: .multiply() now only accepts one argument. Use .multiplyQuaternions( a, b ) instead."),this.multiplyQuaternions(t,e)):this.multiplyQuaternions(this,t)},multiplyQuaternions:function(t,e){var n=t._x,r=t._y,i=t._z,o=t._w,s=e._x,a=e._y,u=e._z,h=e._w;return this._x=n*h+o*s+r*u-i*a,this._y=r*h+o*a+i*s-n*u,this._z=i*h+o*u+n*a-r*s,this._w=o*h-n*s-r*a-i*u,this.onChangeCallback(),this},multiplyVector3:function(t){return THREE.warn("THREE.Quaternion: .multiplyVector3() has been removed. Use is now vector.applyQuaternion( quaternion ) instead."),t.applyQuaternion(this)},slerp:function(t,e){if(0===e)return this;if(1===e)return this.copy(t);var n=this._x,r=this._y,i=this._z,o=this._w,s=o*t._w+n*t._x+r*t._y+i*t._z;if(0>s?(this._w=-t._w,this._x=-t._x,this._y=-t._y,this._z=-t._z,s=-s):this.copy(t),s>=1)return this._w=o,this._x=n,this._y=r,this._z=i,this;var a=Math.acos(s),u=Math.sqrt(1-s*s);if(Math.abs(u)<.001)return this._w=.5*(o+this._w),this._x=.5*(n+this._x),this._y=.5*(r+this._y),this._z=.5*(i+this._z),this;var h=Math.sin((1-e)*a)/u,l=Math.sin(e*a)/u;return this._w=o*h+this._w*l,this._x=n*h+this._x*l,this._y=r*h+this._y*l,this._z=i*h+this._z*l,this.onChangeCallback(),this},equals:function(t){return t._x===this._x&&t._y===this._y&&t._z===this._z&&t._w===this._w},fromArray:function(t,e){return void 0===e&&(e=0),this._x=t[e],this._y=t[e+1],this._z=t[e+2],this._w=t[e+3],this.onChangeCallback(),this},toArray:function(t,e){return void 0===t&&(t=[]),void 0===e&&(e=0),t[e]=this._x,t[e+1]=this._y,t[e+2]=this._z,t[e+3]=this._w,t},onChange:function(t){return this.onChangeCallback=t,this},onChangeCallback:function(){},clone:function(){return new THREE.Quaternion(this._x,this._y,this._z,this._w)}},THREE.Quaternion.slerp=function(t,e,n,r){return n.copy(t).slerp(e,r)},THREE.Vector2=function(t,e){this.x=t||0,this.y=e||0},THREE.Vector2.prototype={constructor:THREE.Vector2,set:function(t,e){return this.x=t,this.y=e,this},setX:function(t){return this.x=t,this},setY:function(t){return this.y=t,this},setComponent:function(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;default:throw new Error("index is out of range: "+t)}},getComponent:function(t){switch(t){case 0:return this.x;case 1:return this.y;default:throw new Error("index is out of range: "+t)}},copy:function(t){return this.x=t.x,this.y=t.y,this},add:function(t,e){return void 0!==e?(THREE.warn("THREE.Vector2: .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(t,e)):(this.x+=t.x,this.y+=t.y,this)},addScalar:function(t){return this.x+=t,this.y+=t,this},addVectors:function(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this},sub:function(t,e){return void 0!==e?(THREE.warn("THREE.Vector2: .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(t,e)):(this.x-=t.x,this.y-=t.y,this)},subScalar:function(t){return this.x-=t,this.y-=t,this},subVectors:function(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this},multiply:function(t){return this.x*=t.x,this.y*=t.y,this},multiplyScalar:function(t){return this.x*=t,this.y*=t,this},divide:function(t){return this.x/=t.x,this.y/=t.y,this},divideScalar:function(t){if(0!==t){var e=1/t;this.x*=e,this.y*=e}else this.x=0,this.y=0;return this},min:function(t){return this.x>t.x&&(this.x=t.x),this.y>t.y&&(this.y=t.y),this},max:function(t){return this.xe.x&&(this.x=e.x),this.ye.y&&(this.y=e.y),this},clampScalar:function(){var t,e;return function(n,r){return void 0===t&&(t=new THREE.Vector2,e=new THREE.Vector2),t.set(n,n),e.set(r,r),this.clamp(t,e)}}(),floor:function(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this},ceil:function(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this},round:function(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this},roundToZero:function(){return this.x=this.x<0?Math.ceil(this.x):Math.floor(this.x),this.y=this.y<0?Math.ceil(this.y):Math.floor(this.y),this},negate:function(){return this.x=-this.x,this.y=-this.y,this},dot:function(t){return this.x*t.x+this.y*t.y},lengthSq:function(){return this.x*this.x+this.y*this.y},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y)},normalize:function(){return this.divideScalar(this.length())},distanceTo:function(t){return Math.sqrt(this.distanceToSquared(t))},distanceToSquared:function(t){var e=this.x-t.x,n=this.y-t.y;return e*e+n*n},setLength:function(t){var e=this.length();return 0!==e&&t!==e&&this.multiplyScalar(t/e),this},lerp:function(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this},lerpVectors:function(t,e,n){return this.subVectors(e,t).multiplyScalar(n).add(t),this},equals:function(t){return t.x===this.x&&t.y===this.y},fromArray:function(t,e){return void 0===e&&(e=0),this.x=t[e],this.y=t[e+1],this},toArray:function(t,e){return void 0===t&&(t=[]),void 0===e&&(e=0),t[e]=this.x,t[e+1]=this.y,t},fromAttribute:function(t,e,n){return void 0===n&&(n=0),e=e*t.itemSize+n,this.x=t.array[e],this.y=t.array[e+1],this},clone:function(){return new THREE.Vector2(this.x,this.y)}},THREE.Vector3=function(t,e,n){this.x=t||0,this.y=e||0,this.z=n||0},THREE.Vector3.prototype={constructor:THREE.Vector3,set:function(t,e,n){return this.x=t,this.y=e,this.z=n,this},setX:function(t){return this.x=t,this},setY:function(t){return this.y=t,this},setZ:function(t){return this.z=t,this},setComponent:function(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;case 2:this.z=e;break;default:throw new Error("index is out of range: "+t)}},getComponent:function(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw new Error("index is out of range: "+t)}},copy:function(t){return this.x=t.x,this.y=t.y,this.z=t.z,this},add:function(t,e){return void 0!==e?(THREE.warn("THREE.Vector3: .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(t,e)):(this.x+=t.x,this.y+=t.y,this.z+=t.z,this)},addScalar:function(t){return this.x+=t,this.y+=t,this.z+=t,this},addVectors:function(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this},sub:function(t,e){return void 0!==e?(THREE.warn("THREE.Vector3: .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(t,e)):(this.x-=t.x,this.y-=t.y,this.z-=t.z,this)},subScalar:function(t){return this.x-=t,this.y-=t,this.z-=t,this},subVectors:function(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this},multiply:function(t,e){return void 0!==e?(THREE.warn("THREE.Vector3: .multiply() now only accepts one argument. Use .multiplyVectors( a, b ) instead."),this.multiplyVectors(t,e)):(this.x*=t.x,this.y*=t.y,this.z*=t.z,this)},multiplyScalar:function(t){return this.x*=t,this.y*=t,this.z*=t,this},multiplyVectors:function(t,e){return this.x=t.x*e.x,this.y=t.y*e.y,this.z=t.z*e.z,this},applyEuler:function(){var t;return function(e){return e instanceof THREE.Euler==!1&&THREE.error("THREE.Vector3: .applyEuler() now expects a Euler rotation rather than a Vector3 and order."),void 0===t&&(t=new THREE.Quaternion),this.applyQuaternion(t.setFromEuler(e)),this}}(),applyAxisAngle:function(){var t;return function(e,n){return void 0===t&&(t=new THREE.Quaternion),this.applyQuaternion(t.setFromAxisAngle(e,n)),this}}(),applyMatrix3:function(t){var e=this.x,n=this.y,r=this.z,i=t.elements;return this.x=i[0]*e+i[3]*n+i[6]*r,this.y=i[1]*e+i[4]*n+i[7]*r,this.z=i[2]*e+i[5]*n+i[8]*r,this},applyMatrix4:function(t){var e=this.x,n=this.y,r=this.z,i=t.elements;return this.x=i[0]*e+i[4]*n+i[8]*r+i[12],this.y=i[1]*e+i[5]*n+i[9]*r+i[13],this.z=i[2]*e+i[6]*n+i[10]*r+i[14],this},applyProjection:function(t){var e=this.x,n=this.y,r=this.z,i=t.elements,o=1/(i[3]*e+i[7]*n+i[11]*r+i[15]);return this.x=(i[0]*e+i[4]*n+i[8]*r+i[12])*o,this.y=(i[1]*e+i[5]*n+i[9]*r+i[13])*o,this.z=(i[2]*e+i[6]*n+i[10]*r+i[14])*o,this},applyQuaternion:function(t){var e=this.x,n=this.y,r=this.z,i=t.x,o=t.y,s=t.z,a=t.w,u=a*e+o*r-s*n,h=a*n+s*e-i*r,l=a*r+i*n-o*e,c=-i*e-o*n-s*r;return this.x=u*a+c*-i+h*-s-l*-o,this.y=h*a+c*-o+l*-i-u*-s,this.z=l*a+c*-s+u*-o-h*-i,this},project:function(){var t;return function(e){return void 0===t&&(t=new THREE.Matrix4),t.multiplyMatrices(e.projectionMatrix,t.getInverse(e.matrixWorld)),this.applyProjection(t)}}(),unproject:function(){var t;return function(e){return void 0===t&&(t=new THREE.Matrix4),t.multiplyMatrices(e.matrixWorld,t.getInverse(e.projectionMatrix)),this.applyProjection(t)}}(),transformDirection:function(t){var e=this.x,n=this.y,r=this.z,i=t.elements;return this.x=i[0]*e+i[4]*n+i[8]*r,this.y=i[1]*e+i[5]*n+i[9]*r,this.z=i[2]*e+i[6]*n+i[10]*r,this.normalize(),this},divide:function(t){return this.x/=t.x,this.y/=t.y,this.z/=t.z,this},divideScalar:function(t){if(0!==t){var e=1/t;this.x*=e,this.y*=e,this.z*=e}else this.x=0,this.y=0,this.z=0;return this},min:function(t){return this.x>t.x&&(this.x=t.x),this.y>t.y&&(this.y=t.y),this.z>t.z&&(this.z=t.z),this},max:function(t){return this.xe.x&&(this.x=e.x),this.ye.y&&(this.y=e.y),this.ze.z&&(this.z=e.z),this},clampScalar:function(){var t,e;return function(n,r){return void 0===t&&(t=new THREE.Vector3,e=new THREE.Vector3),t.set(n,n,n),e.set(r,r,r),this.clamp(t,e)}}(),floor:function(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this},ceil:function(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this},round:function(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this},roundToZero:function(){return this.x=this.x<0?Math.ceil(this.x):Math.floor(this.x),this.y=this.y<0?Math.ceil(this.y):Math.floor(this.y),this.z=this.z<0?Math.ceil(this.z):Math.floor(this.z),this},negate:function(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this},dot:function(t){return this.x*t.x+this.y*t.y+this.z*t.z},lengthSq:function(){return this.x*this.x+this.y*this.y+this.z*this.z},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)},lengthManhattan:function(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)},normalize:function(){return this.divideScalar(this.length())},setLength:function(t){var e=this.length();return 0!==e&&t!==e&&this.multiplyScalar(t/e),this},lerp:function(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this.z+=(t.z-this.z)*e,this},lerpVectors:function(t,e,n){return this.subVectors(e,t).multiplyScalar(n).add(t),this},cross:function(t,e){if(void 0!==e)return THREE.warn("THREE.Vector3: .cross() now only accepts one argument. Use .crossVectors( a, b ) instead."),this.crossVectors(t,e);var n=this.x,r=this.y,i=this.z;return this.x=r*t.z-i*t.y,this.y=i*t.x-n*t.z,this.z=n*t.y-r*t.x,this},crossVectors:function(t,e){var n=t.x,r=t.y,i=t.z,o=e.x,s=e.y,a=e.z;return this.x=r*a-i*s,this.y=i*o-n*a,this.z=n*s-r*o,this},projectOnVector:function(){var t,e;return function(n){return void 0===t&&(t=new THREE.Vector3),t.copy(n).normalize(),e=this.dot(t),this.copy(t).multiplyScalar(e)}}(),projectOnPlane:function(){var t;return function(e){return void 0===t&&(t=new THREE.Vector3),t.copy(this).projectOnVector(e),this.sub(t)}}(),reflect:function(){var t;return function(e){return void 0===t&&(t=new THREE.Vector3),this.sub(t.copy(e).multiplyScalar(2*this.dot(e)))}}(),angleTo:function(t){var e=this.dot(t)/(this.length()*t.length());return Math.acos(THREE.Math.clamp(e,-1,1))},distanceTo:function(t){return Math.sqrt(this.distanceToSquared(t))},distanceToSquared:function(t){var e=this.x-t.x,n=this.y-t.y,r=this.z-t.z;return e*e+n*n+r*r},setEulerFromRotationMatrix:function(t,e){THREE.error("THREE.Vector3: .setEulerFromRotationMatrix() has been removed. Use Euler.setFromRotationMatrix() instead.")},setEulerFromQuaternion:function(t,e){THREE.error("THREE.Vector3: .setEulerFromQuaternion() has been removed. Use Euler.setFromQuaternion() instead.")},getPositionFromMatrix:function(t){return THREE.warn("THREE.Vector3: .getPositionFromMatrix() has been renamed to .setFromMatrixPosition()."),this.setFromMatrixPosition(t)},getScaleFromMatrix:function(t){return THREE.warn("THREE.Vector3: .getScaleFromMatrix() has been renamed to .setFromMatrixScale()."),this.setFromMatrixScale(t)},getColumnFromMatrix:function(t,e){return THREE.warn("THREE.Vector3: .getColumnFromMatrix() has been renamed to .setFromMatrixColumn()."),this.setFromMatrixColumn(t,e)},setFromMatrixPosition:function(t){return this.x=t.elements[12],this.y=t.elements[13],this.z=t.elements[14],this},setFromMatrixScale:function(t){var e=this.set(t.elements[0],t.elements[1],t.elements[2]).length(),n=this.set(t.elements[4],t.elements[5],t.elements[6]).length(),r=this.set(t.elements[8],t.elements[9],t.elements[10]).length();return this.x=e,this.y=n,this.z=r,this},setFromMatrixColumn:function(t,e){var n=4*t,r=e.elements;return this.x=r[n],this.y=r[n+1],this.z=r[n+2],this},equals:function(t){return t.x===this.x&&t.y===this.y&&t.z===this.z},fromArray:function(t,e){return void 0===e&&(e=0),this.x=t[e],this.y=t[e+1],this.z=t[e+2],this},toArray:function(t,e){return void 0===t&&(t=[]),void 0===e&&(e=0),t[e]=this.x,t[e+1]=this.y,t[e+2]=this.z,t},fromAttribute:function(t,e,n){return void 0===n&&(n=0),e=e*t.itemSize+n,this.x=t.array[e],this.y=t.array[e+1],this.z=t.array[e+2],this},clone:function(){return new THREE.Vector3(this.x,this.y,this.z)}},THREE.Vector4=function(t,e,n,r){this.x=t||0,this.y=e||0,this.z=n||0,this.w=void 0!==r?r:1},THREE.Vector4.prototype={constructor:THREE.Vector4,set:function(t,e,n,r){return this.x=t,this.y=e,this.z=n,this.w=r,this},setX:function(t){return this.x=t,this},setY:function(t){return this.y=t,this},setZ:function(t){return this.z=t,this},setW:function(t){return this.w=t,this},setComponent:function(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;case 2:this.z=e;break;case 3:this.w=e;break;default:throw new Error("index is out of range: "+t)}},getComponent:function(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;case 3:return this.w;default:throw new Error("index is out of range: "+t)}},copy:function(t){return this.x=t.x,this.y=t.y,this.z=t.z,this.w=void 0!==t.w?t.w:1,this},add:function(t,e){return void 0!==e?(THREE.warn("THREE.Vector4: .add() now only accepts one argument. Use .addVectors( a, b ) instead."),this.addVectors(t,e)):(this.x+=t.x,this.y+=t.y,this.z+=t.z,this.w+=t.w,this)},addScalar:function(t){return this.x+=t,this.y+=t,this.z+=t,this.w+=t,this},addVectors:function(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this.w=t.w+e.w,this},sub:function(t,e){return void 0!==e?(THREE.warn("THREE.Vector4: .sub() now only accepts one argument. Use .subVectors( a, b ) instead."),this.subVectors(t,e)):(this.x-=t.x,this.y-=t.y,this.z-=t.z,this.w-=t.w,this)},subScalar:function(t){return this.x-=t,this.y-=t,this.z-=t,this.w-=t,this},subVectors:function(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this.w=t.w-e.w,this},multiplyScalar:function(t){return this.x*=t,this.y*=t,this.z*=t,this.w*=t,this},applyMatrix4:function(t){var e=this.x,n=this.y,r=this.z,i=this.w,o=t.elements;return this.x=o[0]*e+o[4]*n+o[8]*r+o[12]*i,this.y=o[1]*e+o[5]*n+o[9]*r+o[13]*i,this.z=o[2]*e+o[6]*n+o[10]*r+o[14]*i,this.w=o[3]*e+o[7]*n+o[11]*r+o[15]*i,this},divideScalar:function(t){if(0!==t){var e=1/t;this.x*=e,this.y*=e,this.z*=e,this.w*=e}else this.x=0,this.y=0,this.z=0,this.w=1;return this},setAxisAngleFromQuaternion:function(t){this.w=2*Math.acos(t.w);var e=Math.sqrt(1-t.w*t.w);return 1e-4>e?(this.x=1,this.y=0,this.z=0):(this.x=t.x/e,this.y=t.y/e,this.z=t.z/e),this},setAxisAngleFromRotationMatrix:function(t){var e,n,r,i,o=.01,s=.1,a=t.elements,u=a[0],h=a[4],l=a[8],c=a[1],p=a[5],f=a[9],d=a[2],m=a[6],v=a[10];if(Math.abs(h-c)E&&g>y?o>g?(n=0,r=.707106781,i=.707106781):(n=Math.sqrt(g),r=_/n,i=b/n):E>y?o>E?(n=.707106781,r=0,i=.707106781):(r=Math.sqrt(E),n=_/r,i=T/r):o>y?(n=.707106781,r=.707106781,i=0):(i=Math.sqrt(y),n=b/i,r=T/i),this.set(n,r,i,e),this}var x=Math.sqrt((m-f)*(m-f)+(l-d)*(l-d)+(c-h)*(c-h));return Math.abs(x)<.001&&(x=1),this.x=(m-f)/x,this.y=(l-d)/x,this.z=(c-h)/x,this.w=Math.acos((u+p+v-1)/2),this},min:function(t){return this.x>t.x&&(this.x=t.x),this.y>t.y&&(this.y=t.y),this.z>t.z&&(this.z=t.z),this.w>t.w&&(this.w=t.w),this},max:function(t){return this.xe.x&&(this.x=e.x),this.ye.y&&(this.y=e.y),this.ze.z&&(this.z=e.z),this.we.w&&(this.w=e.w),this},clampScalar:function(){var t,e;return function(n,r){return void 0===t&&(t=new THREE.Vector4,e=new THREE.Vector4),t.set(n,n,n,n),e.set(r,r,r,r),this.clamp(t,e)}}(),floor:function(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this.w=Math.floor(this.w),this},ceil:function(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this.w=Math.ceil(this.w),this},round:function(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this.w=Math.round(this.w),this},roundToZero:function(){return this.x=this.x<0?Math.ceil(this.x):Math.floor(this.x),this.y=this.y<0?Math.ceil(this.y):Math.floor(this.y),this.z=this.z<0?Math.ceil(this.z):Math.floor(this.z),this.w=this.w<0?Math.ceil(this.w):Math.floor(this.w),this},negate:function(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this.w=-this.w,this},dot:function(t){return this.x*t.x+this.y*t.y+this.z*t.z+this.w*t.w},lengthSq:function(){return this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w},length:function(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w)},lengthManhattan:function(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)+Math.abs(this.w)},normalize:function(){return this.divideScalar(this.length())},setLength:function(t){var e=this.length();return 0!==e&&t!==e&&this.multiplyScalar(t/e),this},lerp:function(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this.z+=(t.z-this.z)*e,this.w+=(t.w-this.w)*e,this},lerpVectors:function(t,e,n){return this.subVectors(e,t).multiplyScalar(n).add(t),this},equals:function(t){return t.x===this.x&&t.y===this.y&&t.z===this.z&&t.w===this.w},fromArray:function(t,e){return void 0===e&&(e=0),this.x=t[e],this.y=t[e+1],this.z=t[e+2],this.w=t[e+3],this},toArray:function(t,e){return void 0===t&&(t=[]),void 0===e&&(e=0),t[e]=this.x,t[e+1]=this.y,t[e+2]=this.z,t[e+3]=this.w,t},fromAttribute:function(t,e,n){return void 0===n&&(n=0),e=e*t.itemSize+n,this.x=t.array[e],this.y=t.array[e+1],this.z=t.array[e+2],this.w=t.array[e+3],this; -},clone:function(){return new THREE.Vector4(this.x,this.y,this.z,this.w)}},THREE.Euler=function(t,e,n,r){this._x=t||0,this._y=e||0,this._z=n||0,this._order=r||THREE.Euler.DefaultOrder},THREE.Euler.RotationOrders=["XYZ","YZX","ZXY","XZY","YXZ","ZYX"],THREE.Euler.DefaultOrder="XYZ",THREE.Euler.prototype={constructor:THREE.Euler,_x:0,_y:0,_z:0,_order:THREE.Euler.DefaultOrder,get x(){return this._x},set x(t){this._x=t,this.onChangeCallback()},get y(){return this._y},set y(t){this._y=t,this.onChangeCallback()},get z(){return this._z},set z(t){this._z=t,this.onChangeCallback()},get order(){return this._order},set order(t){this._order=t,this.onChangeCallback()},set:function(t,e,n,r){return this._x=t,this._y=e,this._z=n,this._order=r||this._order,this.onChangeCallback(),this},copy:function(t){return this._x=t._x,this._y=t._y,this._z=t._z,this._order=t._order,this.onChangeCallback(),this},setFromRotationMatrix:function(t,e,n){var r=THREE.Math.clamp,i=t.elements,o=i[0],s=i[4],a=i[8],u=i[1],h=i[5],l=i[9],c=i[2],p=i[6],f=i[10];return e=e||this._order,"XYZ"===e?(this._y=Math.asin(r(a,-1,1)),Math.abs(a)<.99999?(this._x=Math.atan2(-l,f),this._z=Math.atan2(-s,o)):(this._x=Math.atan2(p,h),this._z=0)):"YXZ"===e?(this._x=Math.asin(-r(l,-1,1)),Math.abs(l)<.99999?(this._y=Math.atan2(a,f),this._z=Math.atan2(u,h)):(this._y=Math.atan2(-c,o),this._z=0)):"ZXY"===e?(this._x=Math.asin(r(p,-1,1)),Math.abs(p)<.99999?(this._y=Math.atan2(-c,f),this._z=Math.atan2(-s,h)):(this._y=0,this._z=Math.atan2(u,o))):"ZYX"===e?(this._y=Math.asin(-r(c,-1,1)),Math.abs(c)<.99999?(this._x=Math.atan2(p,f),this._z=Math.atan2(u,o)):(this._x=0,this._z=Math.atan2(-s,h))):"YZX"===e?(this._z=Math.asin(r(u,-1,1)),Math.abs(u)<.99999?(this._x=Math.atan2(-l,h),this._y=Math.atan2(-c,o)):(this._x=0,this._y=Math.atan2(a,f))):"XZY"===e?(this._z=Math.asin(-r(s,-1,1)),Math.abs(s)<.99999?(this._x=Math.atan2(p,h),this._y=Math.atan2(a,o)):(this._x=Math.atan2(-l,f),this._y=0)):THREE.warn("THREE.Euler: .setFromRotationMatrix() given unsupported order: "+e),this._order=e,n!==!1&&this.onChangeCallback(),this},setFromQuaternion:function(){var t;return function(e,n,r){return void 0===t&&(t=new THREE.Matrix4),t.makeRotationFromQuaternion(e),this.setFromRotationMatrix(t,n,r),this}}(),setFromVector3:function(t,e){return this.set(t.x,t.y,t.z,e||this._order)},reorder:function(){var t=new THREE.Quaternion;return function(e){t.setFromEuler(this),this.setFromQuaternion(t,e)}}(),equals:function(t){return t._x===this._x&&t._y===this._y&&t._z===this._z&&t._order===this._order},fromArray:function(t){return this._x=t[0],this._y=t[1],this._z=t[2],void 0!==t[3]&&(this._order=t[3]),this.onChangeCallback(),this},toArray:function(t,e){return void 0===t&&(t=[]),void 0===e&&(e=0),t[e]=this._x,t[e+1]=this._y,t[e+2]=this._z,t[e+3]=this._order,t},toVector3:function(t){return t?t.set(this._x,this._y,this._z):new THREE.Vector3(this._x,this._y,this._z)},onChange:function(t){return this.onChangeCallback=t,this},onChangeCallback:function(){},clone:function(){return new THREE.Euler(this._x,this._y,this._z,this._order)}},THREE.Line3=function(t,e){this.start=void 0!==t?t:new THREE.Vector3,this.end=void 0!==e?e:new THREE.Vector3},THREE.Line3.prototype={constructor:THREE.Line3,set:function(t,e){return this.start.copy(t),this.end.copy(e),this},copy:function(t){return this.start.copy(t.start),this.end.copy(t.end),this},center:function(t){var e=t||new THREE.Vector3;return e.addVectors(this.start,this.end).multiplyScalar(.5)},delta:function(t){var e=t||new THREE.Vector3;return e.subVectors(this.end,this.start)},distanceSq:function(){return this.start.distanceToSquared(this.end)},distance:function(){return this.start.distanceTo(this.end)},at:function(t,e){var n=e||new THREE.Vector3;return this.delta(n).multiplyScalar(t).add(this.start)},closestPointToPointParameter:function(){var t=new THREE.Vector3,e=new THREE.Vector3;return function(n,r){t.subVectors(n,this.start),e.subVectors(this.end,this.start);var i=e.dot(e),o=e.dot(t),s=o/i;return r&&(s=THREE.Math.clamp(s,0,1)),s}}(),closestPointToPoint:function(t,e,n){var r=this.closestPointToPointParameter(t,e),i=n||new THREE.Vector3;return this.delta(i).multiplyScalar(r).add(this.start)},applyMatrix4:function(t){return this.start.applyMatrix4(t),this.end.applyMatrix4(t),this},equals:function(t){return t.start.equals(this.start)&&t.end.equals(this.end)},clone:function(){return(new THREE.Line3).copy(this)}},THREE.Box2=function(t,e){this.min=void 0!==t?t:new THREE.Vector2(1/0,1/0),this.max=void 0!==e?e:new THREE.Vector2(-(1/0),-(1/0))},THREE.Box2.prototype={constructor:THREE.Box2,set:function(t,e){return this.min.copy(t),this.max.copy(e),this},setFromPoints:function(t){this.makeEmpty();for(var e=0,n=t.length;n>e;e++)this.expandByPoint(t[e]);return this},setFromCenterAndSize:function(){var t=new THREE.Vector2;return function(e,n){var r=t.copy(n).multiplyScalar(.5);return this.min.copy(e).sub(r),this.max.copy(e).add(r),this}}(),copy:function(t){return this.min.copy(t.min),this.max.copy(t.max),this},makeEmpty:function(){return this.min.x=this.min.y=1/0,this.max.x=this.max.y=-(1/0),this},empty:function(){return this.max.xthis.max.x||t.ythis.max.y?!1:!0},containsBox:function(t){return this.min.x<=t.min.x&&t.max.x<=this.max.x&&this.min.y<=t.min.y&&t.max.y<=this.max.y?!0:!1},getParameter:function(t,e){var n=e||new THREE.Vector2;return n.set((t.x-this.min.x)/(this.max.x-this.min.x),(t.y-this.min.y)/(this.max.y-this.min.y))},isIntersectionBox:function(t){return t.max.xthis.max.x||t.max.ythis.max.y?!1:!0},clampPoint:function(t,e){var n=e||new THREE.Vector2;return n.copy(t).clamp(this.min,this.max)},distanceToPoint:function(){var t=new THREE.Vector2;return function(e){var n=t.copy(e).clamp(this.min,this.max);return n.sub(e).length()}}(),intersect:function(t){return this.min.max(t.min),this.max.min(t.max),this},union:function(t){return this.min.min(t.min),this.max.max(t.max),this},translate:function(t){return this.min.add(t),this.max.add(t),this},equals:function(t){return t.min.equals(this.min)&&t.max.equals(this.max)},clone:function(){return(new THREE.Box2).copy(this)}},THREE.Box3=function(t,e){this.min=void 0!==t?t:new THREE.Vector3(1/0,1/0,1/0),this.max=void 0!==e?e:new THREE.Vector3(-(1/0),-(1/0),-(1/0))},THREE.Box3.prototype={constructor:THREE.Box3,set:function(t,e){return this.min.copy(t),this.max.copy(e),this},setFromPoints:function(t){this.makeEmpty();for(var e=0,n=t.length;n>e;e++)this.expandByPoint(t[e]);return this},setFromCenterAndSize:function(){var t=new THREE.Vector3;return function(e,n){var r=t.copy(n).multiplyScalar(.5);return this.min.copy(e).sub(r),this.max.copy(e).add(r),this}}(),setFromObject:function(){var t=new THREE.Vector3;return function(e){var n=this;return e.updateMatrixWorld(!0),this.makeEmpty(),e.traverse(function(e){var r=e.geometry;if(void 0!==r)if(r instanceof THREE.Geometry)for(var i=r.vertices,o=0,s=i.length;s>o;o++)t.copy(i[o]),t.applyMatrix4(e.matrixWorld),n.expandByPoint(t);else if(r instanceof THREE.BufferGeometry&&void 0!==r.attributes.position)for(var a=r.attributes.position.array,o=0,s=a.length;s>o;o+=3)t.set(a[o],a[o+1],a[o+2]),t.applyMatrix4(e.matrixWorld),n.expandByPoint(t)}),this}}(),copy:function(t){return this.min.copy(t.min),this.max.copy(t.max),this},makeEmpty:function(){return this.min.x=this.min.y=this.min.z=1/0,this.max.x=this.max.y=this.max.z=-(1/0),this},empty:function(){return this.max.xthis.max.x||t.ythis.max.y||t.zthis.max.z?!1:!0},containsBox:function(t){return this.min.x<=t.min.x&&t.max.x<=this.max.x&&this.min.y<=t.min.y&&t.max.y<=this.max.y&&this.min.z<=t.min.z&&t.max.z<=this.max.z?!0:!1},getParameter:function(t,e){var n=e||new THREE.Vector3;return n.set((t.x-this.min.x)/(this.max.x-this.min.x),(t.y-this.min.y)/(this.max.y-this.min.y),(t.z-this.min.z)/(this.max.z-this.min.z))},isIntersectionBox:function(t){return t.max.xthis.max.x||t.max.ythis.max.y||t.max.zthis.max.z?!1:!0},clampPoint:function(t,e){var n=e||new THREE.Vector3;return n.copy(t).clamp(this.min,this.max)},distanceToPoint:function(){var t=new THREE.Vector3;return function(e){var n=t.copy(e).clamp(this.min,this.max);return n.sub(e).length()}}(),getBoundingSphere:function(){var t=new THREE.Vector3;return function(e){var n=e||new THREE.Sphere;return n.center=this.center(),n.radius=.5*this.size(t).length(),n}}(),intersect:function(t){return this.min.max(t.min),this.max.min(t.max),this},union:function(t){return this.min.min(t.min),this.max.max(t.max),this},applyMatrix4:function(){var t=[new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3,new THREE.Vector3];return function(e){return t[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(e),t[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(e),t[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(e),t[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(e),t[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(e),t[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(e),t[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(e),t[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(e),this.makeEmpty(),this.setFromPoints(t),this}}(),translate:function(t){return this.min.add(t),this.max.add(t),this},equals:function(t){return t.min.equals(this.min)&&t.max.equals(this.max)},clone:function(){return(new THREE.Box3).copy(this)}},THREE.Matrix3=function(){this.elements=new Float32Array([1,0,0,0,1,0,0,0,1]),arguments.length>0&&THREE.error("THREE.Matrix3: the constructor no longer reads arguments. use .set() instead.")},THREE.Matrix3.prototype={constructor:THREE.Matrix3,set:function(t,e,n,r,i,o,s,a,u){var h=this.elements;return h[0]=t,h[3]=e,h[6]=n,h[1]=r,h[4]=i,h[7]=o,h[2]=s,h[5]=a,h[8]=u,this},identity:function(){return this.set(1,0,0,0,1,0,0,0,1),this},copy:function(t){var e=t.elements;return this.set(e[0],e[3],e[6],e[1],e[4],e[7],e[2],e[5],e[8]),this},multiplyVector3:function(t){return THREE.warn("THREE.Matrix3: .multiplyVector3() has been removed. Use vector.applyMatrix3( matrix ) instead."),t.applyMatrix3(this)},multiplyVector3Array:function(t){return THREE.warn("THREE.Matrix3: .multiplyVector3Array() has been renamed. Use matrix.applyToVector3Array( array ) instead."),this.applyToVector3Array(t)},applyToVector3Array:function(){var t=new THREE.Vector3;return function(e,n,r){void 0===n&&(n=0),void 0===r&&(r=e.length);for(var i=0,o=n;r>i;i+=3,o+=3)t.x=e[o],t.y=e[o+1],t.z=e[o+2],t.applyMatrix3(this),e[o]=t.x,e[o+1]=t.y,e[o+2]=t.z;return e}}(),multiplyScalar:function(t){var e=this.elements;return e[0]*=t,e[3]*=t,e[6]*=t,e[1]*=t,e[4]*=t,e[7]*=t,e[2]*=t,e[5]*=t,e[8]*=t,this},determinant:function(){var t=this.elements,e=t[0],n=t[1],r=t[2],i=t[3],o=t[4],s=t[5],a=t[6],u=t[7],h=t[8];return e*o*h-e*s*u-n*i*h+n*s*a+r*i*u-r*o*a},getInverse:function(t,e){var n=t.elements,r=this.elements;r[0]=n[10]*n[5]-n[6]*n[9],r[1]=-n[10]*n[1]+n[2]*n[9],r[2]=n[6]*n[1]-n[2]*n[5],r[3]=-n[10]*n[4]+n[6]*n[8],r[4]=n[10]*n[0]-n[2]*n[8],r[5]=-n[6]*n[0]+n[2]*n[4],r[6]=n[9]*n[4]-n[5]*n[8],r[7]=-n[9]*n[0]+n[1]*n[8],r[8]=n[5]*n[0]-n[1]*n[4];var i=n[0]*r[0]+n[1]*r[3]+n[2]*r[6];if(0===i){var o="Matrix3.getInverse(): can't invert matrix, determinant is 0";if(e)throw new Error(o);return THREE.warn(o),this.identity(),this}return this.multiplyScalar(1/i),this},transpose:function(){var t,e=this.elements;return t=e[1],e[1]=e[3],e[3]=t,t=e[2],e[2]=e[6],e[6]=t,t=e[5],e[5]=e[7],e[7]=t,this},flattenToArrayOffset:function(t,e){var n=this.elements;return t[e]=n[0],t[e+1]=n[1],t[e+2]=n[2],t[e+3]=n[3],t[e+4]=n[4],t[e+5]=n[5],t[e+6]=n[6],t[e+7]=n[7],t[e+8]=n[8],t},getNormalMatrix:function(t){return this.getInverse(t).transpose(),this},transposeIntoArray:function(t){var e=this.elements;return t[0]=e[0],t[1]=e[3],t[2]=e[6],t[3]=e[1],t[4]=e[4],t[5]=e[7],t[6]=e[2],t[7]=e[5],t[8]=e[8],this},fromArray:function(t){return this.elements.set(t),this},toArray:function(){var t=this.elements;return[t[0],t[1],t[2],t[3],t[4],t[5],t[6],t[7],t[8]]},clone:function(){return(new THREE.Matrix3).fromArray(this.elements)}},THREE.Matrix4=function(){this.elements=new Float32Array([1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]),arguments.length>0&&THREE.error("THREE.Matrix4: the constructor no longer reads arguments. use .set() instead.")},THREE.Matrix4.prototype={constructor:THREE.Matrix4,set:function(t,e,n,r,i,o,s,a,u,h,l,c,p,f,d,m){var v=this.elements;return v[0]=t,v[4]=e,v[8]=n,v[12]=r,v[1]=i,v[5]=o,v[9]=s,v[13]=a,v[2]=u,v[6]=h,v[10]=l,v[14]=c,v[3]=p,v[7]=f,v[11]=d,v[15]=m,this},identity:function(){return this.set(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1),this},copy:function(t){return this.elements.set(t.elements),this},extractPosition:function(t){return THREE.warn("THREE.Matrix4: .extractPosition() has been renamed to .copyPosition()."),this.copyPosition(t)},copyPosition:function(t){var e=this.elements,n=t.elements;return e[12]=n[12],e[13]=n[13],e[14]=n[14],this},extractBasis:function(t,e,n){var r=this.elements;return t.set(r[0],r[1],r[2]),e.set(r[4],r[5],r[6]),n.set(r[8],r[9],r[10]),this},makeBasis:function(t,e,n){return this.set(t.x,e.x,n.x,0,t.y,e.y,n.y,0,t.z,e.z,n.z,0,0,0,0,1),this},extractRotation:function(){var t=new THREE.Vector3;return function(e){var n=this.elements,r=e.elements,i=1/t.set(r[0],r[1],r[2]).length(),o=1/t.set(r[4],r[5],r[6]).length(),s=1/t.set(r[8],r[9],r[10]).length();return n[0]=r[0]*i,n[1]=r[1]*i,n[2]=r[2]*i,n[4]=r[4]*o,n[5]=r[5]*o,n[6]=r[6]*o,n[8]=r[8]*s,n[9]=r[9]*s,n[10]=r[10]*s,this}}(),makeRotationFromEuler:function(t){t instanceof THREE.Euler==!1&&THREE.error("THREE.Matrix: .makeRotationFromEuler() now expects a Euler rotation rather than a Vector3 and order.");var e=this.elements,n=t.x,r=t.y,i=t.z,o=Math.cos(n),s=Math.sin(n),a=Math.cos(r),u=Math.sin(r),h=Math.cos(i),l=Math.sin(i);if("XYZ"===t.order){var c=o*h,p=o*l,f=s*h,d=s*l;e[0]=a*h,e[4]=-a*l,e[8]=u,e[1]=p+f*u,e[5]=c-d*u,e[9]=-s*a,e[2]=d-c*u,e[6]=f+p*u,e[10]=o*a}else if("YXZ"===t.order){var m=a*h,v=a*l,g=u*h,E=u*l;e[0]=m+E*s,e[4]=g*s-v,e[8]=o*u,e[1]=o*l,e[5]=o*h,e[9]=-s,e[2]=v*s-g,e[6]=E+m*s,e[10]=o*a}else if("ZXY"===t.order){var m=a*h,v=a*l,g=u*h,E=u*l;e[0]=m-E*s,e[4]=-o*l,e[8]=g+v*s,e[1]=v+g*s,e[5]=o*h,e[9]=E-m*s,e[2]=-o*u,e[6]=s,e[10]=o*a}else if("ZYX"===t.order){var c=o*h,p=o*l,f=s*h,d=s*l;e[0]=a*h,e[4]=f*u-p,e[8]=c*u+d,e[1]=a*l,e[5]=d*u+c,e[9]=p*u-f,e[2]=-u,e[6]=s*a,e[10]=o*a}else if("YZX"===t.order){var y=o*a,_=o*u,b=s*a,T=s*u;e[0]=a*h,e[4]=T-y*l,e[8]=b*l+_,e[1]=l,e[5]=o*h,e[9]=-s*h,e[2]=-u*h,e[6]=_*l+b,e[10]=y-T*l}else if("XZY"===t.order){var y=o*a,_=o*u,b=s*a,T=s*u;e[0]=a*h,e[4]=-l,e[8]=u*h,e[1]=y*l+T,e[5]=o*h,e[9]=_*l-b,e[2]=b*l-_,e[6]=s*h,e[10]=T*l+y}return e[3]=0,e[7]=0,e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1,this},setRotationFromQuaternion:function(t){return THREE.warn("THREE.Matrix4: .setRotationFromQuaternion() has been renamed to .makeRotationFromQuaternion()."),this.makeRotationFromQuaternion(t)},makeRotationFromQuaternion:function(t){var e=this.elements,n=t.x,r=t.y,i=t.z,o=t.w,s=n+n,a=r+r,u=i+i,h=n*s,l=n*a,c=n*u,p=r*a,f=r*u,d=i*u,m=o*s,v=o*a,g=o*u;return e[0]=1-(p+d),e[4]=l-g,e[8]=c+v,e[1]=l+g,e[5]=1-(h+d),e[9]=f-m,e[2]=c-v,e[6]=f+m,e[10]=1-(h+p),e[3]=0,e[7]=0,e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1,this},lookAt:function(){var t=new THREE.Vector3,e=new THREE.Vector3,n=new THREE.Vector3;return function(r,i,o){var s=this.elements;return n.subVectors(r,i).normalize(),0===n.length()&&(n.z=1),t.crossVectors(o,n).normalize(),0===t.length()&&(n.x+=1e-4,t.crossVectors(o,n).normalize()),e.crossVectors(n,t),s[0]=t.x,s[4]=e.x,s[8]=n.x,s[1]=t.y,s[5]=e.y,s[9]=n.y,s[2]=t.z,s[6]=e.z,s[10]=n.z,this}}(),multiply:function(t,e){return void 0!==e?(THREE.warn("THREE.Matrix4: .multiply() now only accepts one argument. Use .multiplyMatrices( a, b ) instead."),this.multiplyMatrices(t,e)):this.multiplyMatrices(this,t)},multiplyMatrices:function(t,e){var n=t.elements,r=e.elements,i=this.elements,o=n[0],s=n[4],a=n[8],u=n[12],h=n[1],l=n[5],c=n[9],p=n[13],f=n[2],d=n[6],m=n[10],v=n[14],g=n[3],E=n[7],y=n[11],_=n[15],b=r[0],T=r[4],x=r[8],w=r[12],R=r[1],H=r[5],M=r[9],S=r[13],k=r[2],A=r[6],C=r[10],P=r[14],L=r[3],z=r[7],O=r[11],D=r[15];return i[0]=o*b+s*R+a*k+u*L,i[4]=o*T+s*H+a*A+u*z,i[8]=o*x+s*M+a*C+u*O,i[12]=o*w+s*S+a*P+u*D,i[1]=h*b+l*R+c*k+p*L,i[5]=h*T+l*H+c*A+p*z,i[9]=h*x+l*M+c*C+p*O,i[13]=h*w+l*S+c*P+p*D,i[2]=f*b+d*R+m*k+v*L,i[6]=f*T+d*H+m*A+v*z,i[10]=f*x+d*M+m*C+v*O,i[14]=f*w+d*S+m*P+v*D,i[3]=g*b+E*R+y*k+_*L,i[7]=g*T+E*H+y*A+_*z,i[11]=g*x+E*M+y*C+_*O,i[15]=g*w+E*S+y*P+_*D,this},multiplyToArray:function(t,e,n){var r=this.elements;return this.multiplyMatrices(t,e),n[0]=r[0],n[1]=r[1],n[2]=r[2],n[3]=r[3],n[4]=r[4],n[5]=r[5],n[6]=r[6],n[7]=r[7],n[8]=r[8],n[9]=r[9],n[10]=r[10],n[11]=r[11],n[12]=r[12],n[13]=r[13],n[14]=r[14],n[15]=r[15],this},multiplyScalar:function(t){var e=this.elements;return e[0]*=t,e[4]*=t,e[8]*=t,e[12]*=t,e[1]*=t,e[5]*=t,e[9]*=t,e[13]*=t,e[2]*=t,e[6]*=t,e[10]*=t,e[14]*=t,e[3]*=t,e[7]*=t,e[11]*=t,e[15]*=t,this},multiplyVector3:function(t){return THREE.warn("THREE.Matrix4: .multiplyVector3() has been removed. Use vector.applyMatrix4( matrix ) or vector.applyProjection( matrix ) instead."),t.applyProjection(this)},multiplyVector4:function(t){return THREE.warn("THREE.Matrix4: .multiplyVector4() has been removed. Use vector.applyMatrix4( matrix ) instead."),t.applyMatrix4(this)},multiplyVector3Array:function(t){return THREE.warn("THREE.Matrix4: .multiplyVector3Array() has been renamed. Use matrix.applyToVector3Array( array ) instead."),this.applyToVector3Array(t)},applyToVector3Array:function(){var t=new THREE.Vector3;return function(e,n,r){void 0===n&&(n=0),void 0===r&&(r=e.length);for(var i=0,o=n;r>i;i+=3,o+=3)t.x=e[o],t.y=e[o+1],t.z=e[o+2],t.applyMatrix4(this),e[o]=t.x,e[o+1]=t.y,e[o+2]=t.z;return e}}(),rotateAxis:function(t){THREE.warn("THREE.Matrix4: .rotateAxis() has been removed. Use Vector3.transformDirection( matrix ) instead."),t.transformDirection(this)},crossVector:function(t){return THREE.warn("THREE.Matrix4: .crossVector() has been removed. Use vector.applyMatrix4( matrix ) instead."),t.applyMatrix4(this)},determinant:function(){var t=this.elements,e=t[0],n=t[4],r=t[8],i=t[12],o=t[1],s=t[5],a=t[9],u=t[13],h=t[2],l=t[6],c=t[10],p=t[14],f=t[3],d=t[7],m=t[11],v=t[15];return f*(+i*a*l-r*u*l-i*s*c+n*u*c+r*s*p-n*a*p)+d*(+e*a*p-e*u*c+i*o*c-r*o*p+r*u*h-i*a*h)+m*(+e*u*l-e*s*p-i*o*l+n*o*p+i*s*h-n*u*h)+v*(-r*s*h-e*a*l+e*s*c+r*o*l-n*o*c+n*a*h)},transpose:function(){var t,e=this.elements;return t=e[1],e[1]=e[4],e[4]=t,t=e[2],e[2]=e[8],e[8]=t,t=e[6],e[6]=e[9],e[9]=t,t=e[3],e[3]=e[12],e[12]=t,t=e[7],e[7]=e[13],e[13]=t,t=e[11],e[11]=e[14],e[14]=t,this},flattenToArrayOffset:function(t,e){var n=this.elements;return t[e]=n[0],t[e+1]=n[1],t[e+2]=n[2],t[e+3]=n[3],t[e+4]=n[4],t[e+5]=n[5],t[e+6]=n[6],t[e+7]=n[7],t[e+8]=n[8],t[e+9]=n[9],t[e+10]=n[10],t[e+11]=n[11],t[e+12]=n[12],t[e+13]=n[13],t[e+14]=n[14],t[e+15]=n[15],t},getPosition:function(){var t=new THREE.Vector3;return function(){THREE.warn("THREE.Matrix4: .getPosition() has been removed. Use Vector3.setFromMatrixPosition( matrix ) instead.");var e=this.elements;return t.set(e[12],e[13],e[14])}}(),setPosition:function(t){var e=this.elements;return e[12]=t.x,e[13]=t.y,e[14]=t.z,this},getInverse:function(t,e){var n=this.elements,r=t.elements,i=r[0],o=r[4],s=r[8],a=r[12],u=r[1],h=r[5],l=r[9],c=r[13],p=r[2],f=r[6],d=r[10],m=r[14],v=r[3],g=r[7],E=r[11],y=r[15];n[0]=l*m*g-c*d*g+c*f*E-h*m*E-l*f*y+h*d*y,n[4]=a*d*g-s*m*g-a*f*E+o*m*E+s*f*y-o*d*y,n[8]=s*c*g-a*l*g+a*h*E-o*c*E-s*h*y+o*l*y,n[12]=a*l*f-s*c*f-a*h*d+o*c*d+s*h*m-o*l*m,n[1]=c*d*v-l*m*v-c*p*E+u*m*E+l*p*y-u*d*y,n[5]=s*m*v-a*d*v+a*p*E-i*m*E-s*p*y+i*d*y,n[9]=a*l*v-s*c*v-a*u*E+i*c*E+s*u*y-i*l*y,n[13]=s*c*p-a*l*p+a*u*d-i*c*d-s*u*m+i*l*m,n[2]=h*m*v-c*f*v+c*p*g-u*m*g-h*p*y+u*f*y,n[6]=a*f*v-o*m*v-a*p*g+i*m*g+o*p*y-i*f*y,n[10]=o*c*v-a*h*v+a*u*g-i*c*g-o*u*y+i*h*y,n[14]=a*h*p-o*c*p-a*u*f+i*c*f+o*u*m-i*h*m,n[3]=l*f*v-h*d*v-l*p*g+u*d*g+h*p*E-u*f*E,n[7]=o*d*v-s*f*v+s*p*g-i*d*g-o*p*E+i*f*E,n[11]=s*h*v-o*l*v-s*u*g+i*l*g+o*u*E-i*h*E,n[15]=o*l*p-s*h*p+s*u*f-i*l*f-o*u*d+i*h*d;var _=i*n[0]+u*n[4]+p*n[8]+v*n[12];if(0==_){var b="THREE.Matrix4.getInverse(): can't invert matrix, determinant is 0";if(e)throw new Error(b);return THREE.warn(b),this.identity(),this}return this.multiplyScalar(1/_),this},translate:function(t){THREE.error("THREE.Matrix4: .translate() has been removed.")},rotateX:function(t){THREE.error("THREE.Matrix4: .rotateX() has been removed.")},rotateY:function(t){THREE.error("THREE.Matrix4: .rotateY() has been removed.")},rotateZ:function(t){THREE.error("THREE.Matrix4: .rotateZ() has been removed.")},rotateByAxis:function(t,e){THREE.error("THREE.Matrix4: .rotateByAxis() has been removed.")},scale:function(t){var e=this.elements,n=t.x,r=t.y,i=t.z;return e[0]*=n,e[4]*=r,e[8]*=i,e[1]*=n,e[5]*=r,e[9]*=i,e[2]*=n,e[6]*=r,e[10]*=i,e[3]*=n,e[7]*=r,e[11]*=i,this},getMaxScaleOnAxis:function(){var t=this.elements,e=t[0]*t[0]+t[1]*t[1]+t[2]*t[2],n=t[4]*t[4]+t[5]*t[5]+t[6]*t[6],r=t[8]*t[8]+t[9]*t[9]+t[10]*t[10];return Math.sqrt(Math.max(e,Math.max(n,r)))},makeTranslation:function(t,e,n){return this.set(1,0,0,t,0,1,0,e,0,0,1,n,0,0,0,1),this},makeRotationX:function(t){var e=Math.cos(t),n=Math.sin(t);return this.set(1,0,0,0,0,e,-n,0,0,n,e,0,0,0,0,1),this},makeRotationY:function(t){var e=Math.cos(t),n=Math.sin(t);return this.set(e,0,n,0,0,1,0,0,-n,0,e,0,0,0,0,1),this},makeRotationZ:function(t){var e=Math.cos(t),n=Math.sin(t);return this.set(e,-n,0,0,n,e,0,0,0,0,1,0,0,0,0,1),this},makeRotationAxis:function(t,e){var n=Math.cos(e),r=Math.sin(e),i=1-n,o=t.x,s=t.y,a=t.z,u=i*o,h=i*s;return this.set(u*o+n,u*s-r*a,u*a+r*s,0,u*s+r*a,h*s+n,h*a-r*o,0,u*a-r*s,h*a+r*o,i*a*a+n,0,0,0,0,1),this},makeScale:function(t,e,n){return this.set(t,0,0,0,0,e,0,0,0,0,n,0,0,0,0,1),this},compose:function(t,e,n){return this.makeRotationFromQuaternion(e),this.scale(n),this.setPosition(t),this},decompose:function(){var t=new THREE.Vector3,e=new THREE.Matrix4;return function(n,r,i){var o=this.elements,s=t.set(o[0],o[1],o[2]).length(),a=t.set(o[4],o[5],o[6]).length(),u=t.set(o[8],o[9],o[10]).length(),h=this.determinant();0>h&&(s=-s),n.x=o[12],n.y=o[13],n.z=o[14],e.elements.set(this.elements);var l=1/s,c=1/a,p=1/u;return e.elements[0]*=l,e.elements[1]*=l,e.elements[2]*=l,e.elements[4]*=c,e.elements[5]*=c,e.elements[6]*=c,e.elements[8]*=p,e.elements[9]*=p,e.elements[10]*=p,r.setFromRotationMatrix(e),i.x=s,i.y=a,i.z=u,this}}(),makeFrustum:function(t,e,n,r,i,o){var s=this.elements,a=2*i/(e-t),u=2*i/(r-n),h=(e+t)/(e-t),l=(r+n)/(r-n),c=-(o+i)/(o-i),p=-2*o*i/(o-i);return s[0]=a,s[4]=0,s[8]=h,s[12]=0,s[1]=0,s[5]=u,s[9]=l,s[13]=0,s[2]=0,s[6]=0,s[10]=c,s[14]=p,s[3]=0,s[7]=0,s[11]=-1,s[15]=0,this},makePerspective:function(t,e,n,r){var i=n*Math.tan(THREE.Math.degToRad(.5*t)),o=-i,s=o*e,a=i*e;return this.makeFrustum(s,a,o,i,n,r)},makeOrthographic:function(t,e,n,r,i,o){var s=this.elements,a=e-t,u=n-r,h=o-i,l=(e+t)/a,c=(n+r)/u,p=(o+i)/h;return s[0]=2/a,s[4]=0,s[8]=0,s[12]=-l,s[1]=0,s[5]=2/u,s[9]=0,s[13]=-c,s[2]=0,s[6]=0,s[10]=-2/h,s[14]=-p,s[3]=0,s[7]=0,s[11]=0,s[15]=1,this},fromArray:function(t){return this.elements.set(t),this},toArray:function(){var t=this.elements;return[t[0],t[1],t[2],t[3],t[4],t[5],t[6],t[7],t[8],t[9],t[10],t[11],t[12],t[13],t[14],t[15]]},clone:function(){return(new THREE.Matrix4).fromArray(this.elements)}},THREE.Ray=function(t,e){this.origin=void 0!==t?t:new THREE.Vector3,this.direction=void 0!==e?e:new THREE.Vector3},THREE.Ray.prototype={constructor:THREE.Ray,set:function(t,e){return this.origin.copy(t),this.direction.copy(e),this},copy:function(t){return this.origin.copy(t.origin),this.direction.copy(t.direction),this},at:function(t,e){var n=e||new THREE.Vector3;return n.copy(this.direction).multiplyScalar(t).add(this.origin)},recast:function(){var t=new THREE.Vector3;return function(e){return this.origin.copy(this.at(e,t)),this}}(),closestPointToPoint:function(t,e){var n=e||new THREE.Vector3;n.subVectors(t,this.origin);var r=n.dot(this.direction);return 0>r?n.copy(this.origin):n.copy(this.direction).multiplyScalar(r).add(this.origin)},distanceToPoint:function(){var t=new THREE.Vector3;return function(e){var n=t.subVectors(e,this.origin).dot(this.direction);return 0>n?this.origin.distanceTo(e):(t.copy(this.direction).multiplyScalar(n).add(this.origin),t.distanceTo(e))}}(),distanceSqToSegment:function(){var t=new THREE.Vector3,e=new THREE.Vector3,n=new THREE.Vector3;return function(r,i,o,s){t.copy(r).add(i).multiplyScalar(.5),e.copy(i).sub(r).normalize(),n.copy(this.origin).sub(t);var a,u,h,l,c=.5*r.distanceTo(i),p=-this.direction.dot(e),f=n.dot(this.direction),d=-n.dot(e),m=n.lengthSq(),v=Math.abs(1-p*p);if(v>0)if(a=p*d-f,u=p*f-d,l=c*v,a>=0)if(u>=-l)if(l>=u){var g=1/v;a*=g,u*=g,h=a*(a+p*u+2*f)+u*(p*a+u+2*d)+m}else u=c,a=Math.max(0,-(p*u+f)),h=-a*a+u*(u+2*d)+m;else u=-c,a=Math.max(0,-(p*u+f)),h=-a*a+u*(u+2*d)+m;else-l>=u?(a=Math.max(0,-(-p*c+f)),u=a>0?-c:Math.min(Math.max(-c,-d),c),h=-a*a+u*(u+2*d)+m):l>=u?(a=0,u=Math.min(Math.max(-c,-d),c),h=u*(u+2*d)+m):(a=Math.max(0,-(p*c+f)),u=a>0?c:Math.min(Math.max(-c,-d),c),h=-a*a+u*(u+2*d)+m);else u=p>0?-c:c,a=Math.max(0,-(p*u+f)),h=-a*a+u*(u+2*d)+m;return o&&o.copy(this.direction).multiplyScalar(a).add(this.origin),s&&s.copy(e).multiplyScalar(u).add(t),h}}(),isIntersectionSphere:function(t){return this.distanceToPoint(t.center)<=t.radius},intersectSphere:function(){var t=new THREE.Vector3;return function(e,n){t.subVectors(e.center,this.origin);var r=t.dot(this.direction),i=t.dot(t)-r*r,o=e.radius*e.radius;if(i>o)return null;var s=Math.sqrt(o-i),a=r-s,u=r+s;return 0>a&&0>u?null:0>a?this.at(u,n):this.at(a,n)}}(),isIntersectionPlane:function(t){var e=t.distanceToPoint(this.origin);if(0===e)return!0;var n=t.normal.dot(this.direction);return 0>n*e?!0:!1},distanceToPlane:function(t){var e=t.normal.dot(this.direction);if(0==e)return 0==t.distanceToPoint(this.origin)?0:null;var n=-(this.origin.dot(t.normal)+t.constant)/e;return n>=0?n:null},intersectPlane:function(t,e){var n=this.distanceToPlane(t);return null===n?null:this.at(n,e)},isIntersectionBox:function(){var t=new THREE.Vector3;return function(e){return null!==this.intersectBox(e,t)}}(),intersectBox:function(t,e){var n,r,i,o,s,a,u=1/this.direction.x,h=1/this.direction.y,l=1/this.direction.z,c=this.origin;return u>=0?(n=(t.min.x-c.x)*u,r=(t.max.x-c.x)*u):(n=(t.max.x-c.x)*u,r=(t.min.x-c.x)*u),h>=0?(i=(t.min.y-c.y)*h,o=(t.max.y-c.y)*h):(i=(t.max.y-c.y)*h,o=(t.min.y-c.y)*h),n>o||i>r?null:((i>n||n!==n)&&(n=i),(r>o||r!==r)&&(r=o),l>=0?(s=(t.min.z-c.z)*l,a=(t.max.z-c.z)*l):(s=(t.max.z-c.z)*l,a=(t.min.z-c.z)*l),n>a||s>r?null:((s>n||n!==n)&&(n=s),(r>a||r!==r)&&(r=a),0>r?null:this.at(n>=0?n:r,e)))},intersectTriangle:function(){var t=new THREE.Vector3,e=new THREE.Vector3,n=new THREE.Vector3,r=new THREE.Vector3;return function(i,o,s,a,u){e.subVectors(o,i),n.subVectors(s,i),r.crossVectors(e,n);var h,l=this.direction.dot(r);if(l>0){if(a)return null;h=1}else{if(!(0>l))return null;h=-1,l=-l}t.subVectors(this.origin,i);var c=h*this.direction.dot(n.crossVectors(t,n));if(0>c)return null;var p=h*this.direction.dot(e.cross(t));if(0>p)return null;if(c+p>l)return null;var f=-h*t.dot(r);return 0>f?null:this.at(f/l,u)}}(),applyMatrix4:function(t){return this.direction.add(this.origin).applyMatrix4(t),this.origin.applyMatrix4(t),this.direction.sub(this.origin),this.direction.normalize(),this},equals:function(t){return t.origin.equals(this.origin)&&t.direction.equals(this.direction)},clone:function(){return(new THREE.Ray).copy(this)}},THREE.Sphere=function(t,e){this.center=void 0!==t?t:new THREE.Vector3,this.radius=void 0!==e?e:0},THREE.Sphere.prototype={constructor:THREE.Sphere,set:function(t,e){return this.center.copy(t),this.radius=e,this},setFromPoints:function(){var t=new THREE.Box3;return function(e,n){var r=this.center;void 0!==n?r.copy(n):t.setFromPoints(e).center(r);for(var i=0,o=0,s=e.length;s>o;o++)i=Math.max(i,r.distanceToSquared(e[o]));return this.radius=Math.sqrt(i),this}}(),copy:function(t){return this.center.copy(t.center),this.radius=t.radius,this},empty:function(){return this.radius<=0},containsPoint:function(t){return t.distanceToSquared(this.center)<=this.radius*this.radius},distanceToPoint:function(t){return t.distanceTo(this.center)-this.radius},intersectsSphere:function(t){var e=this.radius+t.radius;return t.center.distanceToSquared(this.center)<=e*e},clampPoint:function(t,e){var n=this.center.distanceToSquared(t),r=e||new THREE.Vector3;return r.copy(t),n>this.radius*this.radius&&(r.sub(this.center).normalize(),r.multiplyScalar(this.radius).add(this.center)),r},getBoundingBox:function(t){var e=t||new THREE.Box3;return e.set(this.center,this.center),e.expandByScalar(this.radius),e},applyMatrix4:function(t){return this.center.applyMatrix4(t),this.radius=this.radius*t.getMaxScaleOnAxis(),this},translate:function(t){return this.center.add(t),this},equals:function(t){return t.center.equals(this.center)&&t.radius===this.radius},clone:function(){return(new THREE.Sphere).copy(this)}},THREE.Frustum=function(t,e,n,r,i,o){this.planes=[void 0!==t?t:new THREE.Plane,void 0!==e?e:new THREE.Plane,void 0!==n?n:new THREE.Plane,void 0!==r?r:new THREE.Plane,void 0!==i?i:new THREE.Plane,void 0!==o?o:new THREE.Plane]},THREE.Frustum.prototype={constructor:THREE.Frustum,set:function(t,e,n,r,i,o){var s=this.planes;return s[0].copy(t),s[1].copy(e),s[2].copy(n),s[3].copy(r),s[4].copy(i),s[5].copy(o),this},copy:function(t){for(var e=this.planes,n=0;6>n;n++)e[n].copy(t.planes[n]);return this},setFromMatrix:function(t){var e=this.planes,n=t.elements,r=n[0],i=n[1],o=n[2],s=n[3],a=n[4],u=n[5],h=n[6],l=n[7],c=n[8],p=n[9],f=n[10],d=n[11],m=n[12],v=n[13],g=n[14],E=n[15];return e[0].setComponents(s-r,l-a,d-c,E-m).normalize(),e[1].setComponents(s+r,l+a,d+c,E+m).normalize(),e[2].setComponents(s+i,l+u,d+p,E+v).normalize(),e[3].setComponents(s-i,l-u,d-p,E-v).normalize(),e[4].setComponents(s-o,l-h,d-f,E-g).normalize(),e[5].setComponents(s+o,l+h,d+f,E+g).normalize(),this},intersectsObject:function(){var t=new THREE.Sphere;return function(e){var n=e.geometry;return null===n.boundingSphere&&n.computeBoundingSphere(),t.copy(n.boundingSphere),t.applyMatrix4(e.matrixWorld),this.intersectsSphere(t)}}(),intersectsSphere:function(t){for(var e=this.planes,n=t.center,r=-t.radius,i=0;6>i;i++){var o=e[i].distanceToPoint(n);if(r>o)return!1}return!0},intersectsBox:function(){var t=new THREE.Vector3,e=new THREE.Vector3;return function(n){for(var r=this.planes,i=0;6>i;i++){var o=r[i];t.x=o.normal.x>0?n.min.x:n.max.x,e.x=o.normal.x>0?n.max.x:n.min.x,t.y=o.normal.y>0?n.min.y:n.max.y,e.y=o.normal.y>0?n.max.y:n.min.y,t.z=o.normal.z>0?n.min.z:n.max.z,e.z=o.normal.z>0?n.max.z:n.min.z;var s=o.distanceToPoint(t),a=o.distanceToPoint(e);if(0>s&&0>a)return!1}return!0}}(),containsPoint:function(t){for(var e=this.planes,n=0;6>n;n++)if(e[n].distanceToPoint(t)<0)return!1;return!0},clone:function(){return(new THREE.Frustum).copy(this); -}},THREE.Plane=function(t,e){this.normal=void 0!==t?t:new THREE.Vector3(1,0,0),this.constant=void 0!==e?e:0},THREE.Plane.prototype={constructor:THREE.Plane,set:function(t,e){return this.normal.copy(t),this.constant=e,this},setComponents:function(t,e,n,r){return this.normal.set(t,e,n),this.constant=r,this},setFromNormalAndCoplanarPoint:function(t,e){return this.normal.copy(t),this.constant=-e.dot(this.normal),this},setFromCoplanarPoints:function(){var t=new THREE.Vector3,e=new THREE.Vector3;return function(n,r,i){var o=t.subVectors(i,r).cross(e.subVectors(n,r)).normalize();return this.setFromNormalAndCoplanarPoint(o,n),this}}(),copy:function(t){return this.normal.copy(t.normal),this.constant=t.constant,this},normalize:function(){var t=1/this.normal.length();return this.normal.multiplyScalar(t),this.constant*=t,this},negate:function(){return this.constant*=-1,this.normal.negate(),this},distanceToPoint:function(t){return this.normal.dot(t)+this.constant},distanceToSphere:function(t){return this.distanceToPoint(t.center)-t.radius},projectPoint:function(t,e){return this.orthoPoint(t,e).sub(t).negate()},orthoPoint:function(t,e){var n=this.distanceToPoint(t),r=e||new THREE.Vector3;return r.copy(this.normal).multiplyScalar(n)},isIntersectionLine:function(t){var e=this.distanceToPoint(t.start),n=this.distanceToPoint(t.end);return 0>e&&n>0||0>n&&e>0},intersectLine:function(){var t=new THREE.Vector3;return function(e,n){var r=n||new THREE.Vector3,i=e.delta(t),o=this.normal.dot(i);if(0==o)return 0==this.distanceToPoint(e.start)?r.copy(e.start):void 0;var s=-(e.start.dot(this.normal)+this.constant)/o;return 0>s||s>1?void 0:r.copy(i).multiplyScalar(s).add(e.start)}}(),coplanarPoint:function(t){var e=t||new THREE.Vector3;return e.copy(this.normal).multiplyScalar(-this.constant)},applyMatrix4:function(){var t=new THREE.Vector3,e=new THREE.Vector3,n=new THREE.Matrix3;return function(r,i){var o=i||n.getNormalMatrix(r),s=t.copy(this.normal).applyMatrix3(o),a=this.coplanarPoint(e);return a.applyMatrix4(r),this.setFromNormalAndCoplanarPoint(s,a),this}}(),translate:function(t){return this.constant=this.constant-t.dot(this.normal),this},equals:function(t){return t.normal.equals(this.normal)&&t.constant==this.constant},clone:function(){return(new THREE.Plane).copy(this)}},THREE.Math={generateUUID:function(){var t,e="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split(""),n=new Array(36),r=0;return function(){for(var i=0;36>i;i++)8==i||13==i||18==i||23==i?n[i]="-":14==i?n[i]="4":(2>=r&&(r=33554432+16777216*Math.random()|0),t=15&r,r>>=4,n[i]=e[19==i?3&t|8:t]);return n.join("")}}(),clamp:function(t,e,n){return e>t?e:t>n?n:t},clampBottom:function(t,e){return e>t?e:t},mapLinear:function(t,e,n,r,i){return r+(t-e)*(i-r)/(n-e)},smoothstep:function(t,e,n){return e>=t?0:t>=n?1:(t=(t-e)/(n-e),t*t*(3-2*t))},smootherstep:function(t,e,n){return e>=t?0:t>=n?1:(t=(t-e)/(n-e),t*t*t*(t*(6*t-15)+10))},random16:function(){return(65280*Math.random()+255*Math.random())/65535},randInt:function(t,e){return Math.floor(this.randFloat(t,e))},randFloat:function(t,e){return t+Math.random()*(e-t)},randFloatSpread:function(t){return t*(.5-Math.random())},degToRad:function(){var t=Math.PI/180;return function(e){return e*t}}(),radToDeg:function(){var t=180/Math.PI;return function(e){return e*t}}(),isPowerOfTwo:function(t){return 0===(t&t-1)&&0!==t},nextPowerOfTwo:function(t){return t--,t|=t>>1,t|=t>>2,t|=t>>4,t|=t>>8,t|=t>>16,t++,t}},THREE.Spline=function(t){function e(t,e,n,r,i,o,s){var a=.5*(n-t),u=.5*(r-e);return(2*(e-n)+a+u)*s+(-3*(e-n)-2*a-u)*o+a*i+e}this.points=t;var n,r,i,o,s,a,u,h,l,c=[],p={x:0,y:0,z:0};this.initFromArray=function(t){this.points=[];for(var e=0;ethis.points.length-2?this.points.length-1:r+1,c[3]=r>this.points.length-3?this.points.length-1:r+2,a=this.points[c[0]],u=this.points[c[1]],h=this.points[c[2]],l=this.points[c[3]],o=i*i,s=i*o,p.x=e(a.x,u.x,h.x,l.x,i,o,s),p.y=e(a.y,u.y,h.y,l.y,i,o,s),p.z=e(a.z,u.z,h.z,l.z,i,o,s),p},this.getControlPointsArray=function(){var t,e,n=this.points.length,r=[];for(t=0;n>t;t++)e=this.points[t],r[t]=[e.x,e.y,e.z];return r},this.getLength=function(t){var e,n,r,i,o=0,s=0,a=0,u=new THREE.Vector3,h=new THREE.Vector3,l=[],c=0;for(l[0]=0,t||(t=100),r=this.points.length*t,u.copy(this.points[0]),e=1;r>e;e++)n=e/r,i=this.getPoint(n),h.copy(i),c+=h.distanceTo(u),u.copy(i),o=(this.points.length-1)*n,s=Math.floor(o),s!=a&&(l[s]=c,a=s);return l[l.length]=c,{chunks:l,total:c}},this.reparametrizeByArcLength=function(t){var e,n,r,i,o,s,a,u,h=[],l=new THREE.Vector3,c=this.getLength();for(h.push(l.copy(this.points[0]).clone()),e=1;en;n++)r=i+n*(1/a)*(o-i),u=this.getPoint(r),h.push(l.copy(u).clone());h.push(l.copy(this.points[e]).clone())}this.points=h}},THREE.Triangle=function(t,e,n){this.a=void 0!==t?t:new THREE.Vector3,this.b=void 0!==e?e:new THREE.Vector3,this.c=void 0!==n?n:new THREE.Vector3},THREE.Triangle.normal=function(){var t=new THREE.Vector3;return function(e,n,r,i){var o=i||new THREE.Vector3;o.subVectors(r,n),t.subVectors(e,n),o.cross(t);var s=o.lengthSq();return s>0?o.multiplyScalar(1/Math.sqrt(s)):o.set(0,0,0)}}(),THREE.Triangle.barycoordFromPoint=function(){var t=new THREE.Vector3,e=new THREE.Vector3,n=new THREE.Vector3;return function(r,i,o,s,a){t.subVectors(s,i),e.subVectors(o,i),n.subVectors(r,i);var u=t.dot(t),h=t.dot(e),l=t.dot(n),c=e.dot(e),p=e.dot(n),f=u*c-h*h,d=a||new THREE.Vector3;if(0==f)return d.set(-2,-1,-1);var m=1/f,v=(c*l-h*p)*m,g=(u*p-h*l)*m;return d.set(1-v-g,g,v)}}(),THREE.Triangle.containsPoint=function(){var t=new THREE.Vector3;return function(e,n,r,i){var o=THREE.Triangle.barycoordFromPoint(e,n,r,i,t);return o.x>=0&&o.y>=0&&o.x+o.y<=1}}(),THREE.Triangle.prototype={constructor:THREE.Triangle,set:function(t,e,n){return this.a.copy(t),this.b.copy(e),this.c.copy(n),this},setFromPointsAndIndices:function(t,e,n,r){return this.a.copy(t[e]),this.b.copy(t[n]),this.c.copy(t[r]),this},copy:function(t){return this.a.copy(t.a),this.b.copy(t.b),this.c.copy(t.c),this},area:function(){var t=new THREE.Vector3,e=new THREE.Vector3;return function(){return t.subVectors(this.c,this.b),e.subVectors(this.a,this.b),.5*t.cross(e).length()}}(),midpoint:function(t){var e=t||new THREE.Vector3;return e.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)},normal:function(t){return THREE.Triangle.normal(this.a,this.b,this.c,t)},plane:function(t){var e=t||new THREE.Plane;return e.setFromCoplanarPoints(this.a,this.b,this.c)},barycoordFromPoint:function(t,e){return THREE.Triangle.barycoordFromPoint(t,this.a,this.b,this.c,e)},containsPoint:function(t){return THREE.Triangle.containsPoint(t,this.a,this.b,this.c)},equals:function(t){return t.a.equals(this.a)&&t.b.equals(this.b)&&t.c.equals(this.c)},clone:function(){return(new THREE.Triangle).copy(this)}},THREE.Clock=function(t){this.autoStart=void 0!==t?t:!0,this.startTime=0,this.oldTime=0,this.elapsedTime=0,this.running=!1},THREE.Clock.prototype={constructor:THREE.Clock,start:function(){this.startTime=void 0!==self.performance&&void 0!==self.performance.now?self.performance.now():Date.now(),this.oldTime=this.startTime,this.running=!0},stop:function(){this.getElapsedTime(),this.running=!1},getElapsedTime:function(){return this.getDelta(),this.elapsedTime},getDelta:function(){var t=0;if(this.autoStart&&!this.running&&this.start(),this.running){var e=void 0!==self.performance&&void 0!==self.performance.now?self.performance.now():Date.now();t=.001*(e-this.oldTime),this.oldTime=e,this.elapsedTime+=t}return t}},THREE.EventDispatcher=function(){},THREE.EventDispatcher.prototype={constructor:THREE.EventDispatcher,apply:function(t){t.addEventListener=THREE.EventDispatcher.prototype.addEventListener,t.hasEventListener=THREE.EventDispatcher.prototype.hasEventListener,t.removeEventListener=THREE.EventDispatcher.prototype.removeEventListener,t.dispatchEvent=THREE.EventDispatcher.prototype.dispatchEvent},addEventListener:function(t,e){void 0===this._listeners&&(this._listeners={});var n=this._listeners;void 0===n[t]&&(n[t]=[]),-1===n[t].indexOf(e)&&n[t].push(e)},hasEventListener:function(t,e){if(void 0===this._listeners)return!1;var n=this._listeners;return void 0!==n[t]&&-1!==n[t].indexOf(e)?!0:!1},removeEventListener:function(t,e){if(void 0!==this._listeners){var n=this._listeners,r=n[t];if(void 0!==r){var i=r.indexOf(e);-1!==i&&r.splice(i,1)}}},dispatchEvent:function(t){if(void 0!==this._listeners){var e=this._listeners,n=e[t.type];if(void 0!==n){t.target=this;for(var r=[],i=n.length,o=0;i>o;o++)r[o]=n[o];for(var o=0;i>o;o++)r[o].call(this,t)}}}},function(t){t.Raycaster=function(e,n,r,i){this.ray=new t.Ray(e,n),this.near=r||0,this.far=i||1/0,this.params={Sprite:{},Mesh:{},PointCloud:{threshold:1},LOD:{},Line:{}}};var e=function(t,e){return t.distance-e.distance},n=function(t,e,r,i){if(t.raycast(e,r),i===!0)for(var o=t.children,s=0,a=o.length;a>s;s++)n(o[s],e,r,!0)};t.Raycaster.prototype={constructor:t.Raycaster,precision:1e-4,linePrecision:1,set:function(t,e){this.ray.set(t,e)},setFromCamera:function(e,n){n instanceof t.PerspectiveCamera?(this.ray.origin.copy(n.position),this.ray.direction.set(e.x,e.y,.5).unproject(n).sub(n.position).normalize()):n instanceof t.OrthographicCamera?(this.ray.origin.set(e.x,e.y,-1).unproject(n),this.ray.direction.set(0,0,-1).transformDirection(n.matrixWorld)):t.error("THREE.Raycaster: Unsupported camera type.")},intersectObject:function(t,r){var i=[];return n(t,this,i,r),i.sort(e),i},intersectObjects:function(r,i){var o=[];if(r instanceof Array==!1)return t.warn("THREE.Raycaster.intersectObjects: objects is not an Array."),o;for(var s=0,a=r.length;a>s;s++)n(r[s],this,o,i);return o.sort(e),o}}}(THREE),THREE.Object3D=function(){Object.defineProperty(this,"id",{value:THREE.Object3DIdCount++}),this.uuid=THREE.Math.generateUUID(),this.name="",this.type="Object3D",this.parent=void 0,this.children=[],this.up=THREE.Object3D.DefaultUp.clone();var t=new THREE.Vector3,e=new THREE.Euler,n=new THREE.Quaternion,r=new THREE.Vector3(1,1,1),i=function(){n.setFromEuler(e,!1)},o=function(){e.setFromQuaternion(n,void 0,!1)};e.onChange(i),n.onChange(o),Object.defineProperties(this,{position:{enumerable:!0,value:t},rotation:{enumerable:!0,value:e},quaternion:{enumerable:!0,value:n},scale:{enumerable:!0,value:r}}),this.rotationAutoUpdate=!0,this.matrix=new THREE.Matrix4,this.matrixWorld=new THREE.Matrix4,this.matrixAutoUpdate=!0,this.matrixWorldNeedsUpdate=!1,this.visible=!0,this.castShadow=!1,this.receiveShadow=!1,this.frustumCulled=!0,this.renderOrder=0,this.userData={}},THREE.Object3D.DefaultUp=new THREE.Vector3(0,1,0),THREE.Object3D.prototype={constructor:THREE.Object3D,get eulerOrder(){return THREE.warn("THREE.Object3D: .eulerOrder has been moved to .rotation.order."),this.rotation.order},set eulerOrder(t){THREE.warn("THREE.Object3D: .eulerOrder has been moved to .rotation.order."),this.rotation.order=t},get useQuaternion(){THREE.warn("THREE.Object3D: .useQuaternion has been removed. The library now uses quaternions by default.")},set useQuaternion(t){THREE.warn("THREE.Object3D: .useQuaternion has been removed. The library now uses quaternions by default.")},applyMatrix:function(t){this.matrix.multiplyMatrices(t,this.matrix),this.matrix.decompose(this.position,this.quaternion,this.scale)},setRotationFromAxisAngle:function(t,e){this.quaternion.setFromAxisAngle(t,e)},setRotationFromEuler:function(t){this.quaternion.setFromEuler(t,!0)},setRotationFromMatrix:function(t){this.quaternion.setFromRotationMatrix(t)},setRotationFromQuaternion:function(t){this.quaternion.copy(t)},rotateOnAxis:function(){var t=new THREE.Quaternion;return function(e,n){return t.setFromAxisAngle(e,n),this.quaternion.multiply(t),this}}(),rotateX:function(){var t=new THREE.Vector3(1,0,0);return function(e){return this.rotateOnAxis(t,e)}}(),rotateY:function(){var t=new THREE.Vector3(0,1,0);return function(e){return this.rotateOnAxis(t,e)}}(),rotateZ:function(){var t=new THREE.Vector3(0,0,1);return function(e){return this.rotateOnAxis(t,e)}}(),translateOnAxis:function(){var t=new THREE.Vector3;return function(e,n){return t.copy(e).applyQuaternion(this.quaternion),this.position.add(t.multiplyScalar(n)),this}}(),translate:function(t,e){return THREE.warn("THREE.Object3D: .translate() has been removed. Use .translateOnAxis( axis, distance ) instead."),this.translateOnAxis(e,t)},translateX:function(){var t=new THREE.Vector3(1,0,0);return function(e){return this.translateOnAxis(t,e)}}(),translateY:function(){var t=new THREE.Vector3(0,1,0);return function(e){return this.translateOnAxis(t,e)}}(),translateZ:function(){var t=new THREE.Vector3(0,0,1);return function(e){return this.translateOnAxis(t,e)}}(),localToWorld:function(t){return t.applyMatrix4(this.matrixWorld)},worldToLocal:function(){var t=new THREE.Matrix4;return function(e){return e.applyMatrix4(t.getInverse(this.matrixWorld))}}(),lookAt:function(){var t=new THREE.Matrix4;return function(e){t.lookAt(e,this.position,this.up),this.quaternion.setFromRotationMatrix(t)}}(),add:function(t){if(arguments.length>1){for(var e=0;e1)for(var e=0;en;n++){var i=this.children[n],o=i.getObjectByProperty(t,e);if(void 0!==o)return o}return void 0},getWorldPosition:function(t){var e=t||new THREE.Vector3;return this.updateMatrixWorld(!0),e.setFromMatrixPosition(this.matrixWorld)},getWorldQuaternion:function(){var t=new THREE.Vector3,e=new THREE.Vector3;return function(n){var r=n||new THREE.Quaternion;return this.updateMatrixWorld(!0),this.matrixWorld.decompose(t,r,e),r}}(),getWorldRotation:function(){var t=new THREE.Quaternion;return function(e){var n=e||new THREE.Euler;return this.getWorldQuaternion(t),n.setFromQuaternion(t,this.rotation.order,!1)}}(),getWorldScale:function(){var t=new THREE.Vector3,e=new THREE.Quaternion;return function(n){var r=n||new THREE.Vector3;return this.updateMatrixWorld(!0),this.matrixWorld.decompose(t,e,r),r}}(),getWorldDirection:function(){var t=new THREE.Quaternion;return function(e){var n=e||new THREE.Vector3;return this.getWorldQuaternion(t),n.set(0,0,1).applyQuaternion(t)}}(),raycast:function(){},traverse:function(t){t(this);for(var e=0,n=this.children.length;n>e;e++)this.children[e].traverse(t)},traverseVisible:function(t){if(this.visible!==!1){t(this);for(var e=0,n=this.children.length;n>e;e++)this.children[e].traverseVisible(t)}},traverseAncestors:function(t){this.parent&&(t(this.parent),this.parent.traverseAncestors(t))},updateMatrix:function(){this.matrix.compose(this.position,this.quaternion,this.scale),this.matrixWorldNeedsUpdate=!0},updateMatrixWorld:function(t){this.matrixAutoUpdate===!0&&this.updateMatrix(),(this.matrixWorldNeedsUpdate===!0||t===!0)&&(void 0===this.parent?this.matrixWorld.copy(this.matrix):this.matrixWorld.multiplyMatrices(this.parent.matrixWorld,this.matrix),this.matrixWorldNeedsUpdate=!1,t=!0);for(var e=0,n=this.children.length;n>e;e++)this.children[e].updateMatrixWorld(t)},toJSON:function(){var t={metadata:{version:4.3,type:"Object",generator:"ObjectExporter"}},e={},n=function(n){if(void 0===t.geometries&&(t.geometries=[]),void 0===e[n.uuid]){var r=n.toJSON();delete r.metadata,e[n.uuid]=r,t.geometries.push(r)}return n.uuid},r={},i=function(e){if(void 0===t.materials&&(t.materials=[]),void 0===r[e.uuid]){var n=e.toJSON();delete n.metadata,r[e.uuid]=n,t.materials.push(n)}return e.uuid},o=function(t){var e={};if(e.uuid=t.uuid,e.type=t.type,""!==t.name&&(e.name=t.name),"{}"!==JSON.stringify(t.userData)&&(e.userData=t.userData),t.visible!==!0&&(e.visible=t.visible),t instanceof THREE.PerspectiveCamera?(e.fov=t.fov,e.aspect=t.aspect,e.near=t.near,e.far=t.far):t instanceof THREE.OrthographicCamera?(e.left=t.left,e.right=t.right,e.top=t.top,e.bottom=t.bottom,e.near=t.near,e.far=t.far):t instanceof THREE.AmbientLight?e.color=t.color.getHex():t instanceof THREE.DirectionalLight?(e.color=t.color.getHex(),e.intensity=t.intensity):t instanceof THREE.PointLight?(e.color=t.color.getHex(),e.intensity=t.intensity,e.distance=t.distance,e.decay=t.decay):t instanceof THREE.SpotLight?(e.color=t.color.getHex(),e.intensity=t.intensity,e.distance=t.distance,e.angle=t.angle,e.exponent=t.exponent,e.decay=t.decay):t instanceof THREE.HemisphereLight?(e.color=t.color.getHex(),e.groundColor=t.groundColor.getHex()):t instanceof THREE.Mesh||t instanceof THREE.Line||t instanceof THREE.PointCloud?(e.geometry=n(t.geometry),e.material=i(t.material),t instanceof THREE.Line&&(e.mode=t.mode)):t instanceof THREE.Sprite&&(e.material=i(t.material)),e.matrix=t.matrix.toArray(),t.children.length>0){e.children=[];for(var r=0;re;e++)t.vertexNormals[e]=this.vertexNormals[e].clone();for(var e=0,n=this.vertexColors.length;n>e;e++)t.vertexColors[e]=this.vertexColors[e].clone();for(var e=0,n=this.vertexTangents.length;n>e;e++)t.vertexTangents[e]=this.vertexTangents[e].clone();return t}},THREE.Face4=function(t,e,n,r,i,o,s){return THREE.warn("THREE.Face4 has been removed. A THREE.Face3 will be created instead."),new THREE.Face3(t,e,n,i,o,s)},THREE.BufferAttribute=function(t,e){this.array=t,this.itemSize=e,this.needsUpdate=!1},THREE.BufferAttribute.prototype={constructor:THREE.BufferAttribute,get length(){return this.array.length},copyAt:function(t,e,n){t*=this.itemSize,n*=e.itemSize;for(var r=0,i=this.itemSize;i>r;r++)this.array[t+r]=e.array[n+r];return this},set:function(t,e){return void 0===e&&(e=0),this.array.set(t,e),this},setX:function(t,e){return this.array[t*this.itemSize]=e,this},setY:function(t,e){return this.array[t*this.itemSize+1]=e,this},setZ:function(t,e){return this.array[t*this.itemSize+2]=e,this},setXY:function(t,e,n){return t*=this.itemSize,this.array[t]=e,this.array[t+1]=n,this},setXYZ:function(t,e,n,r){return t*=this.itemSize,this.array[t]=e,this.array[t+1]=n,this.array[t+2]=r,this},setXYZW:function(t,e,n,r,i){return t*=this.itemSize,this.array[t]=e,this.array[t+1]=n,this.array[t+2]=r,this.array[t+3]=i,this},clone:function(){return new THREE.BufferAttribute(new this.array.constructor(this.array),this.itemSize)}},THREE.Int8Attribute=function(t,e){return THREE.warn("THREE.Int8Attribute has been removed. Use THREE.BufferAttribute( array, itemSize ) instead."),new THREE.BufferAttribute(t,e)},THREE.Uint8Attribute=function(t,e){return THREE.warn("THREE.Uint8Attribute has been removed. Use THREE.BufferAttribute( array, itemSize ) instead."),new THREE.BufferAttribute(t,e)},THREE.Uint8ClampedAttribute=function(t,e){return THREE.warn("THREE.Uint8ClampedAttribute has been removed. Use THREE.BufferAttribute( array, itemSize ) instead."),new THREE.BufferAttribute(t,e)},THREE.Int16Attribute=function(t,e){return THREE.warn("THREE.Int16Attribute has been removed. Use THREE.BufferAttribute( array, itemSize ) instead."),new THREE.BufferAttribute(t,e)},THREE.Uint16Attribute=function(t,e){return THREE.warn("THREE.Uint16Attribute has been removed. Use THREE.BufferAttribute( array, itemSize ) instead."),new THREE.BufferAttribute(t,e)},THREE.Int32Attribute=function(t,e){return THREE.warn("THREE.Int32Attribute has been removed. Use THREE.BufferAttribute( array, itemSize ) instead."),new THREE.BufferAttribute(t,e)},THREE.Uint32Attribute=function(t,e){return THREE.warn("THREE.Uint32Attribute has been removed. Use THREE.BufferAttribute( array, itemSize ) instead."),new THREE.BufferAttribute(t,e)},THREE.Float32Attribute=function(t,e){return THREE.warn("THREE.Float32Attribute has been removed. Use THREE.BufferAttribute( array, itemSize ) instead."),new THREE.BufferAttribute(t,e)},THREE.Float64Attribute=function(t,e){return THREE.warn("THREE.Float64Attribute has been removed. Use THREE.BufferAttribute( array, itemSize ) instead."),new THREE.BufferAttribute(t,e)},THREE.DynamicBufferAttribute=function(t,e){THREE.BufferAttribute.call(this,t,e),this.updateRange={offset:0,count:-1}},THREE.DynamicBufferAttribute.prototype=Object.create(THREE.BufferAttribute.prototype),THREE.DynamicBufferAttribute.prototype.constructor=THREE.DynamicBufferAttribute,THREE.DynamicBufferAttribute.prototype.clone=function(){return new THREE.DynamicBufferAttribute(new this.array.constructor(this.array),this.itemSize)},THREE.BufferGeometry=function(){Object.defineProperty(this,"id",{value:THREE.GeometryIdCount++}),this.uuid=THREE.Math.generateUUID(),this.name="",this.type="BufferGeometry",this.attributes={},this.attributesKeys=[],this.drawcalls=[],this.offsets=this.drawcalls,this.boundingBox=null,this.boundingSphere=null},THREE.BufferGeometry.prototype={constructor:THREE.BufferGeometry,addAttribute:function(t,e){return e instanceof THREE.BufferAttribute==!1?(THREE.warn("THREE.BufferGeometry: .addAttribute() now expects ( name, attribute )."),void(this.attributes[t]={array:arguments[1],itemSize:arguments[2]})):(this.attributes[t]=e,void(this.attributesKeys=Object.keys(this.attributes)))},getAttribute:function(t){return this.attributes[t]},addDrawCall:function(t,e,n){this.drawcalls.push({start:t,count:e,index:void 0!==n?n:0})},applyMatrix:function(t){var e=this.attributes.position;void 0!==e&&(t.applyToVector3Array(e.array),e.needsUpdate=!0);var n=this.attributes.normal;if(void 0!==n){var r=(new THREE.Matrix3).getNormalMatrix(t);r.applyToVector3Array(n.array),n.needsUpdate=!0}null!==this.boundingBox&&this.computeBoundingBox(),null!==this.boundingSphere&&this.computeBoundingSphere()},center:function(){this.computeBoundingBox();var t=this.boundingBox.center().negate();return this.applyMatrix((new THREE.Matrix4).setPosition(t)),t},fromGeometry:function(t,e){e=e||{vertexColors:THREE.NoColors};var n=t.vertices,r=t.faces,i=t.faceVertexUvs,o=e.vertexColors,s=i[0].length>0,a=3==r[0].vertexNormals.length,u=new Float32Array(3*r.length*3);this.addAttribute("position",new THREE.BufferAttribute(u,3));var h=new Float32Array(3*r.length*3);if(this.addAttribute("normal",new THREE.BufferAttribute(h,3)),o!==THREE.NoColors){var l=new Float32Array(3*r.length*3);this.addAttribute("color",new THREE.BufferAttribute(l,3))}if(s===!0){var c=new Float32Array(3*r.length*2);this.addAttribute("uv",new THREE.BufferAttribute(c,2))}for(var p=0,f=0,d=0;pr;r+=3)t.set(e[r],e[r+1],e[r+2]),n.expandByPoint(t)}(void 0===e||0===e.length)&&(this.boundingBox.min.set(0,0,0),this.boundingBox.max.set(0,0,0)),(isNaN(this.boundingBox.min.x)||isNaN(this.boundingBox.min.y)||isNaN(this.boundingBox.min.z))&&THREE.error('THREE.BufferGeometry.computeBoundingBox: Computed min/max have NaN values. The "position" attribute is likely to have NaN values.')}}(),computeBoundingSphere:function(){var t=new THREE.Box3,e=new THREE.Vector3;return function(){null===this.boundingSphere&&(this.boundingSphere=new THREE.Sphere);var n=this.attributes.position.array;if(n){t.makeEmpty();for(var r=this.boundingSphere.center,i=0,o=n.length;o>i;i+=3)e.set(n[i],n[i+1],n[i+2]),t.expandByPoint(e);t.center(r);for(var s=0,i=0,o=n.length;o>i;i+=3)e.set(n[i],n[i+1],n[i+2]),s=Math.max(s,r.distanceToSquared(e));this.boundingSphere.radius=Math.sqrt(s),isNaN(this.boundingSphere.radius)&&THREE.error('THREE.BufferGeometry.computeBoundingSphere(): Computed radius is NaN. The "position" attribute is likely to have NaN values.')}}}(),computeFaceNormals:function(){},computeVertexNormals:function(){var t=this.attributes;if(t.position){var e=t.position.array;if(void 0===t.normal)this.addAttribute("normal",new THREE.BufferAttribute(new Float32Array(e.length),3));else for(var n=t.normal.array,r=0,i=n.length;i>r;r++)n[r]=0;var o,s,a,n=t.normal.array,u=new THREE.Vector3,h=new THREE.Vector3,l=new THREE.Vector3,c=new THREE.Vector3,p=new THREE.Vector3;if(t.index)for(var f=t.index.array,d=this.offsets.length>0?this.offsets:[{start:0,count:f.length,index:0}],m=0,v=d.length;v>m;++m)for(var g=d[m].start,E=d[m].count,y=d[m].index,r=g,i=g+E;i>r;r+=3)o=3*(y+f[r]),s=3*(y+f[r+1]),a=3*(y+f[r+2]),u.fromArray(e,o),h.fromArray(e,s),l.fromArray(e,a),c.subVectors(l,h),p.subVectors(u,h),c.cross(p),n[o]+=c.x,n[o+1]+=c.y,n[o+2]+=c.z,n[s]+=c.x,n[s+1]+=c.y,n[s+2]+=c.z,n[a]+=c.x,n[a+1]+=c.y,n[a+2]+=c.z;else for(var r=0,i=e.length;i>r;r+=9)u.fromArray(e,r),h.fromArray(e,r+3),l.fromArray(e,r+6),c.subVectors(l,h),p.subVectors(u,h),c.cross(p),n[r]=c.x,n[r+1]=c.y,n[r+2]=c.z,n[r+3]=c.x,n[r+4]=c.y,n[r+5]=c.z,n[r+6]=c.x,n[r+7]=c.y,n[r+8]=c.z;this.normalizeNormals(),t.normal.needsUpdate=!0}},computeTangents:function(){function t(t,e,n){k.fromArray(r,3*t),A.fromArray(r,3*e),C.fromArray(r,3*n),P.fromArray(o,2*t),L.fromArray(o,2*e),z.fromArray(o,2*n),c=A.x-k.x,p=C.x-k.x,f=A.y-k.y,d=C.y-k.y,m=A.z-k.z,v=C.z-k.z,g=L.x-P.x,E=z.x-P.x,y=L.y-P.y,_=z.y-P.y,b=1/(g*_-E*y),O.set((_*c-y*p)*b,(_*f-y*d)*b,(_*m-y*v)*b),D.set((g*p-E*c)*b,(g*d-E*f)*b,(g*v-E*m)*b),u[t].add(O),u[e].add(O),u[n].add(O),h[t].add(D),h[e].add(D),h[n].add(D)}function e(t){q.fromArray(i,3*t),X.copy(q),I=u[t],G.copy(I),G.sub(q.multiplyScalar(q.dot(I))).normalize(),W.crossVectors(X,I),j=W.dot(h[t]),V=0>j?-1:1,a[4*t]=G.x,a[4*t+1]=G.y,a[4*t+2]=G.z,a[4*t+3]=V}if(void 0===this.attributes.index||void 0===this.attributes.position||void 0===this.attributes.normal||void 0===this.attributes.uv)return void THREE.warn("THREE.BufferGeometry: Missing required attributes (index, position, normal or uv) in BufferGeometry.computeTangents()");var n=this.attributes.index.array,r=this.attributes.position.array,i=this.attributes.normal.array,o=this.attributes.uv.array,s=r.length/3;void 0===this.attributes.tangent&&this.addAttribute("tangent",new THREE.BufferAttribute(new Float32Array(4*s),4));for(var a=this.attributes.tangent.array,u=[],h=[],l=0;s>l;l++)u[l]=new THREE.Vector3,h[l]=new THREE.Vector3;var c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S,k=new THREE.Vector3,A=new THREE.Vector3,C=new THREE.Vector3,P=new THREE.Vector2,L=new THREE.Vector2,z=new THREE.Vector2,O=new THREE.Vector3,D=new THREE.Vector3;0===this.drawcalls.length&&this.addDrawCall(0,n.length,0);var F=this.drawcalls;for(w=0,R=F.length;R>w;++w){var U=F[w].start,B=F[w].count,N=F[w].index;for(T=U,x=U+B;x>T;T+=3)H=N+n[T],M=N+n[T+1],S=N+n[T+2],t(H,M,S)}var V,I,j,G=new THREE.Vector3,W=new THREE.Vector3,q=new THREE.Vector3,X=new THREE.Vector3;for(w=0,R=F.length;R>w;++w){var U=F[w].start,B=F[w].count,N=F[w].index;for(T=U,x=U+B;x>T;T+=3)H=N+n[T],M=N+n[T+1],S=N+n[T+2],e(H),e(M),e(S)}},computeOffsets:function(t){void 0===t&&(t=65535);for(var e=this.attributes.index.array,n=this.attributes.position.array,r=e.length/3,i=new Uint16Array(e.length),o=0,s=0,a=[{start:0,count:0,index:0}],u=a[0],h=0,l=0,c=new Int32Array(6),p=new Int32Array(n.length),f=new Int32Array(n.length),d=0;dm;m++){l=0;for(var v=0;3>v;v++){var g=e[3*m+v];-1==p[g]?(c[2*v]=g,c[2*v+1]=-1,l++):p[g]u.index+t){var y={start:o,count:0,index:s};a.push(y),u=y;for(var _=0;6>_;_+=2){var b=c[_+1];b>-1&&b_;_+=2){var g=c[_],b=c[_+1];-1===b&&(b=s++),p[g]=b,f[b]=g,i[o++]=b-u.index,u.count++}}return this.reorderBuffers(i,f,s),this.offsets=a,this.drawcalls=a,a},merge:function(t,e){if(t instanceof THREE.BufferGeometry==!1)return void THREE.error("THREE.BufferGeometry.merge(): geometry not an instance of THREE.BufferGeometry.",t);void 0===e&&(e=0);var n=this.attributes;for(var r in n)if(void 0!==t.attributes[r])for(var i=n[r],o=i.array,s=t.attributes[r],a=s.array,u=s.itemSize,h=0,l=u*e;ho;o+=3)t=i[o],e=i[o+1],n=i[o+2],r=1/Math.sqrt(t*t+e*e+n*n),i[o]*=r,i[o+1]*=r,i[o+2]*=r},reorderBuffers:function(t,e,n){var r={};for(var i in this.attributes)if("index"!=i){var o=this.attributes[i].array;r[i]=new o.constructor(this.attributes[i].itemSize*n)}for(var s=0;n>s;s++){var a=e[s];for(var i in this.attributes)if("index"!=i)for(var u=this.attributes[i].array,h=this.attributes[i].itemSize,l=r[i],c=0;h>c;c++)l[s*h+c]=u[a*h+c]}this.attributes.index.array=t;for(var i in this.attributes)"index"!=i&&(this.attributes[i].array=r[i],this.attributes[i].numItems=this.attributes[i].itemSize*n)},toJSON:function(){var t={metadata:{version:4,type:"BufferGeometry",generator:"BufferGeometryExporter"},uuid:this.uuid,type:this.type,data:{attributes:{}}},e=this.attributes,n=this.offsets,r=this.boundingSphere; -for(var i in e){var o=e[i],s=Array.prototype.slice.call(o.array);t.data.attributes[i]={itemSize:o.itemSize,type:o.array.constructor.name,array:s}}return n.length>0&&(t.data.offsets=JSON.parse(JSON.stringify(n))),null!==r&&(t.data.boundingSphere={center:r.center.toArray(),radius:r.radius}),t},clone:function(){var t=new THREE.BufferGeometry;for(var e in this.attributes){var n=this.attributes[e];t.addAttribute(e,n.clone())}for(var r=0,i=this.offsets.length;i>r;r++){var o=this.offsets[r];t.offsets.push({start:o.start,index:o.index,count:o.count})}return t},dispose:function(){this.dispatchEvent({type:"dispose"})}},THREE.EventDispatcher.prototype.apply(THREE.BufferGeometry.prototype),THREE.Geometry=function(){Object.defineProperty(this,"id",{value:THREE.GeometryIdCount++}),this.uuid=THREE.Math.generateUUID(),this.name="",this.type="Geometry",this.vertices=[],this.colors=[],this.faces=[],this.faceVertexUvs=[[]],this.morphTargets=[],this.morphColors=[],this.morphNormals=[],this.skinWeights=[],this.skinIndices=[],this.lineDistances=[],this.boundingBox=null,this.boundingSphere=null,this.hasTangents=!1,this.dynamic=!0,this.verticesNeedUpdate=!1,this.elementsNeedUpdate=!1,this.uvsNeedUpdate=!1,this.normalsNeedUpdate=!1,this.tangentsNeedUpdate=!1,this.colorsNeedUpdate=!1,this.lineDistancesNeedUpdate=!1,this.groupsNeedUpdate=!1},THREE.Geometry.prototype={constructor:THREE.Geometry,applyMatrix:function(t){for(var e=(new THREE.Matrix3).getNormalMatrix(t),n=0,r=this.vertices.length;r>n;n++){var i=this.vertices[n];i.applyMatrix4(t)}for(var n=0,r=this.faces.length;r>n;n++){var o=this.faces[n];o.normal.applyMatrix3(e).normalize();for(var s=0,a=o.vertexNormals.length;a>s;s++)o.vertexNormals[s].applyMatrix3(e).normalize()}null!==this.boundingBox&&this.computeBoundingBox(),null!==this.boundingSphere&&this.computeBoundingSphere(),this.verticesNeedUpdate=!0,this.normalsNeedUpdate=!0},fromBufferGeometry:function(t){for(var e=this,n=t.attributes,r=n.position.array,i=void 0!==n.index?n.index.array:void 0,o=void 0!==n.normal?n.normal.array:void 0,s=void 0!==n.color?n.color.array:void 0,a=void 0!==n.uv?n.uv.array:void 0,u=[],h=[],l=0,c=0;l0)for(var l=0;lc;c+=3)p(g+i[c],g+i[c+1],g+i[c+2]);else for(var l=0;ln;n++){var i=this.faces[n],o=this.vertices[i.a],s=this.vertices[i.b],a=this.vertices[i.c];t.subVectors(a,s),e.subVectors(o,s),t.cross(e),t.normalize(),i.normal.copy(t)}},computeVertexNormals:function(t){var e,n,r,i,o,s;for(s=new Array(this.vertices.length),e=0,n=this.vertices.length;n>e;e++)s[e]=new THREE.Vector3;if(t){var a,u,h,l=new THREE.Vector3,c=new THREE.Vector3;for(r=0,i=this.faces.length;i>r;r++)o=this.faces[r],a=this.vertices[o.a],u=this.vertices[o.b],h=this.vertices[o.c],l.subVectors(h,u),c.subVectors(a,u),l.cross(c),s[o.a].add(l),s[o.b].add(l),s[o.c].add(l)}else for(r=0,i=this.faces.length;i>r;r++)o=this.faces[r],s[o.a].add(o.normal),s[o.b].add(o.normal),s[o.c].add(o.normal);for(e=0,n=this.vertices.length;n>e;e++)s[e].normalize();for(r=0,i=this.faces.length;i>r;r++)o=this.faces[r],o.vertexNormals[0]=s[o.a].clone(),o.vertexNormals[1]=s[o.b].clone(),o.vertexNormals[2]=s[o.c].clone()},computeMorphNormals:function(){var t,e,n,r,i;for(n=0,r=this.faces.length;r>n;n++)for(i=this.faces[n],i.__originalFaceNormal?i.__originalFaceNormal.copy(i.normal):i.__originalFaceNormal=i.normal.clone(),i.__originalVertexNormals||(i.__originalVertexNormals=[]),t=0,e=i.vertexNormals.length;e>t;t++)i.__originalVertexNormals[t]?i.__originalVertexNormals[t].copy(i.vertexNormals[t]):i.__originalVertexNormals[t]=i.vertexNormals[t].clone();var o=new THREE.Geometry;for(o.faces=this.faces,t=0,e=this.morphTargets.length;e>t;t++){if(!this.morphNormals[t]){this.morphNormals[t]={},this.morphNormals[t].faceNormals=[],this.morphNormals[t].vertexNormals=[];var s,a,u=this.morphNormals[t].faceNormals,h=this.morphNormals[t].vertexNormals;for(n=0,r=this.faces.length;r>n;n++)s=new THREE.Vector3,a={a:new THREE.Vector3,b:new THREE.Vector3,c:new THREE.Vector3},u.push(s),h.push(a)}var l=this.morphNormals[t];o.vertices=this.morphTargets[t].vertices,o.computeFaceNormals(),o.computeVertexNormals();var s,a;for(n=0,r=this.faces.length;r>n;n++)i=this.faces[n],s=l.faceNormals[n],a=l.vertexNormals[n],s.copy(i.normal),a.a.copy(i.vertexNormals[0]),a.b.copy(i.vertexNormals[1]),a.c.copy(i.vertexNormals[2])}for(n=0,r=this.faces.length;r>n;n++)i=this.faces[n],i.normal=i.__originalFaceNormal,i.vertexNormals=i.__originalVertexNormals},computeTangents:function(){function t(t,e,n,r,i,o,s){h=t.vertices[e],l=t.vertices[n],c=t.vertices[r],p=u[i],f=u[o],d=u[s],m=l.x-h.x,v=c.x-h.x,g=l.y-h.y,E=c.y-h.y,y=l.z-h.z,_=c.z-h.z,b=f.x-p.x,T=d.x-p.x,x=f.y-p.y,w=d.y-p.y,R=1/(b*w-T*x),C.set((w*m-x*v)*R,(w*g-x*E)*R,(w*y-x*_)*R),P.set((b*v-T*m)*R,(b*E-T*g)*R,(b*_-T*y)*R),k[e].add(C),k[n].add(C),k[r].add(C),A[e].add(P),A[n].add(P),A[r].add(P)}var e,n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S,k=[],A=[],C=new THREE.Vector3,P=new THREE.Vector3,L=new THREE.Vector3,z=new THREE.Vector3,O=new THREE.Vector3;for(r=0,i=this.vertices.length;i>r;r++)k[r]=new THREE.Vector3,A[r]=new THREE.Vector3;for(e=0,n=this.faces.length;n>e;e++)a=this.faces[e],u=this.faceVertexUvs[0][e],t(this,a.a,a.b,a.c,0,1,2);var D=["a","b","c","d"];for(e=0,n=this.faces.length;n>e;e++)for(a=this.faces[e],o=0;oM?-1:1,a.vertexTangents[o]=new THREE.Vector4(L.x,L.y,L.z,S);this.hasTangents=!0},computeLineDistances:function(){for(var t=0,e=this.vertices,n=0,r=e.length;r>n;n++)n>0&&(t+=e[n].distanceTo(e[n-1])),this.lineDistances[n]=t},computeBoundingBox:function(){null===this.boundingBox&&(this.boundingBox=new THREE.Box3),this.boundingBox.setFromPoints(this.vertices)},computeBoundingSphere:function(){null===this.boundingSphere&&(this.boundingSphere=new THREE.Sphere),this.boundingSphere.setFromPoints(this.vertices)},merge:function(t,e,n){if(t instanceof THREE.Geometry==!1)return void THREE.error("THREE.Geometry.merge(): geometry not an instance of THREE.Geometry.",t);var r,i=this.vertices.length,o=this.vertices,s=t.vertices,a=this.faces,u=t.faces,h=this.faceVertexUvs[0],l=t.faceVertexUvs[0];void 0===n&&(n=0),void 0!==e&&(r=(new THREE.Matrix3).getNormalMatrix(e));for(var c=0,p=s.length;p>c;c++){var f=s[c],d=f.clone();void 0!==e&&d.applyMatrix4(e),o.push(d)}for(c=0,p=u.length;p>c;c++){var m,v,g,E=u[c],y=E.vertexNormals,_=E.vertexColors;m=new THREE.Face3(E.a+i,E.b+i,E.c+i),m.normal.copy(E.normal),void 0!==r&&m.normal.applyMatrix3(r).normalize();for(var b=0,T=y.length;T>b;b++)v=y[b].clone(),void 0!==r&&v.applyMatrix3(r).normalize(),m.vertexNormals.push(v);m.color.copy(E.color);for(var b=0,T=_.length;T>b;b++)g=_[b],m.vertexColors.push(g.clone());m.materialIndex=E.materialIndex+n,a.push(m)}for(c=0,p=l.length;p>c;c++){var x=l[c],w=[];if(void 0!==x){for(var b=0,T=x.length;T>b;b++)w.push(x[b].clone());h.push(w)}}},mergeMesh:function(t){return t instanceof THREE.Mesh==!1?void THREE.error("THREE.Geometry.mergeMesh(): mesh not an instance of THREE.Mesh.",t):(t.matrixAutoUpdate&&t.updateMatrix(),void this.merge(t.geometry,t.matrix))},mergeVertices:function(){var t,e,n,r,i,o,s,a,u={},h=[],l=[],c=4,p=Math.pow(10,c);for(n=0,r=this.vertices.length;r>n;n++)t=this.vertices[n],e=Math.round(t.x*p)+"_"+Math.round(t.y*p)+"_"+Math.round(t.z*p),void 0===u[e]?(u[e]=n,h.push(this.vertices[n]),l[n]=h.length-1):l[n]=l[u[e]];var f=[];for(n=0,r=this.faces.length;r>n;n++){i=this.faces[n],i.a=l[i.a],i.b=l[i.b],i.c=l[i.c],o=[i.a,i.b,i.c];for(var d=-1,m=0;3>m;m++)if(o[m]==o[(m+1)%3]){d=m,f.push(n);break}}for(n=f.length-1;n>=0;n--){var v=f[n];for(this.faces.splice(v,1),s=0,a=this.faceVertexUvs.length;a>s;s++)this.faceVertexUvs[s].splice(v,1)}var g=this.vertices.length-h.length;return this.vertices=h,g},toJSON:function(){function t(t,e,n){return n?t|1<0,T=g.vertexNormals.length>0,x=1!==g.color.r||1!==g.color.g||1!==g.color.b,w=g.vertexColors.length>0,R=0;if(R=t(R,0,0),R=t(R,1,E),R=t(R,2,y),R=t(R,3,_),R=t(R,4,b),R=t(R,5,T),R=t(R,6,x),R=t(R,7,w),l.push(R),l.push(g.a,g.b,g.c),_){var H=this.faceVertexUvs[0][u];l.push(r(H[0]),r(H[1]),r(H[2]))}if(b&&l.push(e(g.normal)),T){var M=g.vertexNormals;l.push(e(M[0]),e(M[1]),e(M[2]))}if(x&&l.push(n(g.color)),w){var S=g.vertexColors;l.push(n(S[0]),n(S[1]),n(S[2]))}}return i.data={},i.data.vertices=a,i.data.normals=c,f.length>0&&(i.data.colors=f),m.length>0&&(i.data.uvs=[m]),i.data.faces=l,i},clone:function(){for(var t=new THREE.Geometry,e=this.vertices,n=0,r=e.length;r>n;n++)t.vertices.push(e[n].clone());for(var i=this.faces,n=0,r=i.length;r>n;n++)t.faces.push(i[n].clone());for(var n=0,r=this.faceVertexUvs.length;r>n;n++){var o=this.faceVertexUvs[n];void 0===t.faceVertexUvs[n]&&(t.faceVertexUvs[n]=[]);for(var s=0,a=o.length;a>s;s++){for(var u=o[s],h=[],l=0,c=u.length;c>l;l++){var p=u[l];h.push(p.clone())}t.faceVertexUvs[n].push(h)}}return t},dispose:function(){this.dispatchEvent({type:"dispose"})}},THREE.EventDispatcher.prototype.apply(THREE.Geometry.prototype),THREE.GeometryIdCount=0,THREE.Camera=function(){THREE.Object3D.call(this),this.type="Camera",this.matrixWorldInverse=new THREE.Matrix4,this.projectionMatrix=new THREE.Matrix4},THREE.Camera.prototype=Object.create(THREE.Object3D.prototype),THREE.Camera.prototype.constructor=THREE.Camera,THREE.Camera.prototype.getWorldDirection=function(){var t=new THREE.Quaternion;return function(e){var n=e||new THREE.Vector3;return this.getWorldQuaternion(t),n.set(0,0,-1).applyQuaternion(t)}}(),THREE.Camera.prototype.lookAt=function(){var t=new THREE.Matrix4;return function(e){t.lookAt(this.position,e,this.up),this.quaternion.setFromRotationMatrix(t)}}(),THREE.Camera.prototype.clone=function(t){return void 0===t&&(t=new THREE.Camera),THREE.Object3D.prototype.clone.call(this,t),t.matrixWorldInverse.copy(this.matrixWorldInverse),t.projectionMatrix.copy(this.projectionMatrix),t},THREE.CubeCamera=function(t,e,n){THREE.Object3D.call(this),this.type="CubeCamera";var r=90,i=1,o=new THREE.PerspectiveCamera(r,i,t,e);o.up.set(0,-1,0),o.lookAt(new THREE.Vector3(1,0,0)),this.add(o);var s=new THREE.PerspectiveCamera(r,i,t,e);s.up.set(0,-1,0),s.lookAt(new THREE.Vector3(-1,0,0)),this.add(s);var a=new THREE.PerspectiveCamera(r,i,t,e);a.up.set(0,0,1),a.lookAt(new THREE.Vector3(0,1,0)),this.add(a);var u=new THREE.PerspectiveCamera(r,i,t,e);u.up.set(0,0,-1),u.lookAt(new THREE.Vector3(0,-1,0)),this.add(u);var h=new THREE.PerspectiveCamera(r,i,t,e);h.up.set(0,-1,0),h.lookAt(new THREE.Vector3(0,0,1)),this.add(h);var l=new THREE.PerspectiveCamera(r,i,t,e);l.up.set(0,-1,0),l.lookAt(new THREE.Vector3(0,0,-1)),this.add(l),this.renderTarget=new THREE.WebGLRenderTargetCube(n,n,{format:THREE.RGBFormat,magFilter:THREE.LinearFilter,minFilter:THREE.LinearFilter}),this.updateCubeMap=function(t,e){var n=this.renderTarget,r=n.generateMipmaps;n.generateMipmaps=!1,n.activeCubeFace=0,t.render(e,o,n),n.activeCubeFace=1,t.render(e,s,n),n.activeCubeFace=2,t.render(e,a,n),n.activeCubeFace=3,t.render(e,u,n),n.activeCubeFace=4,t.render(e,h,n),n.generateMipmaps=r,n.activeCubeFace=5,t.render(e,l,n)}},THREE.CubeCamera.prototype=Object.create(THREE.Object3D.prototype),THREE.CubeCamera.prototype.constructor=THREE.CubeCamera,THREE.OrthographicCamera=function(t,e,n,r,i,o){THREE.Camera.call(this),this.type="OrthographicCamera",this.zoom=1,this.left=t,this.right=e,this.top=n,this.bottom=r,this.near=void 0!==i?i:.1,this.far=void 0!==o?o:2e3,this.updateProjectionMatrix()},THREE.OrthographicCamera.prototype=Object.create(THREE.Camera.prototype),THREE.OrthographicCamera.prototype.constructor=THREE.OrthographicCamera,THREE.OrthographicCamera.prototype.updateProjectionMatrix=function(){var t=(this.right-this.left)/(2*this.zoom),e=(this.top-this.bottom)/(2*this.zoom),n=(this.right+this.left)/2,r=(this.top+this.bottom)/2;this.projectionMatrix.makeOrthographic(n-t,n+t,r+e,r-e,this.near,this.far)},THREE.OrthographicCamera.prototype.clone=function(){var t=new THREE.OrthographicCamera;return THREE.Camera.prototype.clone.call(this,t),t.zoom=this.zoom,t.left=this.left,t.right=this.right,t.top=this.top,t.bottom=this.bottom,t.near=this.near,t.far=this.far,t.projectionMatrix.copy(this.projectionMatrix),t},THREE.PerspectiveCamera=function(t,e,n,r){THREE.Camera.call(this),this.type="PerspectiveCamera",this.zoom=1,this.fov=void 0!==t?t:50,this.aspect=void 0!==e?e:1,this.near=void 0!==n?n:.1,this.far=void 0!==r?r:2e3,this.updateProjectionMatrix()},THREE.PerspectiveCamera.prototype=Object.create(THREE.Camera.prototype),THREE.PerspectiveCamera.prototype.constructor=THREE.PerspectiveCamera,THREE.PerspectiveCamera.prototype.setLens=function(t,e){void 0===e&&(e=24),this.fov=2*THREE.Math.radToDeg(Math.atan(e/(2*t))),this.updateProjectionMatrix()},THREE.PerspectiveCamera.prototype.setViewOffset=function(t,e,n,r,i,o){this.fullWidth=t,this.fullHeight=e,this.x=n,this.y=r,this.width=i,this.height=o,this.updateProjectionMatrix()},THREE.PerspectiveCamera.prototype.updateProjectionMatrix=function(){var t=THREE.Math.radToDeg(2*Math.atan(Math.tan(.5*THREE.Math.degToRad(this.fov))/this.zoom));if(this.fullWidth){var e=this.fullWidth/this.fullHeight,n=Math.tan(THREE.Math.degToRad(.5*t))*this.near,r=-n,i=e*r,o=e*n,s=Math.abs(o-i),a=Math.abs(n-r);this.projectionMatrix.makeFrustum(i+this.x*s/this.fullWidth,i+(this.x+this.width)*s/this.fullWidth,n-(this.y+this.height)*a/this.fullHeight,n-this.y*a/this.fullHeight,this.near,this.far)}else this.projectionMatrix.makePerspective(t,this.aspect,this.near,this.far)},THREE.PerspectiveCamera.prototype.clone=function(){var t=new THREE.PerspectiveCamera;return THREE.Camera.prototype.clone.call(this,t),t.zoom=this.zoom,t.fov=this.fov,t.aspect=this.aspect,t.near=this.near,t.far=this.far,t.projectionMatrix.copy(this.projectionMatrix),t},THREE.Light=function(t){THREE.Object3D.call(this),this.type="Light",this.color=new THREE.Color(t)},THREE.Light.prototype=Object.create(THREE.Object3D.prototype),THREE.Light.prototype.constructor=THREE.Light,THREE.Light.prototype.clone=function(t){return void 0===t&&(t=new THREE.Light),THREE.Object3D.prototype.clone.call(this,t),t.color.copy(this.color),t},THREE.AmbientLight=function(t){THREE.Light.call(this,t),this.type="AmbientLight"},THREE.AmbientLight.prototype=Object.create(THREE.Light.prototype),THREE.AmbientLight.prototype.constructor=THREE.AmbientLight,THREE.AmbientLight.prototype.clone=function(){var t=new THREE.AmbientLight;return THREE.Light.prototype.clone.call(this,t),t},THREE.AreaLight=function(t,e){THREE.Light.call(this,t),this.type="AreaLight",this.normal=new THREE.Vector3(0,-1,0),this.right=new THREE.Vector3(1,0,0),this.intensity=void 0!==e?e:1,this.width=1,this.height=1,this.constantAttenuation=1.5,this.linearAttenuation=.5,this.quadraticAttenuation=.1},THREE.AreaLight.prototype=Object.create(THREE.Light.prototype),THREE.AreaLight.prototype.constructor=THREE.AreaLight,THREE.DirectionalLight=function(t,e){THREE.Light.call(this,t),this.type="DirectionalLight",this.position.set(0,1,0),this.target=new THREE.Object3D,this.intensity=void 0!==e?e:1,this.castShadow=!1,this.onlyShadow=!1,this.shadowCameraNear=50,this.shadowCameraFar=5e3,this.shadowCameraLeft=-500,this.shadowCameraRight=500,this.shadowCameraTop=500,this.shadowCameraBottom=-500,this.shadowCameraVisible=!1,this.shadowBias=0,this.shadowDarkness=.5,this.shadowMapWidth=512,this.shadowMapHeight=512,this.shadowCascade=!1,this.shadowCascadeOffset=new THREE.Vector3(0,0,-1e3),this.shadowCascadeCount=2,this.shadowCascadeBias=[0,0,0],this.shadowCascadeWidth=[512,512,512],this.shadowCascadeHeight=[512,512,512],this.shadowCascadeNearZ=[-1,.99,.998],this.shadowCascadeFarZ=[.99,.998,1],this.shadowCascadeArray=[],this.shadowMap=null,this.shadowMapSize=null,this.shadowCamera=null,this.shadowMatrix=null},THREE.DirectionalLight.prototype=Object.create(THREE.Light.prototype),THREE.DirectionalLight.prototype.constructor=THREE.DirectionalLight,THREE.DirectionalLight.prototype.clone=function(){var t=new THREE.DirectionalLight;return THREE.Light.prototype.clone.call(this,t),t.target=this.target.clone(),t.intensity=this.intensity,t.castShadow=this.castShadow,t.onlyShadow=this.onlyShadow,t.shadowCameraNear=this.shadowCameraNear,t.shadowCameraFar=this.shadowCameraFar,t.shadowCameraLeft=this.shadowCameraLeft,t.shadowCameraRight=this.shadowCameraRight,t.shadowCameraTop=this.shadowCameraTop,t.shadowCameraBottom=this.shadowCameraBottom,t.shadowCameraVisible=this.shadowCameraVisible,t.shadowBias=this.shadowBias,t.shadowDarkness=this.shadowDarkness,t.shadowMapWidth=this.shadowMapWidth,t.shadowMapHeight=this.shadowMapHeight,t.shadowCascade=this.shadowCascade,t.shadowCascadeOffset.copy(this.shadowCascadeOffset),t.shadowCascadeCount=this.shadowCascadeCount,t.shadowCascadeBias=this.shadowCascadeBias.slice(0),t.shadowCascadeWidth=this.shadowCascadeWidth.slice(0),t.shadowCascadeHeight=this.shadowCascadeHeight.slice(0),t.shadowCascadeNearZ=this.shadowCascadeNearZ.slice(0),t.shadowCascadeFarZ=this.shadowCascadeFarZ.slice(0),t},THREE.HemisphereLight=function(t,e,n){THREE.Light.call(this,t),this.type="HemisphereLight",this.position.set(0,100,0),this.groundColor=new THREE.Color(e),this.intensity=void 0!==n?n:1},THREE.HemisphereLight.prototype=Object.create(THREE.Light.prototype),THREE.HemisphereLight.prototype.constructor=THREE.HemisphereLight,THREE.HemisphereLight.prototype.clone=function(){var t=new THREE.HemisphereLight;return THREE.Light.prototype.clone.call(this,t),t.groundColor.copy(this.groundColor),t.intensity=this.intensity,t},THREE.PointLight=function(t,e,n,r){THREE.Light.call(this,t),this.type="PointLight",this.intensity=void 0!==e?e:1,this.distance=void 0!==n?n:0,this.decay=void 0!==r?r:1},THREE.PointLight.prototype=Object.create(THREE.Light.prototype),THREE.PointLight.prototype.constructor=THREE.PointLight,THREE.PointLight.prototype.clone=function(){var t=new THREE.PointLight;return THREE.Light.prototype.clone.call(this,t),t.intensity=this.intensity,t.distance=this.distance,t.decay=this.decay,t},THREE.SpotLight=function(t,e,n,r,i,o){THREE.Light.call(this,t),this.type="SpotLight",this.position.set(0,1,0),this.target=new THREE.Object3D,this.intensity=void 0!==e?e:1,this.distance=void 0!==n?n:0,this.angle=void 0!==r?r:Math.PI/3,this.exponent=void 0!==i?i:10,this.decay=void 0!==o?o:1,this.castShadow=!1,this.onlyShadow=!1,this.shadowCameraNear=50,this.shadowCameraFar=5e3,this.shadowCameraFov=50,this.shadowCameraVisible=!1,this.shadowBias=0,this.shadowDarkness=.5,this.shadowMapWidth=512,this.shadowMapHeight=512,this.shadowMap=null,this.shadowMapSize=null,this.shadowCamera=null,this.shadowMatrix=null},THREE.SpotLight.prototype=Object.create(THREE.Light.prototype),THREE.SpotLight.prototype.constructor=THREE.SpotLight,THREE.SpotLight.prototype.clone=function(){var t=new THREE.SpotLight;return THREE.Light.prototype.clone.call(this,t),t.target=this.target.clone(),t.intensity=this.intensity,t.distance=this.distance,t.angle=this.angle,t.exponent=this.exponent,t.decay=this.decay,t.castShadow=this.castShadow,t.onlyShadow=this.onlyShadow,t.shadowCameraNear=this.shadowCameraNear,t.shadowCameraFar=this.shadowCameraFar,t.shadowCameraFov=this.shadowCameraFov,t.shadowCameraVisible=this.shadowCameraVisible,t.shadowBias=this.shadowBias,t.shadowDarkness=this.shadowDarkness,t.shadowMapWidth=this.shadowMapWidth,t.shadowMapHeight=this.shadowMapHeight,t},THREE.Cache={files:{},add:function(t,e){this.files[t]=e},get:function(t){return this.files[t]},remove:function(t){delete this.files[t]},clear:function(){this.files={}}},THREE.Loader=function(t){this.showStatus=t,this.statusDomElement=t?THREE.Loader.prototype.addStatusElement():null,this.imageLoader=new THREE.ImageLoader,this.onLoadStart=function(){},this.onLoadProgress=function(){},this.onLoadComplete=function(){}},THREE.Loader.prototype={constructor:THREE.Loader,crossOrigin:void 0,addStatusElement:function(){var t=document.createElement("div");return t.style.position="absolute",t.style.right="0px",t.style.top="0px",t.style.fontSize="0.8em",t.style.textAlign="left",t.style.background="rgba(0,0,0,0.25)",t.style.color="#fff",t.style.width="120px",t.style.padding="0.5em 0.5em 0.5em 0.5em",t.style.zIndex=1e3,t.innerHTML="Loading ...",t},updateProgress:function(t){var e="Loaded ";e+=t.total?(100*t.loaded/t.total).toFixed(0)+"%":(t.loaded/1024).toFixed(2)+" KB",this.statusDomElement.innerHTML=e},extractUrlBase:function(t){var e=t.split("/");return 1===e.length?"./":(e.pop(),e.join("/")+"/")},initMaterials:function(t,e){for(var n=[],r=0;re;e++){var r=t[e];if(r instanceof THREE.ShaderMaterial)return!0}return!1},createMaterial:function(t,e){function n(t){var e=Math.log(t)/Math.LN2;return Math.pow(2,Math.round(e))}function r(t,r,i,s,a,u,h){var l,c=e+i,p=THREE.Loader.Handlers.get(c);if(null!==p?l=p.load(c):(l=new THREE.Texture,p=o.imageLoader,p.crossOrigin=o.crossOrigin,p.load(c,function(t){if(THREE.Math.isPowerOfTwo(t.width)===!1||THREE.Math.isPowerOfTwo(t.height)===!1){var e=n(t.width),r=n(t.height),i=document.createElement("canvas");i.width=e,i.height=r;var o=i.getContext("2d");o.drawImage(t,0,0,e,r),l.image=i}else l.image=t;l.needsUpdate=!0})),l.sourceFile=i,s&&(l.repeat.set(s[0],s[1]),1!==s[0]&&(l.wrapS=THREE.RepeatWrapping),1!==s[1]&&(l.wrapT=THREE.RepeatWrapping)),a&&l.offset.set(a[0],a[1]),u){var f={repeat:THREE.RepeatWrapping,mirror:THREE.MirroredRepeatWrapping};void 0!==f[u[0]]&&(l.wrapS=f[u[0]]),void 0!==f[u[1]]&&(l.wrapT=f[u[1]])}h&&(l.anisotropy=h),t[r]=l}function i(t){return(255*t[0]<<16)+(255*t[1]<<8)+255*t[2]}var o=this,s="MeshLambertMaterial",a={color:15658734,opacity:1,map:null,lightMap:null,normalMap:null,bumpMap:null,wireframe:!1};if(t.shading){var u=t.shading.toLowerCase();"phong"===u?s="MeshPhongMaterial":"basic"===u&&(s="MeshBasicMaterial")}void 0!==t.blending&&void 0!==THREE[t.blending]&&(a.blending=THREE[t.blending]),void 0!==t.transparent&&(a.transparent=t.transparent),void 0!==t.opacity&&t.opacity<1&&(a.transparent=!0),void 0!==t.depthTest&&(a.depthTest=t.depthTest),void 0!==t.depthWrite&&(a.depthWrite=t.depthWrite),void 0!==t.visible&&(a.visible=t.visible),void 0!==t.flipSided&&(a.side=THREE.BackSide),void 0!==t.doubleSided&&(a.side=THREE.DoubleSide),void 0!==t.wireframe&&(a.wireframe=t.wireframe),void 0!==t.vertexColors&&("face"===t.vertexColors?a.vertexColors=THREE.FaceColors:t.vertexColors&&(a.vertexColors=THREE.VertexColors)),t.colorDiffuse?a.color=i(t.colorDiffuse):t.DbgColor&&(a.color=t.DbgColor),t.colorSpecular&&(a.specular=i(t.colorSpecular)),t.colorEmissive&&(a.emissive=i(t.colorEmissive)),void 0!==t.transparency&&(console.warn("THREE.Loader: transparency has been renamed to opacity"),t.opacity=t.transparency),void 0!==t.opacity&&(a.opacity=t.opacity),t.specularCoef&&(a.shininess=t.specularCoef),t.mapDiffuse&&e&&r(a,"map",t.mapDiffuse,t.mapDiffuseRepeat,t.mapDiffuseOffset,t.mapDiffuseWrap,t.mapDiffuseAnisotropy),t.mapLight&&e&&r(a,"lightMap",t.mapLight,t.mapLightRepeat,t.mapLightOffset,t.mapLightWrap,t.mapLightAnisotropy),t.mapBump&&e&&r(a,"bumpMap",t.mapBump,t.mapBumpRepeat,t.mapBumpOffset,t.mapBumpWrap,t.mapBumpAnisotropy),t.mapNormal&&e&&r(a,"normalMap",t.mapNormal,t.mapNormalRepeat,t.mapNormalOffset,t.mapNormalWrap,t.mapNormalAnisotropy),t.mapSpecular&&e&&r(a,"specularMap",t.mapSpecular,t.mapSpecularRepeat,t.mapSpecularOffset,t.mapSpecularWrap,t.mapSpecularAnisotropy),t.mapAlpha&&e&&r(a,"alphaMap",t.mapAlpha,t.mapAlphaRepeat,t.mapAlphaOffset,t.mapAlphaWrap,t.mapAlphaAnisotropy),t.mapBumpScale&&(a.bumpScale=t.mapBumpScale),t.mapNormalFactor&&(a.normalScale=new THREE.Vector2(t.mapNormalFactor,t.mapNormalFactor));var h=new THREE[s](a);return void 0!==t.DbgName&&(h.name=t.DbgName),h}},THREE.Loader.Handlers={handlers:[],add:function(t,e){this.handlers.push(t,e)},get:function(t){for(var e=0,n=this.handlers.length;n>e;e+=2){var r=this.handlers[e],i=this.handlers[e+1];if(r.test(t))return i}return null}},THREE.XHRLoader=function(t){this.manager=void 0!==t?t:THREE.DefaultLoadingManager},THREE.XHRLoader.prototype={constructor:THREE.XHRLoader,load:function(t,e,n,r){var i=this,o=THREE.Cache.get(t);if(void 0!==o)return void(e&&e(o));var s=new XMLHttpRequest;s.open("GET",t,!0),s.addEventListener("load",function(n){THREE.Cache.add(t,this.response),e&&e(this.response),i.manager.itemEnd(t)},!1),void 0!==n&&s.addEventListener("progress",function(t){n(t)},!1),void 0!==r&&s.addEventListener("error",function(t){r(t)},!1),void 0!==this.crossOrigin&&(s.crossOrigin=this.crossOrigin),void 0!==this.responseType&&(s.responseType=this.responseType),s.send(null),i.manager.itemStart(t)},setResponseType:function(t){this.responseType=t},setCrossOrigin:function(t){this.crossOrigin=t}},THREE.ImageLoader=function(t){this.manager=void 0!==t?t:THREE.DefaultLoadingManager},THREE.ImageLoader.prototype={constructor:THREE.ImageLoader,load:function(t,e,n,r){var i=this,o=THREE.Cache.get(t);if(void 0!==o)return void e(o);var s=document.createElement("img");return s.addEventListener("load",function(n){THREE.Cache.add(t,this),e&&e(this),i.manager.itemEnd(t)},!1),void 0!==n&&s.addEventListener("progress",function(t){n(t)},!1),void 0!==r&&s.addEventListener("error",function(t){r(t)},!1),void 0!==this.crossOrigin&&(s.crossOrigin=this.crossOrigin),s.src=t,i.manager.itemStart(t),s},setCrossOrigin:function(t){this.crossOrigin=t}},THREE.JSONLoader=function(t){THREE.Loader.call(this,t),this.withCredentials=!1},THREE.JSONLoader.prototype=Object.create(THREE.Loader.prototype),THREE.JSONLoader.prototype.constructor=THREE.JSONLoader,THREE.JSONLoader.prototype.load=function(t,e,n){n=n&&"string"==typeof n?n:this.extractUrlBase(t),this.onLoadStart(),this.loadAjaxJSON(this,t,e,n)},THREE.JSONLoader.prototype.loadAjaxJSON=function(t,e,n,r,i){var o=new XMLHttpRequest,s=0;o.onreadystatechange=function(){if(o.readyState===o.DONE)if(200===o.status||0===o.status){if(o.responseText){var a=JSON.parse(o.responseText),u=a.metadata;if(void 0!==u){if("object"===u.type)return void THREE.error("THREE.JSONLoader: "+e+" should be loaded with THREE.ObjectLoader instead.");if("scene"===u.type)return void THREE.error("THREE.JSONLoader: "+e+" seems to be a Scene. Use THREE.SceneLoader instead.")}var h=t.parse(a,r);n(h.geometry,h.materials)}else THREE.error("THREE.JSONLoader: "+e+" seems to be unreachable or the file is empty.");t.onLoadComplete()}else THREE.error("THREE.JSONLoader: Couldn't load "+e+" ("+o.status+")");else o.readyState===o.LOADING?i&&(0===s&&(s=o.getResponseHeader("Content-Length")),i({total:s,loaded:o.responseText.length})):o.readyState===o.HEADERS_RECEIVED&&void 0!==i&&(s=o.getResponseHeader("Content-Length"))},o.open("GET",e,!0),o.withCredentials=this.withCredentials,o.send(null)},THREE.JSONLoader.prototype.parse=function(t,e){function n(e){function n(t,e){return t&1<r;r++)o.faceVertexUvs[r]=[]}for(a=0,u=P.length;u>a;)b=new THREE.Vector3,b.x=P[a++]*e,b.y=P[a++]*e,b.z=P[a++]*e,o.vertices.push(b);for(a=0,u=C.length;u>a;)if(f=C[a++],d=n(f,0),m=n(f,1),v=n(f,3),g=n(f,4),E=n(f,5),y=n(f,6),_=n(f,7),d){if(x=new THREE.Face3,x.a=C[a],x.b=C[a+1],x.c=C[a+3],w=new THREE.Face3,w.a=C[a+1],w.b=C[a+2],w.c=C[a+3],a+=4,m&&(p=C[a++],x.materialIndex=p,w.materialIndex=p),s=o.faces.length,v)for(r=0;O>r;r++)for(M=t.uvs[r],o.faceVertexUvs[r][s]=[],o.faceVertexUvs[r][s+1]=[],i=0;4>i;i++)c=C[a++],k=M[2*c],A=M[2*c+1],S=new THREE.Vector2(k,A),2!==i&&o.faceVertexUvs[r][s].push(S),0!==i&&o.faceVertexUvs[r][s+1].push(S);if(g&&(l=3*C[a++],x.normal.set(L[l++],L[l++],L[l]),w.normal.copy(x.normal)),E)for(r=0;4>r;r++)l=3*C[a++],H=new THREE.Vector3(L[l++],L[l++],L[l]),2!==r&&x.vertexNormals.push(H),0!==r&&w.vertexNormals.push(H);if(y&&(h=C[a++],R=z[h],x.color.setHex(R),w.color.setHex(R)),_)for(r=0;4>r;r++)h=C[a++],R=z[h],2!==r&&x.vertexColors.push(new THREE.Color(R)),0!==r&&w.vertexColors.push(new THREE.Color(R));o.faces.push(x),o.faces.push(w)}else{if(T=new THREE.Face3,T.a=C[a++],T.b=C[a++],T.c=C[a++],m&&(p=C[a++],T.materialIndex=p),s=o.faces.length,v)for(r=0;O>r;r++)for(M=t.uvs[r],o.faceVertexUvs[r][s]=[],i=0;3>i;i++)c=C[a++],k=M[2*c],A=M[2*c+1],S=new THREE.Vector2(k,A),o.faceVertexUvs[r][s].push(S);if(g&&(l=3*C[a++],T.normal.set(L[l++],L[l++],L[l])),E)for(r=0;3>r;r++)l=3*C[a++],H=new THREE.Vector3(L[l++],L[l++],L[l]),T.vertexNormals.push(H);if(y&&(h=C[a++],T.color.setHex(z[h])),_)for(r=0;3>r;r++)h=C[a++],T.vertexColors.push(new THREE.Color(z[h]));o.faces.push(T)}}function r(){var e=void 0!==t.influencesPerVertex?t.influencesPerVertex:2;if(t.skinWeights)for(var n=0,r=t.skinWeights.length;r>n;n+=e){var i=t.skinWeights[n],s=e>1?t.skinWeights[n+1]:0,a=e>2?t.skinWeights[n+2]:0,u=e>3?t.skinWeights[n+3]:0;o.skinWeights.push(new THREE.Vector4(i,s,a,u))}if(t.skinIndices)for(var n=0,r=t.skinIndices.length;r>n;n+=e){var h=t.skinIndices[n],l=e>1?t.skinIndices[n+1]:0,c=e>2?t.skinIndices[n+2]:0,p=e>3?t.skinIndices[n+3]:0;o.skinIndices.push(new THREE.Vector4(h,l,c,p))}o.bones=t.bones,o.bones&&o.bones.length>0&&(o.skinWeights.length!==o.skinIndices.length||o.skinIndices.length!==o.vertices.length)&&THREE.warn("THREE.JSONLoader: When skinning, number of vertices ("+o.vertices.length+"), skinIndices ("+o.skinIndices.length+"), and skinWeights ("+o.skinWeights.length+") should match."),o.animation=t.animation,o.animations=t.animations}function i(e){if(void 0!==t.morphTargets){var n,r,i,s,a,u;for(n=0,r=t.morphTargets.length;r>n;n++)for(o.morphTargets[n]={},o.morphTargets[n].name=t.morphTargets[n].name,o.morphTargets[n].vertices=[],a=o.morphTargets[n].vertices,u=t.morphTargets[n].vertices,i=0,s=u.length;s>i;i+=3){var h=new THREE.Vector3;h.x=u[i]*e,h.y=u[i+1]*e,h.z=u[i+2]*e,a.push(h)}}if(void 0!==t.morphColors){var n,r,l,c,p,f,d;for(n=0,r=t.morphColors.length;r>n;n++)for(o.morphColors[n]={}, -o.morphColors[n].name=t.morphColors[n].name,o.morphColors[n].colors=[],p=o.morphColors[n].colors,f=t.morphColors[n].colors,l=0,c=f.length;c>l;l+=3)d=new THREE.Color(16755200),d.setRGB(f[l],f[l+1],f[l+2]),p.push(d)}}var o=new THREE.Geometry,s=void 0!==t.scale?1/t.scale:1;if(n(s),r(),i(s),o.computeFaceNormals(),o.computeBoundingSphere(),void 0===t.materials||0===t.materials.length)return{geometry:o};var a=this.initMaterials(t.materials,e);return this.needsTangents(a)&&o.computeTangents(),{geometry:o,materials:a}},THREE.LoadingManager=function(t,e,n){var r=this,i=0,o=0;this.onLoad=t,this.onProgress=e,this.onError=n,this.itemStart=function(t){o++},this.itemEnd=function(t){i++,void 0!==r.onProgress&&r.onProgress(t,i,o),i===o&&void 0!==r.onLoad&&r.onLoad()}},THREE.DefaultLoadingManager=new THREE.LoadingManager,THREE.BufferGeometryLoader=function(t){this.manager=void 0!==t?t:THREE.DefaultLoadingManager},THREE.BufferGeometryLoader.prototype={constructor:THREE.BufferGeometryLoader,load:function(t,e,n,r){var i=this,o=new THREE.XHRLoader(i.manager);o.setCrossOrigin(this.crossOrigin),o.load(t,function(t){e(i.parse(JSON.parse(t)))},n,r)},setCrossOrigin:function(t){this.crossOrigin=t},parse:function(t){var e=new THREE.BufferGeometry,n=t.data.attributes;for(var r in n){var i=n[r],o=new self[i.type](i.array);e.addAttribute(r,new THREE.BufferAttribute(o,i.itemSize))}var s=t.data.offsets;void 0!==s&&(e.offsets=JSON.parse(JSON.stringify(s)));var a=t.data.boundingSphere;if(void 0!==a){var u=new THREE.Vector3;void 0!==a.center&&u.fromArray(a.center),e.boundingSphere=new THREE.Sphere(u,a.radius)}return e}},THREE.MaterialLoader=function(t){this.manager=void 0!==t?t:THREE.DefaultLoadingManager},THREE.MaterialLoader.prototype={constructor:THREE.MaterialLoader,load:function(t,e,n,r){var i=this,o=new THREE.XHRLoader(i.manager);o.setCrossOrigin(this.crossOrigin),o.load(t,function(t){e(i.parse(JSON.parse(t)))},n,r)},setCrossOrigin:function(t){this.crossOrigin=t},parse:function(t){var e=new THREE[t.type];if(void 0!==t.color&&e.color.setHex(t.color),void 0!==t.emissive&&e.emissive.setHex(t.emissive),void 0!==t.specular&&e.specular.setHex(t.specular),void 0!==t.shininess&&(e.shininess=t.shininess),void 0!==t.uniforms&&(e.uniforms=t.uniforms),void 0!==t.vertexShader&&(e.vertexShader=t.vertexShader),void 0!==t.fragmentShader&&(e.fragmentShader=t.fragmentShader),void 0!==t.vertexColors&&(e.vertexColors=t.vertexColors),void 0!==t.shading&&(e.shading=t.shading),void 0!==t.blending&&(e.blending=t.blending),void 0!==t.side&&(e.side=t.side),void 0!==t.opacity&&(e.opacity=t.opacity),void 0!==t.transparent&&(e.transparent=t.transparent),void 0!==t.wireframe&&(e.wireframe=t.wireframe),void 0!==t.size&&(e.size=t.size),void 0!==t.sizeAttenuation&&(e.sizeAttenuation=t.sizeAttenuation),void 0!==t.materials)for(var n=0,r=t.materials.length;r>n;n++)e.materials.push(this.parse(t.materials[n]));return e}},THREE.ObjectLoader=function(t){this.manager=void 0!==t?t:THREE.DefaultLoadingManager,this.texturePath=""},THREE.ObjectLoader.prototype={constructor:THREE.ObjectLoader,load:function(t,e,n,r){""===this.texturePath&&(this.texturePath=t.substring(0,t.lastIndexOf("/")+1));var i=this,o=new THREE.XHRLoader(i.manager);o.setCrossOrigin(this.crossOrigin),o.load(t,function(t){i.parse(JSON.parse(t),e)},n,r)},setTexturePath:function(t){this.texturePath=t},setCrossOrigin:function(t){this.crossOrigin=t},parse:function(t,e){var n=this.parseGeometries(t.geometries),r=this.parseImages(t.images,function(){void 0!==e&&e(s)}),i=this.parseTextures(t.textures,r),o=this.parseMaterials(t.materials,i),s=this.parseObject(t.object,n,o);return(void 0===t.images||0===t.images.length)&&void 0!==e&&e(s),s},parseGeometries:function(t){var e={};if(void 0!==t)for(var n=new THREE.JSONLoader,r=new THREE.BufferGeometryLoader,i=0,o=t.length;o>i;i++){var s,a=t[i];switch(a.type){case"PlaneGeometry":case"PlaneBufferGeometry":s=new THREE[a.type](a.width,a.height,a.widthSegments,a.heightSegments);break;case"BoxGeometry":case"CubeGeometry":s=new THREE.BoxGeometry(a.width,a.height,a.depth,a.widthSegments,a.heightSegments,a.depthSegments);break;case"CircleGeometry":s=new THREE.CircleGeometry(a.radius,a.segments);break;case"CylinderGeometry":s=new THREE.CylinderGeometry(a.radiusTop,a.radiusBottom,a.height,a.radialSegments,a.heightSegments,a.openEnded);break;case"SphereGeometry":s=new THREE.SphereGeometry(a.radius,a.widthSegments,a.heightSegments,a.phiStart,a.phiLength,a.thetaStart,a.thetaLength);break;case"IcosahedronGeometry":s=new THREE.IcosahedronGeometry(a.radius,a.detail);break;case"TorusGeometry":s=new THREE.TorusGeometry(a.radius,a.tube,a.radialSegments,a.tubularSegments,a.arc);break;case"TorusKnotGeometry":s=new THREE.TorusKnotGeometry(a.radius,a.tube,a.radialSegments,a.tubularSegments,a.p,a.q,a.heightScale);break;case"BufferGeometry":s=r.parse(a);break;case"Geometry":s=n.parse(a.data).geometry}s.uuid=a.uuid,void 0!==a.name&&(s.name=a.name),e[a.uuid]=s}return e},parseMaterials:function(t,e){var n={};if(void 0!==t)for(var r=function(t){return void 0===e[t]&&THREE.warn("THREE.ObjectLoader: Undefined texture",t),e[t]},i=new THREE.MaterialLoader,o=0,s=t.length;s>o;o++){var a=t[o],u=i.parse(a);u.uuid=a.uuid,void 0!==a.name&&(u.name=a.name),void 0!==a.map&&(u.map=r(a.map)),void 0!==a.bumpMap&&(u.bumpMap=r(a.bumpMap),a.bumpScale&&(u.bumpScale=new THREE.Vector2(a.bumpScale,a.bumpScale))),void 0!==a.alphaMap&&(u.alphaMap=r(a.alphaMap)),void 0!==a.envMap&&(u.envMap=r(a.envMap)),void 0!==a.normalMap&&(u.normalMap=r(a.normalMap),a.normalScale&&(u.normalScale=new THREE.Vector2(a.normalScale,a.normalScale))),void 0!==a.lightMap&&(u.lightMap=r(a.lightMap)),void 0!==a.specularMap&&(u.specularMap=r(a.specularMap)),n[a.uuid]=u}return n},parseImages:function(t,e){var n=this,r={};if(void 0!==t&&t.length>0){var i=new THREE.LoadingManager(e),o=new THREE.ImageLoader(i);o.setCrossOrigin(this.crossOrigin);for(var s=function(t){return n.manager.itemStart(t),o.load(t,function(){n.manager.itemEnd(t)})},a=0,u=t.length;u>a;a++){var h=t[a],l=/^(\/\/)|([a-z]+:(\/\/)?)/i.test(h.url)?h.url:n.texturePath+h.url;r[h.uuid]=s(l)}}return r},parseTextures:function(t,e){var n={};if(void 0!==t)for(var r=0,i=t.length;i>r;r++){var o=t[r];void 0===o.image&&THREE.warn('THREE.ObjectLoader: No "image" speficied for',o.uuid),void 0===e[o.image]&&THREE.warn("THREE.ObjectLoader: Undefined image",o.image);var s=new THREE.Texture(e[o.image]);s.needsUpdate=!0,s.uuid=o.uuid,void 0!==o.name&&(s.name=o.name),void 0!==o.repeat&&(s.repeat=new THREE.Vector2(o.repeat[0],o.repeat[1])),void 0!==o.minFilter&&(s.minFilter=THREE[o.minFilter]),void 0!==o.magFilter&&(s.magFilter=THREE[o.magFilter]),void 0!==o.anisotropy&&(s.anisotropy=o.anisotropy),o.wrap instanceof Array&&(s.wrapS=THREE[o.wrap[0]],s.wrapT=THREE[o.wrap[1]]),n[o.uuid]=s}return n},parseObject:function(){var t=new THREE.Matrix4;return function(e,n,r){var i,o=function(t){return void 0===n[t]&&THREE.warn("THREE.ObjectLoader: Undefined geometry",t),n[t]},s=function(t){return void 0===r[t]&&THREE.warn("THREE.ObjectLoader: Undefined material",t),r[t]};switch(e.type){case"Scene":i=new THREE.Scene;break;case"PerspectiveCamera":i=new THREE.PerspectiveCamera(e.fov,e.aspect,e.near,e.far);break;case"OrthographicCamera":i=new THREE.OrthographicCamera(e.left,e.right,e.top,e.bottom,e.near,e.far);break;case"AmbientLight":i=new THREE.AmbientLight(e.color);break;case"DirectionalLight":i=new THREE.DirectionalLight(e.color,e.intensity);break;case"PointLight":i=new THREE.PointLight(e.color,e.intensity,e.distance,e.decay);break;case"SpotLight":i=new THREE.SpotLight(e.color,e.intensity,e.distance,e.angle,e.exponent,e.decay);break;case"HemisphereLight":i=new THREE.HemisphereLight(e.color,e.groundColor,e.intensity);break;case"Mesh":i=new THREE.Mesh(o(e.geometry),s(e.material));break;case"Line":i=new THREE.Line(o(e.geometry),s(e.material),e.mode);break;case"PointCloud":i=new THREE.PointCloud(o(e.geometry),s(e.material));break;case"Sprite":i=new THREE.Sprite(s(e.material));break;case"Group":i=new THREE.Group;break;default:i=new THREE.Object3D}if(i.uuid=e.uuid,void 0!==e.name&&(i.name=e.name),void 0!==e.matrix?(t.fromArray(e.matrix),t.decompose(i.position,i.quaternion,i.scale)):(void 0!==e.position&&i.position.fromArray(e.position),void 0!==e.rotation&&i.rotation.fromArray(e.rotation),void 0!==e.scale&&i.scale.fromArray(e.scale)),void 0!==e.visible&&(i.visible=e.visible),void 0!==e.userData&&(i.userData=e.userData),void 0!==e.children)for(var a in e.children)i.add(this.parseObject(e.children[a],n,r));return i}}()},THREE.TextureLoader=function(t){this.manager=void 0!==t?t:THREE.DefaultLoadingManager},THREE.TextureLoader.prototype={constructor:THREE.TextureLoader,load:function(t,e,n,r){var i=this,o=new THREE.ImageLoader(i.manager);o.setCrossOrigin(this.crossOrigin),o.load(t,function(t){var n=new THREE.Texture(t);n.needsUpdate=!0,void 0!==e&&e(n)},n,r)},setCrossOrigin:function(t){this.crossOrigin=t}},THREE.DataTextureLoader=THREE.BinaryTextureLoader=function(){this._parser=null},THREE.BinaryTextureLoader.prototype={constructor:THREE.BinaryTextureLoader,load:function(t,e,n,r){var i=this,o=new THREE.DataTexture,s=new THREE.XHRLoader;return s.setResponseType("arraybuffer"),s.load(t,function(t){var n=i._parser(t);n&&(void 0!==n.image?o.image=n.image:void 0!==n.data&&(o.image.width=n.width,o.image.height=n.height,o.image.data=n.data),o.wrapS=void 0!==n.wrapS?n.wrapS:THREE.ClampToEdgeWrapping,o.wrapT=void 0!==n.wrapT?n.wrapT:THREE.ClampToEdgeWrapping,o.magFilter=void 0!==n.magFilter?n.magFilter:THREE.LinearFilter,o.minFilter=void 0!==n.minFilter?n.minFilter:THREE.LinearMipMapLinearFilter,o.anisotropy=void 0!==n.anisotropy?n.anisotropy:1,void 0!==n.format&&(o.format=n.format),void 0!==n.type&&(o.type=n.type),void 0!==n.mipmaps&&(o.mipmaps=n.mipmaps),1===n.mipmapCount&&(o.minFilter=THREE.LinearFilter),o.needsUpdate=!0,e&&e(o,n))},n,r),o}},THREE.CompressedTextureLoader=function(){this._parser=null},THREE.CompressedTextureLoader.prototype={constructor:THREE.CompressedTextureLoader,load:function(t,e,n){var r=this,i=[],o=new THREE.CompressedTexture;o.image=i;var s=new THREE.XHRLoader;if(s.setResponseType("arraybuffer"),t instanceof Array)for(var a=0,u=function(n){s.load(t[n],function(t){var s=r._parser(t,!0);i[n]={width:s.width,height:s.height,format:s.format,mipmaps:s.mipmaps},a+=1,6===a&&(1==s.mipmapCount&&(o.minFilter=THREE.LinearFilter),o.format=s.format,o.needsUpdate=!0,e&&e(o))})},h=0,l=t.length;l>h;++h)u(h);else s.load(t,function(t){var n=r._parser(t,!0);if(n.isCubemap)for(var s=n.mipmaps.length/n.mipmapCount,a=0;s>a;a++){i[a]={mipmaps:[]};for(var u=0;ue;e++)t.materials.push(this.materials[e].toJSON());return t},clone:function(){for(var t=new THREE.MeshFaceMaterial,e=0;es){var u=e.closestPointToPoint(t);u.applyMatrix4(i.matrixWorld);var h=n.ray.origin.distanceTo(u);r.push({distance:h,distanceToRay:s,point:u.clone(),index:o,face:null,object:i})}};if(o instanceof THREE.BufferGeometry){var l=o.attributes,c=l.position.array;if(void 0!==l.index){var p=l.index.array,f=o.offsets;if(0===f.length){var d={start:0,count:p.length,index:0};f=[d]}for(var m=0,v=f.length;v>m;++m)for(var g=f[m].start,E=f[m].count,y=f[m].index,_=g,b=g+E;b>_;_++){var T=y+p[_];u.fromArray(c,3*T),h(u,T)}}else for(var x=c.length/3,_=0;x>_;_++)u.set(c[3*_],c[3*_+1],c[3*_+2]),h(u,_)}else for(var w=this.geometry.vertices,_=0;_b;b+=p){var T=_+d[b],x=_+d[b+1];u.fromArray(m,3*T),h.fromArray(m,3*x);var w=e.distanceSqToSegment(u,h,c,l);if(!(w>s)){var R=e.origin.distanceTo(c);Rr.far||i.push({distance:R,point:l.clone().applyMatrix4(this.matrixWorld),index:b,offsetIndex:g,face:null,faceIndex:null,object:this})}}}else for(var m=f.position.array,b=0;bs)){var R=e.origin.distanceTo(c);Rr.far||i.push({distance:R,point:l.clone().applyMatrix4(this.matrixWorld),index:b,face:null,faceIndex:null,object:this})}}}else if(a instanceof THREE.Geometry)for(var H=a.vertices,M=H.length,b=0;M-1>b;b+=p){var w=e.distanceSqToSegment(H[b],H[b+1],c,l);if(!(w>s)){var R=e.origin.distanceTo(c);Rr.far||i.push({distance:R,point:l.clone().applyMatrix4(this.matrixWorld),index:b,face:null,faceIndex:null,object:this})}}}}}(),THREE.Line.prototype.clone=function(t){return void 0===t&&(t=new THREE.Line(this.geometry,this.material,this.mode)),THREE.Object3D.prototype.clone.call(this,t),t},THREE.Mesh=function(t,e){THREE.Object3D.call(this),this.type="Mesh",this.geometry=void 0!==t?t:new THREE.Geometry,this.material=void 0!==e?e:new THREE.MeshBasicMaterial({color:16777215*Math.random()}),this.updateMorphTargets()},THREE.Mesh.prototype=Object.create(THREE.Object3D.prototype),THREE.Mesh.prototype.constructor=THREE.Mesh,THREE.Mesh.prototype.updateMorphTargets=function(){if(void 0!==this.geometry.morphTargets&&this.geometry.morphTargets.length>0){this.morphTargetBase=-1,this.morphTargetForcedOrder=[],this.morphTargetInfluences=[],this.morphTargetDictionary={};for(var t=0,e=this.geometry.morphTargets.length;e>t;t++)this.morphTargetInfluences.push(0),this.morphTargetDictionary[this.geometry.morphTargets[t].name]=t}},THREE.Mesh.prototype.getMorphTargetIndexByName=function(t){return void 0!==this.morphTargetDictionary[t]?this.morphTargetDictionary[t]:(THREE.warn("THREE.Mesh.getMorphTargetIndexByName: morph target "+t+" does not exist. Returning 0."),0)},THREE.Mesh.prototype.raycast=function(){var t=new THREE.Matrix4,e=new THREE.Ray,n=new THREE.Sphere,r=new THREE.Vector3,i=new THREE.Vector3,o=new THREE.Vector3;return function(s,a){var u=this.geometry;if(null===u.boundingSphere&&u.computeBoundingSphere(),n.copy(u.boundingSphere),n.applyMatrix4(this.matrixWorld),s.ray.isIntersectionSphere(n)!==!1&&(t.getInverse(this.matrixWorld),e.copy(s.ray).applyMatrix4(t),null===u.boundingBox||e.isIntersectionBox(u.boundingBox)!==!1))if(u instanceof THREE.BufferGeometry){var h=this.material;if(void 0===h)return;var l,c,p,f=u.attributes,d=s.precision;if(void 0!==f.index){var m=f.index.array,v=f.position.array,g=u.offsets;0===g.length&&(g=[{start:0,count:m.length,index:0}]);for(var E=0,y=g.length;y>E;++E)for(var _=g[E].start,b=g[E].count,T=g[E].index,x=_,w=_+b;w>x;x+=3){if(l=T+m[x],c=T+m[x+1],p=T+m[x+2],r.fromArray(v,3*l),i.fromArray(v,3*c),o.fromArray(v,3*p),h.side===THREE.BackSide)var R=e.intersectTriangle(o,i,r,!0);else var R=e.intersectTriangle(r,i,o,h.side!==THREE.DoubleSide);if(null!==R){R.applyMatrix4(this.matrixWorld);var H=s.ray.origin.distanceTo(R);d>H||Hs.far||a.push({distance:H,point:R,face:new THREE.Face3(l,c,p,THREE.Triangle.normal(r,i,o)),faceIndex:null,object:this})}}}else for(var v=f.position.array,x=0,M=0,w=v.length;w>x;x+=3,M+=9){if(l=x,c=x+1,p=x+2,r.fromArray(v,M),i.fromArray(v,M+3),o.fromArray(v,M+6),h.side===THREE.BackSide)var R=e.intersectTriangle(o,i,r,!0);else var R=e.intersectTriangle(r,i,o,h.side!==THREE.DoubleSide);if(null!==R){R.applyMatrix4(this.matrixWorld);var H=s.ray.origin.distanceTo(R);d>H||Hs.far||a.push({distance:H,point:R,face:new THREE.Face3(l,c,p,THREE.Triangle.normal(r,i,o)),faceIndex:null,object:this})}}}else if(u instanceof THREE.Geometry)for(var l,c,p,S=this.material instanceof THREE.MeshFaceMaterial,k=S===!0?this.material.materials:null,d=s.precision,A=u.vertices,C=0,P=u.faces.length;P>C;C++){var L=u.faces[C],h=S===!0?k[L.materialIndex]:this.material;if(void 0!==h){if(l=A[L.a],c=A[L.b],p=A[L.c],h.morphTargets===!0){var z=u.morphTargets,O=this.morphTargetInfluences;r.set(0,0,0),i.set(0,0,0),o.set(0,0,0);for(var D=0,F=z.length;F>D;D++){var U=O[D];if(0!==U){var B=z[D].vertices;r.x+=(B[L.a].x-l.x)*U,r.y+=(B[L.a].y-l.y)*U,r.z+=(B[L.a].z-l.z)*U,i.x+=(B[L.b].x-c.x)*U,i.y+=(B[L.b].y-c.y)*U,i.z+=(B[L.b].z-c.z)*U,o.x+=(B[L.c].x-p.x)*U,o.y+=(B[L.c].y-p.y)*U,o.z+=(B[L.c].z-p.z)*U}}r.add(l),i.add(c),o.add(p),l=r,c=i,p=o}if(h.side===THREE.BackSide)var R=e.intersectTriangle(p,c,l,!0);else var R=e.intersectTriangle(l,c,p,h.side!==THREE.DoubleSide);if(null!==R){R.applyMatrix4(this.matrixWorld);var H=s.ray.origin.distanceTo(R);d>H||Hs.far||a.push({distance:H,point:R,face:L,faceIndex:C,object:this})}}}}}(),THREE.Mesh.prototype.clone=function(t,e){return void 0===t&&(t=new THREE.Mesh(this.geometry,this.material)),THREE.Object3D.prototype.clone.call(this,t,e),t},THREE.Bone=function(t){THREE.Object3D.call(this),this.type="Bone",this.skin=t},THREE.Bone.prototype=Object.create(THREE.Object3D.prototype),THREE.Bone.prototype.constructor=THREE.Bone,THREE.Skeleton=function(t,e,n){if(this.useVertexTexture=void 0!==n?n:!0,this.identityMatrix=new THREE.Matrix4,t=t||[],this.bones=t.slice(0),this.useVertexTexture){var r;r=this.bones.length>256?64:this.bones.length>64?32:this.bones.length>16?16:8,this.boneTextureWidth=r,this.boneTextureHeight=r,this.boneMatrices=new Float32Array(this.boneTextureWidth*this.boneTextureHeight*4),this.boneTexture=new THREE.DataTexture(this.boneMatrices,this.boneTextureWidth,this.boneTextureHeight,THREE.RGBAFormat,THREE.FloatType),this.boneTexture.minFilter=THREE.NearestFilter,this.boneTexture.magFilter=THREE.NearestFilter,this.boneTexture.generateMipmaps=!1,this.boneTexture.flipY=!1}else this.boneMatrices=new Float32Array(16*this.bones.length);if(void 0===e)this.calculateInverses();else if(this.bones.length===e.length)this.boneInverses=e.slice(0);else{THREE.warn("THREE.Skeleton bonInverses is the wrong length."),this.boneInverses=[];for(var i=0,o=this.bones.length;o>i;i++)this.boneInverses.push(new THREE.Matrix4)}},THREE.Skeleton.prototype.calculateInverses=function(){this.boneInverses=[];for(var t=0,e=this.bones.length;e>t;t++){var n=new THREE.Matrix4;this.bones[t]&&n.getInverse(this.bones[t].matrixWorld),this.boneInverses.push(n)}},THREE.Skeleton.prototype.pose=function(){for(var t,e=0,n=this.bones.length;n>e;e++)t=this.bones[e],t&&t.matrixWorld.getInverse(this.boneInverses[e]);for(var e=0,n=this.bones.length;n>e;e++)t=this.bones[e],t&&(t.parent?(t.matrix.getInverse(t.parent.matrixWorld),t.matrix.multiply(t.matrixWorld)):t.matrix.copy(t.matrixWorld),t.matrix.decompose(t.position,t.quaternion,t.scale))},THREE.Skeleton.prototype.update=function(){var t=new THREE.Matrix4;return function(){for(var e=0,n=this.bones.length;n>e;e++){var r=this.bones[e]?this.bones[e].matrixWorld:this.identityMatrix;t.multiplyMatrices(r,this.boneInverses[e]),t.flattenToArrayOffset(this.boneMatrices,16*e)}this.useVertexTexture&&(this.boneTexture.needsUpdate=!0)}}(),THREE.SkinnedMesh=function(t,e,n){THREE.Mesh.call(this,t,e),this.type="SkinnedMesh",this.bindMode="attached",this.bindMatrix=new THREE.Matrix4,this.bindMatrixInverse=new THREE.Matrix4;var r=[];if(this.geometry&&void 0!==this.geometry.bones){for(var i,o,s,a,u,h=0,l=this.geometry.bones.length;l>h;++h)o=this.geometry.bones[h],s=o.pos,a=o.rotq,u=o.scl,i=new THREE.Bone(this),r.push(i),i.name=o.name,i.position.set(s[0],s[1],s[2]),i.quaternion.set(a[0],a[1],a[2],a[3]),void 0!==u?i.scale.set(u[0],u[1],u[2]):i.scale.set(1,1,1);for(var h=0,l=this.geometry.bones.length;l>h;++h)o=this.geometry.bones[h],-1!==o.parent?r[o.parent].add(r[h]):this.add(r[h])}this.normalizeSkinWeights(),this.updateMatrixWorld(!0),this.bind(new THREE.Skeleton(r,void 0,n))},THREE.SkinnedMesh.prototype=Object.create(THREE.Mesh.prototype),THREE.SkinnedMesh.prototype.constructor=THREE.SkinnedMesh,THREE.SkinnedMesh.prototype.bind=function(t,e){this.skeleton=t,void 0===e&&(this.updateMatrixWorld(!0),e=this.matrixWorld),this.bindMatrix.copy(e),this.bindMatrixInverse.getInverse(e)},THREE.SkinnedMesh.prototype.pose=function(){this.skeleton.pose()},THREE.SkinnedMesh.prototype.normalizeSkinWeights=function(){if(this.geometry instanceof THREE.Geometry)for(var t=0;ti;i++){var s=t.morphTargets[i],a=s.name.match(r);if(a&&a.length>1){var u=a[1];n[u]||(n[u]={start:1/0,end:-(1/0)});var h=n[u];ih.end&&(h.end=i),e||(e=u)}}t.firstAnimation=e},THREE.MorphAnimMesh.prototype.setAnimationLabel=function(t,e,n){this.geometry.animations||(this.geometry.animations={}),this.geometry.animations[t]={start:e,end:n}},THREE.MorphAnimMesh.prototype.playAnimation=function(t,e){var n=this.geometry.animations[t];n?(this.setFrameRange(n.start,n.end),this.duration=1e3*((n.end-n.start)/e),this.time=0):THREE.warn("THREE.MorphAnimMesh: animation["+t+"] undefined in .playAnimation()")},THREE.MorphAnimMesh.prototype.updateAnimation=function(t){var e=this.duration/this.length;this.time+=this.direction*t,this.mirroredLoop?(this.time>this.duration||this.time<0)&&(this.direction*=-1,this.time>this.duration&&(this.time=this.duration,this.directionBackwards=!0),this.time<0&&(this.time=0,this.directionBackwards=!1)):(this.time=this.time%this.duration,this.time<0&&(this.time+=this.duration));var n=this.startKeyframe+THREE.Math.clamp(Math.floor(this.time/e),0,this.length-1);n!==this.currentKeyframe&&(this.morphTargetInfluences[this.lastKeyframe]=0,this.morphTargetInfluences[this.currentKeyframe]=1,this.morphTargetInfluences[n]=0,this.lastKeyframe=this.currentKeyframe,this.currentKeyframe=n);var r=this.time%e/e;this.directionBackwards&&(r=1-r),this.morphTargetInfluences[this.currentKeyframe]=r,this.morphTargetInfluences[this.lastKeyframe]=1-r},THREE.MorphAnimMesh.prototype.interpolateTargets=function(t,e,n){for(var r=this.morphTargetInfluences,i=0,o=r.length;o>i;i++)r[i]=0;t>-1&&(r[t]=1-n),e>-1&&(r[e]=n)},THREE.MorphAnimMesh.prototype.clone=function(t){return void 0===t&&(t=new THREE.MorphAnimMesh(this.geometry,this.material)),t.duration=this.duration,t.mirroredLoop=this.mirroredLoop,t.time=this.time,t.lastKeyframe=this.lastKeyframe,t.currentKeyframe=this.currentKeyframe,t.direction=this.direction,t.directionBackwards=this.directionBackwards,THREE.Mesh.prototype.clone.call(this,t),t},THREE.LOD=function(){THREE.Object3D.call(this),this.objects=[]},THREE.LOD.prototype=Object.create(THREE.Object3D.prototype),THREE.LOD.prototype.constructor=THREE.LOD,THREE.LOD.prototype.addLevel=function(t,e){void 0===e&&(e=0),e=Math.abs(e);for(var n=0;ne&&!(t1){t.setFromMatrixPosition(n.matrixWorld),e.setFromMatrixPosition(this.matrixWorld);var r=t.distanceTo(e);this.objects[0].object.visible=!0;for(var i=1,o=this.objects.length;o>i&&r>=this.objects[i].distance;i++)this.objects[i-1].object.visible=!1,this.objects[i].object.visible=!0;for(;o>i;i++)this.objects[i].object.visible=!1}}}(),THREE.LOD.prototype.clone=function(t){void 0===t&&(t=new THREE.LOD),THREE.Object3D.prototype.clone.call(this,t);for(var e=0,n=this.objects.length;n>e;e++){var r=this.objects[e].object.clone();r.visible=0===e,t.addLevel(r,this.objects[e].distance)}return t},THREE.Sprite=function(){var t=new Uint16Array([0,1,2,0,2,3]),e=new Float32Array([-.5,-.5,0,.5,-.5,0,.5,.5,0,-.5,.5,0]),n=new Float32Array([0,0,1,0,1,1,0,1]),r=new THREE.BufferGeometry;return r.addAttribute("index",new THREE.BufferAttribute(t,1)),r.addAttribute("position",new THREE.BufferAttribute(e,3)),r.addAttribute("uv",new THREE.BufferAttribute(n,2)),function(t){THREE.Object3D.call(this),this.type="Sprite",this.geometry=r,this.material=void 0!==t?t:new THREE.SpriteMaterial}}(),THREE.Sprite.prototype=Object.create(THREE.Object3D.prototype),THREE.Sprite.prototype.constructor=THREE.Sprite,THREE.Sprite.prototype.raycast=function(){var t=new THREE.Vector3;return function(e,n){t.setFromMatrixPosition(this.matrixWorld);var r=e.ray.distanceToPoint(t);r>this.scale.x||n.push({distance:r,point:this.position,face:null,object:this})}}(),THREE.Sprite.prototype.clone=function(t){return void 0===t&&(t=new THREE.Sprite(this.material)),THREE.Object3D.prototype.clone.call(this,t),t},THREE.Particle=THREE.Sprite,THREE.LensFlare=function(t,e,n,r,i){THREE.Object3D.call(this),this.lensFlares=[],this.positionScreen=new THREE.Vector3,this.customUpdateCallback=void 0,void 0!==t&&this.add(t,e,n,r,i)},THREE.LensFlare.prototype=Object.create(THREE.Object3D.prototype),THREE.LensFlare.prototype.constructor=THREE.LensFlare,THREE.LensFlare.prototype.add=function(t,e,n,r,i,o){void 0===e&&(e=-1),void 0===n&&(n=0),void 0===o&&(o=1),void 0===i&&(i=new THREE.Color(16777215)),void 0===r&&(r=THREE.NormalBlending),n=Math.min(n,Math.max(0,n)),this.lensFlares.push({texture:t,size:e,distance:n,x:0,y:0,z:0,scale:1,rotation:1,opacity:o,color:i,blending:r})},THREE.LensFlare.prototype.updateLensFlares=function(){var t,e,n=this.lensFlares.length,r=2*-this.positionScreen.x,i=2*-this.positionScreen.y;for(t=0;n>t;t++)e=this.lensFlares[t],e.x=this.positionScreen.x+r*e.distance,e.y=this.positionScreen.y+i*e.distance,e.wantedRotation=e.x*Math.PI*.25,e.rotation+=.25*(e.wantedRotation-e.rotation)},THREE.Scene=function(){THREE.Object3D.call(this),this.type="Scene",this.fog=null,this.overrideMaterial=null,this.autoUpdate=!0},THREE.Scene.prototype=Object.create(THREE.Object3D.prototype),THREE.Scene.prototype.constructor=THREE.Scene,THREE.Scene.prototype.clone=function(t){return void 0===t&&(t=new THREE.Scene),THREE.Object3D.prototype.clone.call(this,t),null!==this.fog&&(t.fog=this.fog.clone()),null!==this.overrideMaterial&&(t.overrideMaterial=this.overrideMaterial.clone()),t.autoUpdate=this.autoUpdate,t.matrixAutoUpdate=this.matrixAutoUpdate,t},THREE.Fog=function(t,e,n){this.name="",this.color=new THREE.Color(t),this.near=void 0!==e?e:1,this.far=void 0!==n?n:1e3},THREE.Fog.prototype.clone=function(){return new THREE.Fog(this.color.getHex(),this.near,this.far)},THREE.FogExp2=function(t,e){this.name="",this.color=new THREE.Color(t),this.density=void 0!==e?e:25e-5},THREE.FogExp2.prototype.clone=function(){return new THREE.FogExp2(this.color.getHex(),this.density)},THREE.ShaderChunk={},THREE.ShaderChunk.common="#define PI 3.14159\n#define PI2 6.28318\n#define RECIPROCAL_PI2 0.15915494\n#define LOG2 1.442695\n#define EPSILON 1e-6\n\nfloat square( in float a ) { return a*a; }\nvec2 square( in vec2 a ) { return vec2( a.x*a.x, a.y*a.y ); }\nvec3 square( in vec3 a ) { return vec3( a.x*a.x, a.y*a.y, a.z*a.z ); }\nvec4 square( in vec4 a ) { return vec4( a.x*a.x, a.y*a.y, a.z*a.z, a.w*a.w ); }\nfloat saturate( in float a ) { return clamp( a, 0.0, 1.0 ); }\nvec2 saturate( in vec2 a ) { return clamp( a, 0.0, 1.0 ); }\nvec3 saturate( in vec3 a ) { return clamp( a, 0.0, 1.0 ); }\nvec4 saturate( in vec4 a ) { return clamp( a, 0.0, 1.0 ); }\nfloat average( in float a ) { return a; }\nfloat average( in vec2 a ) { return ( a.x + a.y) * 0.5; }\nfloat average( in vec3 a ) { return ( a.x + a.y + a.z) / 3.0; }\nfloat average( in vec4 a ) { return ( a.x + a.y + a.z + a.w) * 0.25; }\nfloat whiteCompliment( in float a ) { return saturate( 1.0 - a ); }\nvec2 whiteCompliment( in vec2 a ) { return saturate( vec2(1.0) - a ); }\nvec3 whiteCompliment( in vec3 a ) { return saturate( vec3(1.0) - a ); }\nvec4 whiteCompliment( in vec4 a ) { return saturate( vec4(1.0) - a ); }\nvec3 transformDirection( in vec3 normal, in mat4 matrix ) {\n return normalize( ( matrix * vec4( normal, 0.0 ) ).xyz );\n}\n// http://en.wikibooks.org/wiki/GLSL_Programming/Applying_Matrix_Transformations\nvec3 inverseTransformDirection( in vec3 normal, in mat4 matrix ) {\n return normalize( ( vec4( normal, 0.0 ) * matrix ).xyz );\n}\nvec3 projectOnPlane(in vec3 point, in vec3 pointOnPlane, in vec3 planeNormal) {\n float distance = dot( planeNormal, point-pointOnPlane );\n return point - distance * planeNormal;\n}\nfloat sideOfPlane( in vec3 point, in vec3 pointOnPlane, in vec3 planeNormal ) {\n return sign( dot( point - pointOnPlane, planeNormal ) );\n}\nvec3 linePlaneIntersect( in vec3 pointOnLine, in vec3 lineDirection, in vec3 pointOnPlane, in vec3 planeNormal ) {\n return pointOnLine + lineDirection * ( dot( planeNormal, pointOnPlane - pointOnLine ) / dot( planeNormal, lineDirection ) );\n}\nfloat calcLightAttenuation( float lightDistance, float cutoffDistance, float decayExponent ) {\n if ( decayExponent > 0.0 ) {\n return pow( saturate( 1.0 - lightDistance / cutoffDistance ), decayExponent );\n }\n return 1.0;\n}\n\nvec3 inputToLinear( in vec3 a ) {\n#ifdef GAMMA_INPUT\n return pow( a, vec3( float( GAMMA_FACTOR ) ) );\n#else\n return a;\n#endif\n}\nvec3 linearToOutput( in vec3 a ) {\n#ifdef GAMMA_OUTPUT\n return pow( a, vec3( 1.0 / float( GAMMA_FACTOR ) ) );\n#else\n return a;\n#endif\n}\n",THREE.ShaderChunk.alphatest_fragment="#ifdef ALPHATEST\n\n if ( diffuseColor.a < ALPHATEST ) discard;\n\n#endif\n",THREE.ShaderChunk.lights_lambert_vertex="vLightFront = vec3( 0.0 );\n\n#ifdef DOUBLE_SIDED\n\n vLightBack = vec3( 0.0 );\n\n#endif\n\ntransformedNormal = normalize( transformedNormal );\n\n#if MAX_DIR_LIGHTS > 0\n\nfor( int i = 0; i < MAX_DIR_LIGHTS; i ++ ) {\n\n vec3 dirVector = transformDirection( directionalLightDirection[ i ], viewMatrix );\n\n float dotProduct = dot( transformedNormal, dirVector );\n vec3 directionalLightWeighting = vec3( max( dotProduct, 0.0 ) );\n\n #ifdef DOUBLE_SIDED\n\n vec3 directionalLightWeightingBack = vec3( max( -dotProduct, 0.0 ) );\n\n #ifdef WRAP_AROUND\n\n vec3 directionalLightWeightingHalfBack = vec3( max( -0.5 * dotProduct + 0.5, 0.0 ) );\n\n #endif\n\n #endif\n\n #ifdef WRAP_AROUND\n\n vec3 directionalLightWeightingHalf = vec3( max( 0.5 * dotProduct + 0.5, 0.0 ) );\n directionalLightWeighting = mix( directionalLightWeighting, directionalLightWeightingHalf, wrapRGB );\n\n #ifdef DOUBLE_SIDED\n\n directionalLightWeightingBack = mix( directionalLightWeightingBack, directionalLightWeightingHalfBack, wrapRGB );\n\n #endif\n\n #endif\n\n vLightFront += directionalLightColor[ i ] * directionalLightWeighting;\n\n #ifdef DOUBLE_SIDED\n\n vLightBack += directionalLightColor[ i ] * directionalLightWeightingBack;\n\n #endif\n\n}\n\n#endif\n\n#if MAX_POINT_LIGHTS > 0\n\n for( int i = 0; i < MAX_POINT_LIGHTS; i ++ ) {\n\n vec4 lPosition = viewMatrix * vec4( pointLightPosition[ i ], 1.0 );\n vec3 lVector = lPosition.xyz - mvPosition.xyz;\n\n float attenuation = calcLightAttenuation( length( lVector ), pointLightDistance[ i ], pointLightDecay[ i ] );\n\n lVector = normalize( lVector );\n float dotProduct = dot( transformedNormal, lVector );\n\n vec3 pointLightWeighting = vec3( max( dotProduct, 0.0 ) );\n\n #ifdef DOUBLE_SIDED\n\n vec3 pointLightWeightingBack = vec3( max( -dotProduct, 0.0 ) );\n\n #ifdef WRAP_AROUND\n\n vec3 pointLightWeightingHalfBack = vec3( max( -0.5 * dotProduct + 0.5, 0.0 ) );\n\n #endif\n\n #endif\n\n #ifdef WRAP_AROUND\n\n vec3 pointLightWeightingHalf = vec3( max( 0.5 * dotProduct + 0.5, 0.0 ) );\n pointLightWeighting = mix( pointLightWeighting, pointLightWeightingHalf, wrapRGB );\n\n #ifdef DOUBLE_SIDED\n\n pointLightWeightingBack = mix( pointLightWeightingBack, pointLightWeightingHalfBack, wrapRGB );\n\n #endif\n\n #endif\n\n vLightFront += pointLightColor[ i ] * pointLightWeighting * attenuation;\n\n #ifdef DOUBLE_SIDED\n\n vLightBack += pointLightColor[ i ] * pointLightWeightingBack * attenuation;\n\n #endif\n\n }\n\n#endif\n\n#if MAX_SPOT_LIGHTS > 0\n\n for( int i = 0; i < MAX_SPOT_LIGHTS; i ++ ) {\n\n vec4 lPosition = viewMatrix * vec4( spotLightPosition[ i ], 1.0 );\n vec3 lVector = lPosition.xyz - mvPosition.xyz;\n\n float spotEffect = dot( spotLightDirection[ i ], normalize( spotLightPosition[ i ] - worldPosition.xyz ) );\n\n if ( spotEffect > spotLightAngleCos[ i ] ) {\n\n spotEffect = max( pow( max( spotEffect, 0.0 ), spotLightExponent[ i ] ), 0.0 );\n\n float attenuation = calcLightAttenuation( length( lVector ), spotLightDistance[ i ], spotLightDecay[ i ] );\n\n lVector = normalize( lVector );\n\n float dotProduct = dot( transformedNormal, lVector );\n vec3 spotLightWeighting = vec3( max( dotProduct, 0.0 ) );\n\n #ifdef DOUBLE_SIDED\n\n vec3 spotLightWeightingBack = vec3( max( -dotProduct, 0.0 ) );\n\n #ifdef WRAP_AROUND\n\n vec3 spotLightWeightingHalfBack = vec3( max( -0.5 * dotProduct + 0.5, 0.0 ) );\n\n #endif\n\n #endif\n\n #ifdef WRAP_AROUND\n\n vec3 spotLightWeightingHalf = vec3( max( 0.5 * dotProduct + 0.5, 0.0 ) );\n spotLightWeighting = mix( spotLightWeighting, spotLightWeightingHalf, wrapRGB );\n\n #ifdef DOUBLE_SIDED\n\n spotLightWeightingBack = mix( spotLightWeightingBack, spotLightWeightingHalfBack, wrapRGB );\n\n #endif\n\n #endif\n\n vLightFront += spotLightColor[ i ] * spotLightWeighting * attenuation * spotEffect;\n\n #ifdef DOUBLE_SIDED\n\n vLightBack += spotLightColor[ i ] * spotLightWeightingBack * attenuation * spotEffect;\n\n #endif\n\n }\n\n }\n\n#endif\n\n#if MAX_HEMI_LIGHTS > 0\n\n for( int i = 0; i < MAX_HEMI_LIGHTS; i ++ ) {\n\n vec3 lVector = transformDirection( hemisphereLightDirection[ i ], viewMatrix );\n\n float dotProduct = dot( transformedNormal, lVector );\n\n float hemiDiffuseWeight = 0.5 * dotProduct + 0.5;\n float hemiDiffuseWeightBack = -0.5 * dotProduct + 0.5;\n\n vLightFront += mix( hemisphereLightGroundColor[ i ], hemisphereLightSkyColor[ i ], hemiDiffuseWeight );\n\n #ifdef DOUBLE_SIDED\n\n vLightBack += mix( hemisphereLightGroundColor[ i ], hemisphereLightSkyColor[ i ], hemiDiffuseWeightBack );\n\n #endif\n\n }\n\n#endif\n\nvLightFront += ambientLightColor;\n\n#ifdef DOUBLE_SIDED\n\n vLightBack += ambientLightColor;\n\n#endif\n",THREE.ShaderChunk.map_particle_pars_fragment="#ifdef USE_MAP\n\n uniform vec4 offsetRepeat;\n uniform sampler2D map;\n\n#endif\n",THREE.ShaderChunk.default_vertex="#ifdef USE_SKINNING\n\n vec4 mvPosition = modelViewMatrix * skinned;\n\n#elif defined( USE_MORPHTARGETS )\n\n vec4 mvPosition = modelViewMatrix * vec4( morphed, 1.0 );\n\n#else\n\n vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );\n\n#endif\n\ngl_Position = projectionMatrix * mvPosition;\n",THREE.ShaderChunk.map_pars_fragment="#if defined( USE_MAP ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( USE_SPECULARMAP ) || defined( USE_ALPHAMAP )\n\n varying vec2 vUv;\n\n#endif\n\n#ifdef USE_MAP\n\n uniform sampler2D map;\n\n#endif",THREE.ShaderChunk.skinnormal_vertex="#ifdef USE_SKINNING\n\n mat4 skinMatrix = mat4( 0.0 );\n skinMatrix += skinWeight.x * boneMatX;\n skinMatrix += skinWeight.y * boneMatY;\n skinMatrix += skinWeight.z * boneMatZ;\n skinMatrix += skinWeight.w * boneMatW;\n skinMatrix = bindMatrixInverse * skinMatrix * bindMatrix;\n\n #ifdef USE_MORPHNORMALS\n\n vec4 skinnedNormal = skinMatrix * vec4( morphedNormal, 0.0 );\n\n #else\n\n vec4 skinnedNormal = skinMatrix * vec4( normal, 0.0 );\n\n #endif\n\n#endif\n",THREE.ShaderChunk.logdepthbuf_pars_vertex="#ifdef USE_LOGDEPTHBUF\n\n #ifdef USE_LOGDEPTHBUF_EXT\n\n varying float vFragDepth;\n\n #endif\n\n uniform float logDepthBufFC;\n\n#endif",THREE.ShaderChunk.lightmap_pars_vertex="#ifdef USE_LIGHTMAP\n\n varying vec2 vUv2;\n\n#endif",THREE.ShaderChunk.lights_phong_fragment="#ifndef FLAT_SHADED\n\n vec3 normal = normalize( vNormal );\n\n #ifdef DOUBLE_SIDED\n\n normal = normal * ( -1.0 + 2.0 * float( gl_FrontFacing ) );\n\n #endif\n\n#else\n\n vec3 fdx = dFdx( vViewPosition );\n vec3 fdy = dFdy( vViewPosition );\n vec3 normal = normalize( cross( fdx, fdy ) );\n\n#endif\n\nvec3 viewPosition = normalize( vViewPosition );\n\n#ifdef USE_NORMALMAP\n\n normal = perturbNormal2Arb( -vViewPosition, normal );\n\n#elif defined( USE_BUMPMAP )\n\n normal = perturbNormalArb( -vViewPosition, normal, dHdxy_fwd() );\n\n#endif\n\nvec3 totalDiffuseLight = vec3( 0.0 );\nvec3 totalSpecularLight = vec3( 0.0 );\n\n#if MAX_POINT_LIGHTS > 0\n\n for ( int i = 0; i < MAX_POINT_LIGHTS; i ++ ) {\n\n vec4 lPosition = viewMatrix * vec4( pointLightPosition[ i ], 1.0 );\n vec3 lVector = lPosition.xyz + vViewPosition.xyz;\n\n float attenuation = calcLightAttenuation( length( lVector ), pointLightDistance[ i ], pointLightDecay[ i ] );\n\n lVector = normalize( lVector );\n\n // diffuse\n\n float dotProduct = dot( normal, lVector );\n\n #ifdef WRAP_AROUND\n\n float pointDiffuseWeightFull = max( dotProduct, 0.0 );\n float pointDiffuseWeightHalf = max( 0.5 * dotProduct + 0.5, 0.0 );\n\n vec3 pointDiffuseWeight = mix( vec3( pointDiffuseWeightFull ), vec3( pointDiffuseWeightHalf ), wrapRGB );\n\n #else\n\n float pointDiffuseWeight = max( dotProduct, 0.0 );\n\n #endif\n\n totalDiffuseLight += pointLightColor[ i ] * pointDiffuseWeight * attenuation;\n\n // specular\n\n vec3 pointHalfVector = normalize( lVector + viewPosition );\n float pointDotNormalHalf = max( dot( normal, pointHalfVector ), 0.0 );\n float pointSpecularWeight = specularStrength * max( pow( pointDotNormalHalf, shininess ), 0.0 );\n\n float specularNormalization = ( shininess + 2.0 ) / 8.0;\n\n vec3 schlick = specular + vec3( 1.0 - specular ) * pow( max( 1.0 - dot( lVector, pointHalfVector ), 0.0 ), 5.0 );\n totalSpecularLight += schlick * pointLightColor[ i ] * pointSpecularWeight * pointDiffuseWeight * attenuation * specularNormalization;\n\n }\n\n#endif\n\n#if MAX_SPOT_LIGHTS > 0\n\n for ( int i = 0; i < MAX_SPOT_LIGHTS; i ++ ) {\n\n vec4 lPosition = viewMatrix * vec4( spotLightPosition[ i ], 1.0 );\n vec3 lVector = lPosition.xyz + vViewPosition.xyz;\n\n float attenuation = calcLightAttenuation( length( lVector ), spotLightDistance[ i ], spotLightDecay[ i ] );\n\n lVector = normalize( lVector );\n\n float spotEffect = dot( spotLightDirection[ i ], normalize( spotLightPosition[ i ] - vWorldPosition ) );\n\n if ( spotEffect > spotLightAngleCos[ i ] ) {\n\n spotEffect = max( pow( max( spotEffect, 0.0 ), spotLightExponent[ i ] ), 0.0 );\n\n // diffuse\n\n float dotProduct = dot( normal, lVector );\n\n #ifdef WRAP_AROUND\n\n float spotDiffuseWeightFull = max( dotProduct, 0.0 );\n float spotDiffuseWeightHalf = max( 0.5 * dotProduct + 0.5, 0.0 );\n\n vec3 spotDiffuseWeight = mix( vec3( spotDiffuseWeightFull ), vec3( spotDiffuseWeightHalf ), wrapRGB );\n\n #else\n\n float spotDiffuseWeight = max( dotProduct, 0.0 );\n\n #endif\n\n totalDiffuseLight += spotLightColor[ i ] * spotDiffuseWeight * attenuation * spotEffect;\n\n // specular\n\n vec3 spotHalfVector = normalize( lVector + viewPosition );\n float spotDotNormalHalf = max( dot( normal, spotHalfVector ), 0.0 );\n float spotSpecularWeight = specularStrength * max( pow( spotDotNormalHalf, shininess ), 0.0 );\n\n float specularNormalization = ( shininess + 2.0 ) / 8.0;\n\n vec3 schlick = specular + vec3( 1.0 - specular ) * pow( max( 1.0 - dot( lVector, spotHalfVector ), 0.0 ), 5.0 );\n totalSpecularLight += schlick * spotLightColor[ i ] * spotSpecularWeight * spotDiffuseWeight * attenuation * specularNormalization * spotEffect;\n\n }\n\n }\n\n#endif\n\n#if MAX_DIR_LIGHTS > 0\n\n for( int i = 0; i < MAX_DIR_LIGHTS; i ++ ) {\n\n vec3 dirVector = transformDirection( directionalLightDirection[ i ], viewMatrix );\n\n // diffuse\n\n float dotProduct = dot( normal, dirVector );\n\n #ifdef WRAP_AROUND\n\n float dirDiffuseWeightFull = max( dotProduct, 0.0 );\n float dirDiffuseWeightHalf = max( 0.5 * dotProduct + 0.5, 0.0 );\n\n vec3 dirDiffuseWeight = mix( vec3( dirDiffuseWeightFull ), vec3( dirDiffuseWeightHalf ), wrapRGB );\n\n #else\n\n float dirDiffuseWeight = max( dotProduct, 0.0 );\n\n #endif\n\n totalDiffuseLight += directionalLightColor[ i ] * dirDiffuseWeight;\n\n // specular\n\n vec3 dirHalfVector = normalize( dirVector + viewPosition );\n float dirDotNormalHalf = max( dot( normal, dirHalfVector ), 0.0 );\n float dirSpecularWeight = specularStrength * max( pow( dirDotNormalHalf, shininess ), 0.0 );\n\n /*\n // fresnel term from skin shader\n const float F0 = 0.128;\n\n float base = 1.0 - dot( viewPosition, dirHalfVector );\n float exponential = pow( base, 5.0 );\n\n float fresnel = exponential + F0 * ( 1.0 - exponential );\n */\n\n /*\n // fresnel term from fresnel shader\n const float mFresnelBias = 0.08;\n const float mFresnelScale = 0.3;\n const float mFresnelPower = 5.0;\n\n float fresnel = mFresnelBias + mFresnelScale * pow( 1.0 + dot( normalize( -viewPosition ), normal ), mFresnelPower );\n */\n\n float specularNormalization = ( shininess + 2.0 ) / 8.0;\n\n // dirSpecular += specular * directionalLightColor[ i ] * dirSpecularWeight * dirDiffuseWeight * specularNormalization * fresnel;\n\n vec3 schlick = specular + vec3( 1.0 - specular ) * pow( max( 1.0 - dot( dirVector, dirHalfVector ), 0.0 ), 5.0 );\n totalSpecularLight += schlick * directionalLightColor[ i ] * dirSpecularWeight * dirDiffuseWeight * specularNormalization;\n\n\n }\n\n#endif\n\n#if MAX_HEMI_LIGHTS > 0\n\n for( int i = 0; i < MAX_HEMI_LIGHTS; i ++ ) {\n\n vec3 lVector = transformDirection( hemisphereLightDirection[ i ], viewMatrix );\n\n // diffuse\n\n float dotProduct = dot( normal, lVector );\n float hemiDiffuseWeight = 0.5 * dotProduct + 0.5;\n\n vec3 hemiColor = mix( hemisphereLightGroundColor[ i ], hemisphereLightSkyColor[ i ], hemiDiffuseWeight );\n\n totalDiffuseLight += hemiColor;\n\n // specular (sky light)\n\n vec3 hemiHalfVectorSky = normalize( lVector + viewPosition );\n float hemiDotNormalHalfSky = 0.5 * dot( normal, hemiHalfVectorSky ) + 0.5;\n float hemiSpecularWeightSky = specularStrength * max( pow( max( hemiDotNormalHalfSky, 0.0 ), shininess ), 0.0 );\n\n // specular (ground light)\n\n vec3 lVectorGround = -lVector;\n\n vec3 hemiHalfVectorGround = normalize( lVectorGround + viewPosition );\n float hemiDotNormalHalfGround = 0.5 * dot( normal, hemiHalfVectorGround ) + 0.5;\n float hemiSpecularWeightGround = specularStrength * max( pow( max( hemiDotNormalHalfGround, 0.0 ), shininess ), 0.0 );\n\n float dotProductGround = dot( normal, lVectorGround );\n\n float specularNormalization = ( shininess + 2.0 ) / 8.0;\n\n vec3 schlickSky = specular + vec3( 1.0 - specular ) * pow( max( 1.0 - dot( lVector, hemiHalfVectorSky ), 0.0 ), 5.0 );\n vec3 schlickGround = specular + vec3( 1.0 - specular ) * pow( max( 1.0 - dot( lVectorGround, hemiHalfVectorGround ), 0.0 ), 5.0 );\n totalSpecularLight += hemiColor * specularNormalization * ( schlickSky * hemiSpecularWeightSky * max( dotProduct, 0.0 ) + schlickGround * hemiSpecularWeightGround * max( dotProductGround, 0.0 ) );\n\n }\n\n#endif\n\n#ifdef METAL\n\n outgoingLight += diffuseColor.rgb * ( totalDiffuseLight + ambientLightColor ) * specular + totalSpecularLight + emissive;\n\n#else\n\n outgoingLight += diffuseColor.rgb * ( totalDiffuseLight + ambientLightColor ) + totalSpecularLight + emissive;\n\n#endif\n", -THREE.ShaderChunk.fog_pars_fragment="#ifdef USE_FOG\n\n uniform vec3 fogColor;\n\n #ifdef FOG_EXP2\n\n uniform float fogDensity;\n\n #else\n\n uniform float fogNear;\n uniform float fogFar;\n #endif\n\n#endif",THREE.ShaderChunk.morphnormal_vertex="#ifdef USE_MORPHNORMALS\n\n vec3 morphedNormal = vec3( 0.0 );\n\n morphedNormal += ( morphNormal0 - normal ) * morphTargetInfluences[ 0 ];\n morphedNormal += ( morphNormal1 - normal ) * morphTargetInfluences[ 1 ];\n morphedNormal += ( morphNormal2 - normal ) * morphTargetInfluences[ 2 ];\n morphedNormal += ( morphNormal3 - normal ) * morphTargetInfluences[ 3 ];\n\n morphedNormal += normal;\n\n#endif",THREE.ShaderChunk.envmap_pars_fragment="#ifdef USE_ENVMAP\n\n uniform float reflectivity;\n #ifdef ENVMAP_TYPE_CUBE\n uniform samplerCube envMap;\n #else\n uniform sampler2D envMap;\n #endif\n uniform float flipEnvMap;\n\n #if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG )\n\n uniform float refractionRatio;\n\n #else\n\n varying vec3 vReflect;\n\n #endif\n\n#endif\n",THREE.ShaderChunk.logdepthbuf_fragment="#if defined(USE_LOGDEPTHBUF) && defined(USE_LOGDEPTHBUF_EXT)\n\n gl_FragDepthEXT = log2(vFragDepth) * logDepthBufFC * 0.5;\n\n#endif",THREE.ShaderChunk.normalmap_pars_fragment="#ifdef USE_NORMALMAP\n\n uniform sampler2D normalMap;\n uniform vec2 normalScale;\n\n // Per-Pixel Tangent Space Normal Mapping\n // http://hacksoflife.blogspot.ch/2009/11/per-pixel-tangent-space-normal-mapping.html\n\n vec3 perturbNormal2Arb( vec3 eye_pos, vec3 surf_norm ) {\n\n vec3 q0 = dFdx( eye_pos.xyz );\n vec3 q1 = dFdy( eye_pos.xyz );\n vec2 st0 = dFdx( vUv.st );\n vec2 st1 = dFdy( vUv.st );\n\n vec3 S = normalize( q0 * st1.t - q1 * st0.t );\n vec3 T = normalize( -q0 * st1.s + q1 * st0.s );\n vec3 N = normalize( surf_norm );\n\n vec3 mapN = texture2D( normalMap, vUv ).xyz * 2.0 - 1.0;\n mapN.xy = normalScale * mapN.xy;\n mat3 tsn = mat3( S, T, N );\n return normalize( tsn * mapN );\n\n }\n\n#endif\n",THREE.ShaderChunk.lights_phong_pars_vertex="#if MAX_SPOT_LIGHTS > 0 || defined( USE_BUMPMAP ) || defined( USE_ENVMAP )\n\n varying vec3 vWorldPosition;\n\n#endif\n",THREE.ShaderChunk.lightmap_pars_fragment="#ifdef USE_LIGHTMAP\n\n varying vec2 vUv2;\n uniform sampler2D lightMap;\n\n#endif",THREE.ShaderChunk.shadowmap_vertex="#ifdef USE_SHADOWMAP\n\n for( int i = 0; i < MAX_SHADOWS; i ++ ) {\n\n vShadowCoord[ i ] = shadowMatrix[ i ] * worldPosition;\n\n }\n\n#endif",THREE.ShaderChunk.lights_phong_vertex="#if MAX_SPOT_LIGHTS > 0 || defined( USE_BUMPMAP ) || defined( USE_ENVMAP )\n\n vWorldPosition = worldPosition.xyz;\n\n#endif",THREE.ShaderChunk.map_fragment="#ifdef USE_MAP\n\n vec4 texelColor = texture2D( map, vUv );\n\n texelColor.xyz = inputToLinear( texelColor.xyz );\n\n diffuseColor *= texelColor;\n\n#endif",THREE.ShaderChunk.lightmap_vertex="#ifdef USE_LIGHTMAP\n\n vUv2 = uv2;\n\n#endif",THREE.ShaderChunk.map_particle_fragment="#ifdef USE_MAP\n\n diffuseColor *= texture2D( map, vec2( gl_PointCoord.x, 1.0 - gl_PointCoord.y ) * offsetRepeat.zw + offsetRepeat.xy );\n\n#endif\n",THREE.ShaderChunk.color_pars_fragment="#ifdef USE_COLOR\n\n varying vec3 vColor;\n\n#endif\n",THREE.ShaderChunk.color_vertex="#ifdef USE_COLOR\n\n vColor.xyz = inputToLinear( color.xyz );\n\n#endif",THREE.ShaderChunk.skinning_vertex="#ifdef USE_SKINNING\n\n #ifdef USE_MORPHTARGETS\n\n vec4 skinVertex = bindMatrix * vec4( morphed, 1.0 );\n\n #else\n\n vec4 skinVertex = bindMatrix * vec4( position, 1.0 );\n\n #endif\n\n vec4 skinned = vec4( 0.0 );\n skinned += boneMatX * skinVertex * skinWeight.x;\n skinned += boneMatY * skinVertex * skinWeight.y;\n skinned += boneMatZ * skinVertex * skinWeight.z;\n skinned += boneMatW * skinVertex * skinWeight.w;\n skinned = bindMatrixInverse * skinned;\n\n#endif\n",THREE.ShaderChunk.envmap_pars_vertex="#if defined( USE_ENVMAP ) && ! defined( USE_BUMPMAP ) && ! defined( USE_NORMALMAP ) && ! defined( PHONG )\n\n varying vec3 vReflect;\n\n uniform float refractionRatio;\n\n#endif\n",THREE.ShaderChunk.linear_to_gamma_fragment="\n outgoingLight = linearToOutput( outgoingLight );\n",THREE.ShaderChunk.color_pars_vertex="#ifdef USE_COLOR\n\n varying vec3 vColor;\n\n#endif",THREE.ShaderChunk.lights_lambert_pars_vertex="uniform vec3 ambientLightColor;\n\n#if MAX_DIR_LIGHTS > 0\n\n uniform vec3 directionalLightColor[ MAX_DIR_LIGHTS ];\n uniform vec3 directionalLightDirection[ MAX_DIR_LIGHTS ];\n\n#endif\n\n#if MAX_HEMI_LIGHTS > 0\n\n uniform vec3 hemisphereLightSkyColor[ MAX_HEMI_LIGHTS ];\n uniform vec3 hemisphereLightGroundColor[ MAX_HEMI_LIGHTS ];\n uniform vec3 hemisphereLightDirection[ MAX_HEMI_LIGHTS ];\n\n#endif\n\n#if MAX_POINT_LIGHTS > 0\n\n uniform vec3 pointLightColor[ MAX_POINT_LIGHTS ];\n uniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];\n uniform float pointLightDistance[ MAX_POINT_LIGHTS ];\n uniform float pointLightDecay[ MAX_POINT_LIGHTS ];\n\n#endif\n\n#if MAX_SPOT_LIGHTS > 0\n\n uniform vec3 spotLightColor[ MAX_SPOT_LIGHTS ];\n uniform vec3 spotLightPosition[ MAX_SPOT_LIGHTS ];\n uniform vec3 spotLightDirection[ MAX_SPOT_LIGHTS ];\n uniform float spotLightDistance[ MAX_SPOT_LIGHTS ];\n uniform float spotLightAngleCos[ MAX_SPOT_LIGHTS ];\n uniform float spotLightExponent[ MAX_SPOT_LIGHTS ];\n uniform float spotLightDecay[ MAX_SPOT_LIGHTS ];\n\n#endif\n\n#ifdef WRAP_AROUND\n\n uniform vec3 wrapRGB;\n\n#endif\n",THREE.ShaderChunk.map_pars_vertex="#if defined( USE_MAP ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( USE_SPECULARMAP ) || defined( USE_ALPHAMAP )\n\n varying vec2 vUv;\n uniform vec4 offsetRepeat;\n\n#endif\n",THREE.ShaderChunk.envmap_fragment="#ifdef USE_ENVMAP\n\n #if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG )\n\n vec3 cameraToVertex = normalize( vWorldPosition - cameraPosition );\n\n // Transforming Normal Vectors with the Inverse Transformation\n vec3 worldNormal = inverseTransformDirection( normal, viewMatrix );\n\n #ifdef ENVMAP_MODE_REFLECTION\n\n vec3 reflectVec = reflect( cameraToVertex, worldNormal );\n\n #else\n\n vec3 reflectVec = refract( cameraToVertex, worldNormal, refractionRatio );\n\n #endif\n\n #else\n\n vec3 reflectVec = vReflect;\n\n #endif\n\n #ifdef DOUBLE_SIDED\n float flipNormal = ( -1.0 + 2.0 * float( gl_FrontFacing ) );\n #else\n float flipNormal = 1.0;\n #endif\n\n #ifdef ENVMAP_TYPE_CUBE\n vec4 envColor = textureCube( envMap, flipNormal * vec3( flipEnvMap * reflectVec.x, reflectVec.yz ) );\n\n #elif defined( ENVMAP_TYPE_EQUIREC )\n vec2 sampleUV;\n sampleUV.y = saturate( flipNormal * reflectVec.y * 0.5 + 0.5 );\n sampleUV.x = atan( flipNormal * reflectVec.z, flipNormal * reflectVec.x ) * RECIPROCAL_PI2 + 0.5;\n vec4 envColor = texture2D( envMap, sampleUV );\n\n #elif defined( ENVMAP_TYPE_SPHERE )\n vec3 reflectView = flipNormal * normalize((viewMatrix * vec4( reflectVec, 0.0 )).xyz + vec3(0.0,0.0,1.0));\n vec4 envColor = texture2D( envMap, reflectView.xy * 0.5 + 0.5 );\n #endif\n\n envColor.xyz = inputToLinear( envColor.xyz );\n\n #ifdef ENVMAP_BLENDING_MULTIPLY\n\n outgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity );\n\n #elif defined( ENVMAP_BLENDING_MIX )\n\n outgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity );\n\n #elif defined( ENVMAP_BLENDING_ADD )\n\n outgoingLight += envColor.xyz * specularStrength * reflectivity;\n\n #endif\n\n#endif\n",THREE.ShaderChunk.specularmap_pars_fragment="#ifdef USE_SPECULARMAP\n\n uniform sampler2D specularMap;\n\n#endif",THREE.ShaderChunk.logdepthbuf_vertex="#ifdef USE_LOGDEPTHBUF\n\n gl_Position.z = log2(max( EPSILON, gl_Position.w + 1.0 )) * logDepthBufFC;\n\n #ifdef USE_LOGDEPTHBUF_EXT\n\n vFragDepth = 1.0 + gl_Position.w;\n\n#else\n\n gl_Position.z = (gl_Position.z - 1.0) * gl_Position.w;\n\n #endif\n\n#endif",THREE.ShaderChunk.morphtarget_pars_vertex="#ifdef USE_MORPHTARGETS\n\n #ifndef USE_MORPHNORMALS\n\n uniform float morphTargetInfluences[ 8 ];\n\n #else\n\n uniform float morphTargetInfluences[ 4 ];\n\n #endif\n\n#endif",THREE.ShaderChunk.specularmap_fragment="float specularStrength;\n\n#ifdef USE_SPECULARMAP\n\n vec4 texelSpecular = texture2D( specularMap, vUv );\n specularStrength = texelSpecular.r;\n\n#else\n\n specularStrength = 1.0;\n\n#endif",THREE.ShaderChunk.fog_fragment="#ifdef USE_FOG\n\n #ifdef USE_LOGDEPTHBUF_EXT\n\n float depth = gl_FragDepthEXT / gl_FragCoord.w;\n\n #else\n\n float depth = gl_FragCoord.z / gl_FragCoord.w;\n\n #endif\n\n #ifdef FOG_EXP2\n\n float fogFactor = exp2( - square( fogDensity ) * square( depth ) * LOG2 );\n fogFactor = whiteCompliment( fogFactor );\n\n #else\n\n float fogFactor = smoothstep( fogNear, fogFar, depth );\n\n #endif\n \n outgoingLight = mix( outgoingLight, fogColor, fogFactor );\n\n#endif",THREE.ShaderChunk.bumpmap_pars_fragment="#ifdef USE_BUMPMAP\n\n uniform sampler2D bumpMap;\n uniform float bumpScale;\n\n // Derivative maps - bump mapping unparametrized surfaces by Morten Mikkelsen\n // http://mmikkelsen3d.blogspot.sk/2011/07/derivative-maps.html\n\n // Evaluate the derivative of the height w.r.t. screen-space using forward differencing (listing 2)\n\n vec2 dHdxy_fwd() {\n\n vec2 dSTdx = dFdx( vUv );\n vec2 dSTdy = dFdy( vUv );\n\n float Hll = bumpScale * texture2D( bumpMap, vUv ).x;\n float dBx = bumpScale * texture2D( bumpMap, vUv + dSTdx ).x - Hll;\n float dBy = bumpScale * texture2D( bumpMap, vUv + dSTdy ).x - Hll;\n\n return vec2( dBx, dBy );\n\n }\n\n vec3 perturbNormalArb( vec3 surf_pos, vec3 surf_norm, vec2 dHdxy ) {\n\n vec3 vSigmaX = dFdx( surf_pos );\n vec3 vSigmaY = dFdy( surf_pos );\n vec3 vN = surf_norm; // normalized\n\n vec3 R1 = cross( vSigmaY, vN );\n vec3 R2 = cross( vN, vSigmaX );\n\n float fDet = dot( vSigmaX, R1 );\n\n vec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 );\n return normalize( abs( fDet ) * surf_norm - vGrad );\n\n }\n\n#endif\n",THREE.ShaderChunk.defaultnormal_vertex="#ifdef USE_SKINNING\n\n vec3 objectNormal = skinnedNormal.xyz;\n\n#elif defined( USE_MORPHNORMALS )\n\n vec3 objectNormal = morphedNormal;\n\n#else\n\n vec3 objectNormal = normal;\n\n#endif\n\n#ifdef FLIP_SIDED\n\n objectNormal = -objectNormal;\n\n#endif\n\nvec3 transformedNormal = normalMatrix * objectNormal;\n",THREE.ShaderChunk.lights_phong_pars_fragment="uniform vec3 ambientLightColor;\n\n#if MAX_DIR_LIGHTS > 0\n\n uniform vec3 directionalLightColor[ MAX_DIR_LIGHTS ];\n uniform vec3 directionalLightDirection[ MAX_DIR_LIGHTS ];\n\n#endif\n\n#if MAX_HEMI_LIGHTS > 0\n\n uniform vec3 hemisphereLightSkyColor[ MAX_HEMI_LIGHTS ];\n uniform vec3 hemisphereLightGroundColor[ MAX_HEMI_LIGHTS ];\n uniform vec3 hemisphereLightDirection[ MAX_HEMI_LIGHTS ];\n\n#endif\n\n#if MAX_POINT_LIGHTS > 0\n\n uniform vec3 pointLightColor[ MAX_POINT_LIGHTS ];\n\n uniform vec3 pointLightPosition[ MAX_POINT_LIGHTS ];\n uniform float pointLightDistance[ MAX_POINT_LIGHTS ];\n uniform float pointLightDecay[ MAX_POINT_LIGHTS ];\n\n#endif\n\n#if MAX_SPOT_LIGHTS > 0\n\n uniform vec3 spotLightColor[ MAX_SPOT_LIGHTS ];\n uniform vec3 spotLightPosition[ MAX_SPOT_LIGHTS ];\n uniform vec3 spotLightDirection[ MAX_SPOT_LIGHTS ];\n uniform float spotLightAngleCos[ MAX_SPOT_LIGHTS ];\n uniform float spotLightExponent[ MAX_SPOT_LIGHTS ];\n uniform float spotLightDistance[ MAX_SPOT_LIGHTS ];\n uniform float spotLightDecay[ MAX_SPOT_LIGHTS ];\n\n#endif\n\n#if MAX_SPOT_LIGHTS > 0 || defined( USE_BUMPMAP ) || defined( USE_ENVMAP )\n\n varying vec3 vWorldPosition;\n\n#endif\n\n#ifdef WRAP_AROUND\n\n uniform vec3 wrapRGB;\n\n#endif\n\nvarying vec3 vViewPosition;\n\n#ifndef FLAT_SHADED\n\n varying vec3 vNormal;\n\n#endif\n",THREE.ShaderChunk.skinbase_vertex="#ifdef USE_SKINNING\n\n mat4 boneMatX = getBoneMatrix( skinIndex.x );\n mat4 boneMatY = getBoneMatrix( skinIndex.y );\n mat4 boneMatZ = getBoneMatrix( skinIndex.z );\n mat4 boneMatW = getBoneMatrix( skinIndex.w );\n\n#endif",THREE.ShaderChunk.map_vertex="#if defined( USE_MAP ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( USE_SPECULARMAP ) || defined( USE_ALPHAMAP )\n\n vUv = uv * offsetRepeat.zw + offsetRepeat.xy;\n\n#endif",THREE.ShaderChunk.lightmap_fragment="#ifdef USE_LIGHTMAP\n\n outgoingLight *= diffuseColor.xyz * texture2D( lightMap, vUv2 ).xyz;\n\n#endif",THREE.ShaderChunk.shadowmap_pars_vertex="#ifdef USE_SHADOWMAP\n\n varying vec4 vShadowCoord[ MAX_SHADOWS ];\n uniform mat4 shadowMatrix[ MAX_SHADOWS ];\n\n#endif",THREE.ShaderChunk.color_fragment="#ifdef USE_COLOR\n\n diffuseColor.rgb *= vColor;\n\n#endif",THREE.ShaderChunk.morphtarget_vertex="#ifdef USE_MORPHTARGETS\n\n vec3 morphed = vec3( 0.0 );\n morphed += ( morphTarget0 - position ) * morphTargetInfluences[ 0 ];\n morphed += ( morphTarget1 - position ) * morphTargetInfluences[ 1 ];\n morphed += ( morphTarget2 - position ) * morphTargetInfluences[ 2 ];\n morphed += ( morphTarget3 - position ) * morphTargetInfluences[ 3 ];\n\n #ifndef USE_MORPHNORMALS\n\n morphed += ( morphTarget4 - position ) * morphTargetInfluences[ 4 ];\n morphed += ( morphTarget5 - position ) * morphTargetInfluences[ 5 ];\n morphed += ( morphTarget6 - position ) * morphTargetInfluences[ 6 ];\n morphed += ( morphTarget7 - position ) * morphTargetInfluences[ 7 ];\n\n #endif\n\n morphed += position;\n\n#endif",THREE.ShaderChunk.envmap_vertex="#if defined( USE_ENVMAP ) && ! defined( USE_BUMPMAP ) && ! defined( USE_NORMALMAP ) && ! defined( PHONG )\n\n vec3 worldNormal = transformDirection( objectNormal, modelMatrix );\n\n vec3 cameraToVertex = normalize( worldPosition.xyz - cameraPosition );\n\n #ifdef ENVMAP_MODE_REFLECTION\n\n vReflect = reflect( cameraToVertex, worldNormal );\n\n #else\n\n vReflect = refract( cameraToVertex, worldNormal, refractionRatio );\n\n #endif\n\n#endif\n",THREE.ShaderChunk.shadowmap_fragment="#ifdef USE_SHADOWMAP\n\n #ifdef SHADOWMAP_DEBUG\n\n vec3 frustumColors[3];\n frustumColors[0] = vec3( 1.0, 0.5, 0.0 );\n frustumColors[1] = vec3( 0.0, 1.0, 0.8 );\n frustumColors[2] = vec3( 0.0, 0.5, 1.0 );\n\n #endif\n\n #ifdef SHADOWMAP_CASCADE\n\n int inFrustumCount = 0;\n\n #endif\n\n float fDepth;\n vec3 shadowColor = vec3( 1.0 );\n\n for( int i = 0; i < MAX_SHADOWS; i ++ ) {\n\n vec3 shadowCoord = vShadowCoord[ i ].xyz / vShadowCoord[ i ].w;\n\n // if ( something && something ) breaks ATI OpenGL shader compiler\n // if ( all( something, something ) ) using this instead\n\n bvec4 inFrustumVec = bvec4 ( shadowCoord.x >= 0.0, shadowCoord.x <= 1.0, shadowCoord.y >= 0.0, shadowCoord.y <= 1.0 );\n bool inFrustum = all( inFrustumVec );\n\n // don't shadow pixels outside of light frustum\n // use just first frustum (for cascades)\n // don't shadow pixels behind far plane of light frustum\n\n #ifdef SHADOWMAP_CASCADE\n\n inFrustumCount += int( inFrustum );\n bvec3 frustumTestVec = bvec3( inFrustum, inFrustumCount == 1, shadowCoord.z <= 1.0 );\n\n #else\n\n bvec2 frustumTestVec = bvec2( inFrustum, shadowCoord.z <= 1.0 );\n\n #endif\n\n bool frustumTest = all( frustumTestVec );\n\n if ( frustumTest ) {\n\n shadowCoord.z += shadowBias[ i ];\n\n #if defined( SHADOWMAP_TYPE_PCF )\n\n // Percentage-close filtering\n // (9 pixel kernel)\n // http://fabiensanglard.net/shadowmappingPCF/\n\n float shadow = 0.0;\n\n /*\n // nested loops breaks shader compiler / validator on some ATI cards when using OpenGL\n // must enroll loop manually\n\n for ( float y = -1.25; y <= 1.25; y += 1.25 )\n for ( float x = -1.25; x <= 1.25; x += 1.25 ) {\n\n vec4 rgbaDepth = texture2D( shadowMap[ i ], vec2( x * xPixelOffset, y * yPixelOffset ) + shadowCoord.xy );\n\n // doesn't seem to produce any noticeable visual difference compared to simple texture2D lookup\n //vec4 rgbaDepth = texture2DProj( shadowMap[ i ], vec4( vShadowCoord[ i ].w * ( vec2( x * xPixelOffset, y * yPixelOffset ) + shadowCoord.xy ), 0.05, vShadowCoord[ i ].w ) );\n\n float fDepth = unpackDepth( rgbaDepth );\n\n if ( fDepth < shadowCoord.z )\n shadow += 1.0;\n\n }\n\n shadow /= 9.0;\n\n */\n\n const float shadowDelta = 1.0 / 9.0;\n\n float xPixelOffset = 1.0 / shadowMapSize[ i ].x;\n float yPixelOffset = 1.0 / shadowMapSize[ i ].y;\n\n float dx0 = -1.25 * xPixelOffset;\n float dy0 = -1.25 * yPixelOffset;\n float dx1 = 1.25 * xPixelOffset;\n float dy1 = 1.25 * yPixelOffset;\n\n fDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, dy0 ) ) );\n if ( fDepth < shadowCoord.z ) shadow += shadowDelta;\n\n fDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( 0.0, dy0 ) ) );\n if ( fDepth < shadowCoord.z ) shadow += shadowDelta;\n\n fDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, dy0 ) ) );\n if ( fDepth < shadowCoord.z ) shadow += shadowDelta;\n\n fDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, 0.0 ) ) );\n if ( fDepth < shadowCoord.z ) shadow += shadowDelta;\n\n fDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy ) );\n if ( fDepth < shadowCoord.z ) shadow += shadowDelta;\n\n fDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, 0.0 ) ) );\n if ( fDepth < shadowCoord.z ) shadow += shadowDelta;\n\n fDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, dy1 ) ) );\n if ( fDepth < shadowCoord.z ) shadow += shadowDelta;\n\n fDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( 0.0, dy1 ) ) );\n if ( fDepth < shadowCoord.z ) shadow += shadowDelta;\n\n fDepth = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, dy1 ) ) );\n if ( fDepth < shadowCoord.z ) shadow += shadowDelta;\n\n shadowColor = shadowColor * vec3( ( 1.0 - shadowDarkness[ i ] * shadow ) );\n\n #elif defined( SHADOWMAP_TYPE_PCF_SOFT )\n\n // Percentage-close filtering\n // (9 pixel kernel)\n // http://fabiensanglard.net/shadowmappingPCF/\n\n float shadow = 0.0;\n\n float xPixelOffset = 1.0 / shadowMapSize[ i ].x;\n float yPixelOffset = 1.0 / shadowMapSize[ i ].y;\n\n float dx0 = -1.0 * xPixelOffset;\n float dy0 = -1.0 * yPixelOffset;\n float dx1 = 1.0 * xPixelOffset;\n float dy1 = 1.0 * yPixelOffset;\n\n mat3 shadowKernel;\n mat3 depthKernel;\n\n depthKernel[0][0] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, dy0 ) ) );\n depthKernel[0][1] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, 0.0 ) ) );\n depthKernel[0][2] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx0, dy1 ) ) );\n depthKernel[1][0] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( 0.0, dy0 ) ) );\n depthKernel[1][1] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy ) );\n depthKernel[1][2] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( 0.0, dy1 ) ) );\n depthKernel[2][0] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, dy0 ) ) );\n depthKernel[2][1] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, 0.0 ) ) );\n depthKernel[2][2] = unpackDepth( texture2D( shadowMap[ i ], shadowCoord.xy + vec2( dx1, dy1 ) ) );\n\n vec3 shadowZ = vec3( shadowCoord.z );\n shadowKernel[0] = vec3(lessThan(depthKernel[0], shadowZ ));\n shadowKernel[0] *= vec3(0.25);\n\n shadowKernel[1] = vec3(lessThan(depthKernel[1], shadowZ ));\n shadowKernel[1] *= vec3(0.25);\n\n shadowKernel[2] = vec3(lessThan(depthKernel[2], shadowZ ));\n shadowKernel[2] *= vec3(0.25);\n\n vec2 fractionalCoord = 1.0 - fract( shadowCoord.xy * shadowMapSize[i].xy );\n\n shadowKernel[0] = mix( shadowKernel[1], shadowKernel[0], fractionalCoord.x );\n shadowKernel[1] = mix( shadowKernel[2], shadowKernel[1], fractionalCoord.x );\n\n vec4 shadowValues;\n shadowValues.x = mix( shadowKernel[0][1], shadowKernel[0][0], fractionalCoord.y );\n shadowValues.y = mix( shadowKernel[0][2], shadowKernel[0][1], fractionalCoord.y );\n shadowValues.z = mix( shadowKernel[1][1], shadowKernel[1][0], fractionalCoord.y );\n shadowValues.w = mix( shadowKernel[1][2], shadowKernel[1][1], fractionalCoord.y );\n\n shadow = dot( shadowValues, vec4( 1.0 ) );\n\n shadowColor = shadowColor * vec3( ( 1.0 - shadowDarkness[ i ] * shadow ) );\n\n #else\n\n vec4 rgbaDepth = texture2D( shadowMap[ i ], shadowCoord.xy );\n float fDepth = unpackDepth( rgbaDepth );\n\n if ( fDepth < shadowCoord.z )\n\n // spot with multiple shadows is darker\n\n shadowColor = shadowColor * vec3( 1.0 - shadowDarkness[ i ] );\n\n // spot with multiple shadows has the same color as single shadow spot\n\n // shadowColor = min( shadowColor, vec3( shadowDarkness[ i ] ) );\n\n #endif\n\n }\n\n\n #ifdef SHADOWMAP_DEBUG\n\n #ifdef SHADOWMAP_CASCADE\n\n if ( inFrustum && inFrustumCount == 1 ) outgoingLight *= frustumColors[ i ];\n\n #else\n\n if ( inFrustum ) outgoingLight *= frustumColors[ i ];\n\n #endif\n\n #endif\n\n }\n\n // NOTE: I am unsure if this is correct in linear space. -bhouston, Dec 29, 2014\n shadowColor = inputToLinear( shadowColor );\n\n outgoingLight = outgoingLight * shadowColor;\n\n#endif\n",THREE.ShaderChunk.worldpos_vertex="#if defined( USE_ENVMAP ) || defined( PHONG ) || defined( LAMBERT ) || defined ( USE_SHADOWMAP )\n\n #ifdef USE_SKINNING\n\n vec4 worldPosition = modelMatrix * skinned;\n\n #elif defined( USE_MORPHTARGETS )\n\n vec4 worldPosition = modelMatrix * vec4( morphed, 1.0 );\n\n #else\n\n vec4 worldPosition = modelMatrix * vec4( position, 1.0 );\n\n #endif\n\n#endif\n",THREE.ShaderChunk.shadowmap_pars_fragment="#ifdef USE_SHADOWMAP\n\n uniform sampler2D shadowMap[ MAX_SHADOWS ];\n uniform vec2 shadowMapSize[ MAX_SHADOWS ];\n\n uniform float shadowDarkness[ MAX_SHADOWS ];\n uniform float shadowBias[ MAX_SHADOWS ];\n\n varying vec4 vShadowCoord[ MAX_SHADOWS ];\n\n float unpackDepth( const in vec4 rgba_depth ) {\n\n const vec4 bit_shift = vec4( 1.0 / ( 256.0 * 256.0 * 256.0 ), 1.0 / ( 256.0 * 256.0 ), 1.0 / 256.0, 1.0 );\n float depth = dot( rgba_depth, bit_shift );\n return depth;\n\n }\n\n#endif",THREE.ShaderChunk.skinning_pars_vertex="#ifdef USE_SKINNING\n\n uniform mat4 bindMatrix;\n uniform mat4 bindMatrixInverse;\n\n #ifdef BONE_TEXTURE\n\n uniform sampler2D boneTexture;\n uniform int boneTextureWidth;\n uniform int boneTextureHeight;\n\n mat4 getBoneMatrix( const in float i ) {\n\n float j = i * 4.0;\n float x = mod( j, float( boneTextureWidth ) );\n float y = floor( j / float( boneTextureWidth ) );\n\n float dx = 1.0 / float( boneTextureWidth );\n float dy = 1.0 / float( boneTextureHeight );\n\n y = dy * ( y + 0.5 );\n\n vec4 v1 = texture2D( boneTexture, vec2( dx * ( x + 0.5 ), y ) );\n vec4 v2 = texture2D( boneTexture, vec2( dx * ( x + 1.5 ), y ) );\n vec4 v3 = texture2D( boneTexture, vec2( dx * ( x + 2.5 ), y ) );\n vec4 v4 = texture2D( boneTexture, vec2( dx * ( x + 3.5 ), y ) );\n\n mat4 bone = mat4( v1, v2, v3, v4 );\n\n return bone;\n\n }\n\n #else\n\n uniform mat4 boneGlobalMatrices[ MAX_BONES ];\n\n mat4 getBoneMatrix( const in float i ) {\n\n mat4 bone = boneGlobalMatrices[ int(i) ];\n return bone;\n\n }\n\n #endif\n\n#endif\n",THREE.ShaderChunk.logdepthbuf_pars_fragment="#ifdef USE_LOGDEPTHBUF\n\n uniform float logDepthBufFC;\n\n #ifdef USE_LOGDEPTHBUF_EXT\n\n #extension GL_EXT_frag_depth : enable\n varying float vFragDepth;\n\n #endif\n\n#endif",THREE.ShaderChunk.alphamap_fragment="#ifdef USE_ALPHAMAP\n\n diffuseColor.a *= texture2D( alphaMap, vUv ).g;\n\n#endif\n",THREE.ShaderChunk.alphamap_pars_fragment="#ifdef USE_ALPHAMAP\n\n uniform sampler2D alphaMap;\n\n#endif\n",THREE.UniformsUtils={merge:function(t){for(var e={},n=0;n dashSize ) {"," discard;"," }"," vec3 outgoingLight = vec3( 0.0 );"," vec4 diffuseColor = vec4( diffuse, opacity );",THREE.ShaderChunk.logdepthbuf_fragment,THREE.ShaderChunk.color_fragment," outgoingLight = diffuseColor.rgb;",THREE.ShaderChunk.fog_fragment," gl_FragColor = vec4( outgoingLight, diffuseColor.a );","}"].join("\n")},depth:{uniforms:{mNear:{type:"f",value:1},mFar:{type:"f",value:2e3},opacity:{type:"f",value:1}},vertexShader:[THREE.ShaderChunk.common,THREE.ShaderChunk.morphtarget_pars_vertex,THREE.ShaderChunk.logdepthbuf_pars_vertex,"void main() {",THREE.ShaderChunk.morphtarget_vertex,THREE.ShaderChunk.default_vertex,THREE.ShaderChunk.logdepthbuf_vertex,"}"].join("\n"),fragmentShader:["uniform float mNear;","uniform float mFar;","uniform float opacity;",THREE.ShaderChunk.common,THREE.ShaderChunk.logdepthbuf_pars_fragment,"void main() {",THREE.ShaderChunk.logdepthbuf_fragment," #ifdef USE_LOGDEPTHBUF_EXT"," float depth = gl_FragDepthEXT / gl_FragCoord.w;"," #else"," float depth = gl_FragCoord.z / gl_FragCoord.w;"," #endif"," float color = 1.0 - smoothstep( mNear, mFar, depth );"," gl_FragColor = vec4( vec3( color ), opacity );","}"].join("\n")},normal:{uniforms:{opacity:{type:"f",value:1}},vertexShader:["varying vec3 vNormal;",THREE.ShaderChunk.common,THREE.ShaderChunk.morphtarget_pars_vertex,THREE.ShaderChunk.logdepthbuf_pars_vertex,"void main() {"," vNormal = normalize( normalMatrix * normal );",THREE.ShaderChunk.morphtarget_vertex,THREE.ShaderChunk.default_vertex,THREE.ShaderChunk.logdepthbuf_vertex,"}"].join("\n"),fragmentShader:["uniform float opacity;","varying vec3 vNormal;",THREE.ShaderChunk.common,THREE.ShaderChunk.logdepthbuf_pars_fragment,"void main() {"," gl_FragColor = vec4( 0.5 * normalize( vNormal ) + 0.5, opacity );",THREE.ShaderChunk.logdepthbuf_fragment,"}"].join("\n")},cube:{uniforms:{tCube:{type:"t",value:null},tFlip:{type:"f",value:-1}},vertexShader:["varying vec3 vWorldPosition;",THREE.ShaderChunk.common,THREE.ShaderChunk.logdepthbuf_pars_vertex,"void main() {"," vWorldPosition = transformDirection( position, modelMatrix );"," gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",THREE.ShaderChunk.logdepthbuf_vertex,"}"].join("\n"),fragmentShader:["uniform samplerCube tCube;","uniform float tFlip;","varying vec3 vWorldPosition;",THREE.ShaderChunk.common,THREE.ShaderChunk.logdepthbuf_pars_fragment,"void main() {"," gl_FragColor = textureCube( tCube, vec3( tFlip * vWorldPosition.x, vWorldPosition.yz ) );",THREE.ShaderChunk.logdepthbuf_fragment,"}"].join("\n")},equirect:{uniforms:{tEquirect:{type:"t",value:null},tFlip:{type:"f",value:-1}},vertexShader:["varying vec3 vWorldPosition;",THREE.ShaderChunk.common,THREE.ShaderChunk.logdepthbuf_pars_vertex,"void main() {"," vWorldPosition = transformDirection( position, modelMatrix );"," gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",THREE.ShaderChunk.logdepthbuf_vertex,"}"].join("\n"),fragmentShader:["uniform sampler2D tEquirect;","uniform float tFlip;","varying vec3 vWorldPosition;",THREE.ShaderChunk.common,THREE.ShaderChunk.logdepthbuf_pars_fragment,"void main() {","vec3 direction = normalize( vWorldPosition );","vec2 sampleUV;","sampleUV.y = saturate( tFlip * direction.y * -0.5 + 0.5 );","sampleUV.x = atan( direction.z, direction.x ) * RECIPROCAL_PI2 + 0.5;","gl_FragColor = texture2D( tEquirect, sampleUV );",THREE.ShaderChunk.logdepthbuf_fragment,"}"].join("\n")},depthRGBA:{uniforms:{},vertexShader:[THREE.ShaderChunk.common,THREE.ShaderChunk.morphtarget_pars_vertex,THREE.ShaderChunk.skinning_pars_vertex,THREE.ShaderChunk.logdepthbuf_pars_vertex,"void main() {",THREE.ShaderChunk.skinbase_vertex,THREE.ShaderChunk.morphtarget_vertex,THREE.ShaderChunk.skinning_vertex,THREE.ShaderChunk.default_vertex,THREE.ShaderChunk.logdepthbuf_vertex,"}"].join("\n"),fragmentShader:[THREE.ShaderChunk.common,THREE.ShaderChunk.logdepthbuf_pars_fragment,"vec4 pack_depth( const in float depth ) {"," const vec4 bit_shift = vec4( 256.0 * 256.0 * 256.0, 256.0 * 256.0, 256.0, 1.0 );"," const vec4 bit_mask = vec4( 0.0, 1.0 / 256.0, 1.0 / 256.0, 1.0 / 256.0 );"," vec4 res = mod( depth * bit_shift * vec4( 255 ), vec4( 256 ) ) / vec4( 255 );"," res -= res.xxyz * bit_mask;"," return res;","}","void main() {",THREE.ShaderChunk.logdepthbuf_fragment," #ifdef USE_LOGDEPTHBUF_EXT"," gl_FragData[ 0 ] = pack_depth( gl_FragDepthEXT );"," #else"," gl_FragData[ 0 ] = pack_depth( gl_FragCoord.z );"," #endif","}"].join("\n")}},THREE.WebGLRenderer=function(t){function e(t){t.__webglVertexBuffer=At.createBuffer(),t.__webglColorBuffer=At.createBuffer(),Ct.info.memory.geometries++}function n(t){t.__webglVertexBuffer=At.createBuffer(),t.__webglColorBuffer=At.createBuffer(),t.__webglLineDistanceBuffer=At.createBuffer(),Ct.info.memory.geometries++}function r(t){t.__webglVertexBuffer=At.createBuffer(),t.__webglNormalBuffer=At.createBuffer(),t.__webglTangentBuffer=At.createBuffer(),t.__webglColorBuffer=At.createBuffer(),t.__webglUVBuffer=At.createBuffer(),t.__webglUV2Buffer=At.createBuffer(),t.__webglSkinIndicesBuffer=At.createBuffer(),t.__webglSkinWeightsBuffer=At.createBuffer(),t.__webglFaceBuffer=At.createBuffer(),t.__webglLineBuffer=At.createBuffer();var e=t.numMorphTargets;if(e){t.__webglMorphTargetsBuffers=[];for(var n=0,r=e;r>n;n++)t.__webglMorphTargetsBuffers.push(At.createBuffer())}var i=t.numMorphNormals;if(i){t.__webglMorphNormalsBuffers=[];for(var n=0,r=i;r>n;n++)t.__webglMorphNormalsBuffers.push(At.createBuffer())}Ct.info.memory.geometries++}function i(t){var e=t.geometry,n=t.material,r=e.vertices.length;if(n.attributes){void 0===e.__webglCustomAttributesList&&(e.__webglCustomAttributesList=[]);for(var i in n.attributes){var o=n.attributes[i];if(!o.__webglInitialized||o.createUniqueBuffers){o.__webglInitialized=!0;var s=1;"v2"===o.type?s=2:"v3"===o.type?s=3:"v4"===o.type?s=4:"c"===o.type&&(s=3),o.size=s,o.array=new Float32Array(r*s),o.buffer=At.createBuffer(),o.buffer.belongsToAttribute=i,o.needsUpdate=!0}e.__webglCustomAttributesList.push(o)}}}function o(t,e){var n=t.vertices.length;t.__vertexArray=new Float32Array(3*n),t.__colorArray=new Float32Array(3*n),t.__webglParticleCount=n,i(e)}function s(t,e){var n=t.vertices.length;t.__vertexArray=new Float32Array(3*n),t.__colorArray=new Float32Array(3*n),t.__lineDistanceArray=new Float32Array(1*n),t.__webglLineCount=n,i(e)}function a(t,e){var n=e.geometry,r=t.faces3,i=3*r.length,o=1*r.length,s=3*r.length,a=u(e,t);t.__vertexArray=new Float32Array(3*i),t.__normalArray=new Float32Array(3*i),t.__colorArray=new Float32Array(3*i),t.__uvArray=new Float32Array(2*i),n.faceVertexUvs.length>1&&(t.__uv2Array=new Float32Array(2*i)),n.hasTangents&&(t.__tangentArray=new Float32Array(4*i)),e.geometry.skinWeights.length&&e.geometry.skinIndices.length&&(t.__skinIndexArray=new Float32Array(4*i),t.__skinWeightArray=new Float32Array(4*i));var h=null!==te.get("OES_element_index_uint")&&o>21845?Uint32Array:Uint16Array;t.__typeArray=h,t.__faceArray=new h(3*o),t.__lineArray=new h(2*s);var l=t.numMorphTargets;if(l){t.__morphTargetsArrays=[];for(var c=0,p=l;p>c;c++)t.__morphTargetsArrays.push(new Float32Array(3*i))}var f=t.numMorphNormals;if(f){t.__morphNormalsArrays=[];for(var c=0,p=f;p>c;c++)t.__morphNormalsArrays.push(new Float32Array(3*i))}if(t.__webglFaceCount=3*o,t.__webglLineCount=2*s,a.attributes){void 0===t.__webglCustomAttributesList&&(t.__webglCustomAttributesList=[]);for(var d in a.attributes){var m=a.attributes[d],v={};for(var g in m)v[g]=m[g];if(!v.__webglInitialized||v.createUniqueBuffers){v.__webglInitialized=!0;var E=1;"v2"===v.type?E=2:"v3"===v.type?E=3:"v4"===v.type?E=4:"c"===v.type&&(E=3),v.size=E,v.array=new Float32Array(i*E),v.buffer=At.createBuffer(),v.buffer.belongsToAttribute=d,m.needsUpdate=!0,v.__original=m}t.__webglCustomAttributesList.push(v)}}t.__inittedArrays=!0}function u(t,e){return t.material instanceof THREE.MeshFaceMaterial?t.material.materials[e.materialIndex]:t.material}function h(t){return t instanceof THREE.MeshPhongMaterial==!1&&t.shading===THREE.FlatShading}function l(t,e,n){var r,i,o,s,a,u,h,l,c,p,f,d=t.vertices,m=d.length,v=t.colors,g=v.length,E=t.__vertexArray,y=t.__colorArray,_=t.verticesNeedUpdate,b=t.colorsNeedUpdate,T=t.__webglCustomAttributesList;if(_){for(r=0;m>r;r++)o=d[r],s=3*r,E[s]=o.x,E[s+1]=o.y,E[s+2]=o.z;At.bindBuffer(At.ARRAY_BUFFER,t.__webglVertexBuffer),At.bufferData(At.ARRAY_BUFFER,E,e)}if(b){for(i=0;g>i;i++)a=v[i],s=3*i,y[s]=a.r,y[s+1]=a.g,y[s+2]=a.b;At.bindBuffer(At.ARRAY_BUFFER,t.__webglColorBuffer),At.bufferData(At.ARRAY_BUFFER,y,e)}if(T)for(u=0,h=T.length;h>u;u++){if(f=T[u],f.needsUpdate&&(void 0===f.boundTo||"vertices"===f.boundTo))if(c=f.value.length,s=0,1===f.size)for(l=0;c>l;l++)f.array[l]=f.value[l];else if(2===f.size)for(l=0;c>l;l++)p=f.value[l],f.array[s]=p.x,f.array[s+1]=p.y,s+=2;else if(3===f.size)if("c"===f.type)for(l=0;c>l;l++)p=f.value[l],f.array[s]=p.r,f.array[s+1]=p.g,f.array[s+2]=p.b,s+=3;else for(l=0;c>l;l++)p=f.value[l],f.array[s]=p.x,f.array[s+1]=p.y,f.array[s+2]=p.z,s+=3;else if(4===f.size)for(l=0;c>l;l++)p=f.value[l],f.array[s]=p.x,f.array[s+1]=p.y,f.array[s+2]=p.z,f.array[s+3]=p.w,s+=4;At.bindBuffer(At.ARRAY_BUFFER,f.buffer),At.bufferData(At.ARRAY_BUFFER,f.array,e),f.needsUpdate=!1}}function c(t,e){var n,r,i,o,s,a,u,h,l,c,p,f,d=t.vertices,m=t.colors,v=t.lineDistances,g=d.length,E=m.length,y=v.length,_=t.__vertexArray,b=t.__colorArray,T=t.__lineDistanceArray,x=t.verticesNeedUpdate,w=t.colorsNeedUpdate,R=t.lineDistancesNeedUpdate,H=t.__webglCustomAttributesList;if(x){for(n=0;g>n;n++)o=d[n],s=3*n,_[s]=o.x,_[s+1]=o.y,_[s+2]=o.z;At.bindBuffer(At.ARRAY_BUFFER,t.__webglVertexBuffer),At.bufferData(At.ARRAY_BUFFER,_,e)}if(w){for(r=0;E>r;r++)a=m[r],s=3*r,b[s]=a.r,b[s+1]=a.g,b[s+2]=a.b;At.bindBuffer(At.ARRAY_BUFFER,t.__webglColorBuffer),At.bufferData(At.ARRAY_BUFFER,b,e)}if(R){for(i=0;y>i;i++)T[i]=v[i];At.bindBuffer(At.ARRAY_BUFFER,t.__webglLineDistanceBuffer),At.bufferData(At.ARRAY_BUFFER,T,e)}if(H)for(u=0,h=H.length;h>u;u++)if(f=H[u],f.needsUpdate&&(void 0===f.boundTo||"vertices"===f.boundTo)){if(s=0,c=f.value.length,1===f.size)for(l=0;c>l;l++)f.array[l]=f.value[l];else if(2===f.size)for(l=0;c>l;l++)p=f.value[l],f.array[s]=p.x,f.array[s+1]=p.y,s+=2;else if(3===f.size)if("c"===f.type)for(l=0;c>l;l++)p=f.value[l],f.array[s]=p.r,f.array[s+1]=p.g,f.array[s+2]=p.b,s+=3;else for(l=0;c>l;l++)p=f.value[l],f.array[s]=p.x,f.array[s+1]=p.y,f.array[s+2]=p.z,s+=3;else if(4===f.size)for(l=0;c>l;l++)p=f.value[l],f.array[s]=p.x,f.array[s+1]=p.y,f.array[s+2]=p.z,f.array[s+3]=p.w,s+=4;At.bindBuffer(At.ARRAY_BUFFER,f.buffer),At.bufferData(At.ARRAY_BUFFER,f.array,e),f.needsUpdate=!1}}function p(t,e,n,r,i){if(t.__inittedArrays){var o,s,a,u,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S,k,A,C,P,L,z,O,D,F,U,B,N,V,I,j,G,W,q,X,Y=h(i),Z=0,K=0,Q=0,J=0,$=0,tt=0,et=0,nt=0,rt=0,it=0,ot=0,st=0,at=t.__vertexArray,ut=t.__uvArray,ht=t.__uv2Array,lt=t.__normalArray,ct=t.__tangentArray,pt=t.__colorArray,ft=t.__skinIndexArray,dt=t.__skinWeightArray,mt=t.__morphTargetsArrays,vt=t.__morphNormalsArrays,gt=t.__webglCustomAttributesList,Et=t.__faceArray,yt=t.__lineArray,_t=e.geometry,bt=_t.verticesNeedUpdate,Tt=_t.elementsNeedUpdate,xt=_t.uvsNeedUpdate,wt=_t.normalsNeedUpdate,Rt=_t.tangentsNeedUpdate,Ht=_t.colorsNeedUpdate,Mt=_t.morphTargetsNeedUpdate,St=_t.vertices,kt=t.faces3,Ct=_t.faces,Pt=_t.faceVertexUvs[0],Lt=_t.faceVertexUvs[1],zt=_t.skinIndices,Ot=_t.skinWeights,Dt=_t.morphTargets,Ft=_t.morphNormals;if(bt){for(o=0,s=kt.length;s>o;o++)u=Ct[kt[o]],g=St[u.a],E=St[u.b],y=St[u.c],at[K]=g.x,at[K+1]=g.y,at[K+2]=g.z,at[K+3]=E.x,at[K+4]=E.y,at[K+5]=E.z,at[K+6]=y.x,at[K+7]=y.y,at[K+8]=y.z,K+=9;At.bindBuffer(At.ARRAY_BUFFER,t.__webglVertexBuffer),At.bufferData(At.ARRAY_BUFFER,at,n)}if(Mt)for(N=0,V=Dt.length;V>N;N++){for(ot=0,o=0,s=kt.length;s>o;o++)G=kt[o],u=Ct[G],g=Dt[N].vertices[u.a],E=Dt[N].vertices[u.b],y=Dt[N].vertices[u.c],I=mt[N],I[ot]=g.x,I[ot+1]=g.y,I[ot+2]=g.z,I[ot+3]=E.x,I[ot+4]=E.y,I[ot+5]=E.z,I[ot+6]=y.x,I[ot+7]=y.y,I[ot+8]=y.z,i.morphNormals&&(Y?(x=Ft[N].faceNormals[G],w=x,R=x):(W=Ft[N].vertexNormals[G],x=W.a,w=W.b,R=W.c),j=vt[N],j[ot]=x.x,j[ot+1]=x.y,j[ot+2]=x.z,j[ot+3]=w.x,j[ot+4]=w.y,j[ot+5]=w.z,j[ot+6]=R.x,j[ot+7]=R.y,j[ot+8]=R.z),ot+=9;At.bindBuffer(At.ARRAY_BUFFER,t.__webglMorphTargetsBuffers[N]),At.bufferData(At.ARRAY_BUFFER,mt[N],n),i.morphNormals&&(At.bindBuffer(At.ARRAY_BUFFER,t.__webglMorphNormalsBuffers[N]),At.bufferData(At.ARRAY_BUFFER,vt[N],n))}if(Ot.length){for(o=0,s=kt.length;s>o;o++)u=Ct[kt[o]],k=Ot[u.a],A=Ot[u.b],C=Ot[u.c],dt[it]=k.x,dt[it+1]=k.y,dt[it+2]=k.z,dt[it+3]=k.w,dt[it+4]=A.x,dt[it+5]=A.y,dt[it+6]=A.z,dt[it+7]=A.w,dt[it+8]=C.x,dt[it+9]=C.y,dt[it+10]=C.z,dt[it+11]=C.w,P=zt[u.a],L=zt[u.b],z=zt[u.c],ft[it]=P.x,ft[it+1]=P.y,ft[it+2]=P.z,ft[it+3]=P.w,ft[it+4]=L.x,ft[it+5]=L.y,ft[it+6]=L.z,ft[it+7]=L.w,ft[it+8]=z.x,ft[it+9]=z.y,ft[it+10]=z.z,ft[it+11]=z.w,it+=12;it>0&&(At.bindBuffer(At.ARRAY_BUFFER,t.__webglSkinIndicesBuffer),At.bufferData(At.ARRAY_BUFFER,ft,n),At.bindBuffer(At.ARRAY_BUFFER,t.__webglSkinWeightsBuffer),At.bufferData(At.ARRAY_BUFFER,dt,n))}if(Ht){for(o=0,s=kt.length;s>o;o++)u=Ct[kt[o]],p=u.vertexColors,f=u.color,3===p.length&&i.vertexColors===THREE.VertexColors?(H=p[0],M=p[1],S=p[2]):(H=f,M=f,S=f),pt[rt]=H.r,pt[rt+1]=H.g,pt[rt+2]=H.b,pt[rt+3]=M.r,pt[rt+4]=M.g,pt[rt+5]=M.b,pt[rt+6]=S.r,pt[rt+7]=S.g,pt[rt+8]=S.b,rt+=9;rt>0&&(At.bindBuffer(At.ARRAY_BUFFER,t.__webglColorBuffer),At.bufferData(At.ARRAY_BUFFER,pt,n))}if(Rt&&_t.hasTangents){for(o=0,s=kt.length;s>o;o++)u=Ct[kt[o]],d=u.vertexTangents,_=d[0],b=d[1],T=d[2],ct[et]=_.x,ct[et+1]=_.y,ct[et+2]=_.z,ct[et+3]=_.w,ct[et+4]=b.x,ct[et+5]=b.y,ct[et+6]=b.z,ct[et+7]=b.w,ct[et+8]=T.x,ct[et+9]=T.y,ct[et+10]=T.z,ct[et+11]=T.w,et+=12;At.bindBuffer(At.ARRAY_BUFFER,t.__webglTangentBuffer),At.bufferData(At.ARRAY_BUFFER,ct,n)}if(wt){for(o=0,s=kt.length;s>o;o++)if(u=Ct[kt[o]],l=u.vertexNormals,c=u.normal,3===l.length&&Y===!1)for(O=0;3>O;O++)F=l[O],lt[tt]=F.x,lt[tt+1]=F.y,lt[tt+2]=F.z,tt+=3;else for(O=0;3>O;O++)lt[tt]=c.x,lt[tt+1]=c.y,lt[tt+2]=c.z,tt+=3;At.bindBuffer(At.ARRAY_BUFFER,t.__webglNormalBuffer),At.bufferData(At.ARRAY_BUFFER,lt,n)}if(xt&&Pt){for(o=0,s=kt.length;s>o;o++)if(a=kt[o],m=Pt[a],void 0!==m)for(O=0;3>O;O++)U=m[O],ut[Q]=U.x,ut[Q+1]=U.y,Q+=2;Q>0&&(At.bindBuffer(At.ARRAY_BUFFER,t.__webglUVBuffer),At.bufferData(At.ARRAY_BUFFER,ut,n))}if(xt&&Lt){for(o=0,s=kt.length;s>o;o++)if(a=kt[o],v=Lt[a],void 0!==v)for(O=0;3>O;O++)B=v[O],ht[J]=B.x,ht[J+1]=B.y,J+=2;J>0&&(At.bindBuffer(At.ARRAY_BUFFER,t.__webglUV2Buffer),At.bufferData(At.ARRAY_BUFFER,ht,n))}if(Tt){for(o=0,s=kt.length;s>o;o++)Et[$]=Z,Et[$+1]=Z+1,Et[$+2]=Z+2,$+=3,yt[nt]=Z,yt[nt+1]=Z+1,yt[nt+2]=Z,yt[nt+3]=Z+2,yt[nt+4]=Z+1,yt[nt+5]=Z+2,nt+=6,Z+=3;At.bindBuffer(At.ELEMENT_ARRAY_BUFFER,t.__webglFaceBuffer),At.bufferData(At.ELEMENT_ARRAY_BUFFER,Et,n),At.bindBuffer(At.ELEMENT_ARRAY_BUFFER,t.__webglLineBuffer),At.bufferData(At.ELEMENT_ARRAY_BUFFER,yt,n)}if(gt)for(O=0,D=gt.length;D>O;O++)if(X=gt[O],X.__original.needsUpdate){if(st=0,1===X.size){if(void 0===X.boundTo||"vertices"===X.boundTo)for(o=0,s=kt.length;s>o;o++)u=Ct[kt[o]],X.array[st]=X.value[u.a],X.array[st+1]=X.value[u.b],X.array[st+2]=X.value[u.c],st+=3;else if("faces"===X.boundTo)for(o=0,s=kt.length;s>o;o++)q=X.value[kt[o]],X.array[st]=q,X.array[st+1]=q,X.array[st+2]=q,st+=3}else if(2===X.size){if(void 0===X.boundTo||"vertices"===X.boundTo)for(o=0,s=kt.length;s>o;o++)u=Ct[kt[o]],g=X.value[u.a],E=X.value[u.b],y=X.value[u.c],X.array[st]=g.x,X.array[st+1]=g.y,X.array[st+2]=E.x,X.array[st+3]=E.y,X.array[st+4]=y.x,X.array[st+5]=y.y,st+=6;else if("faces"===X.boundTo)for(o=0,s=kt.length;s>o;o++)q=X.value[kt[o]],g=q,E=q,y=q,X.array[st]=g.x,X.array[st+1]=g.y,X.array[st+2]=E.x,X.array[st+3]=E.y,X.array[st+4]=y.x,X.array[st+5]=y.y,st+=6}else if(3===X.size){var Ut;if(Ut="c"===X.type?["r","g","b"]:["x","y","z"],void 0===X.boundTo||"vertices"===X.boundTo)for(o=0,s=kt.length;s>o;o++)u=Ct[kt[o]],g=X.value[u.a],E=X.value[u.b],y=X.value[u.c],X.array[st]=g[Ut[0]],X.array[st+1]=g[Ut[1]],X.array[st+2]=g[Ut[2]],X.array[st+3]=E[Ut[0]],X.array[st+4]=E[Ut[1]],X.array[st+5]=E[Ut[2]],X.array[st+6]=y[Ut[0]],X.array[st+7]=y[Ut[1]],X.array[st+8]=y[Ut[2]],st+=9;else if("faces"===X.boundTo)for(o=0,s=kt.length;s>o;o++)q=X.value[kt[o]],g=q,E=q,y=q,X.array[st]=g[Ut[0]],X.array[st+1]=g[Ut[1]],X.array[st+2]=g[Ut[2]],X.array[st+3]=E[Ut[0]],X.array[st+4]=E[Ut[1]],X.array[st+5]=E[Ut[2]],X.array[st+6]=y[Ut[0]],X.array[st+7]=y[Ut[1]],X.array[st+8]=y[Ut[2]],st+=9;else if("faceVertices"===X.boundTo)for(o=0,s=kt.length;s>o;o++)q=X.value[kt[o]],g=q[0],E=q[1],y=q[2],X.array[st]=g[Ut[0]],X.array[st+1]=g[Ut[1]],X.array[st+2]=g[Ut[2]],X.array[st+3]=E[Ut[0]],X.array[st+4]=E[Ut[1]],X.array[st+5]=E[Ut[2]],X.array[st+6]=y[Ut[0]],X.array[st+7]=y[Ut[1]],X.array[st+8]=y[Ut[2]],st+=9}else if(4===X.size)if(void 0===X.boundTo||"vertices"===X.boundTo)for(o=0,s=kt.length;s>o;o++)u=Ct[kt[o]],g=X.value[u.a],E=X.value[u.b],y=X.value[u.c],X.array[st]=g.x,X.array[st+1]=g.y,X.array[st+2]=g.z,X.array[st+3]=g.w,X.array[st+4]=E.x,X.array[st+5]=E.y,X.array[st+6]=E.z,X.array[st+7]=E.w,X.array[st+8]=y.x,X.array[st+9]=y.y,X.array[st+10]=y.z,X.array[st+11]=y.w,st+=12;else if("faces"===X.boundTo)for(o=0,s=kt.length;s>o;o++)q=X.value[kt[o]],g=q,E=q,y=q,X.array[st]=g.x,X.array[st+1]=g.y,X.array[st+2]=g.z,X.array[st+3]=g.w,X.array[st+4]=E.x,X.array[st+5]=E.y,X.array[st+6]=E.z,X.array[st+7]=E.w,X.array[st+8]=y.x,X.array[st+9]=y.y,X.array[st+10]=y.z,X.array[st+11]=y.w,st+=12;else if("faceVertices"===X.boundTo)for(o=0,s=kt.length;s>o;o++)q=X.value[kt[o]],g=q[0],E=q[1],y=q[2],X.array[st]=g.x,X.array[st+1]=g.y,X.array[st+2]=g.z,X.array[st+3]=g.w,X.array[st+4]=E.x,X.array[st+5]=E.y,X.array[st+6]=E.z,X.array[st+7]=E.w,X.array[st+8]=y.x,X.array[st+9]=y.y,X.array[st+10]=y.z,X.array[st+11]=y.w,st+=12;At.bindBuffer(At.ARRAY_BUFFER,X.buffer),At.bufferData(At.ARRAY_BUFFER,X.array,n)}r&&(delete t.__inittedArrays,delete t.__colorArray,delete t.__normalArray,delete t.__tangentArray,delete t.__uvArray,delete t.__uv2Array,delete t.__faceArray,delete t.__vertexArray,delete t.__lineArray,delete t.__skinIndexArray,delete t.__skinWeightArray)}}function f(t,e,n,r){for(var i=n.attributes,o=e.attributes,s=e.attributesKeys,a=0,u=s.length;u>a;a++){var h=s[a],l=o[h];if(l>=0){var c=i[h];if(void 0!==c){var p=c.itemSize;At.bindBuffer(At.ARRAY_BUFFER,c.buffer),$t.enableAttribute(l),At.vertexAttribPointer(l,p,At.FLOAT,!1,0,r*p*4)}else void 0!==t.defaultAttributeValues&&(2===t.defaultAttributeValues[h].length?At.vertexAttrib2fv(l,t.defaultAttributeValues[h]):3===t.defaultAttributeValues[h].length&&At.vertexAttrib3fv(l,t.defaultAttributeValues[h]))}}$t.disableUnusedAttributes()}function d(t,e,n){var r=t.program.attributes;if(-1!==n.morphTargetBase&&r.position>=0?(At.bindBuffer(At.ARRAY_BUFFER,e.__webglMorphTargetsBuffers[n.morphTargetBase]),$t.enableAttribute(r.position),At.vertexAttribPointer(r.position,3,At.FLOAT,!1,0,0)):r.position>=0&&(At.bindBuffer(At.ARRAY_BUFFER,e.__webglVertexBuffer),$t.enableAttribute(r.position),At.vertexAttribPointer(r.position,3,At.FLOAT,!1,0,0)),n.morphTargetForcedOrder.length)for(var i,o=0,s=n.morphTargetForcedOrder,a=n.morphTargetInfluences;o=0&&(At.bindBuffer(At.ARRAY_BUFFER,e.__webglMorphTargetsBuffers[s[o]]),$t.enableAttribute(i),At.vertexAttribPointer(i,3,At.FLOAT,!1,0,0)),i=r["morphNormal"+o],i>=0&&t.morphNormals&&(At.bindBuffer(At.ARRAY_BUFFER,e.__webglMorphNormalsBuffers[s[o]]),$t.enableAttribute(i),At.vertexAttribPointer(i,3,At.FLOAT,!1,0,0)),n.__webglMorphTargetInfluences[o]=a[s[o]],o++;else{var u=[],a=n.morphTargetInfluences,h=n.geometry.morphTargets;a.length>h.length&&(console.warn("THREE.WebGLRenderer: Influences array is bigger than morphTargets array."),a.length=h.length);for(var l=0,c=a.length;c>l;l++){var p=a[l];u.push([p,l])}u.length>t.numSupportedMorphTargets?(u.sort(g),u.length=t.numSupportedMorphTargets):u.length>t.numSupportedMorphNormals?u.sort(g):0===u.length&&u.push([0,0]);for(var i,o=0,f=t.numSupportedMorphTargets;f>o;o++)if(u[o]){var d=u[o][1];i=r["morphTarget"+o],i>=0&&(At.bindBuffer(At.ARRAY_BUFFER,e.__webglMorphTargetsBuffers[d]),$t.enableAttribute(i),At.vertexAttribPointer(i,3,At.FLOAT,!1,0,0)),i=r["morphNormal"+o],i>=0&&t.morphNormals&&(At.bindBuffer(At.ARRAY_BUFFER,e.__webglMorphNormalsBuffers[d]),$t.enableAttribute(i),At.vertexAttribPointer(i,3,At.FLOAT,!1,0,0)),n.__webglMorphTargetInfluences[o]=a[d]}else n.__webglMorphTargetInfluences[o]=0}null!==t.program.uniforms.morphTargetInfluences&&At.uniform1fv(t.program.uniforms.morphTargetInfluences,n.__webglMorphTargetInfluences)}function m(t,e){return t.object.renderOrder!==e.object.renderOrder?t.object.renderOrder-e.object.renderOrder:t.material.id!==e.material.id?t.material.id-e.material.id:t.z!==e.z?t.z-e.z:t.id-e.id}function v(t,e){return t.object.renderOrder!==e.object.renderOrder?t.object.renderOrder-e.object.renderOrder:t.z!==e.z?e.z-t.z:t.id-e.id}function g(t,e){return e[0]-t[0]}function E(t){if(t.visible!==!1){if(t instanceof THREE.Scene||t instanceof THREE.Group);else if(x(t),t instanceof THREE.Light)xt.push(t);else if(t instanceof THREE.Sprite)St.push(t);else if(t instanceof THREE.LensFlare)kt.push(t);else{var e=wt[t.id];if(e&&(t.frustumCulled===!1||Wt.intersectsObject(t)===!0))for(var n=0,r=e.length;r>n;n++){var i=e[n];T(i),i.render=!0,Ct.sortObjects===!0&&(Xt.setFromMatrixPosition(t.matrixWorld),Xt.applyProjection(qt),i.z=Xt.z)}}for(var n=0,r=t.children.length;r>n;n++)E(t.children[n])}}function y(t,e,n,r,i){for(var o,s=0,a=t.length;a>s;s++){var u=t[s],h=u.object,l=u.buffer;if(Z(h,e),i)o=i;else{if(o=u.material,!o)continue;z(o)}Ct.setMaterialFaces(o),l instanceof THREE.BufferGeometry?Ct.renderBufferDirect(e,n,r,o,l,h):Ct.renderBuffer(e,n,r,o,l,h)}}function _(t,e,n,r,i,o){for(var s,a=0,u=t.length;u>a;a++){var h=t[a],l=h.object;if(l.visible){if(o)s=o;else{if(s=h[e],!s)continue;z(s)}Ct.renderImmediateObject(n,r,i,s,l)}}}function b(t){var e=t.object,n=e.material;n.transparent?(t.transparent=n,t.opaque=null):(t.opaque=n,t.transparent=null)}function T(t){var e=t.object,n=t.buffer,r=e.geometry,i=e.material;if(i instanceof THREE.MeshFaceMaterial){var o=r instanceof THREE.BufferGeometry?0:n.materialIndex;i=i.materials[o],t.material=i,i.transparent?Mt.push(t):Ht.push(t)}else i&&(t.material=i,i.transparent?Mt.push(t):Ht.push(t))}function x(t){void 0===t.__webglInit&&(t.__webglInit=!0,t._modelViewMatrix=new THREE.Matrix4,t._normalMatrix=new THREE.Matrix3,t.addEventListener("removed",_e));var r=t.geometry;if(void 0===r||void 0===r.__webglInit&&(r.__webglInit=!0,r.addEventListener("dispose",be),r instanceof THREE.BufferGeometry?Ct.info.memory.geometries++:t instanceof THREE.Mesh?R(t,r):t instanceof THREE.Line?void 0===r.__webglVertexBuffer&&(n(r),s(r,t),r.verticesNeedUpdate=!0,r.colorsNeedUpdate=!0,r.lineDistancesNeedUpdate=!0):t instanceof THREE.PointCloud&&void 0===r.__webglVertexBuffer&&(e(r),o(r,t),r.verticesNeedUpdate=!0,r.colorsNeedUpdate=!0)),void 0===t.__webglActive)if(t.__webglActive=!0,t instanceof THREE.Mesh){if(r instanceof THREE.BufferGeometry)H(wt,r,t);else if(r instanceof THREE.Geometry)for(var i=Ae[r.id],a=0,u=i.length;u>a;a++)H(wt,i[a],t)}else t instanceof THREE.Line||t instanceof THREE.PointCloud?H(wt,r,t):(t instanceof THREE.ImmediateRenderObject||t.immediateRenderCallback)&&M(Rt,t)}function w(t,e){for(var n,r,i=te.get("OES_element_index_uint")?4294967296:65535,o={},s=t.morphTargets.length,a=t.morphNormals.length,u={},h=[],l=0,c=t.faces.length;c>l;l++){var p=t.faces[l],f=e?p.materialIndex:0;f in o||(o[f]={hash:f,counter:0}),n=o[f].hash+"_"+o[f].counter,n in u||(r={id:Ce++,faces3:[],materialIndex:f,vertices:0,numMorphTargets:s,numMorphNormals:a},u[n]=r,h.push(r)),u[n].vertices+3>i&&(o[f].counter+=1,n=o[f].hash+"_"+o[f].counter,n in u||(r={id:Ce++,faces3:[],materialIndex:f,vertices:0,numMorphTargets:s,numMorphNormals:a},u[n]=r,h.push(r))),u[n].faces3.push(l),u[n].vertices+=3}return h}function R(t,e){var n=t.material,i=!1;(void 0===Ae[e.id]||e.groupsNeedUpdate===!0)&&(delete wt[t.id],Ae[e.id]=w(e,n instanceof THREE.MeshFaceMaterial),e.groupsNeedUpdate=!1);for(var o=Ae[e.id],s=0,u=o.length;u>s;s++){var h=o[s];void 0===h.__webglVertexBuffer?(r(h),a(h,t),e.verticesNeedUpdate=!0,e.morphTargetsNeedUpdate=!0,e.elementsNeedUpdate=!0,e.uvsNeedUpdate=!0,e.normalsNeedUpdate=!0,e.tangentsNeedUpdate=!0,e.colorsNeedUpdate=!0,i=!0):i=!1,(i||void 0===t.__webglActive)&&H(wt,h,t)}t.__webglActive=!0}function H(t,e,n){var r=n.id;t[r]=t[r]||[],t[r].push({id:r,buffer:e,object:n,material:null,z:0})}function M(t,e){t.push({id:null,object:e,opaque:null,transparent:null,z:0})}function S(t){var e=t.geometry;if(e instanceof THREE.BufferGeometry)for(var n=e.attributes,r=e.attributesKeys,i=0,o=r.length;o>i;i++){var s=r[i],a=n[s],h="index"===s?At.ELEMENT_ARRAY_BUFFER:At.ARRAY_BUFFER;void 0===a.buffer?(a.buffer=At.createBuffer(),At.bindBuffer(h,a.buffer),At.bufferData(h,a.array,a instanceof THREE.DynamicBufferAttribute?At.DYNAMIC_DRAW:At.STATIC_DRAW),a.needsUpdate=!1):a.needsUpdate===!0&&(At.bindBuffer(h,a.buffer),void 0===a.updateRange||-1===a.updateRange.count?At.bufferSubData(h,0,a.array):0===a.updateRange.count?console.error("THREE.WebGLRenderer.updateObject: using updateRange for THREE.DynamicBufferAttribute and marked as needsUpdate but count is 0, ensure you are using set methods or updating manually."):(At.bufferSubData(h,a.updateRange.offset*a.array.BYTES_PER_ELEMENT,a.array.subarray(a.updateRange.offset,a.updateRange.offset+a.updateRange.count)),a.updateRange.count=0),a.needsUpdate=!1)}else if(t instanceof THREE.Mesh){e.groupsNeedUpdate===!0&&R(t,e);for(var f=Ae[e.id],i=0,d=f.length;d>i;i++){var m=f[i],v=u(t,m),g=v.attributes&&k(v);(e.verticesNeedUpdate||e.morphTargetsNeedUpdate||e.elementsNeedUpdate||e.uvsNeedUpdate||e.normalsNeedUpdate||e.colorsNeedUpdate||e.tangentsNeedUpdate||g)&&p(m,t,At.DYNAMIC_DRAW,!e.dynamic,v)}e.verticesNeedUpdate=!1,e.morphTargetsNeedUpdate=!1,e.elementsNeedUpdate=!1,e.uvsNeedUpdate=!1,e.normalsNeedUpdate=!1,e.colorsNeedUpdate=!1,e.tangentsNeedUpdate=!1,v.attributes&&A(v)}else if(t instanceof THREE.Line){var v=u(t,e),g=v.attributes&&k(v);(e.verticesNeedUpdate||e.colorsNeedUpdate||e.lineDistancesNeedUpdate||g)&&c(e,At.DYNAMIC_DRAW),e.verticesNeedUpdate=!1,e.colorsNeedUpdate=!1,e.lineDistancesNeedUpdate=!1,v.attributes&&A(v)}else if(t instanceof THREE.PointCloud){var v=u(t,e),g=v.attributes&&k(v);(e.verticesNeedUpdate||e.colorsNeedUpdate||g)&&l(e,At.DYNAMIC_DRAW,t),e.verticesNeedUpdate=!1,e.colorsNeedUpdate=!1,v.attributes&&A(v)}}function k(t){for(var e in t.attributes)if(t.attributes[e].needsUpdate)return!0;return!1}function A(t){for(var e in t.attributes)t.attributes[e].needsUpdate=!1}function C(t){t instanceof THREE.Mesh||t instanceof THREE.PointCloud||t instanceof THREE.Line?delete wt[t.id]:(t instanceof THREE.ImmediateRenderObject||t.immediateRenderCallback)&&P(Rt,t),delete t.__webglInit,delete t._modelViewMatrix,delete t._normalMatrix,delete t.__webglActive}function P(t,e){for(var n=t.length-1;n>=0;n--)t[n].object===e&&t.splice(n,1)}function L(t,e,n,r){t.addEventListener("dispose",we);var i=Pe[t.type];if(i){var o=THREE.ShaderLib[i];t.__webglShader={uniforms:THREE.UniformsUtils.clone(o.uniforms),vertexShader:o.vertexShader,fragmentShader:o.fragmentShader}}else t.__webglShader={uniforms:t.uniforms,vertexShader:t.vertexShader,fragmentShader:t.fragmentShader};var s=ut(e),a=ht(e),u=at(r),h={precision:ft,supportsVertexTextures:ue,map:!!t.map,envMap:!!t.envMap,envMapMode:t.envMap&&t.envMap.mapping,lightMap:!!t.lightMap,bumpMap:!!t.bumpMap,normalMap:!!t.normalMap,specularMap:!!t.specularMap,alphaMap:!!t.alphaMap,combine:t.combine,vertexColors:t.vertexColors,fog:n,useFog:t.fog,fogExp:n instanceof THREE.FogExp2,flatShading:t.shading===THREE.FlatShading,sizeAttenuation:t.sizeAttenuation,logarithmicDepthBuffer:_t,skinning:t.skinning,maxBones:u,useVertexTexture:he&&r&&r.skeleton&&r.skeleton.useVertexTexture,morphTargets:t.morphTargets,morphNormals:t.morphNormals,maxMorphTargets:Ct.maxMorphTargets,maxMorphNormals:Ct.maxMorphNormals,maxDirLights:s.directional,maxPointLights:s.point,maxSpotLights:s.spot,maxHemiLights:s.hemi,maxShadows:a,shadowMapEnabled:Ct.shadowMapEnabled&&r.receiveShadow&&a>0,shadowMapType:Ct.shadowMapType,shadowMapDebug:Ct.shadowMapDebug, -shadowMapCascade:Ct.shadowMapCascade,alphaTest:t.alphaTest,metal:t.metal,wrapAround:t.wrapAround,doubleSided:t.side===THREE.DoubleSide,flipSided:t.side===THREE.BackSide},l=[];if(i?l.push(i):(l.push(t.fragmentShader),l.push(t.vertexShader)),void 0!==t.defines)for(var c in t.defines)l.push(c),l.push(t.defines[c]);for(var c in h)l.push(c),l.push(h[c]);for(var p,f=l.join(),d=0,m=Pt.length;m>d;d++){var v=Pt[d];if(v.code===f){p=v,p.usedTimes++;break}}void 0===p&&(p=new THREE.WebGLProgram(Ct,f,t,h),Pt.push(p),Ct.info.memory.programs=Pt.length),t.program=p;var g=p.attributes;if(t.morphTargets){t.numSupportedMorphTargets=0;for(var E,y="morphTarget",_=0;_=0&&t.numSupportedMorphTargets++}if(t.morphNormals){t.numSupportedMorphNormals=0;var E,y="morphNormal";for(_=0;_=0&&t.numSupportedMorphNormals++}t.uniformsList=[];for(var b in t.__webglShader.uniforms){var T=t.program.uniforms[b];T&&t.uniformsList.push([t.__webglShader.uniforms[b],T])}}function z(t){t.transparent===!0?$t.setBlending(t.blending,t.blendEquation,t.blendSrc,t.blendDst,t.blendEquationAlpha,t.blendSrcAlpha,t.blendDstAlpha):$t.setBlending(THREE.NoBlending),$t.setDepthTest(t.depthTest),$t.setDepthWrite(t.depthWrite),$t.setColorWrite(t.colorWrite),$t.setPolygonOffset(t.polygonOffset,t.polygonOffsetFactor,t.polygonOffsetUnits)}function O(t,e,n,r,i){Ut=0,r.needsUpdate&&(r.program&&ke(r),L(r,e,n,i),r.needsUpdate=!1),r.morphTargets&&(i.__webglMorphTargetInfluences||(i.__webglMorphTargetInfluences=new Float32Array(Ct.maxMorphTargets)));var o=!1,s=!1,a=!1,u=r.program,h=u.uniforms,l=r.__webglShader.uniforms;if(u.id!==Lt&&(At.useProgram(u.program),Lt=u.id,o=!0,s=!0,a=!0),r.id!==Ot&&(-1===Ot&&(a=!0),Ot=r.id,s=!0),(o||t!==Ft)&&(At.uniformMatrix4fv(h.projectionMatrix,!1,t.projectionMatrix.elements),_t&&At.uniform1f(h.logDepthBufFC,2/(Math.log(t.far+1)/Math.LN2)),t!==Ft&&(Ft=t),(r instanceof THREE.ShaderMaterial||r instanceof THREE.MeshPhongMaterial||r.envMap)&&null!==h.cameraPosition&&(Xt.setFromMatrixPosition(t.matrixWorld),At.uniform3f(h.cameraPosition,Xt.x,Xt.y,Xt.z)),(r instanceof THREE.MeshPhongMaterial||r instanceof THREE.MeshLambertMaterial||r instanceof THREE.MeshBasicMaterial||r instanceof THREE.ShaderMaterial||r.skinning)&&null!==h.viewMatrix&&At.uniformMatrix4fv(h.viewMatrix,!1,t.matrixWorldInverse.elements)),r.skinning)if(i.bindMatrix&&null!==h.bindMatrix&&At.uniformMatrix4fv(h.bindMatrix,!1,i.bindMatrix.elements),i.bindMatrixInverse&&null!==h.bindMatrixInverse&&At.uniformMatrix4fv(h.bindMatrixInverse,!1,i.bindMatrixInverse.elements),he&&i.skeleton&&i.skeleton.useVertexTexture){if(null!==h.boneTexture){var c=X();At.uniform1i(h.boneTexture,c),Ct.setTexture(i.skeleton.boneTexture,c)}null!==h.boneTextureWidth&&At.uniform1i(h.boneTextureWidth,i.skeleton.boneTextureWidth),null!==h.boneTextureHeight&&At.uniform1i(h.boneTextureHeight,i.skeleton.boneTextureHeight)}else i.skeleton&&i.skeleton.boneMatrices&&null!==h.boneGlobalMatrices&&At.uniformMatrix4fv(h.boneGlobalMatrices,!1,i.skeleton.boneMatrices);return s&&(n&&r.fog&&N(l,n),(r instanceof THREE.MeshPhongMaterial||r instanceof THREE.MeshLambertMaterial||r.lights)&&(Zt&&(a=!0,Q(e),Zt=!1),a?(j(l,Kt),G(l,!0)):G(l,!1)),(r instanceof THREE.MeshBasicMaterial||r instanceof THREE.MeshLambertMaterial||r instanceof THREE.MeshPhongMaterial)&&D(l,r),r instanceof THREE.LineBasicMaterial?F(l,r):r instanceof THREE.LineDashedMaterial?(F(l,r),U(l,r)):r instanceof THREE.PointCloudMaterial?B(l,r):r instanceof THREE.MeshPhongMaterial?V(l,r):r instanceof THREE.MeshLambertMaterial?I(l,r):r instanceof THREE.MeshDepthMaterial?(l.mNear.value=t.near,l.mFar.value=t.far,l.opacity.value=r.opacity):r instanceof THREE.MeshNormalMaterial&&(l.opacity.value=r.opacity),i.receiveShadow&&!r._shadowPass&&W(l,e),Y(r.uniformsList)),q(h,i),null!==h.modelMatrix&&At.uniformMatrix4fv(h.modelMatrix,!1,i.matrixWorld.elements),u}function D(t,e){t.opacity.value=e.opacity,t.diffuse.value=e.color,t.map.value=e.map,t.lightMap.value=e.lightMap,t.specularMap.value=e.specularMap,t.alphaMap.value=e.alphaMap,e.bumpMap&&(t.bumpMap.value=e.bumpMap,t.bumpScale.value=e.bumpScale),e.normalMap&&(t.normalMap.value=e.normalMap,t.normalScale.value.copy(e.normalScale));var n;if(e.map?n=e.map:e.specularMap?n=e.specularMap:e.normalMap?n=e.normalMap:e.bumpMap?n=e.bumpMap:e.alphaMap&&(n=e.alphaMap),void 0!==n){var r=n.offset,i=n.repeat;t.offsetRepeat.value.set(r.x,r.y,i.x,i.y)}t.envMap.value=e.envMap,t.flipEnvMap.value=e.envMap instanceof THREE.WebGLRenderTargetCube?1:-1,t.reflectivity.value=e.reflectivity,t.refractionRatio.value=e.refractionRatio}function F(t,e){t.diffuse.value=e.color,t.opacity.value=e.opacity}function U(t,e){t.dashSize.value=e.dashSize,t.totalSize.value=e.dashSize+e.gapSize,t.scale.value=e.scale}function B(t,e){if(t.psColor.value=e.color,t.opacity.value=e.opacity,t.size.value=e.size,t.scale.value=lt.height/2,t.map.value=e.map,null!==e.map){var n=e.map.offset,r=e.map.repeat;t.offsetRepeat.value.set(n.x,n.y,r.x,r.y)}}function N(t,e){t.fogColor.value=e.color,e instanceof THREE.Fog?(t.fogNear.value=e.near,t.fogFar.value=e.far):e instanceof THREE.FogExp2&&(t.fogDensity.value=e.density)}function V(t,e){t.shininess.value=e.shininess,t.emissive.value=e.emissive,t.specular.value=e.specular,e.wrapAround&&t.wrapRGB.value.copy(e.wrapRGB)}function I(t,e){t.emissive.value=e.emissive,e.wrapAround&&t.wrapRGB.value.copy(e.wrapRGB)}function j(t,e){t.ambientLightColor.value=e.ambient,t.directionalLightColor.value=e.directional.colors,t.directionalLightDirection.value=e.directional.positions,t.pointLightColor.value=e.point.colors,t.pointLightPosition.value=e.point.positions,t.pointLightDistance.value=e.point.distances,t.pointLightDecay.value=e.point.decays,t.spotLightColor.value=e.spot.colors,t.spotLightPosition.value=e.spot.positions,t.spotLightDistance.value=e.spot.distances,t.spotLightDirection.value=e.spot.directions,t.spotLightAngleCos.value=e.spot.anglesCos,t.spotLightExponent.value=e.spot.exponents,t.spotLightDecay.value=e.spot.decays,t.hemisphereLightSkyColor.value=e.hemi.skyColors,t.hemisphereLightGroundColor.value=e.hemi.groundColors,t.hemisphereLightDirection.value=e.hemi.positions}function G(t,e){t.ambientLightColor.needsUpdate=e,t.directionalLightColor.needsUpdate=e,t.directionalLightDirection.needsUpdate=e,t.pointLightColor.needsUpdate=e,t.pointLightPosition.needsUpdate=e,t.pointLightDistance.needsUpdate=e,t.pointLightDecay.needsUpdate=e,t.spotLightColor.needsUpdate=e,t.spotLightPosition.needsUpdate=e,t.spotLightDistance.needsUpdate=e,t.spotLightDirection.needsUpdate=e,t.spotLightAngleCos.needsUpdate=e,t.spotLightExponent.needsUpdate=e,t.spotLightDecay.needsUpdate=e,t.hemisphereLightSkyColor.needsUpdate=e,t.hemisphereLightGroundColor.needsUpdate=e,t.hemisphereLightDirection.needsUpdate=e}function W(t,e){if(t.shadowMatrix)for(var n=0,r=0,i=e.length;i>r;r++){var o=e[r];o.castShadow&&(o instanceof THREE.SpotLight||o instanceof THREE.DirectionalLight&&!o.shadowCascade)&&(t.shadowMap.value[n]=o.shadowMap,t.shadowMapSize.value[n]=o.shadowMapSize,t.shadowMatrix.value[n]=o.shadowMatrix,t.shadowDarkness.value[n]=o.shadowDarkness,t.shadowBias.value[n]=o.shadowBias,n++)}}function q(t,e){At.uniformMatrix4fv(t.modelViewMatrix,!1,e._modelViewMatrix.elements),t.normalMatrix&&At.uniformMatrix3fv(t.normalMatrix,!1,e._normalMatrix.elements)}function X(){var t=Ut;return t>=ie&&THREE.warn("WebGLRenderer: trying to use "+t+" texture units while this GPU supports only "+ie),Ut+=1,t}function Y(t){for(var e,n,r,i=0,o=t.length;o>i;i++){var s=t[i][0];if(s.needsUpdate!==!1){var a=s.type,u=s.value,h=t[i][1];switch(a){case"1i":At.uniform1i(h,u);break;case"1f":At.uniform1f(h,u);break;case"2f":At.uniform2f(h,u[0],u[1]);break;case"3f":At.uniform3f(h,u[0],u[1],u[2]);break;case"4f":At.uniform4f(h,u[0],u[1],u[2],u[3]);break;case"1iv":At.uniform1iv(h,u);break;case"3iv":At.uniform3iv(h,u);break;case"1fv":At.uniform1fv(h,u);break;case"2fv":At.uniform2fv(h,u);break;case"3fv":At.uniform3fv(h,u);break;case"4fv":At.uniform4fv(h,u);break;case"Matrix3fv":At.uniformMatrix3fv(h,!1,u);break;case"Matrix4fv":At.uniformMatrix4fv(h,!1,u);break;case"i":At.uniform1i(h,u);break;case"f":At.uniform1f(h,u);break;case"v2":At.uniform2f(h,u.x,u.y);break;case"v3":At.uniform3f(h,u.x,u.y,u.z);break;case"v4":At.uniform4f(h,u.x,u.y,u.z,u.w);break;case"c":At.uniform3f(h,u.r,u.g,u.b);break;case"iv1":At.uniform1iv(h,u);break;case"iv":At.uniform3iv(h,u);break;case"fv1":At.uniform1fv(h,u);break;case"fv":At.uniform3fv(h,u);break;case"v2v":void 0===s._array&&(s._array=new Float32Array(2*u.length));for(var l=0,c=u.length;c>l;l++)r=2*l,s._array[r]=u[l].x,s._array[r+1]=u[l].y;At.uniform2fv(h,s._array);break;case"v3v":void 0===s._array&&(s._array=new Float32Array(3*u.length));for(var l=0,c=u.length;c>l;l++)r=3*l,s._array[r]=u[l].x,s._array[r+1]=u[l].y,s._array[r+2]=u[l].z;At.uniform3fv(h,s._array);break;case"v4v":void 0===s._array&&(s._array=new Float32Array(4*u.length));for(var l=0,c=u.length;c>l;l++)r=4*l,s._array[r]=u[l].x,s._array[r+1]=u[l].y,s._array[r+2]=u[l].z,s._array[r+3]=u[l].w;At.uniform4fv(h,s._array);break;case"m3":At.uniformMatrix3fv(h,!1,u.elements);break;case"m3v":void 0===s._array&&(s._array=new Float32Array(9*u.length));for(var l=0,c=u.length;c>l;l++)u[l].flattenToArrayOffset(s._array,9*l);At.uniformMatrix3fv(h,!1,s._array);break;case"m4":At.uniformMatrix4fv(h,!1,u.elements);break;case"m4v":void 0===s._array&&(s._array=new Float32Array(16*u.length));for(var l=0,c=u.length;c>l;l++)u[l].flattenToArrayOffset(s._array,16*l);At.uniformMatrix4fv(h,!1,s._array);break;case"t":if(e=u,n=X(),At.uniform1i(h,n),!e)continue;e instanceof THREE.CubeTexture||e.image instanceof Array&&6===e.image.length?tt(e,n):e instanceof THREE.WebGLRenderTargetCube?et(e,n):Ct.setTexture(e,n);break;case"tv":void 0===s._array&&(s._array=[]);for(var l=0,c=s.value.length;c>l;l++)s._array[l]=X();At.uniform1iv(h,s._array);for(var l=0,c=s.value.length;c>l;l++)e=s.value[l],n=s._array[l],e&&Ct.setTexture(e,n);break;default:THREE.warn("THREE.WebGLRenderer: Unknown uniform type: "+a)}}}}function Z(t,e){t._modelViewMatrix.multiplyMatrices(e.matrixWorldInverse,t.matrixWorld),t._normalMatrix.getNormalMatrix(t._modelViewMatrix)}function K(t,e,n,r){t[e]=n.r*r,t[e+1]=n.g*r,t[e+2]=n.b*r}function Q(t){var e,n,r,i,o,s,a,u,h=0,l=0,c=0,p=Kt,f=p.directional.colors,d=p.directional.positions,m=p.point.colors,v=p.point.positions,g=p.point.distances,E=p.point.decays,y=p.spot.colors,_=p.spot.positions,b=p.spot.distances,T=p.spot.directions,x=p.spot.anglesCos,w=p.spot.exponents,R=p.spot.decays,H=p.hemi.skyColors,M=p.hemi.groundColors,S=p.hemi.positions,k=0,A=0,C=0,P=0,L=0,z=0,O=0,D=0,F=0,U=0,B=0,N=0;for(e=0,n=t.length;n>e;e++)if(r=t[e],!r.onlyShadow)if(i=r.color,a=r.intensity,u=r.distance,r instanceof THREE.AmbientLight){if(!r.visible)continue;h+=i.r,l+=i.g,c+=i.b}else if(r instanceof THREE.DirectionalLight){if(L+=1,!r.visible)continue;Yt.setFromMatrixPosition(r.matrixWorld),Xt.setFromMatrixPosition(r.target.matrixWorld),Yt.sub(Xt),Yt.normalize(),F=3*k,d[F]=Yt.x,d[F+1]=Yt.y,d[F+2]=Yt.z,K(f,F,i,a),k+=1}else if(r instanceof THREE.PointLight){if(z+=1,!r.visible)continue;U=3*A,K(m,U,i,a),Xt.setFromMatrixPosition(r.matrixWorld),v[U]=Xt.x,v[U+1]=Xt.y,v[U+2]=Xt.z,g[A]=u,E[A]=0===r.distance?0:r.decay,A+=1}else if(r instanceof THREE.SpotLight){if(O+=1,!r.visible)continue;B=3*C,K(y,B,i,a),Yt.setFromMatrixPosition(r.matrixWorld),_[B]=Yt.x,_[B+1]=Yt.y,_[B+2]=Yt.z,b[C]=u,Xt.setFromMatrixPosition(r.target.matrixWorld),Yt.sub(Xt),Yt.normalize(),T[B]=Yt.x,T[B+1]=Yt.y,T[B+2]=Yt.z,x[C]=Math.cos(r.angle),w[C]=r.exponent,R[C]=0===r.distance?0:r.decay,C+=1}else if(r instanceof THREE.HemisphereLight){if(D+=1,!r.visible)continue;Yt.setFromMatrixPosition(r.matrixWorld),Yt.normalize(),N=3*P,S[N]=Yt.x,S[N+1]=Yt.y,S[N+2]=Yt.z,o=r.color,s=r.groundColor,K(H,N,o,a),K(M,N,s,a),P+=1}for(e=3*k,n=Math.max(f.length,3*L);n>e;e++)f[e]=0;for(e=3*A,n=Math.max(m.length,3*z);n>e;e++)m[e]=0;for(e=3*C,n=Math.max(y.length,3*O);n>e;e++)y[e]=0;for(e=3*P,n=Math.max(H.length,3*D);n>e;e++)H[e]=0;for(e=3*P,n=Math.max(M.length,3*D);n>e;e++)M[e]=0;p.directional.length=k,p.point.length=A,p.spot.length=C,p.hemi.length=P,p.ambient[0]=h,p.ambient[1]=l,p.ambient[2]=c}function J(t,e,n){var r;n?(At.texParameteri(t,At.TEXTURE_WRAP_S,st(e.wrapS)),At.texParameteri(t,At.TEXTURE_WRAP_T,st(e.wrapT)),At.texParameteri(t,At.TEXTURE_MAG_FILTER,st(e.magFilter)),At.texParameteri(t,At.TEXTURE_MIN_FILTER,st(e.minFilter))):(At.texParameteri(t,At.TEXTURE_WRAP_S,At.CLAMP_TO_EDGE),At.texParameteri(t,At.TEXTURE_WRAP_T,At.CLAMP_TO_EDGE),(e.wrapS!==THREE.ClampToEdgeWrapping||e.wrapT!==THREE.ClampToEdgeWrapping)&&THREE.warn("THREE.WebGLRenderer: Texture is not power of two. Texture.wrapS and Texture.wrapT should be set to THREE.ClampToEdgeWrapping. ( "+e.sourceFile+" )"),At.texParameteri(t,At.TEXTURE_MAG_FILTER,ot(e.magFilter)),At.texParameteri(t,At.TEXTURE_MIN_FILTER,ot(e.minFilter)),e.minFilter!==THREE.NearestFilter&&e.minFilter!==THREE.LinearFilter&&THREE.warn("THREE.WebGLRenderer: Texture is not power of two. Texture.minFilter should be set to THREE.NearestFilter or THREE.LinearFilter. ( "+e.sourceFile+" )")),r=te.get("EXT_texture_filter_anisotropic"),r&&e.type!==THREE.FloatType&&e.type!==THREE.HalfFloatType&&(e.anisotropy>1||e.__currentAnisotropy)&&(At.texParameterf(t,r.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(e.anisotropy,Ct.getMaxAnisotropy())),e.__currentAnisotropy=e.anisotropy)}function $(t,e){if(t.width>e||t.height>e){var n=e/Math.max(t.width,t.height),r=document.createElement("canvas");r.width=Math.floor(t.width*n),r.height=Math.floor(t.height*n);var i=r.getContext("2d");return i.drawImage(t,0,0,t.width,t.height,0,0,r.width,r.height),THREE.warn("THREE.WebGLRenderer: image is too big ("+t.width+"x"+t.height+"). Resized to "+r.width+"x"+r.height,t),r}return t}function tt(t,e){if(6===t.image.length)if(t.needsUpdate){t.image.__webglTextureCube||(t.addEventListener("dispose",Te),t.image.__webglTextureCube=At.createTexture(),Ct.info.memory.textures++),At.activeTexture(At.TEXTURE0+e),At.bindTexture(At.TEXTURE_CUBE_MAP,t.image.__webglTextureCube),At.pixelStorei(At.UNPACK_FLIP_Y_WEBGL,t.flipY);for(var n=t instanceof THREE.CompressedTexture,r=t.image[0]instanceof THREE.DataTexture,i=[],o=0;6>o;o++)!Ct.autoScaleCubemaps||n||r?i[o]=r?t.image[o].image:t.image[o]:i[o]=$(t.image[o],ae);var s=i[0],a=THREE.Math.isPowerOfTwo(s.width)&&THREE.Math.isPowerOfTwo(s.height),u=st(t.format),h=st(t.type);J(At.TEXTURE_CUBE_MAP,t,a);for(var o=0;6>o;o++)if(n)for(var l,c=i[o].mipmaps,p=0,f=c.length;f>p;p++)l=c[p],t.format!==THREE.RGBAFormat&&t.format!==THREE.RGBFormat?de().indexOf(u)>-1?At.compressedTexImage2D(At.TEXTURE_CUBE_MAP_POSITIVE_X+o,p,u,l.width,l.height,0,l.data):THREE.warn("THREE.WebGLRenderer: Attempt to load unsupported compressed texture format in .setCubeTexture()"):At.texImage2D(At.TEXTURE_CUBE_MAP_POSITIVE_X+o,p,u,l.width,l.height,0,u,h,l.data);else r?At.texImage2D(At.TEXTURE_CUBE_MAP_POSITIVE_X+o,0,u,i[o].width,i[o].height,0,u,h,i[o].data):At.texImage2D(At.TEXTURE_CUBE_MAP_POSITIVE_X+o,0,u,u,h,i[o]);t.generateMipmaps&&a&&At.generateMipmap(At.TEXTURE_CUBE_MAP),t.needsUpdate=!1,t.onUpdate&&t.onUpdate()}else At.activeTexture(At.TEXTURE0+e),At.bindTexture(At.TEXTURE_CUBE_MAP,t.image.__webglTextureCube)}function et(t,e){At.activeTexture(At.TEXTURE0+e),At.bindTexture(At.TEXTURE_CUBE_MAP,t.__webglTexture)}function nt(t,e,n){At.bindFramebuffer(At.FRAMEBUFFER,t),At.framebufferTexture2D(At.FRAMEBUFFER,At.COLOR_ATTACHMENT0,n,e.__webglTexture,0)}function rt(t,e){At.bindRenderbuffer(At.RENDERBUFFER,t),e.depthBuffer&&!e.stencilBuffer?(At.renderbufferStorage(At.RENDERBUFFER,At.DEPTH_COMPONENT16,e.width,e.height),At.framebufferRenderbuffer(At.FRAMEBUFFER,At.DEPTH_ATTACHMENT,At.RENDERBUFFER,t)):e.depthBuffer&&e.stencilBuffer?(At.renderbufferStorage(At.RENDERBUFFER,At.DEPTH_STENCIL,e.width,e.height),At.framebufferRenderbuffer(At.FRAMEBUFFER,At.DEPTH_STENCIL_ATTACHMENT,At.RENDERBUFFER,t)):At.renderbufferStorage(At.RENDERBUFFER,At.RGBA4,e.width,e.height)}function it(t){t instanceof THREE.WebGLRenderTargetCube?(At.bindTexture(At.TEXTURE_CUBE_MAP,t.__webglTexture),At.generateMipmap(At.TEXTURE_CUBE_MAP),At.bindTexture(At.TEXTURE_CUBE_MAP,null)):(At.bindTexture(At.TEXTURE_2D,t.__webglTexture),At.generateMipmap(At.TEXTURE_2D),At.bindTexture(At.TEXTURE_2D,null))}function ot(t){return t===THREE.NearestFilter||t===THREE.NearestMipMapNearestFilter||t===THREE.NearestMipMapLinearFilter?At.NEAREST:At.LINEAR}function st(t){var e;if(t===THREE.RepeatWrapping)return At.REPEAT;if(t===THREE.ClampToEdgeWrapping)return At.CLAMP_TO_EDGE;if(t===THREE.MirroredRepeatWrapping)return At.MIRRORED_REPEAT;if(t===THREE.NearestFilter)return At.NEAREST;if(t===THREE.NearestMipMapNearestFilter)return At.NEAREST_MIPMAP_NEAREST;if(t===THREE.NearestMipMapLinearFilter)return At.NEAREST_MIPMAP_LINEAR;if(t===THREE.LinearFilter)return At.LINEAR;if(t===THREE.LinearMipMapNearestFilter)return At.LINEAR_MIPMAP_NEAREST;if(t===THREE.LinearMipMapLinearFilter)return At.LINEAR_MIPMAP_LINEAR;if(t===THREE.UnsignedByteType)return At.UNSIGNED_BYTE;if(t===THREE.UnsignedShort4444Type)return At.UNSIGNED_SHORT_4_4_4_4;if(t===THREE.UnsignedShort5551Type)return At.UNSIGNED_SHORT_5_5_5_1;if(t===THREE.UnsignedShort565Type)return At.UNSIGNED_SHORT_5_6_5;if(t===THREE.ByteType)return At.BYTE;if(t===THREE.ShortType)return At.SHORT;if(t===THREE.UnsignedShortType)return At.UNSIGNED_SHORT;if(t===THREE.IntType)return At.INT;if(t===THREE.UnsignedIntType)return At.UNSIGNED_INT;if(t===THREE.FloatType)return At.FLOAT;if(e=te.get("OES_texture_half_float"),null!==e&&t===THREE.HalfFloatType)return e.HALF_FLOAT_OES;if(t===THREE.AlphaFormat)return At.ALPHA;if(t===THREE.RGBFormat)return At.RGB;if(t===THREE.RGBAFormat)return At.RGBA;if(t===THREE.LuminanceFormat)return At.LUMINANCE;if(t===THREE.LuminanceAlphaFormat)return At.LUMINANCE_ALPHA;if(t===THREE.AddEquation)return At.FUNC_ADD;if(t===THREE.SubtractEquation)return At.FUNC_SUBTRACT;if(t===THREE.ReverseSubtractEquation)return At.FUNC_REVERSE_SUBTRACT;if(t===THREE.ZeroFactor)return At.ZERO;if(t===THREE.OneFactor)return At.ONE;if(t===THREE.SrcColorFactor)return At.SRC_COLOR;if(t===THREE.OneMinusSrcColorFactor)return At.ONE_MINUS_SRC_COLOR;if(t===THREE.SrcAlphaFactor)return At.SRC_ALPHA;if(t===THREE.OneMinusSrcAlphaFactor)return At.ONE_MINUS_SRC_ALPHA;if(t===THREE.DstAlphaFactor)return At.DST_ALPHA;if(t===THREE.OneMinusDstAlphaFactor)return At.ONE_MINUS_DST_ALPHA;if(t===THREE.DstColorFactor)return At.DST_COLOR;if(t===THREE.OneMinusDstColorFactor)return At.ONE_MINUS_DST_COLOR;if(t===THREE.SrcAlphaSaturateFactor)return At.SRC_ALPHA_SATURATE;if(e=te.get("WEBGL_compressed_texture_s3tc"),null!==e){if(t===THREE.RGB_S3TC_DXT1_Format)return e.COMPRESSED_RGB_S3TC_DXT1_EXT;if(t===THREE.RGBA_S3TC_DXT1_Format)return e.COMPRESSED_RGBA_S3TC_DXT1_EXT;if(t===THREE.RGBA_S3TC_DXT3_Format)return e.COMPRESSED_RGBA_S3TC_DXT3_EXT;if(t===THREE.RGBA_S3TC_DXT5_Format)return e.COMPRESSED_RGBA_S3TC_DXT5_EXT}if(e=te.get("WEBGL_compressed_texture_pvrtc"),null!==e){if(t===THREE.RGB_PVRTC_4BPPV1_Format)return e.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;if(t===THREE.RGB_PVRTC_2BPPV1_Format)return e.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;if(t===THREE.RGBA_PVRTC_4BPPV1_Format)return e.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;if(t===THREE.RGBA_PVRTC_2BPPV1_Format)return e.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG}if(e=te.get("EXT_blend_minmax"),null!==e){if(t===THREE.MinEquation)return e.MIN_EXT;if(t===THREE.MaxEquation)return e.MAX_EXT}return 0}function at(t){if(he&&t&&t.skeleton&&t.skeleton.useVertexTexture)return 1024;var e=At.getParameter(At.MAX_VERTEX_UNIFORM_VECTORS),n=Math.floor((e-20)/4),r=n;return void 0!==t&&t instanceof THREE.SkinnedMesh&&(r=Math.min(t.skeleton.bones.length,r),ro;o++){var a=t[o];a.onlyShadow||a.visible===!1||(a instanceof THREE.DirectionalLight&&e++,a instanceof THREE.PointLight&&n++,a instanceof THREE.SpotLight&&r++,a instanceof THREE.HemisphereLight&&i++)}return{directional:e,point:n,spot:r,hemi:i}}function ht(t){for(var e=0,n=0,r=t.length;r>n;n++){var i=t[n];i.castShadow&&(i instanceof THREE.SpotLight&&e++,i instanceof THREE.DirectionalLight&&!i.shadowCascade&&e++)}return e}console.log("THREE.WebGLRenderer",THREE.REVISION),t=t||{};var lt=void 0!==t.canvas?t.canvas:document.createElement("canvas"),ct=void 0!==t.context?t.context:null,pt=1,ft=void 0!==t.precision?t.precision:"highp",dt=void 0!==t.alpha?t.alpha:!1,mt=void 0!==t.depth?t.depth:!0,vt=void 0!==t.stencil?t.stencil:!0,gt=void 0!==t.antialias?t.antialias:!1,Et=void 0!==t.premultipliedAlpha?t.premultipliedAlpha:!0,yt=void 0!==t.preserveDrawingBuffer?t.preserveDrawingBuffer:!1,_t=void 0!==t.logarithmicDepthBuffer?t.logarithmicDepthBuffer:!1,bt=new THREE.Color(0),Tt=0,xt=[],wt={},Rt=[],Ht=[],Mt=[],St=[],kt=[];this.domElement=lt,this.context=null,this.autoClear=!0,this.autoClearColor=!0,this.autoClearDepth=!0,this.autoClearStencil=!0,this.sortObjects=!0,this.gammaFactor=2,this.gammaInput=!1,this.gammaOutput=!1,this.shadowMapEnabled=!1,this.shadowMapType=THREE.PCFShadowMap,this.shadowMapCullFace=THREE.CullFaceFront,this.shadowMapDebug=!1,this.shadowMapCascade=!1,this.maxMorphTargets=8,this.maxMorphNormals=4,this.autoScaleCubemaps=!0,this.info={memory:{programs:0,geometries:0,textures:0},render:{calls:0,vertices:0,faces:0,points:0}};var At,Ct=this,Pt=[],Lt=null,zt=null,Ot=-1,Dt="",Ft=null,Ut=0,Bt=0,Nt=0,Vt=lt.width,It=lt.height,jt=0,Gt=0,Wt=new THREE.Frustum,qt=new THREE.Matrix4,Xt=new THREE.Vector3,Yt=new THREE.Vector3,Zt=!0,Kt={ambient:[0,0,0],directional:{length:0,colors:[],positions:[]},point:{length:0,colors:[],positions:[],distances:[],decays:[]},spot:{length:0,colors:[],positions:[],distances:[],directions:[],anglesCos:[],exponents:[],decays:[]},hemi:{length:0,skyColors:[],groundColors:[],positions:[]}};try{var Qt={alpha:dt,depth:mt,stencil:vt,antialias:gt,premultipliedAlpha:Et,preserveDrawingBuffer:yt};if(At=ct||lt.getContext("webgl",Qt)||lt.getContext("experimental-webgl",Qt),null===At)throw null!==lt.getContext("webgl")?"Error creating WebGL context with your selected attributes.":"Error creating WebGL context.";lt.addEventListener("webglcontextlost",function(t){t.preventDefault(),re(),ne(),wt={}},!1)}catch(Jt){THREE.error("THREE.WebGLRenderer: "+Jt)}var $t=new THREE.WebGLState(At,st);void 0===At.getShaderPrecisionFormat&&(At.getShaderPrecisionFormat=function(){return{rangeMin:1,rangeMax:1,precision:1}});var te=new THREE.WebGLExtensions(At);te.get("OES_texture_float"),te.get("OES_texture_float_linear"),te.get("OES_texture_half_float"),te.get("OES_texture_half_float_linear"),te.get("OES_standard_derivatives"),_t&&te.get("EXT_frag_depth");var ee=function(t,e,n,r){Et===!0&&(t*=r,e*=r,n*=r),At.clearColor(t,e,n,r)},ne=function(){At.clearColor(0,0,0,1),At.clearDepth(1),At.clearStencil(0),At.enable(At.DEPTH_TEST),At.depthFunc(At.LEQUAL),At.frontFace(At.CCW),At.cullFace(At.BACK),At.enable(At.CULL_FACE),At.enable(At.BLEND),At.blendEquation(At.FUNC_ADD),At.blendFunc(At.SRC_ALPHA,At.ONE_MINUS_SRC_ALPHA),At.viewport(Bt,Nt,Vt,It),ee(bt.r,bt.g,bt.b,Tt)},re=function(){Lt=null,Ft=null,Dt="",Ot=-1,Zt=!0,$t.reset()};ne(),this.context=At,this.state=$t;var ie=At.getParameter(At.MAX_TEXTURE_IMAGE_UNITS),oe=At.getParameter(At.MAX_VERTEX_TEXTURE_IMAGE_UNITS),se=At.getParameter(At.MAX_TEXTURE_SIZE),ae=At.getParameter(At.MAX_CUBE_MAP_TEXTURE_SIZE),ue=oe>0,he=ue&&te.get("OES_texture_float"),le=At.getShaderPrecisionFormat(At.VERTEX_SHADER,At.HIGH_FLOAT),ce=At.getShaderPrecisionFormat(At.VERTEX_SHADER,At.MEDIUM_FLOAT),pe=At.getShaderPrecisionFormat(At.FRAGMENT_SHADER,At.HIGH_FLOAT),fe=At.getShaderPrecisionFormat(At.FRAGMENT_SHADER,At.MEDIUM_FLOAT),de=function(){var t;return function(){if(void 0!==t)return t;if(t=[],te.get("WEBGL_compressed_texture_pvrtc")||te.get("WEBGL_compressed_texture_s3tc"))for(var e=At.getParameter(At.COMPRESSED_TEXTURE_FORMATS),n=0;n0&&pe.precision>0,ve=ce.precision>0&&fe.precision>0;"highp"!==ft||me||(ve?(ft="mediump",THREE.warn("THREE.WebGLRenderer: highp not supported, using mediump.")):(ft="lowp",THREE.warn("THREE.WebGLRenderer: highp and mediump not supported, using lowp."))),"mediump"!==ft||ve||(ft="lowp",THREE.warn("THREE.WebGLRenderer: mediump not supported, using lowp."));var ge=new THREE.ShadowMapPlugin(this,xt,wt,Rt),Ee=new THREE.SpritePlugin(this,St),ye=new THREE.LensFlarePlugin(this,kt);this.getContext=function(){return At},this.forceContextLoss=function(){te.get("WEBGL_lose_context").loseContext()},this.supportsVertexTextures=function(){return ue},this.supportsFloatTextures=function(){return te.get("OES_texture_float")},this.supportsHalfFloatTextures=function(){return te.get("OES_texture_half_float")},this.supportsStandardDerivatives=function(){return te.get("OES_standard_derivatives")},this.supportsCompressedTextureS3TC=function(){return te.get("WEBGL_compressed_texture_s3tc")},this.supportsCompressedTexturePVRTC=function(){return te.get("WEBGL_compressed_texture_pvrtc")},this.supportsBlendMinMax=function(){return te.get("EXT_blend_minmax")},this.getMaxAnisotropy=function(){var t;return function(){if(void 0!==t)return t;var e=te.get("EXT_texture_filter_anisotropic");return t=null!==e?At.getParameter(e.MAX_TEXTURE_MAX_ANISOTROPY_EXT):0}}(),this.getPrecision=function(){return ft},this.getPixelRatio=function(){return pt},this.setPixelRatio=function(t){pt=t},this.setSize=function(t,e,n){lt.width=t*pt,lt.height=e*pt,n!==!1&&(lt.style.width=t+"px",lt.style.height=e+"px"),this.setViewport(0,0,t,e)},this.setViewport=function(t,e,n,r){Bt=t*pt,Nt=e*pt,Vt=n*pt,It=r*pt,At.viewport(Bt,Nt,Vt,It)},this.setScissor=function(t,e,n,r){At.scissor(t*pt,e*pt,n*pt,r*pt)},this.enableScissorTest=function(t){t?At.enable(At.SCISSOR_TEST):At.disable(At.SCISSOR_TEST)},this.getClearColor=function(){return bt},this.setClearColor=function(t,e){bt.set(t),Tt=void 0!==e?e:1,ee(bt.r,bt.g,bt.b,Tt)},this.getClearAlpha=function(){return Tt},this.setClearAlpha=function(t){Tt=t,ee(bt.r,bt.g,bt.b,Tt)},this.clear=function(t,e,n){var r=0;(void 0===t||t)&&(r|=At.COLOR_BUFFER_BIT),(void 0===e||e)&&(r|=At.DEPTH_BUFFER_BIT),(void 0===n||n)&&(r|=At.STENCIL_BUFFER_BIT),At.clear(r)},this.clearColor=function(){At.clear(At.COLOR_BUFFER_BIT)},this.clearDepth=function(){At.clear(At.DEPTH_BUFFER_BIT)},this.clearStencil=function(){At.clear(At.STENCIL_BUFFER_BIT)},this.clearTarget=function(t,e,n,r){this.setRenderTarget(t),this.clear(e,n,r)},this.resetGLState=re;var _e=function(t){var e=t.target;e.traverse(function(t){t.removeEventListener("remove",_e),C(t)})},be=function(t){var e=t.target;e.removeEventListener("dispose",be),He(e)},Te=function(t){var e=t.target;e.removeEventListener("dispose",Te),Me(e),Ct.info.memory.textures--},xe=function(t){var e=t.target;e.removeEventListener("dispose",xe),Se(e),Ct.info.memory.textures--},we=function(t){var e=t.target;e.removeEventListener("dispose",we),ke(e)},Re=function(t){for(var e=["__webglVertexBuffer","__webglNormalBuffer","__webglTangentBuffer","__webglColorBuffer","__webglUVBuffer","__webglUV2Buffer","__webglSkinIndicesBuffer","__webglSkinWeightsBuffer","__webglFaceBuffer","__webglLineBuffer","__webglLineDistanceBuffer"],n=0,r=e.length;r>n;n++){var i=e[n];void 0!==t[i]&&(At.deleteBuffer(t[i]),delete t[i])}if(void 0!==t.__webglCustomAttributesList){for(var i in t.__webglCustomAttributesList)At.deleteBuffer(t.__webglCustomAttributesList[i].buffer);delete t.__webglCustomAttributesList}Ct.info.memory.geometries--},He=function(t){if(delete t.__webglInit,t instanceof THREE.BufferGeometry){for(var e in t.attributes){var n=t.attributes[e];void 0!==n.buffer&&(At.deleteBuffer(n.buffer),delete n.buffer)}Ct.info.memory.geometries--}else{var r=Ae[t.id];if(void 0!==r){for(var i=0,o=r.length;o>i;i++){var s=r[i];if(void 0!==s.numMorphTargets){for(var a=0,u=s.numMorphTargets;u>a;a++)At.deleteBuffer(s.__webglMorphTargetsBuffers[a]);delete s.__webglMorphTargetsBuffers}if(void 0!==s.numMorphNormals){for(var a=0,u=s.numMorphNormals;u>a;a++)At.deleteBuffer(s.__webglMorphNormalsBuffers[a]);delete s.__webglMorphNormalsBuffers}Re(s)}delete Ae[t.id]}else Re(t)}Dt=""},Me=function(t){if(t.image&&t.image.__webglTextureCube)At.deleteTexture(t.image.__webglTextureCube),delete t.image.__webglTextureCube;else{if(void 0===t.__webglInit)return;At.deleteTexture(t.__webglTexture),delete t.__webglTexture,delete t.__webglInit}},Se=function(t){if(t&&void 0!==t.__webglTexture){if(At.deleteTexture(t.__webglTexture),delete t.__webglTexture,t instanceof THREE.WebGLRenderTargetCube)for(var e=0;6>e;e++)At.deleteFramebuffer(t.__webglFramebuffer[e]),At.deleteRenderbuffer(t.__webglRenderbuffer[e]);else At.deleteFramebuffer(t.__webglFramebuffer),At.deleteRenderbuffer(t.__webglRenderbuffer);delete t.__webglFramebuffer,delete t.__webglRenderbuffer}},ke=function(t){var e=t.program.program;if(void 0!==e){t.program=void 0;var n,r,i,o=!1;for(n=0,r=Pt.length;r>n;n++)if(i=Pt[n],i.program===e){i.usedTimes--,0===i.usedTimes&&(o=!0);break}if(o===!0){var s=[];for(n=0,r=Pt.length;r>n;n++)i=Pt[n],i.program!==e&&s.push(i);Pt=s,At.deleteProgram(e),Ct.info.memory.programs--}}};this.renderBufferImmediate=function(t,e,n){if($t.initAttributes(),t.hasPositions&&!t.__webglVertexBuffer&&(t.__webglVertexBuffer=At.createBuffer()),t.hasNormals&&!t.__webglNormalBuffer&&(t.__webglNormalBuffer=At.createBuffer()),t.hasUvs&&!t.__webglUvBuffer&&(t.__webglUvBuffer=At.createBuffer()),t.hasColors&&!t.__webglColorBuffer&&(t.__webglColorBuffer=At.createBuffer()),t.hasPositions&&(At.bindBuffer(At.ARRAY_BUFFER,t.__webglVertexBuffer),At.bufferData(At.ARRAY_BUFFER,t.positionArray,At.DYNAMIC_DRAW),$t.enableAttribute(e.attributes.position),At.vertexAttribPointer(e.attributes.position,3,At.FLOAT,!1,0,0)),t.hasNormals){if(At.bindBuffer(At.ARRAY_BUFFER,t.__webglNormalBuffer),n instanceof THREE.MeshPhongMaterial==!1&&n.shading===THREE.FlatShading){var r,i,o,s,a,u,h,l,c,p,f,d,m,v,g=3*t.count;for(v=0;g>v;v+=9)m=t.normalArray,s=m[v],h=m[v+1],p=m[v+2],a=m[v+3],l=m[v+4],f=m[v+5],u=m[v+6],c=m[v+7],d=m[v+8],r=(s+a+u)/3,i=(h+l+c)/3,o=(p+f+d)/3,m[v]=r,m[v+1]=i,m[v+2]=o,m[v+3]=r,m[v+4]=i,m[v+5]=o,m[v+6]=r,m[v+7]=i,m[v+8]=o}At.bufferData(At.ARRAY_BUFFER,t.normalArray,At.DYNAMIC_DRAW),$t.enableAttribute(e.attributes.normal),At.vertexAttribPointer(e.attributes.normal,3,At.FLOAT,!1,0,0)}t.hasUvs&&n.map&&(At.bindBuffer(At.ARRAY_BUFFER,t.__webglUvBuffer),At.bufferData(At.ARRAY_BUFFER,t.uvArray,At.DYNAMIC_DRAW),$t.enableAttribute(e.attributes.uv),At.vertexAttribPointer(e.attributes.uv,2,At.FLOAT,!1,0,0)),t.hasColors&&n.vertexColors!==THREE.NoColors&&(At.bindBuffer(At.ARRAY_BUFFER,t.__webglColorBuffer),At.bufferData(At.ARRAY_BUFFER,t.colorArray,At.DYNAMIC_DRAW),$t.enableAttribute(e.attributes.color),At.vertexAttribPointer(e.attributes.color,3,At.FLOAT,!1,0,0)),$t.disableUnusedAttributes(),At.drawArrays(At.TRIANGLES,0,t.count),t.count=0},this.renderBufferDirect=function(t,e,n,r,i,o){if(r.visible!==!1){S(o);var s=O(t,e,n,r,o),a=!1,u=r.wireframe?1:0,h="direct_"+i.id+"_"+s.id+"_"+u;if(h!==Dt&&(Dt=h,a=!0),a&&$t.initAttributes(),o instanceof THREE.Mesh){var l=r.wireframe===!0?At.LINES:At.TRIANGLES,c=i.attributes.index;if(c){var p,d;c.array instanceof Uint32Array&&te.get("OES_element_index_uint")?(p=At.UNSIGNED_INT,d=4):(p=At.UNSIGNED_SHORT,d=2);var m=i.offsets;if(0===m.length)a&&(f(r,s,i,0),At.bindBuffer(At.ELEMENT_ARRAY_BUFFER,c.buffer)),At.drawElements(l,c.array.length,p,0),Ct.info.render.calls++,Ct.info.render.vertices+=c.array.length,Ct.info.render.faces+=c.array.length/3;else{a=!0;for(var v=0,g=m.length;g>v;v++){var E=m[v].index;a&&(f(r,s,i,E),At.bindBuffer(At.ELEMENT_ARRAY_BUFFER,c.buffer)),At.drawElements(l,m[v].count,p,m[v].start*d),Ct.info.render.calls++,Ct.info.render.vertices+=m[v].count,Ct.info.render.faces+=m[v].count/3}}}else{a&&f(r,s,i,0); -var y=i.attributes.position;At.drawArrays(l,0,y.array.length/y.itemSize),Ct.info.render.calls++,Ct.info.render.vertices+=y.array.length/y.itemSize,Ct.info.render.faces+=y.array.length/(3*y.itemSize)}}else if(o instanceof THREE.PointCloud){var l=At.POINTS,c=i.attributes.index;if(c){var p,d;c.array instanceof Uint32Array&&te.get("OES_element_index_uint")?(p=At.UNSIGNED_INT,d=4):(p=At.UNSIGNED_SHORT,d=2);var m=i.offsets;if(0===m.length)a&&(f(r,s,i,0),At.bindBuffer(At.ELEMENT_ARRAY_BUFFER,c.buffer)),At.drawElements(l,c.array.length,p,0),Ct.info.render.calls++,Ct.info.render.points+=c.array.length;else{m.length>1&&(a=!0);for(var v=0,g=m.length;g>v;v++){var E=m[v].index;a&&(f(r,s,i,E),At.bindBuffer(At.ELEMENT_ARRAY_BUFFER,c.buffer)),At.drawElements(l,m[v].count,p,m[v].start*d),Ct.info.render.calls++,Ct.info.render.points+=m[v].count}}}else{a&&f(r,s,i,0);var y=i.attributes.position,m=i.offsets;if(0===m.length)At.drawArrays(l,0,y.array.length/3),Ct.info.render.calls++,Ct.info.render.points+=y.array.length/3;else for(var v=0,g=m.length;g>v;v++)At.drawArrays(l,m[v].index,m[v].count),Ct.info.render.calls++,Ct.info.render.points+=m[v].count}}else if(o instanceof THREE.Line){var l=o.mode===THREE.LineStrip?At.LINE_STRIP:At.LINES;$t.setLineWidth(r.linewidth*pt);var c=i.attributes.index;if(c){var p,d;c.array instanceof Uint32Array?(p=At.UNSIGNED_INT,d=4):(p=At.UNSIGNED_SHORT,d=2);var m=i.offsets;if(0===m.length)a&&(f(r,s,i,0),At.bindBuffer(At.ELEMENT_ARRAY_BUFFER,c.buffer)),At.drawElements(l,c.array.length,p,0),Ct.info.render.calls++,Ct.info.render.vertices+=c.array.length;else{m.length>1&&(a=!0);for(var v=0,g=m.length;g>v;v++){var E=m[v].index;a&&(f(r,s,i,E),At.bindBuffer(At.ELEMENT_ARRAY_BUFFER,c.buffer)),At.drawElements(l,m[v].count,p,m[v].start*d),Ct.info.render.calls++,Ct.info.render.vertices+=m[v].count}}}else{a&&f(r,s,i,0);var y=i.attributes.position,m=i.offsets;if(0===m.length)At.drawArrays(l,0,y.array.length/3),Ct.info.render.calls++,Ct.info.render.vertices+=y.array.length/3;else for(var v=0,g=m.length;g>v;v++)At.drawArrays(l,m[v].index,m[v].count),Ct.info.render.calls++,Ct.info.render.vertices+=m[v].count}}}},this.renderBuffer=function(t,e,n,r,i,o){if(r.visible!==!1){S(o);var s=O(t,e,n,r,o),a=s.attributes,u=!1,h=r.wireframe?1:0,l=i.id+"_"+s.id+"_"+h;if(l!==Dt&&(Dt=l,u=!0),u&&$t.initAttributes(),!r.morphTargets&&a.position>=0?u&&(At.bindBuffer(At.ARRAY_BUFFER,i.__webglVertexBuffer),$t.enableAttribute(a.position),At.vertexAttribPointer(a.position,3,At.FLOAT,!1,0,0)):o.morphTargetBase&&d(r,i,o),u){if(i.__webglCustomAttributesList)for(var c=0,p=i.__webglCustomAttributesList.length;p>c;c++){var f=i.__webglCustomAttributesList[c];a[f.buffer.belongsToAttribute]>=0&&(At.bindBuffer(At.ARRAY_BUFFER,f.buffer),$t.enableAttribute(a[f.buffer.belongsToAttribute]),At.vertexAttribPointer(a[f.buffer.belongsToAttribute],f.size,At.FLOAT,!1,0,0))}a.color>=0&&(o.geometry.colors.length>0||o.geometry.faces.length>0?(At.bindBuffer(At.ARRAY_BUFFER,i.__webglColorBuffer),$t.enableAttribute(a.color),At.vertexAttribPointer(a.color,3,At.FLOAT,!1,0,0)):void 0!==r.defaultAttributeValues&&At.vertexAttrib3fv(a.color,r.defaultAttributeValues.color)),a.normal>=0&&(At.bindBuffer(At.ARRAY_BUFFER,i.__webglNormalBuffer),$t.enableAttribute(a.normal),At.vertexAttribPointer(a.normal,3,At.FLOAT,!1,0,0)),a.tangent>=0&&(At.bindBuffer(At.ARRAY_BUFFER,i.__webglTangentBuffer),$t.enableAttribute(a.tangent),At.vertexAttribPointer(a.tangent,4,At.FLOAT,!1,0,0)),a.uv>=0&&(o.geometry.faceVertexUvs[0]?(At.bindBuffer(At.ARRAY_BUFFER,i.__webglUVBuffer),$t.enableAttribute(a.uv),At.vertexAttribPointer(a.uv,2,At.FLOAT,!1,0,0)):void 0!==r.defaultAttributeValues&&At.vertexAttrib2fv(a.uv,r.defaultAttributeValues.uv)),a.uv2>=0&&(o.geometry.faceVertexUvs[1]?(At.bindBuffer(At.ARRAY_BUFFER,i.__webglUV2Buffer),$t.enableAttribute(a.uv2),At.vertexAttribPointer(a.uv2,2,At.FLOAT,!1,0,0)):void 0!==r.defaultAttributeValues&&At.vertexAttrib2fv(a.uv2,r.defaultAttributeValues.uv2)),r.skinning&&a.skinIndex>=0&&a.skinWeight>=0&&(At.bindBuffer(At.ARRAY_BUFFER,i.__webglSkinIndicesBuffer),$t.enableAttribute(a.skinIndex),At.vertexAttribPointer(a.skinIndex,4,At.FLOAT,!1,0,0),At.bindBuffer(At.ARRAY_BUFFER,i.__webglSkinWeightsBuffer),$t.enableAttribute(a.skinWeight),At.vertexAttribPointer(a.skinWeight,4,At.FLOAT,!1,0,0)),a.lineDistance>=0&&(At.bindBuffer(At.ARRAY_BUFFER,i.__webglLineDistanceBuffer),$t.enableAttribute(a.lineDistance),At.vertexAttribPointer(a.lineDistance,1,At.FLOAT,!1,0,0))}if($t.disableUnusedAttributes(),o instanceof THREE.Mesh){var m=i.__typeArray===Uint32Array?At.UNSIGNED_INT:At.UNSIGNED_SHORT;r.wireframe?($t.setLineWidth(r.wireframeLinewidth*pt),u&&At.bindBuffer(At.ELEMENT_ARRAY_BUFFER,i.__webglLineBuffer),At.drawElements(At.LINES,i.__webglLineCount,m,0)):(u&&At.bindBuffer(At.ELEMENT_ARRAY_BUFFER,i.__webglFaceBuffer),At.drawElements(At.TRIANGLES,i.__webglFaceCount,m,0)),Ct.info.render.calls++,Ct.info.render.vertices+=i.__webglFaceCount,Ct.info.render.faces+=i.__webglFaceCount/3}else if(o instanceof THREE.Line){var v=o.mode===THREE.LineStrip?At.LINE_STRIP:At.LINES;$t.setLineWidth(r.linewidth*pt),At.drawArrays(v,0,i.__webglLineCount),Ct.info.render.calls++}else o instanceof THREE.PointCloud&&(At.drawArrays(At.POINTS,0,i.__webglParticleCount),Ct.info.render.calls++,Ct.info.render.points+=i.__webglParticleCount)}},this.render=function(t,e,n,r){if(e instanceof THREE.Camera==!1)return void THREE.error("THREE.WebGLRenderer.render: camera is not an instance of THREE.Camera.");var i=t.fog;Dt="",Ot=-1,Ft=null,Zt=!0,t.autoUpdate===!0&&t.updateMatrixWorld(),void 0===e.parent&&e.updateMatrixWorld(),t.traverse(function(t){t instanceof THREE.SkinnedMesh&&t.skeleton.update()}),e.matrixWorldInverse.getInverse(e.matrixWorld),qt.multiplyMatrices(e.projectionMatrix,e.matrixWorldInverse),Wt.setFromMatrix(qt),xt.length=0,Ht.length=0,Mt.length=0,St.length=0,kt.length=0,E(t),Ct.sortObjects===!0&&(Ht.sort(m),Mt.sort(v)),ge.render(t,e),Ct.info.render.calls=0,Ct.info.render.vertices=0,Ct.info.render.faces=0,Ct.info.render.points=0,this.setRenderTarget(n),(this.autoClear||r)&&this.clear(this.autoClearColor,this.autoClearDepth,this.autoClearStencil);for(var o=0,s=Rt.length;s>o;o++){var a=Rt[o],u=a.object;u.visible&&(Z(u,e),b(a))}if(t.overrideMaterial){var h=t.overrideMaterial;z(h),y(Ht,e,xt,i,h),y(Mt,e,xt,i,h),_(Rt,"",e,xt,i,h)}else $t.setBlending(THREE.NoBlending),y(Ht,e,xt,i,null),_(Rt,"opaque",e,xt,i,null),y(Mt,e,xt,i,null),_(Rt,"transparent",e,xt,i,null);Ee.render(t,e),ye.render(t,e,jt,Gt),n&&n.generateMipmaps&&n.minFilter!==THREE.NearestFilter&&n.minFilter!==THREE.LinearFilter&&it(n),$t.setDepthTest(!0),$t.setDepthWrite(!0),$t.setColorWrite(!0)},this.renderImmediateObject=function(t,e,n,r,i){var o=O(t,e,n,r,i);Dt="",Ct.setMaterialFaces(r),i.immediateRenderCallback?i.immediateRenderCallback(o,At,Wt):i.render(function(t){Ct.renderBufferImmediate(t,o,r)})};var Ae={},Ce=0,Pe={MeshDepthMaterial:"depth",MeshNormalMaterial:"normal",MeshBasicMaterial:"basic",MeshLambertMaterial:"lambert",MeshPhongMaterial:"phong",LineBasicMaterial:"basic",LineDashedMaterial:"dashed",PointCloudMaterial:"particle_basic"};this.setFaceCulling=function(t,e){t===THREE.CullFaceNone?At.disable(At.CULL_FACE):(e===THREE.FrontFaceDirectionCW?At.frontFace(At.CW):At.frontFace(At.CCW),t===THREE.CullFaceBack?At.cullFace(At.BACK):t===THREE.CullFaceFront?At.cullFace(At.FRONT):At.cullFace(At.FRONT_AND_BACK),At.enable(At.CULL_FACE))},this.setMaterialFaces=function(t){$t.setDoubleSided(t.side===THREE.DoubleSide),$t.setFlipSided(t.side===THREE.BackSide)},this.uploadTexture=function(t){void 0===t.__webglInit&&(t.__webglInit=!0,t.addEventListener("dispose",Te),t.__webglTexture=At.createTexture(),Ct.info.memory.textures++),At.bindTexture(At.TEXTURE_2D,t.__webglTexture),At.pixelStorei(At.UNPACK_FLIP_Y_WEBGL,t.flipY),At.pixelStorei(At.UNPACK_PREMULTIPLY_ALPHA_WEBGL,t.premultiplyAlpha),At.pixelStorei(At.UNPACK_ALIGNMENT,t.unpackAlignment),t.image=$(t.image,se);var e=t.image,n=THREE.Math.isPowerOfTwo(e.width)&&THREE.Math.isPowerOfTwo(e.height),r=st(t.format),i=st(t.type);J(At.TEXTURE_2D,t,n);var o,s=t.mipmaps;if(t instanceof THREE.DataTexture)if(s.length>0&&n){for(var a=0,u=s.length;u>a;a++)o=s[a],At.texImage2D(At.TEXTURE_2D,a,r,o.width,o.height,0,r,i,o.data);t.generateMipmaps=!1}else At.texImage2D(At.TEXTURE_2D,0,r,e.width,e.height,0,r,i,e.data);else if(t instanceof THREE.CompressedTexture)for(var a=0,u=s.length;u>a;a++)o=s[a],t.format!==THREE.RGBAFormat&&t.format!==THREE.RGBFormat?de().indexOf(r)>-1?At.compressedTexImage2D(At.TEXTURE_2D,a,r,o.width,o.height,0,o.data):THREE.warn("THREE.WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()"):At.texImage2D(At.TEXTURE_2D,a,r,o.width,o.height,0,r,i,o.data);else if(s.length>0&&n){for(var a=0,u=s.length;u>a;a++)o=s[a],At.texImage2D(At.TEXTURE_2D,a,r,r,i,o);t.generateMipmaps=!1}else At.texImage2D(At.TEXTURE_2D,0,r,r,i,t.image);t.generateMipmaps&&n&&At.generateMipmap(At.TEXTURE_2D),t.needsUpdate=!1,t.onUpdate&&t.onUpdate()},this.setTexture=function(t,e){At.activeTexture(At.TEXTURE0+e),t.needsUpdate?Ct.uploadTexture(t):At.bindTexture(At.TEXTURE_2D,t.__webglTexture)},this.setRenderTarget=function(t){var e=t instanceof THREE.WebGLRenderTargetCube;if(t&&void 0===t.__webglFramebuffer){void 0===t.depthBuffer&&(t.depthBuffer=!0),void 0===t.stencilBuffer&&(t.stencilBuffer=!0),t.addEventListener("dispose",xe),t.__webglTexture=At.createTexture(),Ct.info.memory.textures++;var n=THREE.Math.isPowerOfTwo(t.width)&&THREE.Math.isPowerOfTwo(t.height),r=st(t.format),i=st(t.type);if(e){t.__webglFramebuffer=[],t.__webglRenderbuffer=[],At.bindTexture(At.TEXTURE_CUBE_MAP,t.__webglTexture),J(At.TEXTURE_CUBE_MAP,t,n);for(var o=0;6>o;o++)t.__webglFramebuffer[o]=At.createFramebuffer(),t.__webglRenderbuffer[o]=At.createRenderbuffer(),At.texImage2D(At.TEXTURE_CUBE_MAP_POSITIVE_X+o,0,r,t.width,t.height,0,r,i,null),nt(t.__webglFramebuffer[o],t,At.TEXTURE_CUBE_MAP_POSITIVE_X+o),rt(t.__webglRenderbuffer[o],t);n&&At.generateMipmap(At.TEXTURE_CUBE_MAP)}else t.__webglFramebuffer=At.createFramebuffer(),t.shareDepthFrom?t.__webglRenderbuffer=t.shareDepthFrom.__webglRenderbuffer:t.__webglRenderbuffer=At.createRenderbuffer(),At.bindTexture(At.TEXTURE_2D,t.__webglTexture),J(At.TEXTURE_2D,t,n),At.texImage2D(At.TEXTURE_2D,0,r,t.width,t.height,0,r,i,null),nt(t.__webglFramebuffer,t,At.TEXTURE_2D),t.shareDepthFrom?t.depthBuffer&&!t.stencilBuffer?At.framebufferRenderbuffer(At.FRAMEBUFFER,At.DEPTH_ATTACHMENT,At.RENDERBUFFER,t.__webglRenderbuffer):t.depthBuffer&&t.stencilBuffer&&At.framebufferRenderbuffer(At.FRAMEBUFFER,At.DEPTH_STENCIL_ATTACHMENT,At.RENDERBUFFER,t.__webglRenderbuffer):rt(t.__webglRenderbuffer,t),n&&At.generateMipmap(At.TEXTURE_2D);e?At.bindTexture(At.TEXTURE_CUBE_MAP,null):At.bindTexture(At.TEXTURE_2D,null),At.bindRenderbuffer(At.RENDERBUFFER,null),At.bindFramebuffer(At.FRAMEBUFFER,null)}var s,a,u,h,l;t?(s=e?t.__webglFramebuffer[t.activeCubeFace]:t.__webglFramebuffer,a=t.width,u=t.height,h=0,l=0):(s=null,a=Vt,u=It,h=Bt,l=Nt),s!==zt&&(At.bindFramebuffer(At.FRAMEBUFFER,s),At.viewport(h,l,a,u),zt=s),jt=a,Gt=u},this.readRenderTargetPixels=function(t,e,n,r,i,o){if(!(t instanceof THREE.WebGLRenderTarget))return void console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");if(t.__webglFramebuffer){if(t.format!==THREE.RGBAFormat)return void console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA format. readPixels can read only RGBA format.");var s=!1;t.__webglFramebuffer!==zt&&(At.bindFramebuffer(At.FRAMEBUFFER,t.__webglFramebuffer),s=!0),At.checkFramebufferStatus(At.FRAMEBUFFER)===At.FRAMEBUFFER_COMPLETE?At.readPixels(e,n,r,i,At.RGBA,At.UNSIGNED_BYTE,o):console.error("THREE.WebGLRenderer.readRenderTargetPixels: readPixels from renderTarget failed. Framebuffer not complete."),s&&At.bindFramebuffer(At.FRAMEBUFFER,zt)}},this.initMaterial=function(){THREE.warn("THREE.WebGLRenderer: .initMaterial() has been removed.")},this.addPrePlugin=function(){THREE.warn("THREE.WebGLRenderer: .addPrePlugin() has been removed.")},this.addPostPlugin=function(){THREE.warn("THREE.WebGLRenderer: .addPostPlugin() has been removed.")},this.updateShadowMap=function(){THREE.warn("THREE.WebGLRenderer: .updateShadowMap() has been removed.")}},THREE.WebGLRenderTarget=function(t,e,n){this.width=t,this.height=e,n=n||{},this.wrapS=void 0!==n.wrapS?n.wrapS:THREE.ClampToEdgeWrapping,this.wrapT=void 0!==n.wrapT?n.wrapT:THREE.ClampToEdgeWrapping,this.magFilter=void 0!==n.magFilter?n.magFilter:THREE.LinearFilter,this.minFilter=void 0!==n.minFilter?n.minFilter:THREE.LinearMipMapLinearFilter,this.anisotropy=void 0!==n.anisotropy?n.anisotropy:1,this.offset=new THREE.Vector2(0,0),this.repeat=new THREE.Vector2(1,1),this.format=void 0!==n.format?n.format:THREE.RGBAFormat,this.type=void 0!==n.type?n.type:THREE.UnsignedByteType,this.depthBuffer=void 0!==n.depthBuffer?n.depthBuffer:!0,this.stencilBuffer=void 0!==n.stencilBuffer?n.stencilBuffer:!0,this.generateMipmaps=!0,this.shareDepthFrom=void 0!==n.shareDepthFrom?n.shareDepthFrom:null},THREE.WebGLRenderTarget.prototype={constructor:THREE.WebGLRenderTarget,setSize:function(t,e){this.width=t,this.height=e},clone:function(){var t=new THREE.WebGLRenderTarget(this.width,this.height);return t.wrapS=this.wrapS,t.wrapT=this.wrapT,t.magFilter=this.magFilter,t.minFilter=this.minFilter,t.anisotropy=this.anisotropy,t.offset.copy(this.offset),t.repeat.copy(this.repeat),t.format=this.format,t.type=this.type,t.depthBuffer=this.depthBuffer,t.stencilBuffer=this.stencilBuffer,t.generateMipmaps=this.generateMipmaps,t.shareDepthFrom=this.shareDepthFrom,t},dispose:function(){this.dispatchEvent({type:"dispose"})}},THREE.EventDispatcher.prototype.apply(THREE.WebGLRenderTarget.prototype),THREE.WebGLRenderTargetCube=function(t,e,n){THREE.WebGLRenderTarget.call(this,t,e,n),this.activeCubeFace=0},THREE.WebGLRenderTargetCube.prototype=Object.create(THREE.WebGLRenderTarget.prototype),THREE.WebGLRenderTargetCube.prototype.constructor=THREE.WebGLRenderTargetCube,THREE.WebGLExtensions=function(t){var e={};this.get=function(n){if(void 0!==e[n])return e[n];var r;switch(n){case"EXT_texture_filter_anisotropic":r=t.getExtension("EXT_texture_filter_anisotropic")||t.getExtension("MOZ_EXT_texture_filter_anisotropic")||t.getExtension("WEBKIT_EXT_texture_filter_anisotropic");break;case"WEBGL_compressed_texture_s3tc":r=t.getExtension("WEBGL_compressed_texture_s3tc")||t.getExtension("MOZ_WEBGL_compressed_texture_s3tc")||t.getExtension("WEBKIT_WEBGL_compressed_texture_s3tc");break;case"WEBGL_compressed_texture_pvrtc":r=t.getExtension("WEBGL_compressed_texture_pvrtc")||t.getExtension("WEBKIT_WEBGL_compressed_texture_pvrtc");break;default:r=t.getExtension(n)}return null===r&&THREE.warn("THREE.WebGLRenderer: "+n+" extension not supported."),e[n]=r,r}},THREE.WebGLProgram=function(){var t=0,e=function(t){var e,n,r=[];for(var i in t)e=t[i],e!==!1&&(n="#define "+i+" "+e,r.push(n));return r.join("\n")},n=function(t,e,n){for(var r={},i=0,o=n.length;o>i;i++){var s=n[i];r[s]=t.getUniformLocation(e,s)}return r},r=function(t,e,n){for(var r={},i=0,o=n.length;o>i;i++){var s=n[i];r[s]=t.getAttribLocation(e,s)}return r};return function(i,o,s,a){var u=i,h=u.context,l=s.defines,c=s.__webglShader.uniforms,p=s.attributes,f=s.__webglShader.vertexShader,d=s.__webglShader.fragmentShader,m=s.index0AttributeName;void 0===m&&a.morphTargets===!0&&(m="position");var v="SHADOWMAP_TYPE_BASIC";a.shadowMapType===THREE.PCFShadowMap?v="SHADOWMAP_TYPE_PCF":a.shadowMapType===THREE.PCFSoftShadowMap&&(v="SHADOWMAP_TYPE_PCF_SOFT");var g="ENVMAP_TYPE_CUBE",E="ENVMAP_MODE_REFLECTION",y="ENVMAP_BLENDING_MULTIPLY";if(a.envMap){switch(s.envMap.mapping){case THREE.CubeReflectionMapping:case THREE.CubeRefractionMapping:g="ENVMAP_TYPE_CUBE";break;case THREE.EquirectangularReflectionMapping:case THREE.EquirectangularRefractionMapping:g="ENVMAP_TYPE_EQUIREC";break;case THREE.SphericalReflectionMapping:g="ENVMAP_TYPE_SPHERE"}switch(s.envMap.mapping){case THREE.CubeRefractionMapping:case THREE.EquirectangularRefractionMapping:E="ENVMAP_MODE_REFRACTION"}switch(s.combine){case THREE.MultiplyOperation:y="ENVMAP_BLENDING_MULTIPLY";break;case THREE.MixOperation:y="ENVMAP_BLENDING_MIX";break;case THREE.AddOperation:y="ENVMAP_BLENDING_ADD"}}var _,b,T=i.gammaFactor>0?i.gammaFactor:1,x=e(l),w=h.createProgram();s instanceof THREE.RawShaderMaterial?(_="",b=""):(_=["precision "+a.precision+" float;","precision "+a.precision+" int;",x,a.supportsVertexTextures?"#define VERTEX_TEXTURES":"",u.gammaInput?"#define GAMMA_INPUT":"",u.gammaOutput?"#define GAMMA_OUTPUT":"","#define GAMMA_FACTOR "+T,"#define MAX_DIR_LIGHTS "+a.maxDirLights,"#define MAX_POINT_LIGHTS "+a.maxPointLights,"#define MAX_SPOT_LIGHTS "+a.maxSpotLights,"#define MAX_HEMI_LIGHTS "+a.maxHemiLights,"#define MAX_SHADOWS "+a.maxShadows,"#define MAX_BONES "+a.maxBones,a.map?"#define USE_MAP":"",a.envMap?"#define USE_ENVMAP":"",a.envMap?"#define "+E:"",a.lightMap?"#define USE_LIGHTMAP":"",a.bumpMap?"#define USE_BUMPMAP":"",a.normalMap?"#define USE_NORMALMAP":"",a.specularMap?"#define USE_SPECULARMAP":"",a.alphaMap?"#define USE_ALPHAMAP":"",a.vertexColors?"#define USE_COLOR":"",a.flatShading?"#define FLAT_SHADED":"",a.skinning?"#define USE_SKINNING":"",a.useVertexTexture?"#define BONE_TEXTURE":"",a.morphTargets?"#define USE_MORPHTARGETS":"",a.morphNormals?"#define USE_MORPHNORMALS":"",a.wrapAround?"#define WRAP_AROUND":"",a.doubleSided?"#define DOUBLE_SIDED":"",a.flipSided?"#define FLIP_SIDED":"",a.shadowMapEnabled?"#define USE_SHADOWMAP":"",a.shadowMapEnabled?"#define "+v:"",a.shadowMapDebug?"#define SHADOWMAP_DEBUG":"",a.shadowMapCascade?"#define SHADOWMAP_CASCADE":"",a.sizeAttenuation?"#define USE_SIZEATTENUATION":"",a.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"","uniform mat4 modelMatrix;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform mat4 viewMatrix;","uniform mat3 normalMatrix;","uniform vec3 cameraPosition;","attribute vec3 position;","attribute vec3 normal;","attribute vec2 uv;","attribute vec2 uv2;","#ifdef USE_COLOR"," attribute vec3 color;","#endif","#ifdef USE_MORPHTARGETS"," attribute vec3 morphTarget0;"," attribute vec3 morphTarget1;"," attribute vec3 morphTarget2;"," attribute vec3 morphTarget3;"," #ifdef USE_MORPHNORMALS"," attribute vec3 morphNormal0;"," attribute vec3 morphNormal1;"," attribute vec3 morphNormal2;"," attribute vec3 morphNormal3;"," #else"," attribute vec3 morphTarget4;"," attribute vec3 morphTarget5;"," attribute vec3 morphTarget6;"," attribute vec3 morphTarget7;"," #endif","#endif","#ifdef USE_SKINNING"," attribute vec4 skinIndex;"," attribute vec4 skinWeight;","#endif",""].join("\n"),b=["precision "+a.precision+" float;","precision "+a.precision+" int;",a.bumpMap||a.normalMap||a.flatShading?"#extension GL_OES_standard_derivatives : enable":"",x,"#define MAX_DIR_LIGHTS "+a.maxDirLights,"#define MAX_POINT_LIGHTS "+a.maxPointLights,"#define MAX_SPOT_LIGHTS "+a.maxSpotLights,"#define MAX_HEMI_LIGHTS "+a.maxHemiLights,"#define MAX_SHADOWS "+a.maxShadows,a.alphaTest?"#define ALPHATEST "+a.alphaTest:"",u.gammaInput?"#define GAMMA_INPUT":"",u.gammaOutput?"#define GAMMA_OUTPUT":"","#define GAMMA_FACTOR "+T,a.useFog&&a.fog?"#define USE_FOG":"",a.useFog&&a.fogExp?"#define FOG_EXP2":"",a.map?"#define USE_MAP":"",a.envMap?"#define USE_ENVMAP":"",a.envMap?"#define "+g:"",a.envMap?"#define "+E:"",a.envMap?"#define "+y:"",a.lightMap?"#define USE_LIGHTMAP":"",a.bumpMap?"#define USE_BUMPMAP":"",a.normalMap?"#define USE_NORMALMAP":"",a.specularMap?"#define USE_SPECULARMAP":"",a.alphaMap?"#define USE_ALPHAMAP":"",a.vertexColors?"#define USE_COLOR":"",a.flatShading?"#define FLAT_SHADED":"",a.metal?"#define METAL":"",a.wrapAround?"#define WRAP_AROUND":"",a.doubleSided?"#define DOUBLE_SIDED":"",a.flipSided?"#define FLIP_SIDED":"",a.shadowMapEnabled?"#define USE_SHADOWMAP":"",a.shadowMapEnabled?"#define "+v:"",a.shadowMapDebug?"#define SHADOWMAP_DEBUG":"",a.shadowMapCascade?"#define SHADOWMAP_CASCADE":"",a.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"","uniform mat4 viewMatrix;","uniform vec3 cameraPosition;",""].join("\n"));var R=new THREE.WebGLShader(h,h.VERTEX_SHADER,_+f),H=new THREE.WebGLShader(h,h.FRAGMENT_SHADER,b+d);h.attachShader(w,R),h.attachShader(w,H),void 0!==m&&h.bindAttribLocation(w,0,m),h.linkProgram(w);var M=h.getProgramInfoLog(w);h.getProgramParameter(w,h.LINK_STATUS)===!1&&THREE.error("THREE.WebGLProgram: shader error: "+h.getError(),"gl.VALIDATE_STATUS",h.getProgramParameter(w,h.VALIDATE_STATUS),"gl.getPRogramInfoLog",M),""!==M&&THREE.warn("THREE.WebGLProgram: gl.getProgramInfoLog()"+M),h.deleteShader(R),h.deleteShader(H);var S=["viewMatrix","modelViewMatrix","projectionMatrix","normalMatrix","modelMatrix","cameraPosition","morphTargetInfluences","bindMatrix","bindMatrixInverse"];a.useVertexTexture?(S.push("boneTexture"),S.push("boneTextureWidth"),S.push("boneTextureHeight")):S.push("boneGlobalMatrices"),a.logarithmicDepthBuffer&&S.push("logDepthBufFC");for(var k in c)S.push(k);this.uniforms=n(h,w,S),S=["position","normal","uv","uv2","tangent","color","skinIndex","skinWeight","lineDistance"];for(var A=0;At;t++)n[t]=0},this.enableAttribute=function(e){n[e]=1,0===r[e]&&(t.enableVertexAttribArray(e),r[e]=1)},this.disableUnusedAttributes=function(){for(var e=0,i=r.length;i>e;e++)r[e]!==n[e]&&(t.disableVertexAttribArray(e),r[e]=0)},this.setBlending=function(n,r,c,p,f,d,m){n!==i&&(n===THREE.NoBlending?t.disable(t.BLEND):n===THREE.AdditiveBlending?(t.enable(t.BLEND),t.blendEquation(t.FUNC_ADD),t.blendFunc(t.SRC_ALPHA,t.ONE)):n===THREE.SubtractiveBlending?(t.enable(t.BLEND),t.blendEquation(t.FUNC_ADD),t.blendFunc(t.ZERO,t.ONE_MINUS_SRC_COLOR)):n===THREE.MultiplyBlending?(t.enable(t.BLEND),t.blendEquation(t.FUNC_ADD),t.blendFunc(t.ZERO,t.SRC_COLOR)):n===THREE.CustomBlending?t.enable(t.BLEND):(t.enable(t.BLEND),t.blendEquationSeparate(t.FUNC_ADD,t.FUNC_ADD),t.blendFuncSeparate(t.SRC_ALPHA,t.ONE_MINUS_SRC_ALPHA,t.ONE,t.ONE_MINUS_SRC_ALPHA)),i=n),n===THREE.CustomBlending?(f=f||r,d=d||c,m=m||p,(r!==o||f!==u)&&(t.blendEquationSeparate(e(r),e(f)),o=r,u=f),(c!==s||p!==a||d!==h||m!==l)&&(t.blendFuncSeparate(e(c),e(p),e(d),e(m)),s=c,a=p,h=d,l=m)):(o=null,s=null,a=null,u=null,h=null,l=null)},this.setDepthTest=function(e){c!==e&&(e?t.enable(t.DEPTH_TEST):t.disable(t.DEPTH_TEST),c=e)},this.setDepthWrite=function(e){p!==e&&(t.depthMask(e),p=e)},this.setColorWrite=function(e){f!==e&&(t.colorMask(e,e,e,e),f=e)},this.setDoubleSided=function(e){d!==e&&(e?t.disable(t.CULL_FACE):t.enable(t.CULL_FACE),d=e)},this.setFlipSided=function(e){m!==e&&(e?t.frontFace(t.CW):t.frontFace(t.CCW),m=e)},this.setLineWidth=function(e){e!==v&&(t.lineWidth(e),v=e)},this.setPolygonOffset=function(e,n,r){g!==e&&(e?t.enable(t.POLYGON_OFFSET_FILL):t.disable(t.POLYGON_OFFSET_FILL),g=e),!e||E===n&&y===r||(t.polygonOffset(n,r),E=n,y=r)},this.reset=function(){for(var t=0;t0;var p;p=u?{vertexShader:["uniform lowp int renderType;","uniform vec3 screenPosition;","uniform vec2 scale;","uniform float rotation;","uniform sampler2D occlusionMap;","attribute vec2 position;","attribute vec2 uv;","varying vec2 vUV;","varying float vVisibility;","void main() {","vUV = uv;","vec2 pos = position;","if( renderType == 2 ) {","vec4 visibility = texture2D( occlusionMap, vec2( 0.1, 0.1 ) );","visibility += texture2D( occlusionMap, vec2( 0.5, 0.1 ) );","visibility += texture2D( occlusionMap, vec2( 0.9, 0.1 ) );","visibility += texture2D( occlusionMap, vec2( 0.9, 0.5 ) );","visibility += texture2D( occlusionMap, vec2( 0.9, 0.9 ) );","visibility += texture2D( occlusionMap, vec2( 0.5, 0.9 ) );","visibility += texture2D( occlusionMap, vec2( 0.1, 0.9 ) );","visibility += texture2D( occlusionMap, vec2( 0.1, 0.5 ) );","visibility += texture2D( occlusionMap, vec2( 0.5, 0.5 ) );","vVisibility = visibility.r / 9.0;","vVisibility *= 1.0 - visibility.g / 9.0;","vVisibility *= visibility.b / 9.0;","vVisibility *= 1.0 - visibility.a / 9.0;","pos.x = cos( rotation ) * position.x - sin( rotation ) * position.y;","pos.y = sin( rotation ) * position.x + cos( rotation ) * position.y;","}","gl_Position = vec4( ( pos * scale + screenPosition.xy ).xy, screenPosition.z, 1.0 );","}"].join("\n"),fragmentShader:["uniform lowp int renderType;","uniform sampler2D map;","uniform float opacity;","uniform vec3 color;","varying vec2 vUV;","varying float vVisibility;","void main() {","if( renderType == 0 ) {","gl_FragColor = vec4( 1.0, 0.0, 1.0, 0.0 );","} else if( renderType == 1 ) {","gl_FragColor = texture2D( map, vUV );","} else {","vec4 texture = texture2D( map, vUV );","texture.a *= opacity * vVisibility;","gl_FragColor = texture;","gl_FragColor.rgb *= color;","}","}"].join("\n")}:{vertexShader:["uniform lowp int renderType;","uniform vec3 screenPosition;","uniform vec2 scale;","uniform float rotation;","attribute vec2 position;","attribute vec2 uv;","varying vec2 vUV;","void main() {","vUV = uv;","vec2 pos = position;","if( renderType == 2 ) {","pos.x = cos( rotation ) * position.x - sin( rotation ) * position.y;","pos.y = sin( rotation ) * position.x + cos( rotation ) * position.y;","}","gl_Position = vec4( ( pos * scale + screenPosition.xy ).xy, screenPosition.z, 1.0 );","}"].join("\n"),fragmentShader:["precision mediump float;","uniform lowp int renderType;","uniform sampler2D map;","uniform sampler2D occlusionMap;","uniform float opacity;","uniform vec3 color;","varying vec2 vUV;","void main() {","if( renderType == 0 ) {","gl_FragColor = vec4( texture2D( map, vUV ).rgb, 0.0 );","} else if( renderType == 1 ) {","gl_FragColor = texture2D( map, vUV );","} else {","float visibility = texture2D( occlusionMap, vec2( 0.5, 0.1 ) ).a;","visibility += texture2D( occlusionMap, vec2( 0.9, 0.5 ) ).a;","visibility += texture2D( occlusionMap, vec2( 0.5, 0.9 ) ).a;","visibility += texture2D( occlusionMap, vec2( 0.1, 0.5 ) ).a;","visibility = ( 1.0 - visibility / 4.0 );","vec4 texture = texture2D( map, vUV );","texture.a *= opacity * visibility;","gl_FragColor = texture;","gl_FragColor.rgb *= color;","}","}"].join("\n")},o=n(p),s={vertex:c.getAttribLocation(o,"position"),uv:c.getAttribLocation(o,"uv")},a={renderType:c.getUniformLocation(o,"renderType"),map:c.getUniformLocation(o,"map"),occlusionMap:c.getUniformLocation(o,"occlusionMap"),opacity:c.getUniformLocation(o,"opacity"),color:c.getUniformLocation(o,"color"),scale:c.getUniformLocation(o,"scale"),rotation:c.getUniformLocation(o,"rotation"),screenPosition:c.getUniformLocation(o,"screenPosition")}};this.render=function(n,f,d,m){if(0!==e.length){var v=new THREE.Vector3,g=m/d,E=.5*d,y=.5*m,_=16/m,b=new THREE.Vector2(_*g,_),T=new THREE.Vector3(1,1,0),x=new THREE.Vector2(1,1);void 0===o&&p(),c.useProgram(o),c.enableVertexAttribArray(s.vertex),c.enableVertexAttribArray(s.uv),c.uniform1i(a.occlusionMap,0),c.uniform1i(a.map,1),c.bindBuffer(c.ARRAY_BUFFER,r),c.vertexAttribPointer(s.vertex,2,c.FLOAT,!1,16,0),c.vertexAttribPointer(s.uv,2,c.FLOAT,!1,16,8),c.bindBuffer(c.ELEMENT_ARRAY_BUFFER,i),c.disable(c.CULL_FACE),c.depthMask(!1);for(var w=0,R=e.length;R>w;w++){_=16/m,b.set(_*g,_);var H=e[w];if(v.set(H.matrixWorld.elements[12],H.matrixWorld.elements[13],H.matrixWorld.elements[14]),v.applyMatrix4(f.matrixWorldInverse),v.applyProjection(f.projectionMatrix),T.copy(v),x.x=T.x*E+E,x.y=T.y*y+y,u||x.x>0&&x.x0&&x.yM;M++){var k=H.lensFlares[M];k.opacity>.001&&k.scale>.001&&(T.x=k.x,T.y=k.y,T.z=k.z,_=k.size*k.scale/m,b.x=_*g,b.y=_,c.uniform3f(a.screenPosition,T.x,T.y,T.z),c.uniform2f(a.scale,b.x,b.y),c.uniform1f(a.rotation,k.rotation),c.uniform1f(a.opacity,k.opacity),c.uniform3f(a.color,k.color.r,k.color.g,k.color.b),t.state.setBlending(k.blending,k.blendEquation,k.blendSrc,k.blendDst),t.setTexture(k.texture,1),c.drawElements(c.TRIANGLES,6,c.UNSIGNED_SHORT,0))}}}c.enable(c.CULL_FACE),c.enable(c.DEPTH_TEST),c.depthMask(!0),t.resetGLState()}}},THREE.ShadowMapPlugin=function(t,e,n,r){function i(t,e,r){if(e.visible){var o=n[e.id];if(o&&e.castShadow&&(e.frustumCulled===!1||d.intersectsObject(e)===!0))for(var s=0,a=o.length;a>s;s++){var u=o[s];e._modelViewMatrix.multiplyMatrices(r.matrixWorldInverse,e.matrixWorld),y.push(u)}for(var s=0,a=e.children.length;a>s;s++)i(t,e.children[s],r)}}function o(t,e){var n=new THREE.DirectionalLight;n.isVirtual=!0,n.onlyShadow=!0,n.castShadow=!0,n.shadowCameraNear=t.shadowCameraNear,n.shadowCameraFar=t.shadowCameraFar,n.shadowCameraLeft=t.shadowCameraLeft,n.shadowCameraRight=t.shadowCameraRight,n.shadowCameraBottom=t.shadowCameraBottom,n.shadowCameraTop=t.shadowCameraTop,n.shadowCameraVisible=t.shadowCameraVisible,n.shadowDarkness=t.shadowDarkness, -n.shadowBias=t.shadowCascadeBias[e],n.shadowMapWidth=t.shadowCascadeWidth[e],n.shadowMapHeight=t.shadowCascadeHeight[e],n.pointsWorld=[],n.pointsFrustum=[];for(var r=n.pointsWorld,i=n.pointsFrustum,o=0;8>o;o++)r[o]=new THREE.Vector3,i[o]=new THREE.Vector3;var s=t.shadowCascadeNearZ[e],a=t.shadowCascadeFarZ[e];return i[0].set(-1,-1,s),i[1].set(1,-1,s),i[2].set(-1,1,s),i[3].set(1,1,s),i[4].set(-1,-1,a),i[5].set(1,-1,a),i[6].set(-1,1,a),i[7].set(1,1,a),n}function s(t,e){var n=t.shadowCascadeArray[e];n.position.copy(t.position),n.target.position.copy(t.target.position),n.lookAt(n.target),n.shadowCameraVisible=t.shadowCameraVisible,n.shadowDarkness=t.shadowDarkness,n.shadowBias=t.shadowCascadeBias[e];var r=t.shadowCascadeNearZ[e],i=t.shadowCascadeFarZ[e],o=n.pointsFrustum;o[0].z=r,o[1].z=r,o[2].z=r,o[3].z=r,o[4].z=i,o[5].z=i,o[6].z=i,o[7].z=i}function a(t,e){var n=e.shadowCamera,r=e.pointsFrustum,i=e.pointsWorld;v.set(1/0,1/0,1/0),g.set(-(1/0),-(1/0),-(1/0));for(var o=0;8>o;o++){var s=i[o];s.copy(r[o]),s.unproject(t),s.applyMatrix4(n.matrixWorldInverse),s.xg.x&&(g.x=s.x),s.yg.y&&(g.y=s.y),s.zg.z&&(g.z=s.z)}n.left=v.x,n.right=g.x,n.top=g.y,n.bottom=v.y,n.updateProjectionMatrix()}function u(t){return t.material instanceof THREE.MeshFaceMaterial?t.material.materials[0]:t.material}var h,l,c,p,f=t.context,d=new THREE.Frustum,m=new THREE.Matrix4,v=new THREE.Vector3,g=new THREE.Vector3,E=new THREE.Vector3,y=[],_=THREE.ShaderLib.depthRGBA,b=THREE.UniformsUtils.clone(_.uniforms);h=new THREE.ShaderMaterial({uniforms:b,vertexShader:_.vertexShader,fragmentShader:_.fragmentShader}),l=new THREE.ShaderMaterial({uniforms:b,vertexShader:_.vertexShader,fragmentShader:_.fragmentShader,morphTargets:!0}),c=new THREE.ShaderMaterial({uniforms:b,vertexShader:_.vertexShader,fragmentShader:_.fragmentShader,skinning:!0}),p=new THREE.ShaderMaterial({uniforms:b,vertexShader:_.vertexShader,fragmentShader:_.fragmentShader,morphTargets:!0,skinning:!0}),h._shadowPass=!0,l._shadowPass=!0,c._shadowPass=!0,p._shadowPass=!0,this.render=function(n,v){if(t.shadowMapEnabled!==!1){var g,_,b,T,x,w,R,H,M,S,k,A,C,P=[],L=0,z=null;for(f.clearColor(1,1,1,1),f.disable(f.BLEND),f.enable(f.CULL_FACE),f.frontFace(f.CCW),t.shadowMapCullFace===THREE.CullFaceFront?f.cullFace(f.FRONT):f.cullFace(f.BACK),t.state.setDepthTest(!0),g=0,_=e.length;_>g;g++)if(C=e[g],C.castShadow)if(C instanceof THREE.DirectionalLight&&C.shadowCascade)for(x=0;xg;g++){if(C=P[g],!C.shadowMap){var F=THREE.LinearFilter;t.shadowMapType===THREE.PCFSoftShadowMap&&(F=THREE.NearestFilter);var U={minFilter:F,magFilter:F,format:THREE.RGBAFormat};C.shadowMap=new THREE.WebGLRenderTarget(C.shadowMapWidth,C.shadowMapHeight,U),C.shadowMapSize=new THREE.Vector2(C.shadowMapWidth,C.shadowMapHeight),C.shadowMatrix=new THREE.Matrix4}if(!C.shadowCamera){if(C instanceof THREE.SpotLight)C.shadowCamera=new THREE.PerspectiveCamera(C.shadowCameraFov,C.shadowMapWidth/C.shadowMapHeight,C.shadowCameraNear,C.shadowCameraFar);else{if(!(C instanceof THREE.DirectionalLight)){THREE.error("THREE.ShadowMapPlugin: Unsupported light type for shadow",C);continue}C.shadowCamera=new THREE.OrthographicCamera(C.shadowCameraLeft,C.shadowCameraRight,C.shadowCameraTop,C.shadowCameraBottom,C.shadowCameraNear,C.shadowCameraFar)}n.add(C.shadowCamera),n.autoUpdate===!0&&n.updateMatrixWorld()}C.shadowCameraVisible&&!C.cameraHelper&&(C.cameraHelper=new THREE.CameraHelper(C.shadowCamera),n.add(C.cameraHelper)),C.isVirtual&&O.originalCamera==v&&a(v,C),w=C.shadowMap,R=C.shadowMatrix,H=C.shadowCamera,H.position.setFromMatrixPosition(C.matrixWorld),E.setFromMatrixPosition(C.target.matrixWorld),H.lookAt(E),H.updateMatrixWorld(),H.matrixWorldInverse.getInverse(H.matrixWorld),C.cameraHelper&&(C.cameraHelper.visible=C.shadowCameraVisible),C.shadowCameraVisible&&C.cameraHelper.update(),R.set(.5,0,0,.5,0,.5,0,.5,0,0,.5,.5,0,0,0,1),R.multiply(H.projectionMatrix),R.multiply(H.matrixWorldInverse),m.multiplyMatrices(H.projectionMatrix,H.matrixWorldInverse),d.setFromMatrix(m),t.setRenderTarget(w),t.clear(),y.length=0,i(n,n,H);var B,N,V;for(b=0,T=y.length;T>b;b++)k=y[b],A=k.object,M=k.buffer,B=u(A),N=void 0!==A.geometry.morphTargets&&A.geometry.morphTargets.length>0&&B.morphTargets,V=A instanceof THREE.SkinnedMesh&&B.skinning,S=A.customDepthMaterial?A.customDepthMaterial:V?N?p:c:N?l:h,t.setMaterialFaces(B),M instanceof THREE.BufferGeometry?t.renderBufferDirect(H,e,z,S,M,A):t.renderBuffer(H,e,z,S,M,A);for(b=0,T=r.length;T>b;b++)k=r[b],A=k.object,A.visible&&A.castShadow&&(A._modelViewMatrix.multiplyMatrices(H.matrixWorldInverse,A.matrixWorld),t.renderImmediateObject(H,e,z,h,A))}var I=t.getClearColor(),j=t.getClearAlpha();f.clearColor(I.r,I.g,I.b,j),f.enable(f.BLEND),t.shadowMapCullFace===THREE.CullFaceFront&&f.cullFace(f.BACK),t.resetGLState()}}},THREE.SpritePlugin=function(t,e){function n(){var e=l.createProgram(),n=l.createShader(l.VERTEX_SHADER),r=l.createShader(l.FRAGMENT_SHADER);return l.shaderSource(n,["precision "+t.getPrecision()+" float;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform float rotation;","uniform vec2 scale;","uniform vec2 uvOffset;","uniform vec2 uvScale;","attribute vec2 position;","attribute vec2 uv;","varying vec2 vUV;","void main() {","vUV = uvOffset + uv * uvScale;","vec2 alignedPosition = position * scale;","vec2 rotatedPosition;","rotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y;","rotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y;","vec4 finalPosition;","finalPosition = modelViewMatrix * vec4( 0.0, 0.0, 0.0, 1.0 );","finalPosition.xy += rotatedPosition;","finalPosition = projectionMatrix * finalPosition;","gl_Position = finalPosition;","}"].join("\n")),l.shaderSource(r,["precision "+t.getPrecision()+" float;","uniform vec3 color;","uniform sampler2D map;","uniform float opacity;","uniform int fogType;","uniform vec3 fogColor;","uniform float fogDensity;","uniform float fogNear;","uniform float fogFar;","uniform float alphaTest;","varying vec2 vUV;","void main() {","vec4 texture = texture2D( map, vUV );","if ( texture.a < alphaTest ) discard;","gl_FragColor = vec4( color * texture.xyz, texture.a * opacity );","if ( fogType > 0 ) {","float depth = gl_FragCoord.z / gl_FragCoord.w;","float fogFactor = 0.0;","if ( fogType == 1 ) {","fogFactor = smoothstep( fogNear, fogFar, depth );","} else {","const float LOG2 = 1.442695;","float fogFactor = exp2( - fogDensity * fogDensity * depth * depth * LOG2 );","fogFactor = 1.0 - clamp( fogFactor, 0.0, 1.0 );","}","gl_FragColor = mix( gl_FragColor, vec4( fogColor, gl_FragColor.w ), fogFactor );","}","}"].join("\n")),l.compileShader(n),l.compileShader(r),l.attachShader(e,n),l.attachShader(e,r),l.linkProgram(e),e}function r(t,e){return t.z!==e.z?e.z-t.z:e.id-t.id}var i,o,s,a,u,h,l=t.context,c=new THREE.Vector3,p=new THREE.Quaternion,f=new THREE.Vector3,d=function(){var t=new Float32Array([-.5,-.5,0,0,.5,-.5,1,0,.5,.5,1,1,-.5,.5,0,1]),e=new Uint16Array([0,1,2,0,2,3]);i=l.createBuffer(),o=l.createBuffer(),l.bindBuffer(l.ARRAY_BUFFER,i),l.bufferData(l.ARRAY_BUFFER,t,l.STATIC_DRAW),l.bindBuffer(l.ELEMENT_ARRAY_BUFFER,o),l.bufferData(l.ELEMENT_ARRAY_BUFFER,e,l.STATIC_DRAW),s=n(),a={position:l.getAttribLocation(s,"position"),uv:l.getAttribLocation(s,"uv")},u={uvOffset:l.getUniformLocation(s,"uvOffset"),uvScale:l.getUniformLocation(s,"uvScale"),rotation:l.getUniformLocation(s,"rotation"),scale:l.getUniformLocation(s,"scale"),color:l.getUniformLocation(s,"color"),map:l.getUniformLocation(s,"map"),opacity:l.getUniformLocation(s,"opacity"),modelViewMatrix:l.getUniformLocation(s,"modelViewMatrix"),projectionMatrix:l.getUniformLocation(s,"projectionMatrix"),fogType:l.getUniformLocation(s,"fogType"),fogDensity:l.getUniformLocation(s,"fogDensity"),fogNear:l.getUniformLocation(s,"fogNear"),fogFar:l.getUniformLocation(s,"fogFar"),fogColor:l.getUniformLocation(s,"fogColor"),alphaTest:l.getUniformLocation(s,"alphaTest")};var r=document.createElement("canvas");r.width=8,r.height=8;var c=r.getContext("2d");c.fillStyle="white",c.fillRect(0,0,8,8),h=new THREE.Texture(r),h.needsUpdate=!0};this.render=function(n,m){if(0!==e.length){void 0===s&&d(),l.useProgram(s),l.enableVertexAttribArray(a.position),l.enableVertexAttribArray(a.uv),l.disable(l.CULL_FACE),l.enable(l.BLEND),l.bindBuffer(l.ARRAY_BUFFER,i),l.vertexAttribPointer(a.position,2,l.FLOAT,!1,16,0),l.vertexAttribPointer(a.uv,2,l.FLOAT,!1,16,8),l.bindBuffer(l.ELEMENT_ARRAY_BUFFER,o),l.uniformMatrix4fv(u.projectionMatrix,!1,m.projectionMatrix.elements),l.activeTexture(l.TEXTURE0),l.uniform1i(u.map,0);var v=0,g=0,E=n.fog;E?(l.uniform3f(u.fogColor,E.color.r,E.color.g,E.color.b),E instanceof THREE.Fog?(l.uniform1f(u.fogNear,E.near),l.uniform1f(u.fogFar,E.far),l.uniform1i(u.fogType,1),v=1,g=1):E instanceof THREE.FogExp2&&(l.uniform1f(u.fogDensity,E.density),l.uniform1i(u.fogType,2),v=2,g=2)):(l.uniform1i(u.fogType,0),v=0,g=0);for(var y=0,_=e.length;_>y;y++){var b=e[y];b._modelViewMatrix.multiplyMatrices(m.matrixWorldInverse,b.matrixWorld),b.z=-b._modelViewMatrix.elements[14]}e.sort(r);for(var T=[],y=0,_=e.length;_>y;y++){var b=e[y],x=b.material;l.uniform1f(u.alphaTest,x.alphaTest),l.uniformMatrix4fv(u.modelViewMatrix,!1,b._modelViewMatrix.elements),b.matrixWorld.decompose(c,p,f),T[0]=f.x,T[1]=f.y;var w=0;n.fog&&x.fog&&(w=g),v!==w&&(l.uniform1i(u.fogType,w),v=w),null!==x.map?(l.uniform2f(u.uvOffset,x.map.offset.x,x.map.offset.y),l.uniform2f(u.uvScale,x.map.repeat.x,x.map.repeat.y)):(l.uniform2f(u.uvOffset,0,0),l.uniform2f(u.uvScale,1,1)),l.uniform1f(u.opacity,x.opacity),l.uniform3f(u.color,x.color.r,x.color.g,x.color.b),l.uniform1f(u.rotation,x.rotation),l.uniform2fv(u.scale,T),t.state.setBlending(x.blending,x.blendEquation,x.blendSrc,x.blendDst),t.state.setDepthTest(x.depthTest),t.state.setDepthWrite(x.depthWrite),x.map&&x.map.image&&x.map.image.width?t.setTexture(x.map,0):t.setTexture(h,0),l.drawElements(l.TRIANGLES,6,l.UNSIGNED_SHORT,0)}l.enable(l.CULL_FACE),t.resetGLState()}}},THREE.GeometryUtils={merge:function(t,e,n){THREE.warn("THREE.GeometryUtils: .merge() has been moved to Geometry. Use geometry.merge( geometry2, matrix, materialIndexOffset ) instead.");var r;e instanceof THREE.Mesh&&(e.matrixAutoUpdate&&e.updateMatrix(),r=e.matrix,e=e.geometry),t.merge(e,r,n)},center:function(t){return THREE.warn("THREE.GeometryUtils: .center() has been moved to Geometry. Use geometry.center() instead."),t.center()}},THREE.ImageUtils={crossOrigin:void 0,loadTexture:function(t,e,n,r){var i=new THREE.ImageLoader;i.crossOrigin=this.crossOrigin;var o=new THREE.Texture(void 0,e);return i.load(t,function(t){o.image=t,o.needsUpdate=!0,n&&n(o)},void 0,function(t){r&&r(t)}),o.sourceFile=t,o},loadTextureCube:function(t,e,n,r){var i=[],o=new THREE.ImageLoader;o.crossOrigin=this.crossOrigin;var s=new THREE.CubeTexture(i,e);s.flipY=!1;for(var a=0,u=function(e){o.load(t[e],function(t){s.images[e]=t,a+=1,6===a&&(s.needsUpdate=!0,n&&n(s))},void 0,r)},h=0,l=t.length;l>h;++h)u(h);return s},loadCompressedTexture:function(){THREE.error("THREE.ImageUtils.loadCompressedTexture has been removed. Use THREE.DDSLoader instead.")},loadCompressedTextureCube:function(){THREE.error("THREE.ImageUtils.loadCompressedTextureCube has been removed. Use THREE.DDSLoader instead.")},getNormalMap:function(t,e){var n=function(t,e){return[t[1]*e[2]-t[2]*e[1],t[2]*e[0]-t[0]*e[2],t[0]*e[1]-t[1]*e[0]]},r=function(t,e){return[t[0]-e[0],t[1]-e[1],t[2]-e[2]]},i=function(t){var e=Math.sqrt(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]);return[t[0]/e,t[1]/e,t[2]/e]};e=1|e;var o=t.width,s=t.height,a=document.createElement("canvas");a.width=o,a.height=s;var u=a.getContext("2d");u.drawImage(t,0,0);for(var h=u.getImageData(0,0,o,s).data,l=u.createImageData(o,s),c=l.data,p=0;o>p;p++)for(var f=0;s>f;f++){var d=0>f-1?0:f-1,m=f+1>s-1?s-1:f+1,v=0>p-1?0:p-1,g=p+1>o-1?o-1:p+1,E=[],y=[0,0,h[4*(f*o+p)]/255*e];E.push([-1,0,h[4*(f*o+v)]/255*e]),E.push([-1,-1,h[4*(d*o+v)]/255*e]),E.push([0,-1,h[4*(d*o+p)]/255*e]),E.push([1,-1,h[4*(d*o+g)]/255*e]),E.push([1,0,h[4*(f*o+g)]/255*e]),E.push([1,1,h[4*(m*o+g)]/255*e]),E.push([0,1,h[4*(m*o+p)]/255*e]),E.push([-1,1,h[4*(m*o+v)]/255*e]);for(var _=[],b=E.length,T=0;b>T;T++){var x=E[T],w=E[(T+1)%b];x=r(x,y),w=r(w,y),_.push(i(n(x,w)))}for(var R=[0,0,0],T=0;T<_.length;T++)R[0]+=_[T][0],R[1]+=_[T][1],R[2]+=_[T][2];R[0]/=_.length,R[1]/=_.length,R[2]/=_.length;var H=4*(f*o+p);c[H]=(R[0]+1)/2*255|0,c[H+1]=(R[1]+1)/2*255|0,c[H+2]=255*R[2]|0,c[H+3]=255}return u.putImageData(l,0,0),a},generateDataTexture:function(t,e,n){for(var r=t*e,i=new Uint8Array(3*r),o=Math.floor(255*n.r),s=Math.floor(255*n.g),a=Math.floor(255*n.b),u=0;r>u;u++)i[3*u]=o,i[3*u+1]=s,i[3*u+2]=a;var h=new THREE.DataTexture(i,t,e,THREE.RGBFormat);return h.needsUpdate=!0,h}},THREE.SceneUtils={createMultiMaterialObject:function(t,e){for(var n=new THREE.Object3D,r=0,i=e.length;i>r;r++)n.add(new THREE.Mesh(t,e[r]));return n},detach:function(t,e,n){t.applyMatrix(e.matrixWorld),e.remove(t),n.add(t)},attach:function(t,e,n){var r=new THREE.Matrix4;r.getInverse(n.matrixWorld),t.applyMatrix(r),e.remove(t),n.add(t)}},THREE.FontUtils={faces:{},face:"helvetiker",weight:"normal",style:"normal",size:150,divisions:10,getFace:function(){try{return this.faces[this.face][this.weight][this.style]}catch(t){throw"The font "+this.face+" with "+this.weight+" weight and "+this.style+" style is missing."}},loadFace:function(t){var e=t.familyName.toLowerCase(),n=this;return n.faces[e]=n.faces[e]||{},n.faces[e][t.cssFontWeight]=n.faces[e][t.cssFontWeight]||{},n.faces[e][t.cssFontWeight][t.cssFontStyle]=t,n.faces[e][t.cssFontWeight][t.cssFontStyle]=t,t},drawText:function(t){var e,n=this.getFace(),r=this.size/n.resolution,i=0,o=String(t).split(""),s=o.length,a=[];for(e=0;s>e;e++){var u=new THREE.Path,h=this.extractGlyphPoints(o[e],n,r,i,u);i+=h.offset,a.push(h.path)}var l=i/2;return{paths:a,offset:l}},extractGlyphPoints:function(t,e,n,r,i){var o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w=[],R=e.glyphs[t]||e.glyphs["?"];if(R){if(R.o)for(u=R._cachedOutline||(R._cachedOutline=R.o.split(" ")),l=u.length,c=n,p=n,o=0;l>o;)switch(h=u[o++]){case"m":f=u[o++]*c+r,d=u[o++]*p,i.moveTo(f,d);break;case"l":f=u[o++]*c+r,d=u[o++]*p,i.lineTo(f,d);break;case"q":if(m=u[o++]*c+r,v=u[o++]*p,y=u[o++]*c+r,_=u[o++]*p,i.quadraticCurveTo(y,_,m,v),x=w[w.length-1])for(g=x.x,E=x.y,s=1,a=this.divisions;a>=s;s++){var H=s/a;THREE.Shape.Utils.b2(H,g,y,m),THREE.Shape.Utils.b2(H,E,_,v)}break;case"b":if(m=u[o++]*c+r,v=u[o++]*p,y=u[o++]*c+r,_=u[o++]*p,b=u[o++]*c+r,T=u[o++]*p,i.bezierCurveTo(y,_,b,T,m,v),x=w[w.length-1])for(g=x.x,E=x.y,s=1,a=this.divisions;a>=s;s++){var H=s/a;THREE.Shape.Utils.b3(H,g,y,b,m),THREE.Shape.Utils.b3(H,E,_,T,v)}}return{offset:R.ha*n,path:i}}}},THREE.FontUtils.generateShapes=function(t,e){e=e||{};var n=void 0!==e.size?e.size:100,r=void 0!==e.curveSegments?e.curveSegments:4,i=void 0!==e.font?e.font:"helvetiker",o=void 0!==e.weight?e.weight:"normal",s=void 0!==e.style?e.style:"normal";THREE.FontUtils.size=n,THREE.FontUtils.divisions=r,THREE.FontUtils.face=i,THREE.FontUtils.weight=o,THREE.FontUtils.style=s;for(var a=THREE.FontUtils.drawText(t),u=a.paths,h=[],l=0,c=u.length;c>l;l++)Array.prototype.push.apply(h,u[l].toShapes());return h},function(t){var e=1e-10,n=function(t,e){var n=t.length;if(3>n)return null;var o,s,a,u=[],h=[],l=[];if(r(t)>0)for(s=0;n>s;s++)h[s]=s;else for(s=0;n>s;s++)h[s]=n-1-s;var c=n,p=2*c;for(s=c-1;c>2;){if(p--<=0)return THREE.warn("THREE.FontUtils: Warning, unable to triangulate polygon! in Triangulate.process()"),e?l:u;if(o=s,o>=c&&(o=0),s=o+1,s>=c&&(s=0),a=s+1,a>=c&&(a=0),i(t,o,s,a,c,h)){var f,d,m,v,g;for(f=h[o],d=h[s],m=h[a],u.push([t[f],t[d],t[m]]),l.push([h[o],h[s],h[a]]),v=s,g=s+1;c>g;v++,g++)h[v]=h[g];c--,p=2*c}}return e?l:u},r=function(t){for(var e=t.length,n=0,r=e-1,i=0;e>i;r=i++)n+=t[r].x*t[i].y-t[i].x*t[r].y;return.5*n},i=function(t,n,r,i,o,s){var a,u,h,l,c,p,f,d,m;if(u=t[s[n]].x,h=t[s[n]].y,l=t[s[r]].x,c=t[s[r]].y,p=t[s[i]].x,f=t[s[i]].y,e>(l-u)*(f-h)-(c-h)*(p-u))return!1;var v,g,E,y,_,b,T,x,w,R,H,M,S,k,A;for(v=p-l,g=f-c,E=u-p,y=h-f,_=l-u,b=c-h,a=0;o>a;a++)if(d=t[s[a]].x,m=t[s[a]].y,!(d===u&&m===h||d===l&&m===c||d===p&&m===f)&&(T=d-u,x=m-h,w=d-l,R=m-c,H=d-p,M=m-f,A=v*R-g*w,S=_*x-b*T,k=E*M-y*H,A>=-e&&k>=-e&&S>=-e))return!1;return!0};return t.Triangulate=n,t.Triangulate.area=r,t}(THREE.FontUtils),self._typeface_js={faces:THREE.FontUtils.faces,loadFace:THREE.FontUtils.loadFace},THREE.typeface_js=self._typeface_js,THREE.Audio=function(t){THREE.Object3D.call(this),this.type="Audio",this.context=t.context,this.source=this.context.createBufferSource(),this.source.onended=this.onEnded.bind(this),this.gain=this.context.createGain(),this.gain.connect(this.context.destination),this.panner=this.context.createPanner(),this.panner.connect(this.gain),this.autoplay=!1,this.startTime=0,this.isPlaying=!1},THREE.Audio.prototype=Object.create(THREE.Object3D.prototype),THREE.Audio.prototype.constructor=THREE.Audio,THREE.Audio.prototype.load=function(t){var e=this,n=new XMLHttpRequest;return n.open("GET",t,!0),n.responseType="arraybuffer",n.onload=function(t){e.context.decodeAudioData(this.response,function(t){e.source.buffer=t,e.autoplay&&e.play()})},n.send(),this},THREE.Audio.prototype.play=function(){if(this.isPlaying===!0)return void THREE.warn("THREE.Audio: Audio is already playing.");var t=this.context.createBufferSource();t.buffer=this.source.buffer,t.loop=this.source.loop,t.onended=this.source.onended,t.connect(this.panner),t.start(0,this.startTime),this.isPlaying=!0,this.source=t},THREE.Audio.prototype.pause=function(){this.source.stop(),this.startTime=this.context.currentTime},THREE.Audio.prototype.stop=function(){this.source.stop(),this.startTime=0},THREE.Audio.prototype.onEnded=function(){this.isPlaying=!1},THREE.Audio.prototype.setLoop=function(t){this.source.loop=t},THREE.Audio.prototype.setRefDistance=function(t){this.panner.refDistance=t},THREE.Audio.prototype.setRolloffFactor=function(t){this.panner.rolloffFactor=t},THREE.Audio.prototype.setVolume=function(t){this.gain.gain.value=t},THREE.Audio.prototype.updateMatrixWorld=function(){var t=new THREE.Vector3;return function(e){THREE.Object3D.prototype.updateMatrixWorld.call(this,e),t.setFromMatrixPosition(this.matrixWorld),this.panner.setPosition(t.x,t.y,t.z)}}(),THREE.AudioListener=function(){THREE.Object3D.call(this),this.type="AudioListener",this.context=new(window.AudioContext||window.webkitAudioContext)},THREE.AudioListener.prototype=Object.create(THREE.Object3D.prototype),THREE.AudioListener.prototype.constructor=THREE.AudioListener,THREE.AudioListener.prototype.updateMatrixWorld=function(){var t=new THREE.Vector3,e=new THREE.Quaternion,n=new THREE.Vector3,r=new THREE.Vector3,i=new THREE.Vector3,o=new THREE.Vector3;return function(s){THREE.Object3D.prototype.updateMatrixWorld.call(this,s);var a=this.context.listener,u=this.up;this.matrixWorld.decompose(t,e,n),r.set(0,0,-1).applyQuaternion(e),i.subVectors(t,o),a.setPosition(t.x,t.y,t.z),a.setOrientation(r.x,r.y,r.z,u.x,u.y,u.z),a.setVelocity(i.x,i.y,i.z),o.copy(t)}}(),THREE.Curve=function(){},THREE.Curve.prototype.getPoint=function(t){return THREE.warn("THREE.Curve: Warning, getPoint() not implemented!"),null},THREE.Curve.prototype.getPointAt=function(t){var e=this.getUtoTmapping(t);return this.getPoint(e)},THREE.Curve.prototype.getPoints=function(t){t||(t=5);var e,n=[];for(e=0;t>=e;e++)n.push(this.getPoint(e/t));return n},THREE.Curve.prototype.getSpacedPoints=function(t){t||(t=5);var e,n=[];for(e=0;t>=e;e++)n.push(this.getPointAt(e/t));return n},THREE.Curve.prototype.getLength=function(){var t=this.getLengths();return t[t.length-1]},THREE.Curve.prototype.getLengths=function(t){if(t||(t=this.__arcLengthDivisions?this.__arcLengthDivisions:200),this.cacheArcLengths&&this.cacheArcLengths.length==t+1&&!this.needsUpdate)return this.cacheArcLengths;this.needsUpdate=!1;var e,n,r=[],i=this.getPoint(0),o=0;for(r.push(0),n=1;t>=n;n++)e=this.getPoint(n/t),o+=e.distanceTo(i),r.push(o),i=e;return this.cacheArcLengths=r,r},THREE.Curve.prototype.updateArcLengths=function(){this.needsUpdate=!0,this.getLengths()},THREE.Curve.prototype.getUtoTmapping=function(t,e){var n,r=this.getLengths(),i=0,o=r.length;n=e?e:t*r[o-1];for(var s,a=0,u=o-1;u>=a;)if(i=Math.floor(a+(u-a)/2),s=r[i]-n,0>s)a=i+1;else{if(!(s>0)){u=i;break}u=i-1}if(i=u,r[i]==n){var h=i/(o-1);return h}var l=r[i],c=r[i+1],p=c-l,f=(n-l)/p,h=(i+f)/(o-1);return h},THREE.Curve.prototype.getTangent=function(t){var e=1e-4,n=t-e,r=t+e;0>n&&(n=0),r>1&&(r=1);var i=this.getPoint(n),o=this.getPoint(r),s=o.clone().sub(i);return s.normalize()},THREE.Curve.prototype.getTangentAt=function(t){var e=this.getUtoTmapping(t);return this.getTangent(e)},THREE.Curve.Utils={tangentQuadraticBezier:function(t,e,n,r){return 2*(1-t)*(n-e)+2*t*(r-n)},tangentCubicBezier:function(t,e,n,r,i){return-3*e*(1-t)*(1-t)+3*n*(1-t)*(1-t)-6*t*n*(1-t)+6*t*r*(1-t)-3*t*t*r+3*t*t*i},tangentSpline:function(t,e,n,r,i){var o=6*t*t-6*t,s=3*t*t-4*t+1,a=-6*t*t+6*t,u=3*t*t-2*t;return o+s+a+u},interpolate:function(t,e,n,r,i){var o=.5*(n-t),s=.5*(r-e),a=i*i,u=i*a;return(2*e-2*n+o+s)*u+(-3*e+3*n-2*o-s)*a+o*i+e}},THREE.Curve.create=function(t,e){return t.prototype=Object.create(THREE.Curve.prototype),t.prototype.constructor=t,t.prototype.getPoint=e,t},THREE.CurvePath=function(){this.curves=[],this.bends=[],this.autoClose=!1},THREE.CurvePath.prototype=Object.create(THREE.Curve.prototype),THREE.CurvePath.prototype.constructor=THREE.CurvePath,THREE.CurvePath.prototype.add=function(t){this.curves.push(t)},THREE.CurvePath.prototype.checkConnection=function(){},THREE.CurvePath.prototype.closePath=function(){var t=this.curves[0].getPoint(0),e=this.curves[this.curves.length-1].getPoint(1);t.equals(e)||this.curves.push(new THREE.LineCurve(e,t))},THREE.CurvePath.prototype.getPoint=function(t){for(var e,n,r=t*this.getLength(),i=this.getCurveLengths(),o=0;o=r){e=i[o]-r,n=this.curves[o];var s=1-e/n.getLength();return n.getPointAt(s)}o++}return null},THREE.CurvePath.prototype.getLength=function(){var t=this.getCurveLengths();return t[t.length-1]},THREE.CurvePath.prototype.getCurveLengths=function(){if(this.cacheLengths&&this.cacheLengths.length==this.curves.length)return this.cacheLengths;var t,e=[],n=0,r=this.curves.length;for(t=0;r>t;t++)n+=this.curves[t].getLength(),e.push(n);return this.cacheLengths=e,e},THREE.CurvePath.prototype.getBoundingBox=function(){var t,e,n,r,i,o,s=this.getPoints();t=e=Number.NEGATIVE_INFINITY,r=i=Number.POSITIVE_INFINITY;var a,u,h,l,c=s[0]instanceof THREE.Vector3;for(l=c?new THREE.Vector3:new THREE.Vector2,u=0,h=s.length;h>u;u++)a=s[u],a.x>t?t=a.x:a.xe?e=a.y:a.yn?n=a.z:a.zn;n++)i=this.getWrapPoints(i,e[n]);return i},THREE.CurvePath.prototype.getTransformedSpacedPoints=function(t,e){var n,r,i=this.getSpacedPoints(t);for(e||(e=this.bends),n=0,r=e.length;r>n;n++)i=this.getWrapPoints(i,e[n]);return i},THREE.CurvePath.prototype.getWrapPoints=function(t,e){var n,r,i,o,s,a,u=this.getBoundingBox();for(n=0,r=t.length;r>n;n++){i=t[n],o=i.x,s=i.y,a=o/u.maxX,a=e.getUtoTmapping(a,o);var h=e.getPoint(a),l=e.getTangent(a);l.set(-l.y,l.x).multiplyScalar(s),i.x=h.x+l.x,i.y=h.y+l.y}return t},THREE.Gyroscope=function(){THREE.Object3D.call(this)},THREE.Gyroscope.prototype=Object.create(THREE.Object3D.prototype),THREE.Gyroscope.prototype.constructor=THREE.Gyroscope,THREE.Gyroscope.prototype.updateMatrixWorld=function(){var t=new THREE.Vector3,e=new THREE.Quaternion,n=new THREE.Vector3,r=new THREE.Vector3,i=new THREE.Quaternion,o=new THREE.Vector3;return function(s){this.matrixAutoUpdate&&this.updateMatrix(),(this.matrixWorldNeedsUpdate||s)&&(this.parent?(this.matrixWorld.multiplyMatrices(this.parent.matrixWorld,this.matrix),this.matrixWorld.decompose(r,i,o),this.matrix.decompose(t,e,n),this.matrixWorld.compose(r,e,o)):this.matrixWorld.copy(this.matrix),this.matrixWorldNeedsUpdate=!1,s=!0);for(var a=0,u=this.children.length;u>a;a++)this.children[a].updateMatrixWorld(s)}}(),THREE.Path=function(t){THREE.CurvePath.call(this),this.actions=[],t&&this.fromPoints(t)},THREE.Path.prototype=Object.create(THREE.CurvePath.prototype),THREE.Path.prototype.constructor=THREE.Path,THREE.PathActions={MOVE_TO:"moveTo",LINE_TO:"lineTo",QUADRATIC_CURVE_TO:"quadraticCurveTo",BEZIER_CURVE_TO:"bezierCurveTo",CSPLINE_THRU:"splineThru",ARC:"arc",ELLIPSE:"ellipse"},THREE.Path.prototype.fromPoints=function(t){this.moveTo(t[0].x,t[0].y);for(var e=1,n=t.length;n>e;e++)this.lineTo(t[e].x,t[e].y)},THREE.Path.prototype.moveTo=function(t,e){var n=Array.prototype.slice.call(arguments);this.actions.push({action:THREE.PathActions.MOVE_TO,args:n})},THREE.Path.prototype.lineTo=function(t,e){var n=Array.prototype.slice.call(arguments),r=this.actions[this.actions.length-1].args,i=r[r.length-2],o=r[r.length-1],s=new THREE.LineCurve(new THREE.Vector2(i,o),new THREE.Vector2(t,e));this.curves.push(s),this.actions.push({action:THREE.PathActions.LINE_TO,args:n})},THREE.Path.prototype.quadraticCurveTo=function(t,e,n,r){var i=Array.prototype.slice.call(arguments),o=this.actions[this.actions.length-1].args,s=o[o.length-2],a=o[o.length-1],u=new THREE.QuadraticBezierCurve(new THREE.Vector2(s,a),new THREE.Vector2(t,e),new THREE.Vector2(n,r));this.curves.push(u),this.actions.push({action:THREE.PathActions.QUADRATIC_CURVE_TO,args:i})},THREE.Path.prototype.bezierCurveTo=function(t,e,n,r,i,o){var s=Array.prototype.slice.call(arguments),a=this.actions[this.actions.length-1].args,u=a[a.length-2],h=a[a.length-1],l=new THREE.CubicBezierCurve(new THREE.Vector2(u,h),new THREE.Vector2(t,e),new THREE.Vector2(n,r),new THREE.Vector2(i,o));this.curves.push(l),this.actions.push({action:THREE.PathActions.BEZIER_CURVE_TO,args:s})},THREE.Path.prototype.splineThru=function(t){var e=Array.prototype.slice.call(arguments),n=this.actions[this.actions.length-1].args,r=n[n.length-2],i=n[n.length-1],o=[new THREE.Vector2(r,i)];Array.prototype.push.apply(o,t);var s=new THREE.SplineCurve(o);this.curves.push(s),this.actions.push({action:THREE.PathActions.CSPLINE_THRU,args:e})},THREE.Path.prototype.arc=function(t,e,n,r,i,o){var s=this.actions[this.actions.length-1].args,a=s[s.length-2],u=s[s.length-1];this.absarc(t+a,e+u,n,r,i,o)},THREE.Path.prototype.absarc=function(t,e,n,r,i,o){this.absellipse(t,e,n,n,r,i,o)},THREE.Path.prototype.ellipse=function(t,e,n,r,i,o,s){var a=this.actions[this.actions.length-1].args,u=a[a.length-2],h=a[a.length-1];this.absellipse(t+u,e+h,n,r,i,o,s)},THREE.Path.prototype.absellipse=function(t,e,n,r,i,o,s){var a=Array.prototype.slice.call(arguments),u=new THREE.EllipseCurve(t,e,n,r,i,o,s);this.curves.push(u);var h=u.getPoint(1);a.push(h.x),a.push(h.y),this.actions.push({action:THREE.PathActions.ELLIPSE,args:a})},THREE.Path.prototype.getSpacedPoints=function(t,e){t||(t=40);for(var n=[],r=0;t>r;r++)n.push(this.getPoint(r/t));return n},THREE.Path.prototype.getPoints=function(t,e){if(this.useSpacedPoints)return console.log("tata"),this.getSpacedPoints(t,e);t=t||12;var n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_=[];for(n=0,r=this.actions.length;r>n;n++)switch(i=this.actions[n],o=i.action,s=i.args,o){case THREE.PathActions.MOVE_TO:_.push(new THREE.Vector2(s[0],s[1]));break;case THREE.PathActions.LINE_TO:_.push(new THREE.Vector2(s[0],s[1]));break;case THREE.PathActions.QUADRATIC_CURVE_TO:for(a=s[2],u=s[3],c=s[0],p=s[1],_.length>0?(m=_[_.length-1],f=m.x,d=m.y):(m=this.actions[n-1].args,f=m[m.length-2],d=m[m.length-1]),v=1;t>=v;v++)g=v/t,E=THREE.Shape.Utils.b2(g,f,c,a),y=THREE.Shape.Utils.b2(g,d,p,u),_.push(new THREE.Vector2(E,y));break;case THREE.PathActions.BEZIER_CURVE_TO:for(a=s[4],u=s[5],c=s[0],p=s[1],h=s[2],l=s[3],_.length>0?(m=_[_.length-1],f=m.x,d=m.y):(m=this.actions[n-1].args,f=m[m.length-2],d=m[m.length-1]),v=1;t>=v;v++)g=v/t,E=THREE.Shape.Utils.b3(g,f,c,h,a),y=THREE.Shape.Utils.b3(g,d,p,l,u),_.push(new THREE.Vector2(E,y));break;case THREE.PathActions.CSPLINE_THRU:m=this.actions[n-1].args;var b=new THREE.Vector2(m[m.length-2],m[m.length-1]),T=[b],x=t*s[0].length;T=T.concat(s[0]);var w=new THREE.SplineCurve(T);for(v=1;x>=v;v++)_.push(w.getPointAt(v/x));break;case THREE.PathActions.ARC:var R,H=s[0],M=s[1],S=s[2],k=s[3],A=s[4],C=!!s[5],P=A-k,L=2*t;for(v=1;L>=v;v++)g=v/L,C||(g=1-g),R=k+g*P,E=H+S*Math.cos(R),y=M+S*Math.sin(R),_.push(new THREE.Vector2(E,y));break;case THREE.PathActions.ELLIPSE:var R,H=s[0],M=s[1],z=s[2],O=s[3],k=s[4],A=s[5],C=!!s[6],P=A-k,L=2*t;for(v=1;L>=v;v++)g=v/L,C||(g=1-g),R=k+g*P,E=H+z*Math.cos(R),y=M+O*Math.sin(R),_.push(new THREE.Vector2(E,y))}var D=_[_.length-1],F=1e-10;return Math.abs(D.x-_[0].x)e;e++)r=t[e],o=r.args,i=r.action,i==THREE.PathActions.MOVE_TO&&0!=a.actions.length&&(s.push(a),a=new THREE.Path),a[i].apply(a,o);return 0!=a.actions.length&&s.push(a),s}function r(t){for(var e=[],n=0,r=t.length;r>n;n++){var i=t[n],o=new THREE.Shape;o.actions=i.actions,o.curves=i.curves,e.push(o)}return e}function i(t,e){for(var n=1e-10,r=e.length,i=!1,o=r-1,s=0;r>s;o=s++){var a=e[o],u=e[s],h=u.x-a.x,l=u.y-a.y;if(Math.abs(l)>n){if(0>l&&(a=e[s],h=-h,u=e[o],l=-l),t.yu.y)continue;if(t.y==a.y){if(t.x==a.x)return!0}else{var c=l*(t.x-a.x)-h*(t.y-a.y);if(0==c)return!0;if(0>c)continue;i=!i}}else{if(t.y!=a.y)continue;if(u.x<=t.x&&t.x<=a.x||a.x<=t.x&&t.x<=u.x)return!0}}return i}var o=n(this.actions);if(0==o.length)return[];if(e===!0)return r(o);var s,a,u,h=[];if(1==o.length)return a=o[0],u=new THREE.Shape,u.actions=a.actions,u.curves=a.curves,h.push(u),h;var l=!THREE.Shape.Utils.isClockWise(o[0].getPoints());l=t?!l:l;var c,p=[],f=[],d=[],m=0;f[m]=void 0,d[m]=[];var v,g;for(v=0,g=o.length;g>v;v++)a=o[v],c=a.getPoints(),s=THREE.Shape.Utils.isClockWise(c),s=t?!s:s,s?(!l&&f[m]&&m++,f[m]={s:new THREE.Shape,p:c},f[m].s.actions=a.actions,f[m].s.curves=a.curves,l&&m++,d[m]=[]):d[m].push({h:a,p:c[0]});if(!f[0])return r(o);if(f.length>1){for(var E=!1,y=[],_=0,b=f.length;b>_;_++)p[_]=[];for(var _=0,b=f.length;b>_;_++)for(var T=d[_],x=0;x0&&(E||(d=p))}var M,S,k;for(v=0,g=f.length;g>v;v++)for(u=f[v].s,h.push(u),M=d[v],S=0,k=M.length;k>S;S++)u.holes.push(M[S].h);return h},THREE.Shape=function(){THREE.Path.apply(this,arguments),this.holes=[]},THREE.Shape.prototype=Object.create(THREE.Path.prototype),THREE.Shape.prototype.constructor=THREE.Shape,THREE.Shape.prototype.extrude=function(t){var e=new THREE.ExtrudeGeometry(this,t);return e},THREE.Shape.prototype.makeGeometry=function(t){var e=new THREE.ShapeGeometry(this,t);return e},THREE.Shape.prototype.getPointsHoles=function(t){var e,n=this.holes.length,r=[];for(e=0;n>e;e++)r[e]=this.holes[e].getTransformedPoints(t,this.bends);return r},THREE.Shape.prototype.getSpacedPointsHoles=function(t){ -var e,n=this.holes.length,r=[];for(e=0;n>e;e++)r[e]=this.holes[e].getTransformedSpacedPoints(t,this.bends);return r},THREE.Shape.prototype.extractAllPoints=function(t){return{shape:this.getTransformedPoints(t),holes:this.getPointsHoles(t)}},THREE.Shape.prototype.extractPoints=function(t){return this.useSpacedPoints?this.extractAllSpacedPoints(t):this.extractAllPoints(t)},THREE.Shape.prototype.extractAllSpacedPoints=function(t){return{shape:this.getTransformedSpacedPoints(t),holes:this.getSpacedPointsHoles(t)}},THREE.Shape.Utils={triangulateShape:function(t,e){function n(t,e,n){return t.x!=e.x?t.xs){var m;if(f>0){if(0>d||d>f)return[];if(m=l*c-h*p,0>m||m>f)return[]}else{if(d>0||f>d)return[];if(m=l*c-h*p,m>0||f>m)return[]}if(0==m)return!o||0!=d&&d!=f?[t]:[];if(m==f)return!o||0!=d&&d!=f?[e]:[];if(0==d)return[r];if(d==f)return[i];var v=m/f;return[{x:t.x+v*a,y:t.y+v*u}]}if(0!=d||l*c!=h*p)return[];var g=0==a&&0==u,E=0==h&&0==l;if(g&&E)return t.x!=r.x||t.y!=r.y?[]:[t];if(g)return n(r,i,t)?[t]:[];if(E)return n(t,e,r)?[r]:[];var y,_,b,T,x,w,R,H;return 0!=a?(t.x=b?R>T?[]:T==R?o?[]:[x]:H>=T?[x,_]:[x,w]:b>H?[]:b==H?o?[]:[y]:H>=T?[y,_]:[y,w]}function i(t,e,n,r){var i=1e-10,o=e.x-t.x,s=e.y-t.y,a=n.x-t.x,u=n.y-t.y,h=r.x-t.x,l=r.y-t.y,c=o*u-s*a,p=o*l-s*h;if(Math.abs(c)>i){var f=h*u-l*a;return c>0?p>=0&&f>=0:p>=0||f>=0}return p>0}function o(t,e){function n(t,e){var n=E.length-1,r=t-1;0>r&&(r=n);var o=t+1;o>n&&(o=0);var s=i(E[t],E[r],E[o],a[e]);if(!s)return!1;var u=a.length-1,h=e-1;0>h&&(h=u);var l=e+1;return l>u&&(l=0),s=i(a[e],a[h],a[l],E[t]),s?!0:!1}function o(t,e){var n,i,o;for(n=0;n0)return!0;return!1}function s(t,n){var i,o,s,a,u;for(i=0;i0)return!0;return!1}for(var a,u,h,l,c,p,f,d,m,v,g,E=t.concat(),y=[],_=[],b=0,T=e.length;T>b;b++)y.push(b);for(var x=0,w=2*y.length;y.length>0;){if(w--,0>w){console.log("Infinite Loop! Holes left:"+y.length+", Probably Hole outside Shape!");break}for(h=x;h=0)break;_[f]=!0}if(u>=0)break}}return E}for(var s,a,u,h,l,c,p={},f=t.concat(),d=0,m=e.length;m>d;d++)Array.prototype.push.apply(f,e[d]);for(s=0,a=f.length;a>s;s++)l=f[s].x+":"+f[s].y,void 0!==p[l]&&THREE.warn("THREE.Shape: Duplicate point",l),p[l]=s;var v=o(t,e),g=THREE.FontUtils.Triangulate(v,!1);for(s=0,a=g.length;a>s;s++)for(h=g[s],u=0;3>u;u++)l=h[u].x+":"+h[u].y,c=p[l],void 0!==c&&(h[u]=c);return g.concat()},isClockWise:function(t){return THREE.FontUtils.Triangulate.area(t)<0},b2p0:function(t,e){var n=1-t;return n*n*e},b2p1:function(t,e){return 2*(1-t)*t*e},b2p2:function(t,e){return t*t*e},b2:function(t,e,n,r){return this.b2p0(t,e)+this.b2p1(t,n)+this.b2p2(t,r)},b3p0:function(t,e){var n=1-t;return n*n*n*e},b3p1:function(t,e){var n=1-t;return 3*n*n*t*e},b3p2:function(t,e){var n=1-t;return 3*n*t*t*e},b3p3:function(t,e){return t*t*t*e},b3:function(t,e,n,r,i){return this.b3p0(t,e)+this.b3p1(t,n)+this.b3p2(t,r)+this.b3p3(t,i)}},THREE.LineCurve=function(t,e){this.v1=t,this.v2=e},THREE.LineCurve.prototype=Object.create(THREE.Curve.prototype),THREE.LineCurve.prototype.constructor=THREE.LineCurve,THREE.LineCurve.prototype.getPoint=function(t){var e=this.v2.clone().sub(this.v1);return e.multiplyScalar(t).add(this.v1),e},THREE.LineCurve.prototype.getPointAt=function(t){return this.getPoint(t)},THREE.LineCurve.prototype.getTangent=function(t){var e=this.v2.clone().sub(this.v1);return e.normalize()},THREE.QuadraticBezierCurve=function(t,e,n){this.v0=t,this.v1=e,this.v2=n},THREE.QuadraticBezierCurve.prototype=Object.create(THREE.Curve.prototype),THREE.QuadraticBezierCurve.prototype.constructor=THREE.QuadraticBezierCurve,THREE.QuadraticBezierCurve.prototype.getPoint=function(t){var e=new THREE.Vector2;return e.x=THREE.Shape.Utils.b2(t,this.v0.x,this.v1.x,this.v2.x),e.y=THREE.Shape.Utils.b2(t,this.v0.y,this.v1.y,this.v2.y),e},THREE.QuadraticBezierCurve.prototype.getTangent=function(t){var e=new THREE.Vector2;return e.x=THREE.Curve.Utils.tangentQuadraticBezier(t,this.v0.x,this.v1.x,this.v2.x),e.y=THREE.Curve.Utils.tangentQuadraticBezier(t,this.v0.y,this.v1.y,this.v2.y),e.normalize()},THREE.CubicBezierCurve=function(t,e,n,r){this.v0=t,this.v1=e,this.v2=n,this.v3=r},THREE.CubicBezierCurve.prototype=Object.create(THREE.Curve.prototype),THREE.CubicBezierCurve.prototype.constructor=THREE.CubicBezierCurve,THREE.CubicBezierCurve.prototype.getPoint=function(t){var e,n;return e=THREE.Shape.Utils.b3(t,this.v0.x,this.v1.x,this.v2.x,this.v3.x),n=THREE.Shape.Utils.b3(t,this.v0.y,this.v1.y,this.v2.y,this.v3.y),new THREE.Vector2(e,n)},THREE.CubicBezierCurve.prototype.getTangent=function(t){var e,n;e=THREE.Curve.Utils.tangentCubicBezier(t,this.v0.x,this.v1.x,this.v2.x,this.v3.x),n=THREE.Curve.Utils.tangentCubicBezier(t,this.v0.y,this.v1.y,this.v2.y,this.v3.y);var r=new THREE.Vector2(e,n);return r.normalize(),r},THREE.SplineCurve=function(t){this.points=void 0==t?[]:t},THREE.SplineCurve.prototype=Object.create(THREE.Curve.prototype),THREE.SplineCurve.prototype.constructor=THREE.SplineCurve,THREE.SplineCurve.prototype.getPoint=function(t){var e=this.points,n=(e.length-1)*t,r=Math.floor(n),i=n-r,o=e[0==r?r:r-1],s=e[r],a=e[r>e.length-2?e.length-1:r+1],u=e[r>e.length-3?e.length-1:r+2],h=new THREE.Vector2;return h.x=THREE.Curve.Utils.interpolate(o.x,s.x,a.x,u.x,i),h.y=THREE.Curve.Utils.interpolate(o.y,s.y,a.y,u.y,i),h},THREE.EllipseCurve=function(t,e,n,r,i,o,s){this.aX=t,this.aY=e,this.xRadius=n,this.yRadius=r,this.aStartAngle=i,this.aEndAngle=o,this.aClockwise=s},THREE.EllipseCurve.prototype=Object.create(THREE.Curve.prototype),THREE.EllipseCurve.prototype.constructor=THREE.EllipseCurve,THREE.EllipseCurve.prototype.getPoint=function(t){var e=this.aEndAngle-this.aStartAngle;0>e&&(e+=2*Math.PI),e>2*Math.PI&&(e-=2*Math.PI);var n;n=this.aClockwise===!0?this.aEndAngle+(1-t)*(2*Math.PI-e):this.aStartAngle+t*e;var r=new THREE.Vector2;return r.x=this.aX+this.xRadius*Math.cos(n),r.y=this.aY+this.yRadius*Math.sin(n),r},THREE.ArcCurve=function(t,e,n,r,i,o){THREE.EllipseCurve.call(this,t,e,n,n,r,i,o)},THREE.ArcCurve.prototype=Object.create(THREE.EllipseCurve.prototype),THREE.ArcCurve.prototype.constructor=THREE.ArcCurve,THREE.LineCurve3=THREE.Curve.create(function(t,e){this.v1=t,this.v2=e},function(t){var e=new THREE.Vector3;return e.subVectors(this.v2,this.v1),e.multiplyScalar(t),e.add(this.v1),e}),THREE.QuadraticBezierCurve3=THREE.Curve.create(function(t,e,n){this.v0=t,this.v1=e,this.v2=n},function(t){var e=new THREE.Vector3;return e.x=THREE.Shape.Utils.b2(t,this.v0.x,this.v1.x,this.v2.x),e.y=THREE.Shape.Utils.b2(t,this.v0.y,this.v1.y,this.v2.y),e.z=THREE.Shape.Utils.b2(t,this.v0.z,this.v1.z,this.v2.z),e}),THREE.CubicBezierCurve3=THREE.Curve.create(function(t,e,n,r){this.v0=t,this.v1=e,this.v2=n,this.v3=r},function(t){var e=new THREE.Vector3;return e.x=THREE.Shape.Utils.b3(t,this.v0.x,this.v1.x,this.v2.x,this.v3.x),e.y=THREE.Shape.Utils.b3(t,this.v0.y,this.v1.y,this.v2.y,this.v3.y),e.z=THREE.Shape.Utils.b3(t,this.v0.z,this.v1.z,this.v2.z,this.v3.z),e}),THREE.SplineCurve3=THREE.Curve.create(function(t){this.points=void 0==t?[]:t},function(t){var e=this.points,n=(e.length-1)*t,r=Math.floor(n),i=n-r,o=e[0==r?r:r-1],s=e[r],a=e[r>e.length-2?e.length-1:r+1],u=e[r>e.length-3?e.length-1:r+2],h=new THREE.Vector3;return h.x=THREE.Curve.Utils.interpolate(o.x,s.x,a.x,u.x,i),h.y=THREE.Curve.Utils.interpolate(o.y,s.y,a.y,u.y,i),h.z=THREE.Curve.Utils.interpolate(o.z,s.z,a.z,u.z,i),h}),THREE.ClosedSplineCurve3=THREE.Curve.create(function(t){this.points=void 0==t?[]:t},function(t){var e=this.points,n=(e.length-0)*t,r=Math.floor(n),i=n-r;r+=r>0?0:(Math.floor(Math.abs(r)/e.length)+1)*e.length;var o=e[(r-1)%e.length],s=e[r%e.length],a=e[(r+1)%e.length],u=e[(r+2)%e.length],h=new THREE.Vector3;return h.x=THREE.Curve.Utils.interpolate(o.x,s.x,a.x,u.x,i),h.y=THREE.Curve.Utils.interpolate(o.y,s.y,a.y,u.y,i),h.z=THREE.Curve.Utils.interpolate(o.z,s.z,a.z,u.z,i),h}),THREE.AnimationHandler={LINEAR:0,CATMULLROM:1,CATMULLROM_FORWARD:2,add:function(){THREE.warn("THREE.AnimationHandler.add() has been deprecated.")},get:function(){THREE.warn("THREE.AnimationHandler.get() has been deprecated.")},remove:function(){THREE.warn("THREE.AnimationHandler.remove() has been deprecated.")},animations:[],init:function(t){if(t.initialized===!0)return t;for(var e=0;et;t++){var n=this.hierarchy[t];void 0===n.animationCache&&(n.animationCache={animations:{},blending:{positionWeight:0,quaternionWeight:0,scaleWeight:0}});var r=this.data.name,i=n.animationCache.animations,o=i[r];void 0===o&&(o={prevKey:{pos:0,rot:0,scl:0},nextKey:{pos:0,rot:0,scl:0},originalMatrix:n.matrix},i[r]=o);for(var s=0;3>s;s++){for(var a=this.keyTypes[s],u=this.data.hierarchy[t].keys[0],h=this.getNextKeyWith(a,t,1);h.timeu.index;)u=h,h=this.getNextKeyWith(a,t,h.index+1);o.prevKey[a]=u,o.nextKey[a]=h}}},resetBlendWeights:function(){for(var t=0,e=this.hierarchy.length;e>t;t++){var n=this.hierarchy[t],r=n.animationCache;if(void 0!==r){var i=r.blending;i.positionWeight=0,i.quaternionWeight=0,i.scaleWeight=0}}},update:function(){var t=[],e=new THREE.Vector3,n=new THREE.Vector3,r=new THREE.Quaternion,i=function(t,e){var n,r,i,s,a,u,h,l,c,p=[],f=[];return n=(t.length-1)*e,r=Math.floor(n),i=n-r,p[0]=0===r?r:r-1,p[1]=r,p[2]=r>t.length-2?r:r+1,p[3]=r>t.length-3?r:r+2,u=t[p[0]],h=t[p[1]],l=t[p[2]],c=t[p[3]],s=i*i,a=i*s,f[0]=o(u[0],h[0],l[0],c[0],i,s,a),f[1]=o(u[1],h[1],l[1],c[1],i,s,a),f[2]=o(u[2],h[2],l[2],c[2],i,s,a),f},o=function(t,e,n,r,i,o,s){var a=.5*(n-t),u=.5*(r-e);return(2*(e-n)+a+u)*s+(-3*(e-n)-2*a-u)*o+a*i+e};return function(o){if(this.isPlaying!==!1&&(this.currentTime+=o*this.timeScale,0!==this.weight)){var s=this.data.length;(this.currentTime>s||this.currentTime<0)&&(this.loop?(this.currentTime%=s,this.currentTime<0&&(this.currentTime+=s),this.reset()):this.stop());for(var a=0,u=this.hierarchy.length;u>a;a++)for(var h=this.hierarchy[a],l=h.animationCache.animations[this.data.name],c=h.animationCache.blending,p=0;3>p;p++){var f=this.keyTypes[p],d=l.prevKey[f],m=l.nextKey[f];if(this.timeScale>0&&m.time<=this.currentTime||this.timeScale<0&&d.time>=this.currentTime){for(d=this.data.hierarchy[a].keys[0],m=this.getNextKeyWith(f,a,1);m.timed.index;)d=m,m=this.getNextKeyWith(f,a,m.index+1);l.prevKey[f]=d,l.nextKey[f]=m}var v=(this.currentTime-d.time)/(m.time-d.time),g=d[f],E=m[f];if(0>v&&(v=0),v>1&&(v=1),"pos"===f){if(this.interpolationType===THREE.AnimationHandler.LINEAR){n.x=g[0]+(E[0]-g[0])*v,n.y=g[1]+(E[1]-g[1])*v,n.z=g[2]+(E[2]-g[2])*v;var y=this.weight/(this.weight+c.positionWeight);h.position.lerp(n,y),c.positionWeight+=this.weight}else if(this.interpolationType===THREE.AnimationHandler.CATMULLROM||this.interpolationType===THREE.AnimationHandler.CATMULLROM_FORWARD){t[0]=this.getPrevKeyWith("pos",a,d.index-1).pos,t[1]=g,t[2]=E,t[3]=this.getNextKeyWith("pos",a,m.index+1).pos,v=.33*v+.33;var _=i(t,v),y=this.weight/(this.weight+c.positionWeight);c.positionWeight+=this.weight;var b=h.position;if(b.x=b.x+(_[0]-b.x)*y,b.y=b.y+(_[1]-b.y)*y,b.z=b.z+(_[2]-b.z)*y,this.interpolationType===THREE.AnimationHandler.CATMULLROM_FORWARD){var T=i(t,1.01*v);e.set(T[0],T[1],T[2]),e.sub(b),e.y=0,e.normalize();var x=Math.atan2(e.x,e.z);h.rotation.set(0,x,0)}}}else if("rot"===f)if(THREE.Quaternion.slerp(g,E,r,v),0===c.quaternionWeight)h.quaternion.copy(r),c.quaternionWeight=this.weight;else{var y=this.weight/(this.weight+c.quaternionWeight);THREE.Quaternion.slerp(h.quaternion,r,h.quaternion,y),c.quaternionWeight+=this.weight}else if("scl"===f){n.x=g[0]+(E[0]-g[0])*v,n.y=g[1]+(E[1]-g[1])*v,n.z=g[2]+(E[2]-g[2])*v;var y=this.weight/(this.weight+c.scaleWeight);h.scale.lerp(n,y),c.scaleWeight+=this.weight}}return!0}}}(),getNextKeyWith:function(t,e,n){var r=this.data.hierarchy[e].keys;for(this.interpolationType===THREE.AnimationHandler.CATMULLROM||this.interpolationType===THREE.AnimationHandler.CATMULLROM_FORWARD?n=n0?n:0:n>=0?n:n+r.length;n>=0;n--)if(void 0!==r[n][t])return r[n];return this.data.hierarchy[e].keys[r.length-1]}},THREE.KeyFrameAnimation=function(t){this.root=t.node,this.data=THREE.AnimationHandler.init(t),this.hierarchy=THREE.AnimationHandler.parse(this.root),this.currentTime=0,this.timeScale=.001,this.isPlaying=!1,this.isPaused=!0,this.loop=!0;for(var e=0,n=this.hierarchy.length;n>e;e++){var r=this.data.hierarchy[e].keys,i=this.data.hierarchy[e].sids,o=this.hierarchy[e];if(r.length&&i){for(var s=0;se;e++){n=this.hierarchy[e],r=this.data.hierarchy[e],void 0===r.animationCache&&(r.animationCache={},r.animationCache.prevKey=null,r.animationCache.nextKey=null,r.animationCache.originalMatrix=n.matrix);var o=this.data.hierarchy[e].keys;o.length&&(r.animationCache.prevKey=o[0],r.animationCache.nextKey=o[1],this.startTime=Math.min(o[0].time,this.startTime),this.endTime=Math.max(o[o.length-1].time,this.endTime))}this.update(0)}this.isPaused=!1,THREE.AnimationHandler.play(this)},stop:function(){this.isPlaying=!1,this.isPaused=!1,THREE.AnimationHandler.stop(this);for(var t=0;te&&(this.currentTime%=e),this.currentTime=Math.min(this.currentTime,e);for(var n=0,r=this.hierarchy.length;r>n;n++){var i=this.hierarchy[n],o=this.data.hierarchy[n],s=o.keys,a=o.animationCache;if(s.length){var u=a.prevKey,h=a.nextKey;if(h.time<=this.currentTime){for(;h.timeu.index;)u=h,h=s[u.index+1];a.prevKey=u,a.nextKey=h}h.time>=this.currentTime?u.interpolate(h,this.currentTime):u.interpolate(h,h.time),this.data.hierarchy[n].node.updateMatrix(),i.matrixWorldNeedsUpdate=!0}}}},getNextKeyWith:function(t,e,n){var r=this.data.hierarchy[e].keys;for(n%=r.length;n=0?n:n+r.length;n>=0;n--)if(r[n].hasTarget(t))return r[n];return r[r.length-1]}},THREE.MorphAnimation=function(t){this.mesh=t,this.frames=t.morphTargetInfluences.length,this.currentTime=0,this.duration=1e3,this.loop=!0,this.lastFrame=0,this.currentFrame=0,this.isPlaying=!1},THREE.MorphAnimation.prototype={constructor:THREE.MorphAnimation,play:function(){this.isPlaying=!0},pause:function(){this.isPlaying=!1},update:function(t){if(this.isPlaying!==!1){this.currentTime+=t,this.loop===!0&&this.currentTime>this.duration&&(this.currentTime%=this.duration),this.currentTime=Math.min(this.currentTime,this.duration);var e=this.duration/this.frames,n=Math.floor(this.currentTime/e),r=this.mesh.morphTargetInfluences;n!=this.currentFrame&&(r[this.lastFrame]=0,r[this.currentFrame]=1,r[n]=0,this.lastFrame=this.currentFrame,this.currentFrame=n),r[n]=this.currentTime%e/e,r[this.lastFrame]=1-r[n]}}},THREE.BoxGeometry=function(t,e,n,r,i,o){function s(t,e,n,r,i,o,s,u){var h,l,c,p=a.widthSegments,f=a.heightSegments,d=i/2,m=o/2,v=a.vertices.length;"x"===t&&"y"===e||"y"===t&&"x"===e?h="z":"x"===t&&"z"===e||"z"===t&&"x"===e?(h="y",f=a.depthSegments):("z"===t&&"y"===e||"y"===t&&"z"===e)&&(h="x",p=a.depthSegments);var g=p+1,E=f+1,y=i/p,_=o/f,b=new THREE.Vector3;for(b[h]=s>0?1:-1,c=0;E>c;c++)for(l=0;g>l;l++){var T=new THREE.Vector3;T[t]=(l*y-d)*n,T[e]=(c*_-m)*r,T[h]=s,a.vertices.push(T)}for(c=0;f>c;c++)for(l=0;p>l;l++){var x=l+g*c,w=l+g*(c+1),R=l+1+g*(c+1),H=l+1+g*c,M=new THREE.Vector2(l/p,1-c/f),S=new THREE.Vector2(l/p,1-(c+1)/f),k=new THREE.Vector2((l+1)/p,1-(c+1)/f),A=new THREE.Vector2((l+1)/p,1-c/f),C=new THREE.Face3(x+v,w+v,H+v);C.normal.copy(b),C.vertexNormals.push(b.clone(),b.clone(),b.clone()),C.materialIndex=u,a.faces.push(C),a.faceVertexUvs[0].push([M,S,A]),C=new THREE.Face3(w+v,R+v,H+v),C.normal.copy(b),C.vertexNormals.push(b.clone(),b.clone(),b.clone()),C.materialIndex=u,a.faces.push(C),a.faceVertexUvs[0].push([S.clone(),k,A.clone()])}}THREE.Geometry.call(this),this.type="BoxGeometry",this.parameters={width:t,height:e,depth:n,widthSegments:r,heightSegments:i,depthSegments:o},this.widthSegments=r||1,this.heightSegments=i||1,this.depthSegments=o||1;var a=this,u=t/2,h=e/2,l=n/2;s("z","y",-1,-1,n,e,u,0),s("z","y",1,-1,n,e,-u,1),s("x","z",1,1,t,n,h,2),s("x","z",1,-1,t,n,-h,3),s("x","y",1,-1,t,e,l,4),s("x","y",-1,-1,t,e,-l,5),this.mergeVertices()},THREE.BoxGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.BoxGeometry.prototype.constructor=THREE.BoxGeometry,THREE.CircleGeometry=function(t,e,n,r){THREE.Geometry.call(this),this.type="CircleGeometry",this.parameters={radius:t,segments:e,thetaStart:n,thetaLength:r},t=t||50,e=void 0!==e?Math.max(3,e):8,n=void 0!==n?n:0,r=void 0!==r?r:2*Math.PI;var i,o=[],s=new THREE.Vector3,a=new THREE.Vector2(.5,.5);for(this.vertices.push(s),o.push(a),i=0;e>=i;i++){var u=new THREE.Vector3,h=n+i/e*r;u.x=t*Math.cos(h),u.y=t*Math.sin(h),this.vertices.push(u),o.push(new THREE.Vector2((u.x/t+1)/2,(u.y/t+1)/2))}var l=new THREE.Vector3(0,0,1);for(i=1;e>=i;i++)this.faces.push(new THREE.Face3(i,i+1,0,[l.clone(),l.clone(),l.clone()])),this.faceVertexUvs[0].push([o[i].clone(),o[i+1].clone(),a.clone()]);this.computeFaceNormals(),this.boundingSphere=new THREE.Sphere(new THREE.Vector3,t)},THREE.CircleGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.CircleGeometry.prototype.constructor=THREE.CircleGeometry,THREE.CubeGeometry=function(t,e,n,r,i,o){return THREE.warn("THREE.CubeGeometry has been renamed to THREE.BoxGeometry."),new THREE.BoxGeometry(t,e,n,r,i,o)},THREE.CylinderGeometry=function(t,e,n,r,i,o,s,a){THREE.Geometry.call(this),this.type="CylinderGeometry",this.parameters={radiusTop:t,radiusBottom:e,height:n,radialSegments:r,heightSegments:i,openEnded:o,thetaStart:s,thetaLength:a},t=void 0!==t?t:20,e=void 0!==e?e:20,n=void 0!==n?n:100,r=r||8,i=i||1,o=void 0!==o?o:!1,s=void 0!==s?s:0,a=void 0!==a?a:2*Math.PI;var u,h,l=n/2,c=[],p=[];for(h=0;i>=h;h++){var f=[],d=[],m=h/i,v=m*(e-t)+t;for(u=0;r>=u;u++){var g=u/r,E=new THREE.Vector3;E.x=v*Math.sin(g*a+s),E.y=-m*n+l,E.z=v*Math.cos(g*a+s),this.vertices.push(E),f.push(this.vertices.length-1),d.push(new THREE.Vector2(g,1-m))}c.push(f),p.push(d)}var y,_,b=(e-t)/n;for(u=0;r>u;u++)for(0!==t?(y=this.vertices[c[0][u]].clone(),_=this.vertices[c[0][u+1]].clone()):(y=this.vertices[c[1][u]].clone(),_=this.vertices[c[1][u+1]].clone()),y.setY(Math.sqrt(y.x*y.x+y.z*y.z)*b).normalize(),_.setY(Math.sqrt(_.x*_.x+_.z*_.z)*b).normalize(),h=0;i>h;h++){var T=c[h][u],x=c[h+1][u],w=c[h+1][u+1],R=c[h][u+1],H=y.clone(),M=y.clone(),S=_.clone(),k=_.clone(),A=p[h][u].clone(),C=p[h+1][u].clone(),P=p[h+1][u+1].clone(),L=p[h][u+1].clone();this.faces.push(new THREE.Face3(T,x,R,[H,M,k])),this.faceVertexUvs[0].push([A,C,L]),this.faces.push(new THREE.Face3(x,w,R,[M.clone(),S,k.clone()])),this.faceVertexUvs[0].push([C.clone(),P,L.clone()])}if(o===!1&&t>0)for(this.vertices.push(new THREE.Vector3(0,l,0)),u=0;r>u;u++){var T=c[0][u],x=c[0][u+1],w=this.vertices.length-1,H=new THREE.Vector3(0,1,0),M=new THREE.Vector3(0,1,0),S=new THREE.Vector3(0,1,0),A=p[0][u].clone(),C=p[0][u+1].clone(),P=new THREE.Vector2(C.x,0);this.faces.push(new THREE.Face3(T,x,w,[H,M,S])),this.faceVertexUvs[0].push([A,C,P])}if(o===!1&&e>0)for(this.vertices.push(new THREE.Vector3(0,-l,0)),u=0;r>u;u++){var T=c[i][u+1],x=c[i][u],w=this.vertices.length-1,H=new THREE.Vector3(0,-1,0),M=new THREE.Vector3(0,-1,0),S=new THREE.Vector3(0,-1,0),A=p[i][u+1].clone(),C=p[i][u].clone(),P=new THREE.Vector2(C.x,1);this.faces.push(new THREE.Face3(T,x,w,[H,M,S])),this.faceVertexUvs[0].push([A,C,P])}this.computeFaceNormals()},THREE.CylinderGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.CylinderGeometry.prototype.constructor=THREE.CylinderGeometry,THREE.ExtrudeGeometry=function(t,e){return"undefined"==typeof t?void(t=[]):(THREE.Geometry.call(this),this.type="ExtrudeGeometry",t=t instanceof Array?t:[t],this.addShapeList(t,e),void this.computeFaceNormals())},THREE.ExtrudeGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.ExtrudeGeometry.prototype.constructor=THREE.ExtrudeGeometry,THREE.ExtrudeGeometry.prototype.addShapeList=function(t,e){for(var n=t.length,r=0;n>r;r++){var i=t[r];this.addShape(i,e)}},THREE.ExtrudeGeometry.prototype.addShape=function(t,e){function n(t,e,n){return e||THREE.error("THREE.ExtrudeGeometry: vec does not exist"),e.clone().multiplyScalar(n).add(t)}function r(t,e,n){var r,i,o=1e-10,s=1,a=t.x-e.x,u=t.y-e.y,h=n.x-t.x,l=n.y-t.y,c=a*a+u*u,p=a*l-u*h;if(Math.abs(p)>o){var f=Math.sqrt(c),d=Math.sqrt(h*h+l*l),m=e.x-u/f,v=e.y+a/f,g=n.x-l/d,E=n.y+h/d,y=((g-m)*l-(E-v)*h)/(a*l-u*h);r=m+a*y-t.x,i=v+u*y-t.y;var _=r*r+i*i;if(2>=_)return new THREE.Vector2(r,i);s=Math.sqrt(_/2)}else{var b=!1;a>o?h>o&&(b=!0):-o>a?-o>h&&(b=!0):Math.sign(u)==Math.sign(l)&&(b=!0),b?(r=-u,i=a,s=Math.sqrt(c)):(r=a,i=u,s=Math.sqrt(c/2))}return new THREE.Vector2(r/s,i/s)}function i(){if(y){var t=0,e=G*t;for(X=0;W>X;X++)j=D[X],u(j[2]+e,j[1]+e,j[0]+e);for(t=b+2*E,e=G*t,X=0;W>X;X++)j=D[X],u(j[0]+e,j[1]+e,j[2]+e)}else{for(X=0;W>X;X++)j=D[X],u(j[2],j[1],j[0]);for(X=0;W>X;X++)j=D[X],u(j[0]+G*b,j[1]+G*b,j[2]+G*b)}}function o(){var t=0;for(s(F,t),t+=F.length,S=0,k=z.length;k>S;S++)M=z[S],s(M,t),t+=M.length}function s(t,e){var n,r;for(X=t.length;--X>=0;){n=X,r=X-1,0>r&&(r=t.length-1);var i=0,o=b+2*E;for(i=0;o>i;i++){var s=G*i,a=G*(i+1),u=e+n+s,l=e+r+s,c=e+r+a,p=e+n+a;h(u,l,c,p,t,i,o,n,r)}}}function a(t,e,n){A.vertices.push(new THREE.Vector3(t,e,n))}function u(t,e,n){t+=C,e+=C,n+=C,A.faces.push(new THREE.Face3(t,e,n,null,null,w));var r=H.generateTopUV(A,t,e,n);A.faceVertexUvs[0].push(r)}function h(t,e,n,r,i,o,s,a,u){t+=C,e+=C,n+=C,r+=C,A.faces.push(new THREE.Face3(t,e,r,null,null,R)),A.faces.push(new THREE.Face3(e,n,r,null,null,R));var h=H.generateSideWallUV(A,t,e,n,r);A.faceVertexUvs[0].push([h[0],h[1],h[3]]),A.faceVertexUvs[0].push([h[1],h[2],h[3]])}var l,c,p,f,d,m=void 0!==e.amount?e.amount:100,v=void 0!==e.bevelThickness?e.bevelThickness:6,g=void 0!==e.bevelSize?e.bevelSize:v-2,E=void 0!==e.bevelSegments?e.bevelSegments:3,y=void 0!==e.bevelEnabled?e.bevelEnabled:!0,_=void 0!==e.curveSegments?e.curveSegments:12,b=void 0!==e.steps?e.steps:1,T=e.extrudePath,x=!1,w=e.material,R=e.extrudeMaterial,H=void 0!==e.UVGenerator?e.UVGenerator:THREE.ExtrudeGeometry.WorldUVGenerator;T&&(l=T.getSpacedPoints(b),x=!0,y=!1,c=void 0!==e.frames?e.frames:new THREE.TubeGeometry.FrenetFrames(T,b,!1),p=new THREE.Vector3,f=new THREE.Vector3,d=new THREE.Vector3),y||(E=0,v=0,g=0);var M,S,k,A=this,C=this.vertices.length,P=t.extractPoints(_),L=P.shape,z=P.holes,O=!THREE.Shape.Utils.isClockWise(L);if(O){for(L=L.reverse(),S=0,k=z.length;k>S;S++)M=z[S],THREE.Shape.Utils.isClockWise(M)&&(z[S]=M.reverse());O=!1}var D=THREE.Shape.Utils.triangulateShape(L,z),F=L;for(S=0,k=z.length;k>S;S++)M=z[S],L=L.concat(M);for(var U,B,N,V,I,j,G=L.length,W=D.length,q=[],X=0,Y=F.length,Z=Y-1,K=X+1;Y>X;X++,Z++,K++)Z===Y&&(Z=0),K===Y&&(K=0),q[X]=r(F[X],F[Z],F[K]);var Q,J=[],$=q.concat();for(S=0,k=z.length;k>S;S++){for(M=z[S],Q=[],X=0,Y=M.length,Z=Y-1,K=X+1;Y>X;X++,Z++,K++)Z===Y&&(Z=0),K===Y&&(K=0),Q[X]=r(M[X],M[Z],M[K]);J.push(Q),$=$.concat(Q)}for(U=0;E>U;U++){for(N=U/E,V=v*(1-N),B=g*Math.sin(N*Math.PI/2),X=0,Y=F.length;Y>X;X++)I=n(F[X],q[X],B),a(I.x,I.y,-V);for(S=0,k=z.length;k>S;S++)for(M=z[S],Q=J[S],X=0,Y=M.length;Y>X;X++)I=n(M[X],Q[X],B),a(I.x,I.y,-V)}for(B=g,X=0;G>X;X++)I=y?n(L[X],$[X],B):L[X],x?(f.copy(c.normals[0]).multiplyScalar(I.x),p.copy(c.binormals[0]).multiplyScalar(I.y),d.copy(l[0]).add(f).add(p),a(d.x,d.y,d.z)):a(I.x,I.y,0);var tt;for(tt=1;b>=tt;tt++)for(X=0;G>X;X++)I=y?n(L[X],$[X],B):L[X],x?(f.copy(c.normals[tt]).multiplyScalar(I.x),p.copy(c.binormals[tt]).multiplyScalar(I.y),d.copy(l[tt]).add(f).add(p),a(d.x,d.y,d.z)):a(I.x,I.y,m/b*tt);for(U=E-1;U>=0;U--){for(N=U/E,V=v*(1-N),B=g*Math.sin(N*Math.PI/2),X=0,Y=F.length;Y>X;X++)I=n(F[X],q[X],B),a(I.x,I.y,m+V);for(S=0,k=z.length;k>S;S++)for(M=z[S],Q=J[S],X=0,Y=M.length;Y>X;X++)I=n(M[X],Q[X],B),x?a(I.x,I.y+l[b-1].y,l[b-1].x+V):a(I.x,I.y,m+V)}i(),o()},THREE.ExtrudeGeometry.WorldUVGenerator={generateTopUV:function(t,e,n,r){var i=t.vertices,o=i[e],s=i[n],a=i[r];return[new THREE.Vector2(o.x,o.y),new THREE.Vector2(s.x,s.y),new THREE.Vector2(a.x,a.y)]},generateSideWallUV:function(t,e,n,r,i){var o=t.vertices,s=o[e],a=o[n],u=o[r],h=o[i];return Math.abs(s.y-a.y)<.01?[new THREE.Vector2(s.x,1-s.z),new THREE.Vector2(a.x,1-a.z),new THREE.Vector2(u.x,1-u.z),new THREE.Vector2(h.x,1-h.z)]:[new THREE.Vector2(s.y,1-s.z),new THREE.Vector2(a.y,1-a.z),new THREE.Vector2(u.y,1-u.z),new THREE.Vector2(h.y,1-h.z)]}},THREE.ShapeGeometry=function(t,e){THREE.Geometry.call(this),this.type="ShapeGeometry",t instanceof Array==!1&&(t=[t]),this.addShapeList(t,e),this.computeFaceNormals()},THREE.ShapeGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.ShapeGeometry.prototype.constructor=THREE.ShapeGeometry,THREE.ShapeGeometry.prototype.addShapeList=function(t,e){for(var n=0,r=t.length;r>n;n++)this.addShape(t[n],e);return this},THREE.ShapeGeometry.prototype.addShape=function(t,e){void 0===e&&(e={});var n,r,i,o=void 0!==e.curveSegments?e.curveSegments:12,s=e.material,a=void 0===e.UVGenerator?THREE.ExtrudeGeometry.WorldUVGenerator:e.UVGenerator,u=this.vertices.length,h=t.extractPoints(o),l=h.shape,c=h.holes,p=!THREE.Shape.Utils.isClockWise(l);if(p){for(l=l.reverse(),n=0,r=c.length;r>n;n++)i=c[n],THREE.Shape.Utils.isClockWise(i)&&(c[n]=i.reverse());p=!1}var f=THREE.Shape.Utils.triangulateShape(l,c);for(n=0,r=c.length;r>n;n++)i=c[n],l=l.concat(i);var d,m,v=l.length,g=f.length;for(n=0;v>n;n++)d=l[n],this.vertices.push(new THREE.Vector3(d.x,d.y,0));for(n=0;g>n;n++){m=f[n];var E=m[0]+u,y=m[1]+u,_=m[2]+u;this.faces.push(new THREE.Face3(E,y,_,null,null,s)),this.faceVertexUvs[0].push(a.generateTopUV(this,E,y,_))}},THREE.LatheGeometry=function(t,e,n,r){THREE.Geometry.call(this),this.type="LatheGeometry",this.parameters={points:t,segments:e,phiStart:n,phiLength:r},e=e||12,n=n||0,r=r||2*Math.PI;for(var i=1/(t.length-1),o=1/e,s=0,a=e;a>=s;s++)for(var u=n+s*o*r,h=Math.cos(u),l=Math.sin(u),c=0,p=t.length;p>c;c++){var f=t[c],d=new THREE.Vector3;d.x=h*f.x-l*f.y,d.y=l*f.x+h*f.y,d.z=f.z,this.vertices.push(d)}for(var m=t.length,s=0,a=e;a>s;s++)for(var c=0,p=t.length-1;p>c;c++){var v=c+m*s,g=v,E=v+m,h=v+1+m,y=v+1,_=s*o,b=c*i,T=_+o,x=b+i;this.faces.push(new THREE.Face3(g,E,y)),this.faceVertexUvs[0].push([new THREE.Vector2(_,b),new THREE.Vector2(T,b),new THREE.Vector2(_,x)]),this.faces.push(new THREE.Face3(E,h,y)),this.faceVertexUvs[0].push([new THREE.Vector2(T,b),new THREE.Vector2(T,x),new THREE.Vector2(_,x)])}this.mergeVertices(),this.computeFaceNormals(),this.computeVertexNormals()},THREE.LatheGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.LatheGeometry.prototype.constructor=THREE.LatheGeometry,THREE.PlaneGeometry=function(t,e,n,r){console.info("THREE.PlaneGeometry: Consider using THREE.PlaneBufferGeometry for lower memory footprint."),THREE.Geometry.call(this),this.type="PlaneGeometry",this.parameters={width:t,height:e,widthSegments:n,heightSegments:r},this.fromBufferGeometry(new THREE.PlaneBufferGeometry(t,e,n,r))},THREE.PlaneGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.PlaneGeometry.prototype.constructor=THREE.PlaneGeometry,THREE.PlaneBufferGeometry=function(t,e,n,r){THREE.BufferGeometry.call(this),this.type="PlaneBufferGeometry",this.parameters={width:t,height:e,widthSegments:n,heightSegments:r};for(var i=t/2,o=e/2,s=n||1,a=r||1,u=s+1,h=a+1,l=t/s,c=e/a,p=new Float32Array(u*h*3),f=new Float32Array(u*h*3),d=new Float32Array(u*h*2),m=0,v=0,g=0;h>g;g++)for(var E=g*c-o,y=0;u>y;y++){var _=y*l-i;p[m]=_,p[m+1]=-E,f[m+2]=1,d[v]=y/s,d[v+1]=1-g/a,m+=3,v+=2}m=0;for(var b=new(p.length/3>65535?Uint32Array:Uint16Array)(s*a*6),g=0;a>g;g++)for(var y=0;s>y;y++){var T=y+u*g,x=y+u*(g+1),w=y+1+u*(g+1),R=y+1+u*g;b[m]=T,b[m+1]=x,b[m+2]=R,b[m+3]=x,b[m+4]=w,b[m+5]=R,m+=6}this.addAttribute("index",new THREE.BufferAttribute(b,1)),this.addAttribute("position",new THREE.BufferAttribute(p,3)),this.addAttribute("normal",new THREE.BufferAttribute(f,3)), -this.addAttribute("uv",new THREE.BufferAttribute(d,2))},THREE.PlaneBufferGeometry.prototype=Object.create(THREE.BufferGeometry.prototype),THREE.PlaneBufferGeometry.prototype.constructor=THREE.PlaneBufferGeometry,THREE.RingGeometry=function(t,e,n,r,i,o){THREE.Geometry.call(this),this.type="RingGeometry",this.parameters={innerRadius:t,outerRadius:e,thetaSegments:n,phiSegments:r,thetaStart:i,thetaLength:o},t=t||0,e=e||50,i=void 0!==i?i:0,o=void 0!==o?o:2*Math.PI,n=void 0!==n?Math.max(3,n):8,r=void 0!==r?Math.max(1,r):8;var s,a,u=[],h=t,l=(e-t)/r;for(s=0;r+1>s;s++){for(a=0;n+1>a;a++){var c=new THREE.Vector3,p=i+a/n*o;c.x=h*Math.cos(p),c.y=h*Math.sin(p),this.vertices.push(c),u.push(new THREE.Vector2((c.x/e+1)/2,(c.y/e+1)/2))}h+=l}var f=new THREE.Vector3(0,0,1);for(s=0;r>s;s++){var d=s*(n+1);for(a=0;n>a;a++){var p=a+d,m=p,v=p+n+1,g=p+n+2;this.faces.push(new THREE.Face3(m,v,g,[f.clone(),f.clone(),f.clone()])),this.faceVertexUvs[0].push([u[m].clone(),u[v].clone(),u[g].clone()]),m=p,v=p+n+2,g=p+1,this.faces.push(new THREE.Face3(m,v,g,[f.clone(),f.clone(),f.clone()])),this.faceVertexUvs[0].push([u[m].clone(),u[v].clone(),u[g].clone()])}}this.computeFaceNormals(),this.boundingSphere=new THREE.Sphere(new THREE.Vector3,h)},THREE.RingGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.RingGeometry.prototype.constructor=THREE.RingGeometry,THREE.SphereGeometry=function(t,e,n,r,i,o,s){THREE.Geometry.call(this),this.type="SphereGeometry",this.parameters={radius:t,widthSegments:e,heightSegments:n,phiStart:r,phiLength:i,thetaStart:o,thetaLength:s},t=t||50,e=Math.max(3,Math.floor(e)||8),n=Math.max(2,Math.floor(n)||6),r=void 0!==r?r:0,i=void 0!==i?i:2*Math.PI,o=void 0!==o?o:0,s=void 0!==s?s:Math.PI;var a,u,h=[],l=[];for(u=0;n>=u;u++){var c=[],p=[];for(a=0;e>=a;a++){var f=a/e,d=u/n,m=new THREE.Vector3;m.x=-t*Math.cos(r+f*i)*Math.sin(o+d*s),m.y=t*Math.cos(o+d*s),m.z=t*Math.sin(r+f*i)*Math.sin(o+d*s),this.vertices.push(m),c.push(this.vertices.length-1),p.push(new THREE.Vector2(f,1-d))}h.push(c),l.push(p)}for(u=0;n>u;u++)for(a=0;e>a;a++){var v=h[u][a+1],g=h[u][a],E=h[u+1][a],y=h[u+1][a+1],_=this.vertices[v].clone().normalize(),b=this.vertices[g].clone().normalize(),T=this.vertices[E].clone().normalize(),x=this.vertices[y].clone().normalize(),w=l[u][a+1].clone(),R=l[u][a].clone(),H=l[u+1][a].clone(),M=l[u+1][a+1].clone();Math.abs(this.vertices[v].y)===t?(w.x=(w.x+R.x)/2,this.faces.push(new THREE.Face3(v,E,y,[_,T,x])),this.faceVertexUvs[0].push([w,H,M])):Math.abs(this.vertices[E].y)===t?(H.x=(H.x+M.x)/2,this.faces.push(new THREE.Face3(v,g,E,[_,b,T])),this.faceVertexUvs[0].push([w,R,H])):(this.faces.push(new THREE.Face3(v,g,y,[_,b,x])),this.faceVertexUvs[0].push([w,R,M]),this.faces.push(new THREE.Face3(g,E,y,[b.clone(),T,x.clone()])),this.faceVertexUvs[0].push([R.clone(),H,M.clone()]))}this.computeFaceNormals(),this.boundingSphere=new THREE.Sphere(new THREE.Vector3,t)},THREE.SphereGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.SphereGeometry.prototype.constructor=THREE.SphereGeometry,THREE.TextGeometry=function(t,e){e=e||{};var n=THREE.FontUtils.generateShapes(t,e);e.amount=void 0!==e.height?e.height:50,void 0===e.bevelThickness&&(e.bevelThickness=10),void 0===e.bevelSize&&(e.bevelSize=8),void 0===e.bevelEnabled&&(e.bevelEnabled=!1),THREE.ExtrudeGeometry.call(this,n,e),this.type="TextGeometry"},THREE.TextGeometry.prototype=Object.create(THREE.ExtrudeGeometry.prototype),THREE.TextGeometry.prototype.constructor=THREE.TextGeometry,THREE.TorusGeometry=function(t,e,n,r,i){THREE.Geometry.call(this),this.type="TorusGeometry",this.parameters={radius:t,tube:e,radialSegments:n,tubularSegments:r,arc:i},t=t||100,e=e||40,n=n||8,r=r||6,i=i||2*Math.PI;for(var o=new THREE.Vector3,s=[],a=[],u=0;n>=u;u++)for(var h=0;r>=h;h++){var l=h/r*i,c=u/n*Math.PI*2;o.x=t*Math.cos(l),o.y=t*Math.sin(l);var p=new THREE.Vector3;p.x=(t+e*Math.cos(c))*Math.cos(l),p.y=(t+e*Math.cos(c))*Math.sin(l),p.z=e*Math.sin(c),this.vertices.push(p),s.push(new THREE.Vector2(h/r,u/n)),a.push(p.clone().sub(o).normalize())}for(var u=1;n>=u;u++)for(var h=1;r>=h;h++){var f=(r+1)*u+h-1,d=(r+1)*(u-1)+h-1,m=(r+1)*(u-1)+h,v=(r+1)*u+h,g=new THREE.Face3(f,d,v,[a[f].clone(),a[d].clone(),a[v].clone()]);this.faces.push(g),this.faceVertexUvs[0].push([s[f].clone(),s[d].clone(),s[v].clone()]),g=new THREE.Face3(d,m,v,[a[d].clone(),a[m].clone(),a[v].clone()]),this.faces.push(g),this.faceVertexUvs[0].push([s[d].clone(),s[m].clone(),s[v].clone()])}this.computeFaceNormals()},THREE.TorusGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.TorusGeometry.prototype.constructor=THREE.TorusGeometry,THREE.TorusKnotGeometry=function(t,e,n,r,i,o,s){function a(t,e,n,r,i){var o=Math.cos(t),s=Math.sin(t),a=e/n*t,u=Math.cos(a),h=r*(2+u)*.5*o,l=r*(2+u)*s*.5,c=i*r*Math.sin(a)*.5;return new THREE.Vector3(h,l,c)}THREE.Geometry.call(this),this.type="TorusKnotGeometry",this.parameters={radius:t,tube:e,radialSegments:n,tubularSegments:r,p:i,q:o,heightScale:s},t=t||100,e=e||40,n=n||64,r=r||8,i=i||2,o=o||3,s=s||1;for(var u=new Array(n),h=new THREE.Vector3,l=new THREE.Vector3,c=new THREE.Vector3,p=0;n>p;++p){u[p]=new Array(r);var f=p/n*2*i*Math.PI,d=a(f,o,i,t,s),m=a(f+.01,o,i,t,s);h.subVectors(m,d),l.addVectors(m,d),c.crossVectors(h,l),l.crossVectors(c,h),c.normalize(),l.normalize();for(var v=0;r>v;++v){var g=v/r*2*Math.PI,E=-e*Math.cos(g),y=e*Math.sin(g),_=new THREE.Vector3;_.x=d.x+E*l.x+y*c.x,_.y=d.y+E*l.y+y*c.y,_.z=d.z+E*l.z+y*c.z,u[p][v]=this.vertices.push(_)-1}}for(var p=0;n>p;++p)for(var v=0;r>v;++v){var b=(p+1)%n,T=(v+1)%r,x=u[p][v],w=u[b][v],R=u[b][T],H=u[p][T],M=new THREE.Vector2(p/n,v/r),S=new THREE.Vector2((p+1)/n,v/r),k=new THREE.Vector2((p+1)/n,(v+1)/r),A=new THREE.Vector2(p/n,(v+1)/r);this.faces.push(new THREE.Face3(x,w,H)),this.faceVertexUvs[0].push([M,S,A]),this.faces.push(new THREE.Face3(w,R,H)),this.faceVertexUvs[0].push([S.clone(),k,A.clone()])}this.computeFaceNormals(),this.computeVertexNormals()},THREE.TorusKnotGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.TorusKnotGeometry.prototype.constructor=THREE.TorusKnotGeometry,THREE.TubeGeometry=function(t,e,n,r,i,o){function s(t,e,n){return k.vertices.push(new THREE.Vector3(t,e,n))-1}THREE.Geometry.call(this),this.type="TubeGeometry",this.parameters={path:t,segments:e,radius:n,radialSegments:r,closed:i},e=e||64,n=n||1,r=r||8,i=i||!1,o=o||THREE.TubeGeometry.NoTaper;var a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S=[],k=this,A=e+1,C=new THREE.Vector3,P=new THREE.TubeGeometry.FrenetFrames(t,e,i),L=P.tangents,z=P.normals,O=P.binormals;for(this.tangents=L,this.normals=z,this.binormals=O,v=0;A>v;v++)for(S[v]=[],l=v/(A-1),m=t.getPointAt(l),a=L[v],u=z[v],h=O[v],p=n*o(l),g=0;r>g;g++)c=g/r*2*Math.PI,f=-p*Math.cos(c),d=p*Math.sin(c),C.copy(m),C.x+=f*u.x+d*h.x,C.y+=f*u.y+d*h.y,C.z+=f*u.z+d*h.z,S[v][g]=s(C.x,C.y,C.z);for(v=0;e>v;v++)for(g=0;r>g;g++)E=i?(v+1)%e:v+1,y=(g+1)%r,_=S[v][g],b=S[E][g],T=S[E][y],x=S[v][y],w=new THREE.Vector2(v/e,g/r),R=new THREE.Vector2((v+1)/e,g/r),H=new THREE.Vector2((v+1)/e,(g+1)/r),M=new THREE.Vector2(v/e,(g+1)/r),this.faces.push(new THREE.Face3(_,b,x)),this.faceVertexUvs[0].push([w,R,M]),this.faces.push(new THREE.Face3(b,T,x)),this.faceVertexUvs[0].push([R.clone(),H,M.clone()]);this.computeFaceNormals(),this.computeVertexNormals()},THREE.TubeGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.TubeGeometry.prototype.constructor=THREE.TubeGeometry,THREE.TubeGeometry.NoTaper=function(t){return 1},THREE.TubeGeometry.SinusoidalTaper=function(t){return Math.sin(Math.PI*t)},THREE.TubeGeometry.FrenetFrames=function(t,e,n){function r(){f[0]=new THREE.Vector3,d[0]=new THREE.Vector3,o=Number.MAX_VALUE,s=Math.abs(p[0].x),a=Math.abs(p[0].y),u=Math.abs(p[0].z),o>=s&&(o=s,c.set(1,0,0)),o>=a&&(o=a,c.set(0,1,0)),o>=u&&c.set(0,0,1),m.crossVectors(p[0],c).normalize(),f[0].crossVectors(p[0],m),d[0].crossVectors(p[0],f[0])}var i,o,s,a,u,h,l,c=new THREE.Vector3,p=[],f=[],d=[],m=new THREE.Vector3,v=new THREE.Matrix4,g=e+1,E=1e-4;for(this.tangents=p,this.normals=f,this.binormals=d,h=0;g>h;h++)l=h/(g-1),p[h]=t.getTangentAt(l),p[h].normalize();for(r(),h=1;g>h;h++)f[h]=f[h-1].clone(),d[h]=d[h-1].clone(),m.crossVectors(p[h-1],p[h]),m.length()>E&&(m.normalize(),i=Math.acos(THREE.Math.clamp(p[h-1].dot(p[h]),-1,1)),f[h].applyMatrix4(v.makeRotationAxis(m,i))),d[h].crossVectors(p[h],f[h]);if(n)for(i=Math.acos(THREE.Math.clamp(f[0].dot(f[g-1]),-1,1)),i/=g-1,p[0].dot(m.crossVectors(f[0],f[g-1]))>0&&(i=-i),h=1;g>h;h++)f[h].applyMatrix4(v.makeRotationAxis(p[h],i*h)),d[h].crossVectors(p[h],f[h])},THREE.PolyhedronGeometry=function(t,e,n,r){function i(t){var e=t.normalize().clone();e.index=l.vertices.push(e)-1;var n=a(t)/2/Math.PI+.5,r=u(t)/Math.PI+.5;return e.uv=new THREE.Vector2(n,1-r),e}function o(t,e,n){var r=new THREE.Face3(t.index,e.index,n.index,[t.clone(),e.clone(),n.clone()]);l.faces.push(r),y.copy(t).add(e).add(n).divideScalar(3);var i=a(y);l.faceVertexUvs[0].push([h(t.uv,t,i),h(e.uv,e,i),h(n.uv,n,i)])}function s(t,e){for(var n=Math.pow(2,e),r=i(l.vertices[t.a]),s=i(l.vertices[t.b]),a=i(l.vertices[t.c]),u=[],h=0;n>=h;h++){u[h]=[];for(var c=i(r.clone().lerp(a,h/n)),p=i(s.clone().lerp(a,h/n)),f=n-h,d=0;f>=d;d++)0==d&&h==n?u[h][d]=c:u[h][d]=i(c.clone().lerp(p,d/f))}for(var h=0;n>h;h++)for(var d=0;2*(n-h)-1>d;d++){var m=Math.floor(d/2);d%2==0?o(u[h][m+1],u[h+1][m],u[h][m]):o(u[h][m+1],u[h+1][m+1],u[h+1][m])}}function a(t){return Math.atan2(t.z,-t.x)}function u(t){return Math.atan2(-t.y,Math.sqrt(t.x*t.x+t.z*t.z))}function h(t,e,n){return 0>n&&1===t.x&&(t=new THREE.Vector2(t.x-1,t.y)),0===e.x&&0===e.z&&(t=new THREE.Vector2(n/2/Math.PI+.5,t.y)),t.clone()}THREE.Geometry.call(this),this.type="PolyhedronGeometry",this.parameters={vertices:t,indices:e,radius:n,detail:r},n=n||1,r=r||0;for(var l=this,c=0,p=t.length;p>c;c+=3)i(new THREE.Vector3(t[c],t[c+1],t[c+2]));for(var f=this.vertices,d=[],c=0,m=0,p=e.length;p>c;c+=3,m++){var v=f[e[c]],g=f[e[c+1]],E=f[e[c+2]];d[m]=new THREE.Face3(v.index,g.index,E.index,[v.clone(),g.clone(),E.clone()])}for(var y=new THREE.Vector3,c=0,p=d.length;p>c;c++)s(d[c],r);for(var c=0,p=this.faceVertexUvs[0].length;p>c;c++){var _=this.faceVertexUvs[0][c],b=_[0].x,T=_[1].x,x=_[2].x,w=Math.max(b,Math.max(T,x)),R=Math.min(b,Math.min(T,x));w>.9&&.1>R&&(.2>b&&(_[0].x+=1),.2>T&&(_[1].x+=1),.2>x&&(_[2].x+=1))}for(var c=0,p=this.vertices.length;p>c;c++)this.vertices[c].multiplyScalar(n);this.mergeVertices(),this.computeFaceNormals(),this.boundingSphere=new THREE.Sphere(new THREE.Vector3,n)},THREE.PolyhedronGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.PolyhedronGeometry.prototype.constructor=THREE.PolyhedronGeometry,THREE.DodecahedronGeometry=function(t,e){this.parameters={radius:t,detail:e};var n=(1+Math.sqrt(5))/2,r=1/n,i=[-1,-1,-1,-1,-1,1,-1,1,-1,-1,1,1,1,-1,-1,1,-1,1,1,1,-1,1,1,1,0,-r,-n,0,-r,n,0,r,-n,0,r,n,-r,-n,0,-r,n,0,r,-n,0,r,n,0,-n,0,-r,n,0,-r,-n,0,r,n,0,r],o=[3,11,7,3,7,15,3,15,13,7,19,17,7,17,6,7,6,15,17,4,8,17,8,10,17,10,6,8,0,16,8,16,2,8,2,10,0,12,1,0,1,18,0,18,16,6,10,2,6,2,13,6,13,15,2,16,18,2,18,3,2,3,13,18,1,9,18,9,11,18,11,3,4,14,12,4,12,0,4,0,8,11,9,5,11,5,19,11,19,7,19,5,14,19,14,4,19,4,17,1,12,14,1,14,5,1,5,9];THREE.PolyhedronGeometry.call(this,i,o,t,e)},THREE.DodecahedronGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.DodecahedronGeometry.prototype.constructor=THREE.DodecahedronGeometry,THREE.IcosahedronGeometry=function(t,e){var n=(1+Math.sqrt(5))/2,r=[-1,n,0,1,n,0,-1,-n,0,1,-n,0,0,-1,n,0,1,n,0,-1,-n,0,1,-n,n,0,-1,n,0,1,-n,0,-1,-n,0,1],i=[0,11,5,0,5,1,0,1,7,0,7,10,0,10,11,1,5,9,5,11,4,11,10,2,10,7,6,7,1,8,3,9,4,3,4,2,3,2,6,3,6,8,3,8,9,4,9,5,2,4,11,6,2,10,8,6,7,9,8,1];THREE.PolyhedronGeometry.call(this,r,i,t,e),this.type="IcosahedronGeometry",this.parameters={radius:t,detail:e}},THREE.IcosahedronGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.IcosahedronGeometry.prototype.constructor=THREE.IcosahedronGeometry,THREE.OctahedronGeometry=function(t,e){this.parameters={radius:t,detail:e};var n=[1,0,0,-1,0,0,0,1,0,0,-1,0,0,0,1,0,0,-1],r=[0,2,4,0,4,3,0,3,5,0,5,2,1,2,5,1,5,3,1,3,4,1,4,2];THREE.PolyhedronGeometry.call(this,n,r,t,e),this.type="OctahedronGeometry",this.parameters={radius:t,detail:e}},THREE.OctahedronGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.OctahedronGeometry.prototype.constructor=THREE.OctahedronGeometry,THREE.TetrahedronGeometry=function(t,e){var n=[1,1,1,-1,-1,1,-1,1,-1,1,-1,-1],r=[2,1,0,0,3,2,1,3,0,2,3,1];THREE.PolyhedronGeometry.call(this,n,r,t,e),this.type="TetrahedronGeometry",this.parameters={radius:t,detail:e}},THREE.TetrahedronGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.TetrahedronGeometry.prototype.constructor=THREE.TetrahedronGeometry,THREE.ParametricGeometry=function(t,e,n){THREE.Geometry.call(this),this.type="ParametricGeometry",this.parameters={func:t,slices:e,stacks:n};var r,i,o,s,a,u=this.vertices,h=this.faces,l=this.faceVertexUvs[0],c=e+1;for(r=0;n>=r;r++)for(a=r/n,i=0;e>=i;i++)s=i/e,o=t(s,a),u.push(o);var p,f,d,m,v,g,E,y;for(r=0;n>r;r++)for(i=0;e>i;i++)p=r*c+i,f=r*c+i+1,d=(r+1)*c+i+1,m=(r+1)*c+i,v=new THREE.Vector2(i/e,r/n),g=new THREE.Vector2((i+1)/e,r/n),E=new THREE.Vector2((i+1)/e,(r+1)/n),y=new THREE.Vector2(i/e,(r+1)/n),h.push(new THREE.Face3(p,f,m)),l.push([v,g,y]),h.push(new THREE.Face3(f,d,m)),l.push([g.clone(),E,y.clone()]);this.computeFaceNormals(),this.computeVertexNormals()},THREE.ParametricGeometry.prototype=Object.create(THREE.Geometry.prototype),THREE.ParametricGeometry.prototype.constructor=THREE.ParametricGeometry,THREE.AxisHelper=function(t){t=t||1;var e=new Float32Array([0,0,0,t,0,0,0,0,0,0,t,0,0,0,0,0,0,t]),n=new Float32Array([1,0,0,1,.6,0,0,1,0,.6,1,0,0,0,1,0,.6,1]),r=new THREE.BufferGeometry;r.addAttribute("position",new THREE.BufferAttribute(e,3)),r.addAttribute("color",new THREE.BufferAttribute(n,3));var i=new THREE.LineBasicMaterial({vertexColors:THREE.VertexColors});THREE.Line.call(this,r,i,THREE.LinePieces)},THREE.AxisHelper.prototype=Object.create(THREE.Line.prototype),THREE.AxisHelper.prototype.constructor=THREE.AxisHelper,THREE.ArrowHelper=function(){var t=new THREE.Geometry;t.vertices.push(new THREE.Vector3(0,0,0),new THREE.Vector3(0,1,0));var e=new THREE.CylinderGeometry(0,.5,1,5,1);return e.applyMatrix((new THREE.Matrix4).makeTranslation(0,-.5,0)),function(n,r,i,o,s,a){THREE.Object3D.call(this),void 0===o&&(o=16776960),void 0===i&&(i=1),void 0===s&&(s=.2*i),void 0===a&&(a=.2*s),this.position.copy(r),this.line=new THREE.Line(t,new THREE.LineBasicMaterial({color:o})),this.line.matrixAutoUpdate=!1,this.add(this.line),this.cone=new THREE.Mesh(e,new THREE.MeshBasicMaterial({color:o})),this.cone.matrixAutoUpdate=!1,this.add(this.cone),this.setDirection(n),this.setLength(i,s,a)}}(),THREE.ArrowHelper.prototype=Object.create(THREE.Object3D.prototype),THREE.ArrowHelper.prototype.constructor=THREE.ArrowHelper,THREE.ArrowHelper.prototype.setDirection=function(){var t,e=new THREE.Vector3;return function(n){n.y>.99999?this.quaternion.set(0,0,0,1):n.y<-.99999?this.quaternion.set(1,0,0,0):(e.set(n.z,0,-n.x).normalize(),t=Math.acos(n.y),this.quaternion.setFromAxisAngle(e,t))}}(),THREE.ArrowHelper.prototype.setLength=function(t,e,n){void 0===e&&(e=.2*t),void 0===n&&(n=.2*e),this.line.scale.set(1,t-e,1),this.line.updateMatrix(),this.cone.scale.set(n,e,n),this.cone.position.y=t,this.cone.updateMatrix()},THREE.ArrowHelper.prototype.setColor=function(t){this.line.material.color.set(t),this.cone.material.color.set(t)},THREE.BoxHelper=function(t){var e=new THREE.BufferGeometry;e.addAttribute("position",new THREE.BufferAttribute(new Float32Array(72),3)),THREE.Line.call(this,e,new THREE.LineBasicMaterial({color:16776960}),THREE.LinePieces),void 0!==t&&this.update(t)},THREE.BoxHelper.prototype=Object.create(THREE.Line.prototype),THREE.BoxHelper.prototype.constructor=THREE.BoxHelper,THREE.BoxHelper.prototype.update=function(t){var e=t.geometry;null===e.boundingBox&&e.computeBoundingBox();var n=e.boundingBox.min,r=e.boundingBox.max,i=this.geometry.attributes.position.array;i[0]=r.x,i[1]=r.y,i[2]=r.z,i[3]=n.x,i[4]=r.y,i[5]=r.z,i[6]=n.x,i[7]=r.y,i[8]=r.z,i[9]=n.x,i[10]=n.y,i[11]=r.z,i[12]=n.x,i[13]=n.y,i[14]=r.z,i[15]=r.x,i[16]=n.y,i[17]=r.z,i[18]=r.x,i[19]=n.y,i[20]=r.z,i[21]=r.x,i[22]=r.y,i[23]=r.z,i[24]=r.x,i[25]=r.y,i[26]=n.z,i[27]=n.x,i[28]=r.y,i[29]=n.z,i[30]=n.x,i[31]=r.y,i[32]=n.z,i[33]=n.x,i[34]=n.y,i[35]=n.z,i[36]=n.x,i[37]=n.y,i[38]=n.z,i[39]=r.x,i[40]=n.y,i[41]=n.z,i[42]=r.x,i[43]=n.y,i[44]=n.z,i[45]=r.x,i[46]=r.y,i[47]=n.z,i[48]=r.x,i[49]=r.y,i[50]=r.z,i[51]=r.x,i[52]=r.y,i[53]=n.z,i[54]=n.x,i[55]=r.y,i[56]=r.z,i[57]=n.x,i[58]=r.y,i[59]=n.z,i[60]=n.x,i[61]=n.y,i[62]=r.z,i[63]=n.x,i[64]=n.y,i[65]=n.z,i[66]=r.x,i[67]=n.y,i[68]=r.z,i[69]=r.x,i[70]=n.y,i[71]=n.z,this.geometry.attributes.position.needsUpdate=!0,this.geometry.computeBoundingSphere(),this.matrix=t.matrixWorld,this.matrixAutoUpdate=!1},THREE.BoundingBoxHelper=function(t,e){var n=void 0!==e?e:8947848;this.object=t,this.box=new THREE.Box3,THREE.Mesh.call(this,new THREE.BoxGeometry(1,1,1),new THREE.MeshBasicMaterial({color:n,wireframe:!0}))},THREE.BoundingBoxHelper.prototype=Object.create(THREE.Mesh.prototype),THREE.BoundingBoxHelper.prototype.constructor=THREE.BoundingBoxHelper,THREE.BoundingBoxHelper.prototype.update=function(){this.box.setFromObject(this.object),this.box.size(this.scale),this.box.center(this.position)},THREE.CameraHelper=function(t){function e(t,e,r){n(t,r),n(e,r)}function n(t,e){r.vertices.push(new THREE.Vector3),r.colors.push(new THREE.Color(e)),void 0===o[t]&&(o[t]=[]),o[t].push(r.vertices.length-1)}var r=new THREE.Geometry,i=new THREE.LineBasicMaterial({color:16777215,vertexColors:THREE.FaceColors}),o={},s=16755200,a=16711680,u=43775,h=16777215,l=3355443;e("n1","n2",s),e("n2","n4",s),e("n4","n3",s),e("n3","n1",s),e("f1","f2",s),e("f2","f4",s),e("f4","f3",s),e("f3","f1",s),e("n1","f1",s),e("n2","f2",s),e("n3","f3",s),e("n4","f4",s),e("p","n1",a),e("p","n2",a),e("p","n3",a),e("p","n4",a),e("u1","u2",u),e("u2","u3",u),e("u3","u1",u),e("c","t",h),e("p","c",l),e("cn1","cn2",l),e("cn3","cn4",l),e("cf1","cf2",l),e("cf3","cf4",l),THREE.Line.call(this,r,i,THREE.LinePieces),this.camera=t,this.matrix=t.matrixWorld,this.matrixAutoUpdate=!1,this.pointMap=o,this.update()},THREE.CameraHelper.prototype=Object.create(THREE.Line.prototype),THREE.CameraHelper.prototype.constructor=THREE.CameraHelper,THREE.CameraHelper.prototype.update=function(){var t,e,n=new THREE.Vector3,r=new THREE.Camera,i=function(i,o,s,a){n.set(o,s,a).unproject(r);var u=e[i];if(void 0!==u)for(var h=0,l=u.length;l>h;h++)t.vertices[u[h]].copy(n)};return function(){t=this.geometry,e=this.pointMap;var n=1,o=1;r.projectionMatrix.copy(this.camera.projectionMatrix),i("c",0,0,-1),i("t",0,0,1),i("n1",-n,-o,-1),i("n2",n,-o,-1),i("n3",-n,o,-1),i("n4",n,o,-1),i("f1",-n,-o,1),i("f2",n,-o,1),i("f3",-n,o,1),i("f4",n,o,1),i("u1",.7*n,1.1*o,-1),i("u2",.7*-n,1.1*o,-1),i("u3",0,2*o,-1),i("cf1",-n,0,1),i("cf2",n,0,1),i("cf3",0,-o,1),i("cf4",0,o,1),i("cn1",-n,0,-1),i("cn2",n,0,-1),i("cn3",0,-o,-1),i("cn4",0,o,-1),t.verticesNeedUpdate=!0}}(),THREE.DirectionalLightHelper=function(t,e){THREE.Object3D.call(this),this.light=t,this.light.updateMatrixWorld(),this.matrix=t.matrixWorld,this.matrixAutoUpdate=!1,e=e||1;var n=new THREE.Geometry;n.vertices.push(new THREE.Vector3(-e,e,0),new THREE.Vector3(e,e,0),new THREE.Vector3(e,-e,0),new THREE.Vector3(-e,-e,0),new THREE.Vector3(-e,e,0));var r=new THREE.LineBasicMaterial({fog:!1});r.color.copy(this.light.color).multiplyScalar(this.light.intensity),this.lightPlane=new THREE.Line(n,r),this.add(this.lightPlane),n=new THREE.Geometry,n.vertices.push(new THREE.Vector3,new THREE.Vector3),r=new THREE.LineBasicMaterial({fog:!1}),r.color.copy(this.light.color).multiplyScalar(this.light.intensity),this.targetLine=new THREE.Line(n,r),this.add(this.targetLine),this.update()},THREE.DirectionalLightHelper.prototype=Object.create(THREE.Object3D.prototype),THREE.DirectionalLightHelper.prototype.constructor=THREE.DirectionalLightHelper,THREE.DirectionalLightHelper.prototype.dispose=function(){this.lightPlane.geometry.dispose(),this.lightPlane.material.dispose(),this.targetLine.geometry.dispose(),this.targetLine.material.dispose()},THREE.DirectionalLightHelper.prototype.update=function(){var t=new THREE.Vector3,e=new THREE.Vector3,n=new THREE.Vector3;return function(){t.setFromMatrixPosition(this.light.matrixWorld),e.setFromMatrixPosition(this.light.target.matrixWorld),n.subVectors(e,t),this.lightPlane.lookAt(n),this.lightPlane.material.color.copy(this.light.color).multiplyScalar(this.light.intensity),this.targetLine.geometry.vertices[1].copy(n),this.targetLine.geometry.verticesNeedUpdate=!0,this.targetLine.material.color.copy(this.lightPlane.material.color)}}(),THREE.EdgesHelper=function(t,e,n){var r=void 0!==e?e:16777215;n=void 0!==n?n:1;var i,o=Math.cos(THREE.Math.degToRad(n)),s=[0,0],a={},u=function(t,e){return t-e},h=["a","b","c"],l=new THREE.BufferGeometry;t.geometry instanceof THREE.BufferGeometry?(i=new THREE.Geometry,i.fromBufferGeometry(t.geometry)):i=t.geometry.clone(),i.mergeVertices(),i.computeFaceNormals();for(var c=i.vertices,p=i.faces,f=0,d=0,m=p.length;m>d;d++)for(var v=p[d],g=0;3>g;g++){s[0]=v[h[g]],s[1]=v[h[(g+1)%3]],s.sort(u);var E=s.toString();void 0===a[E]?(a[E]={vert1:s[0],vert2:s[1],face1:d,face2:void 0},f++):a[E].face2=d}var y=new Float32Array(2*f*3),_=0;for(var E in a){var b=a[E];if(void 0===b.face2||p[b.face1].normal.dot(p[b.face2].normal)<=o){var T=c[b.vert1];y[_++]=T.x,y[_++]=T.y,y[_++]=T.z,T=c[b.vert2],y[_++]=T.x,y[_++]=T.y,y[_++]=T.z}}l.addAttribute("position",new THREE.BufferAttribute(y,3)),THREE.Line.call(this,l,new THREE.LineBasicMaterial({color:r}),THREE.LinePieces),this.matrix=t.matrixWorld,this.matrixAutoUpdate=!1},THREE.EdgesHelper.prototype=Object.create(THREE.Line.prototype),THREE.EdgesHelper.prototype.constructor=THREE.EdgesHelper,THREE.FaceNormalsHelper=function(t,e,n,r){this.object=t,this.size=void 0!==e?e:1;for(var i=void 0!==n?n:16776960,o=void 0!==r?r:1,s=new THREE.Geometry,a=this.object.geometry.faces,u=0,h=a.length;h>u;u++)s.vertices.push(new THREE.Vector3,new THREE.Vector3);THREE.Line.call(this,s,new THREE.LineBasicMaterial({color:i,linewidth:o}),THREE.LinePieces),this.matrixAutoUpdate=!1,this.normalMatrix=new THREE.Matrix3,this.update()},THREE.FaceNormalsHelper.prototype=Object.create(THREE.Line.prototype),THREE.FaceNormalsHelper.prototype.constructor=THREE.FaceNormalsHelper,THREE.FaceNormalsHelper.prototype.update=function(){var t=this.geometry.vertices,e=this.object,n=e.geometry.vertices,r=e.geometry.faces,i=e.matrixWorld;e.updateMatrixWorld(!0),this.normalMatrix.getNormalMatrix(i);for(var o=0,s=0,a=r.length;a>o;o++,s+=2){var u=r[o];t[s].copy(n[u.a]).add(n[u.b]).add(n[u.c]).divideScalar(3).applyMatrix4(i),t[s+1].copy(u.normal).applyMatrix3(this.normalMatrix).normalize().multiplyScalar(this.size).add(t[s])}return this.geometry.verticesNeedUpdate=!0,this},THREE.GridHelper=function(t,e){var n=new THREE.Geometry,r=new THREE.LineBasicMaterial({vertexColors:THREE.VertexColors});this.color1=new THREE.Color(4473924),this.color2=new THREE.Color(8947848);for(var i=-t;t>=i;i+=e){n.vertices.push(new THREE.Vector3(-t,0,i),new THREE.Vector3(t,0,i),new THREE.Vector3(i,0,-t),new THREE.Vector3(i,0,t));var o=0===i?this.color1:this.color2;n.colors.push(o,o,o,o)}THREE.Line.call(this,n,r,THREE.LinePieces)},THREE.GridHelper.prototype=Object.create(THREE.Line.prototype),THREE.GridHelper.prototype.constructor=THREE.GridHelper,THREE.GridHelper.prototype.setColors=function(t,e){this.color1.set(t),this.color2.set(e),this.geometry.colorsNeedUpdate=!0},THREE.HemisphereLightHelper=function(t,e){THREE.Object3D.call(this),this.light=t,this.light.updateMatrixWorld(),this.matrix=t.matrixWorld,this.matrixAutoUpdate=!1,this.colors=[new THREE.Color,new THREE.Color];var n=new THREE.SphereGeometry(e,4,2);n.applyMatrix((new THREE.Matrix4).makeRotationX(-Math.PI/2));for(var r=0,i=8;i>r;r++)n.faces[r].color=this.colors[4>r?0:1];var o=new THREE.MeshBasicMaterial({vertexColors:THREE.FaceColors,wireframe:!0});this.lightSphere=new THREE.Mesh(n,o),this.add(this.lightSphere),this.update()},THREE.HemisphereLightHelper.prototype=Object.create(THREE.Object3D.prototype),THREE.HemisphereLightHelper.prototype.constructor=THREE.HemisphereLightHelper,THREE.HemisphereLightHelper.prototype.dispose=function(){this.lightSphere.geometry.dispose(),this.lightSphere.material.dispose()},THREE.HemisphereLightHelper.prototype.update=function(){var t=new THREE.Vector3;return function(){this.colors[0].copy(this.light.color).multiplyScalar(this.light.intensity),this.colors[1].copy(this.light.groundColor).multiplyScalar(this.light.intensity),this.lightSphere.lookAt(t.setFromMatrixPosition(this.light.matrixWorld).negate()),this.lightSphere.geometry.colorsNeedUpdate=!0}}(),THREE.PointLightHelper=function(t,e){this.light=t,this.light.updateMatrixWorld();var n=new THREE.SphereGeometry(e,4,2),r=new THREE.MeshBasicMaterial({wireframe:!0,fog:!1});r.color.copy(this.light.color).multiplyScalar(this.light.intensity),THREE.Mesh.call(this,n,r),this.matrix=this.light.matrixWorld,this.matrixAutoUpdate=!1},THREE.PointLightHelper.prototype=Object.create(THREE.Mesh.prototype),THREE.PointLightHelper.prototype.constructor=THREE.PointLightHelper,THREE.PointLightHelper.prototype.dispose=function(){this.geometry.dispose(),this.material.dispose()},THREE.PointLightHelper.prototype.update=function(){this.material.color.copy(this.light.color).multiplyScalar(this.light.intensity)},THREE.SkeletonHelper=function(t){this.bones=this.getBoneList(t);for(var e=new THREE.Geometry,n=0;nu;u++)for(var l=a[u],c=0,p=l.vertexNormals.length;p>c;c++)s.vertices.push(new THREE.Vector3,new THREE.Vector3);THREE.Line.call(this,s,new THREE.LineBasicMaterial({color:i,linewidth:o}),THREE.LinePieces),this.matrixAutoUpdate=!1,this.normalMatrix=new THREE.Matrix3,this.update()},THREE.VertexNormalsHelper.prototype=Object.create(THREE.Line.prototype),THREE.VertexNormalsHelper.prototype.constructor=THREE.VertexNormalsHelper,THREE.VertexNormalsHelper.prototype.update=function(t){var e=new THREE.Vector3;return function(t){var n=["a","b","c","d"];this.object.updateMatrixWorld(!0),this.normalMatrix.getNormalMatrix(this.object.matrixWorld);for(var r=this.geometry.vertices,i=this.object.geometry.vertices,o=this.object.geometry.faces,s=this.object.matrixWorld,a=0,u=0,h=o.length;h>u;u++)for(var l=o[u],c=0,p=l.vertexNormals.length;p>c;c++){var f=l[n[c]],d=i[f],m=l.vertexNormals[c];r[a].copy(d).applyMatrix4(s),e.copy(m).applyMatrix3(this.normalMatrix).normalize().multiplyScalar(this.size),e.add(r[a]),a+=1,r[a].copy(e),a+=1}return this.geometry.verticesNeedUpdate=!0,this}}(),THREE.VertexTangentsHelper=function(t,e,n,r){this.object=t,this.size=void 0!==e?e:1;for(var i=void 0!==n?n:255,o=void 0!==r?r:1,s=new THREE.Geometry,a=t.geometry.faces,u=0,h=a.length;h>u;u++)for(var l=a[u],c=0,p=l.vertexTangents.length;p>c;c++)s.vertices.push(new THREE.Vector3),s.vertices.push(new THREE.Vector3);THREE.Line.call(this,s,new THREE.LineBasicMaterial({color:i,linewidth:o}),THREE.LinePieces),this.matrixAutoUpdate=!1,this.update()},THREE.VertexTangentsHelper.prototype=Object.create(THREE.Line.prototype),THREE.VertexTangentsHelper.prototype.constructor=THREE.VertexTangentsHelper,THREE.VertexTangentsHelper.prototype.update=function(t){var e=new THREE.Vector3;return function(t){var n=["a","b","c","d"];this.object.updateMatrixWorld(!0);for(var r=this.geometry.vertices,i=this.object.geometry.vertices,o=this.object.geometry.faces,s=this.object.matrixWorld,a=0,u=0,h=o.length;h>u;u++)for(var l=o[u],c=0,p=l.vertexTangents.length;p>c;c++){var f=l[n[c]],d=i[f],m=l.vertexTangents[c];r[a].copy(d).applyMatrix4(s),e.copy(m).transformDirection(s).multiplyScalar(this.size),e.add(r[a]),a+=1,r[a].copy(e),a+=1}return this.geometry.verticesNeedUpdate=!0,this}}(),THREE.WireframeHelper=function(t,e){var n=void 0!==e?e:16777215,r=[0,0],i={},o=function(t,e){return t-e},s=["a","b","c"],a=new THREE.BufferGeometry;if(t.geometry instanceof THREE.Geometry){for(var u=t.geometry.vertices,h=t.geometry.faces,l=0,c=new Uint32Array(6*h.length),p=0,f=h.length;f>p;p++)for(var d=h[p],m=0;3>m;m++){r[0]=d[s[m]],r[1]=d[s[(m+1)%3]],r.sort(o);var v=r.toString();void 0===i[v]&&(c[2*l]=r[0],c[2*l+1]=r[1],i[v]=!0,l++)}for(var g=new Float32Array(2*l*3),p=0,f=l;f>p;p++)for(var m=0;2>m;m++){var E=u[c[2*p+m]],y=6*p+3*m;g[y+0]=E.x,g[y+1]=E.y,g[y+2]=E.z}a.addAttribute("position",new THREE.BufferAttribute(g,3))}else if(t.geometry instanceof THREE.BufferGeometry)if(void 0!==t.geometry.attributes.index){var u=t.geometry.attributes.position.array,_=t.geometry.attributes.index.array,b=t.geometry.drawcalls,l=0;0===b.length&&(b=[{count:_.length,index:0,start:0}]);for(var c=new Uint32Array(2*_.length),T=0,x=b.length;x>T;++T)for(var w=b[T].start,R=b[T].count,y=b[T].index,p=w,H=w+R;H>p;p+=3)for(var m=0;3>m;m++){r[0]=y+_[p+m],r[1]=y+_[p+(m+1)%3],r.sort(o);var v=r.toString();void 0===i[v]&&(c[2*l]=r[0],c[2*l+1]=r[1],i[v]=!0,l++)}for(var g=new Float32Array(2*l*3),p=0,f=l;f>p;p++)for(var m=0;2>m;m++){var y=6*p+3*m,M=3*c[2*p+m];g[y+0]=u[M],g[y+1]=u[M+1],g[y+2]=u[M+2]}a.addAttribute("position",new THREE.BufferAttribute(g,3))}else{for(var u=t.geometry.attributes.position.array,l=u.length/3,S=l/3,g=new Float32Array(2*l*3),p=0,f=S;f>p;p++)for(var m=0;3>m;m++){var y=18*p+6*m,k=9*p+3*m;g[y+0]=u[k],g[y+1]=u[k+1],g[y+2]=u[k+2];var M=9*p+3*((m+1)%3);g[y+3]=u[M], -g[y+4]=u[M+1],g[y+5]=u[M+2]}a.addAttribute("position",new THREE.BufferAttribute(g,3))}THREE.Line.call(this,a,new THREE.LineBasicMaterial({color:n}),THREE.LinePieces),this.matrix=t.matrixWorld,this.matrixAutoUpdate=!1},THREE.WireframeHelper.prototype=Object.create(THREE.Line.prototype),THREE.WireframeHelper.prototype.constructor=THREE.WireframeHelper,THREE.ImmediateRenderObject=function(){THREE.Object3D.call(this),this.render=function(t){}},THREE.ImmediateRenderObject.prototype=Object.create(THREE.Object3D.prototype),THREE.ImmediateRenderObject.prototype.constructor=THREE.ImmediateRenderObject,THREE.MorphBlendMesh=function(t,e){THREE.Mesh.call(this,t,e),this.animationsMap={},this.animationsList=[];var n=this.geometry.morphTargets.length,r="__default",i=0,o=n-1,s=n/1;this.createAnimation(r,i,o,s),this.setAnimationWeight(r,1)},THREE.MorphBlendMesh.prototype=Object.create(THREE.Mesh.prototype),THREE.MorphBlendMesh.prototype.constructor=THREE.MorphBlendMesh,THREE.MorphBlendMesh.prototype.createAnimation=function(t,e,n,r){var i={startFrame:e,endFrame:n,length:n-e+1,fps:r,duration:(n-e)/r,lastFrame:0,currentFrame:0,active:!1,time:0,direction:1,weight:1,directionBackwards:!1,mirroredLoop:!1};this.animationsMap[t]=i,this.animationsList.push(i)},THREE.MorphBlendMesh.prototype.autoCreateAnimations=function(t){for(var e,n=/([a-z]+)_?(\d+)/,r={},i=this.geometry,o=0,s=i.morphTargets.length;s>o;o++){var a=i.morphTargets[o],u=a.name.match(n);if(u&&u.length>1){var h=u[1];r[h]||(r[h]={start:1/0,end:-(1/0)});var l=r[h];ol.end&&(l.end=o),e||(e=h)}}for(var h in r){var l=r[h];this.createAnimation(h,l.start,l.end,t)}this.firstAnimation=e},THREE.MorphBlendMesh.prototype.setAnimationDirectionForward=function(t){var e=this.animationsMap[t];e&&(e.direction=1,e.directionBackwards=!1)},THREE.MorphBlendMesh.prototype.setAnimationDirectionBackward=function(t){var e=this.animationsMap[t];e&&(e.direction=-1,e.directionBackwards=!0)},THREE.MorphBlendMesh.prototype.setAnimationFPS=function(t,e){var n=this.animationsMap[t];n&&(n.fps=e,n.duration=(n.end-n.start)/n.fps)},THREE.MorphBlendMesh.prototype.setAnimationDuration=function(t,e){var n=this.animationsMap[t];n&&(n.duration=e,n.fps=(n.end-n.start)/n.duration)},THREE.MorphBlendMesh.prototype.setAnimationWeight=function(t,e){var n=this.animationsMap[t];n&&(n.weight=e)},THREE.MorphBlendMesh.prototype.setAnimationTime=function(t,e){var n=this.animationsMap[t];n&&(n.time=e)},THREE.MorphBlendMesh.prototype.getAnimationTime=function(t){var e=0,n=this.animationsMap[t];return n&&(e=n.time),e},THREE.MorphBlendMesh.prototype.getAnimationDuration=function(t){var e=-1,n=this.animationsMap[t];return n&&(e=n.duration),e},THREE.MorphBlendMesh.prototype.playAnimation=function(t){var e=this.animationsMap[t];e?(e.time=0,e.active=!0):THREE.warn("THREE.MorphBlendMesh: animation["+t+"] undefined in .playAnimation()")},THREE.MorphBlendMesh.prototype.stopAnimation=function(t){var e=this.animationsMap[t];e&&(e.active=!1)},THREE.MorphBlendMesh.prototype.update=function(t){for(var e=0,n=this.animationsList.length;n>e;e++){var r=this.animationsList[e];if(r.active){var i=r.duration/r.length;r.time+=r.direction*t,r.mirroredLoop?(r.time>r.duration||r.time<0)&&(r.direction*=-1,r.time>r.duration&&(r.time=r.duration,r.directionBackwards=!0),r.time<0&&(r.time=0,r.directionBackwards=!1)):(r.time=r.time%r.duration,r.time<0&&(r.time+=r.duration));var o=r.startFrame+THREE.Math.clamp(Math.floor(r.time/i),0,r.length-1),s=r.weight;o!==r.currentFrame&&(this.morphTargetInfluences[r.lastFrame]=0,this.morphTargetInfluences[r.currentFrame]=1*s,this.morphTargetInfluences[o]=0,r.lastFrame=r.currentFrame,r.currentFrame=o);var a=r.time%i/i;r.directionBackwards&&(a=1-a),this.morphTargetInfluences[r.currentFrame]=a*s,this.morphTargetInfluences[r.lastFrame]=(1-a)*s}}},function(){function t(t,e,n){for(var r=(n||0)-1,i=t?t.length:0;++r-1?0:-1:e?0:-1}function n(t){var e=this.cache,n=typeof t;if("boolean"==n||null==t)e[t]=!0;else{"number"!=n&&"string"!=n&&(n="object");var r="number"==n?t:g+t,i=e[n]||(e[n]={});"object"==n?(i[r]||(i[r]=[])).push(t):i[r]=!0}}function r(t){return t.charCodeAt(0)}function i(t,e){for(var n=t.criteria,r=e.criteria,i=-1,o=n.length;++ia||"undefined"==typeof s)return 1;if(a>s||"undefined"==typeof a)return-1}}return t.index-e.index}function o(t){var e=-1,r=t.length,i=t[0],o=t[r/2|0],s=t[r-1];if(i&&"object"==typeof i&&o&&"object"==typeof o&&s&&"object"==typeof s)return!1;var a=u();a["false"]=a["null"]=a["true"]=a.undefined=!1;var h=u();for(h.array=t,h.cache=a,h.push=n;++ei?0:i);++r=E&&s===t,h=[];if(u){var c=o(r);c?(s=e,r=c):u=!1}for(;++i-1:void 0});return i.pop(),o.pop(),E&&(h(i),h(o)),s}function nt(t,e,n,r,i){(Jn(e)?Kt:ur)(e,function(e,o){var s,a,u=e,h=t[o];if(e&&((a=Jn(e))||hr(e))){for(var l=r.length;l--;)if(s=r[l]==e){h=i[l];break}if(!s){var c;n&&(u=n(h,e),(c="undefined"!=typeof u)&&(h=u)),c||(h=a?Jn(h)?h:[]:hr(h)?h:{}),r.push(e),i.push(h),c||nt(h,e,n,r,i)}}else n&&(u=n(h,e),"undefined"==typeof u&&(u=e)),"undefined"!=typeof u&&(h=u);t[o]=h})}function rt(t,e){return t+Cn(Yn()*(e-t+1))}function it(n,r,i){var s=-1,u=ut(),c=n?n.length:0,p=[],f=!r&&c>=E&&u===t,d=i||f?a():p;if(f){var m=o(d);u=e,d=m}for(;++s3&&"function"==typeof e[n-2])var r=K(e[--n-1],e[n--],2);else n>2&&"function"==typeof e[n-1]&&(r=e[--n]);for(var i=c(arguments,1,n),o=-1,s=a(),u=a();++on?Wn(0,o+n):n)||0,Jn(t)?s=i(t,e,n)>-1:"number"==typeof o?s=(Ot(t)?t.indexOf(e,n):i(t,e,n))>-1:ur(t,function(t){return++r>=n?!(s=t===e):void 0}),s}function qt(t,e,n){var r=!0;e=d.createCallback(e,n,3);var i=-1,o=t?t.length:0;if("number"==typeof o)for(;++io&&(o=u)}else e=null==e&&Ot(t)?r:d.createCallback(e,n,3),Kt(t,function(t,n,r){var s=e(t,n,r);s>i&&(i=s,o=t)});return o}function ee(t,e,n){var i=1/0,o=i;if("function"!=typeof e&&n&&n[e]===t&&(e=null),null==e&&Jn(t))for(var s=-1,a=t.length;++su&&(o=u)}else e=null==e&&Ot(t)?r:d.createCallback(e,n,3),Kt(t,function(t,n,r){var s=e(t,n,r);i>s&&(i=s,o=t)});return o}function ne(t,e,n,r){if(!t)return n;var i=arguments.length<3;e=d.createCallback(e,r,4);var o=-1,s=t.length;if("number"==typeof s)for(i&&(n=t[++o]);++or?Wn(0,i+r):r||0}else if(r){var o=He(e,n);return e[o]===n?o:-1}return t(e,n,r)}function Ee(t,e,n){var r=0,i=t?t.length:0;if("number"!=typeof e&&null!=e){var o=i;for(e=d.createCallback(e,n,3);o--&&e(t[o],o,t);)r++}else r=null==e||n?1:e||r;return c(t,0,qn(Wn(0,i-r),i))}function ye(){for(var n=[],r=-1,i=arguments.length,s=a(),u=ut(),c=u===t,p=a();++r=E&&o(r?n[r]:p)))}var d=n[0],m=-1,v=d?d.length:0,g=[];t:for(;++mn?Wn(0,r+n):qn(n,r-1))+1);r--;)if(t[r]===e)return r;return-1}function Te(t){for(var e=arguments,n=0,r=e.length,i=t?t.length:0;++ni;){var s=i+o>>>1;n(t[s])1?arguments:arguments[0],e=-1,n=t?te(fr(t,"length")):0,r=dn(0>n?0:n);++e2?st(t,17,c(arguments,2),null,e):st(t,1,null,null,e)}function Oe(t){for(var e=arguments.length>1?tt(arguments,!0,!1,1):_t(t),n=-1,r=e.length;++n2?st(e,19,c(arguments,2),null,t):st(e,3,null,null,t)}function Fe(){for(var t=arguments,e=t.length;e--;)if(!kt(t[e]))throw new xn;return function(){for(var e=arguments,n=t.length;n--;)e=[t[n].apply(this,e)];return e[0]}}function Ue(t,e){return e="number"==typeof e?e:+e||t.length,st(t,4,null,null,null,e)}function Be(t,e,n){var r,i,o,s,a,u,h,l=0,c=!1,p=!0;if(!kt(t))throw new xn;if(e=Wn(0,e)||0,n===!0){var d=!0;p=!1}else At(n)&&(d=n.leading,c="maxWait"in n&&(Wn(e,n.maxWait)||0),p="trailing"in n?n.trailing:p);var m=function(){var n=e-(mr()-s);if(0>=n){i&&An(i);var c=h;i=u=h=f,c&&(l=mr(),o=t.apply(a,r),u||i||(r=a=null))}else u=Dn(m,n)},v=function(){u&&An(u),i=u=h=f,(p||c!==e)&&(l=mr(),o=t.apply(a,r),u||i||(r=a=null))};return function(){if(r=arguments,s=mr(),a=this,h=p&&(u||!d),c===!1)var n=d&&!u;else{i||d||(l=s);var f=c-(s-l),g=0>=f;g?(i&&(i=An(i)),l=s,o=t.apply(a,r)):i||(i=Dn(v,f))}return g&&u?u=An(u):u||e===c||(u=Dn(m,e)),n&&(g=!0,o=t.apply(a,r)),!g||u||i||(r=a=null),o}}function Ne(t){if(!kt(t))throw new xn;var e=c(arguments,1);return Dn(function(){t.apply(f,e)},1)}function Ve(t,e){if(!kt(t))throw new xn;var n=c(arguments,2);return Dn(function(){t.apply(f,n)},e)}function Ie(t,e){if(!kt(t))throw new xn;var n=function(){var r=n.cache,i=e?e.apply(this,arguments):g+arguments[0];return zn.call(r,i)?r[i]:r[i]=t.apply(this,arguments)};return n.cache={},n}function je(t){var e,n;if(!kt(t))throw new xn;return function(){return e?n:(e=!0,n=t.apply(this,arguments),t=null,n)}}function Ge(t){return st(t,16,c(arguments,1))}function We(t){return st(t,32,null,c(arguments,1))}function qe(t,e,n){var r=!0,i=!0;if(!kt(t))throw new xn;return n===!1?r=!1:At(n)&&(r="leading"in n?n.leading:r,i="trailing"in n?n.trailing:i),G.leading=r,G.maxWait=e,G.trailing=i,Be(t,e,G)}function Xe(t,e){return st(e,16,[t])}function Ye(t){return function(){return t}}function Ze(t,e,n){var r=typeof t;if(null==t||"function"==r)return K(t,e,n);if("object"!=r)return en(t);var i=tr(t),o=i[0],s=t[o];return 1!=i.length||s!==s||At(s)?function(e){for(var n=i.length,r=!1;n--&&(r=et(e[i[n]],t[i[n]],null,!0)););return r}:function(t){var e=t[o];return s===e&&(0!==s||1/s==1/e)}}function Ke(t){return null==t?"":Tn(t).replace(ir,at)}function Qe(t){return t}function Je(t,e,n){var r=!0,i=e&&_t(e);e&&(n||i.length)||(null==n&&(n=e),o=m,e=t,t=d,i=_t(e)),n===!1?r=!1:At(n)&&"chain"in n&&(r=n.chain);var o=t,s=kt(o);Kt(i,function(n){var i=t[n]=e[n];s&&(o.prototype[n]=function(){var e=this.__chain__,n=this.__wrapped__,s=[n];On.apply(s,arguments);var a=i.apply(t,s);if(r||e){if(n===a&&At(a))return this;a=new o(a),a.__chain__=e}return a})})}function $e(){return n._=Hn,this}function tn(){}function en(t){return function(e){return e[t]}}function nn(t,e,n){var r=null==t,i=null==e;if(null==n&&("boolean"==typeof t&&i?(n=t,t=1):i||"boolean"!=typeof e||(n=e,i=!0)),r&&i&&(e=1),t=+t||0,i?(e=t,t=0):e=+e||0,n||t%1||e%1){var o=Yn();return qn(t+o*(e-t+parseFloat("1e-"+((o+"").length-1))),e)}return rt(t,e)}function rn(t,e){if(t){var n=t[e];return kt(n)?t[e]():n}}function on(t,e,n){var r=d.templateSettings;t=Tn(t||""),n=sr({},n,r);var i,o=sr({},n.imports,r.imports),a=tr(o),u=jt(o),h=0,l=n.interpolate||k,c="__p += '",p=bn((n.escape||k).source+"|"+l.source+"|"+(l===M?w:k).source+"|"+(n.evaluate||k).source+"|$","g");t.replace(p,function(e,n,r,o,a,u){return r||(r=o),c+=t.slice(h,u).replace(C,s),n&&(c+="' +\n__e("+n+") +\n'"),a&&(i=!0,c+="';\n"+a+";\n__p += '"),r&&(c+="' +\n((__t = ("+r+")) == null ? '' : __t) +\n'"),h=u+e.length,e}),c+="';\n";var m=n.variable,v=m;v||(m="obj",c="with ("+m+") {\n"+c+"\n}\n"),c=(i?c.replace(b,""):c).replace(T,"$1").replace(x,"$1;"),c="function("+m+") {\n"+(v?"":m+" || ("+m+" = {});\n")+"var __t, __p = '', __e = _.escape"+(i?", __j = Array.prototype.join;\nfunction print() { __p += __j.call(arguments, '') }\n":";\n")+c+"return __p\n}";var g="\n/*\n//# sourceURL="+(n.sourceURL||"/lodash/template/source["+L++ +"]")+"\n*/";try{var E=gn(a,"return "+c+g).apply(f,u)}catch(y){throw y.source=c,y}return e?E(e):(E.source=c,E)}function sn(t,e,n){t=(t=+t)>-1?t:0;var r=-1,i=dn(t);for(e=K(e,n,1);++r/g,evaluate:/<%([\s\S]+?)%>/g,interpolate:M,variable:"",imports:{_:d}},Nn||(Z=function(){function t(){}return function(e){if(At(e)){t.prototype=e;var r=new t;t.prototype=null}return r||n.Object()}}());var Qn=Bn?function(t,e){W.value=e,Bn(t,"__bindData__",W)}:tn,Jn=Vn||function(t){return t&&"object"==typeof t&&"number"==typeof t.length&&Mn.call(t)==O||!1},$n=function(t){var e,n=t,r=[];if(!n)return r;if(!q[typeof t])return r;for(e in n)zn.call(n,e)&&r.push(e);return r},tr=Gn?function(t){return At(t)?Gn(t):[]}:$n,er={"&":"&","<":"<",">":">",'"':""","'":"'"},nr=Tt(er),rr=bn("("+tr(nr).join("|")+")","g"),ir=bn("["+tr(er).join("")+"]","g"),or=function(t,e,n){var r,i=t,o=i;if(!i)return o;var s=arguments,a=0,u="number"==typeof n?2:s.length;if(u>3&&"function"==typeof s[u-2])var h=K(s[--u-1],s[u--],2);else u>2&&"function"==typeof s[u-1]&&(h=s[--u]);for(;++a/g,S=RegExp("^["+_+"]*0+(?=.$)"),k=/($^)/,A=/\bthis\b/,C=/['\n\r\t\u2028\u2029\\]/g,P=["Array","Boolean","Date","Function","Math","Number","Object","RegExp","String","_","attachEvent","clearTimeout","isFinite","isNaN","parseInt","setTimeout"],L=0,z="[object Arguments]",O="[object Array]",D="[object Boolean]",F="[object Date]",U="[object Function]",B="[object Number]",N="[object Object]",V="[object RegExp]",I="[object String]",j={};j[U]=!1,j[z]=j[O]=j[D]=j[F]=j[B]=j[N]=j[V]=j[I]=!0;var G={leading:!1,maxWait:0,trailing:!1},W={configurable:!1,enumerable:!1,value:null,writable:!1},q={"boolean":!1,"function":!0,object:!0,number:!1,string:!1,undefined:!1},X={"\\":"\\","'":"'","\n":"n","\r":"r"," ":"t","\u2028":"u2028","\u2029":"u2029"},Y=q[typeof window]&&window||this,Z=q[typeof exports]&&exports&&!exports.nodeType&&exports,K=q[typeof module]&&module&&!module.nodeType&&module,Q=K&&K.exports===Z&&Z,J=q[typeof global]&&global; -!J||J.global!==J&&J.window!==J||(Y=J);var $=p();"function"==typeof define&&"object"==typeof define.amd&&define.amd?(Y._=$,define(function(){return $})):Z&&K?Q?(K.exports=$)._=$:Z._=$:Y._=$}.call(this),THREE.Binder={bind:function(t,e){return function(n,r){r.__binds||(r.__binds=[]);var i=t;_.isArray(n)&&(i=n[0],n=n[1]);for(var o=/^([^.:]*(?:\.[^.:]+)*)?(?:\:(.*))?$/.exec(n),s=o[1].split(/\./g),a=s.pop(),u=o[2]||a,h=s.shift(),l={"this":r}[h]||e[h]||t[h]||i;l&&(n=s.shift());)l=l[n];if(l&&(l.on||l.addEventListener)){var c=function(e){r[u]&&r[u](e,t)};THREE.Binder._polyfill(l,["addEventListener","on"],function(t){l[t](a,c)});var p={target:l,name:a,callback:c};return r.__binds.push(p),c}throw"Cannot bind '"+n+"' in "+this.__name}},unbind:function(){return function(t){t.__binds&&(t.__binds.forEach(function(t){THREE.Binder._polyfill(t.target,["removeEventListener","off"],function(e){t.target[e](t.name,t.callback)})}.bind(this)),t.__binds=[])}},apply:function(t){THREE.EventDispatcher.prototype.apply(t),t.trigger=THREE.Binder._trigger,t.triggerOnce=THREE.Binder._triggerOnce,t.on=t.addEventListener,t.off=t.removeEventListener,t.dispatchEvent=t.trigger},_triggerOnce:function(t){this.trigger(t),this._listeners&&delete this._listeners[t.type]},_trigger:function(t){if(void 0!==this._listeners){var e=t.type,n=this._listeners[e];if(void 0!==n){n=n.slice();var r=n.length;t.target=this;for(var i=0;r>i;i++)n[i].call(this,t,this)}}},_polyfill:function(t,e,n){e.map(function(e){return t.method}),e.length&&n(e[0])}},THREE.Api={apply:function(t){t.set=function(t){var e=this.options||{},n=_.reduce(t,function(t,n,r){return e[r]!==n&&(t[r]=n),t},{});this.options=_.extend(e,n),this.trigger({type:"change",options:t,changes:n})},t.get=function(){return this.options},t.api=function(t,e){return t=t||{},e&&_.each(t,function(t,n,r){_.isFunction(t)&&(r[n]=_.partialRight(t,e))}),t.set=this.set.bind(this),t.get=this.get.bind(this),t}}},THREE.Bootstrap=function(t){if(t){var e=[].slice.apply(arguments);t={},e[0]instanceof Node&&(node=e[0],e=e.slice(1),t.element=node),_.isString(e[0])&&(t.plugins=e),_.isArray(e[0])&&(t.plugins=e[0]),e[0]&&(t=_.defaults(t,e[0]))}if(!(this instanceof THREE.Bootstrap))return new THREE.Bootstrap(t);var n={init:!0,element:document.body,plugins:["core"],aliases:{},plugindb:THREE.Bootstrap.Plugins||{},aliasdb:THREE.Bootstrap.Aliases||{}};this.__options=_.defaults(t||{},n),this.__inited=!1,this.__destroyed=!1,this.__installed=[];var r=this.__options.element;r===""+r&&(r=document.querySelector(r)),this.plugins={},this.element=r,this.__options.init&&this.init()},THREE.Bootstrap.prototype={init:function(){return this.__inited?void 0:(this.__inited=!0,this.install(this.__options.plugins),this)},destroy:function(){return this.__inited&&!this.__destroyed?(this.__destroyed=!0,this.trigger({type:"destroy"}),this.uninstall(),this):void 0},resolve:function(t){function e(t,n,o){if(o>=256)throw"Plug-in alias recursion detected.";return t=_.filter(t,i),_.each(t,function(t){var i=r[t];i?n=n.concat(e(i,[],o+1)):n.push(t)}),n}t=_.isArray(t)?t:[t];var n=this.__options,r=_.extend({},n.aliasdb,n.aliases),i=function(t){var e=t.split(":");return e[1]?(r[e[0]]=[e[1]],!1):!0};return t=_.filter(t,i),_.each(r,function(t,e){r[e]=_.isArray(t)?t:[t]}),e(t,[],0)},install:function(t){t=_.isArray(t)?t:[t],t=this.resolve(t),_.each(t,this.__install,this),this.__ready()},uninstall:function(t){t&&(t=_.isArray(t)?t:[t],t=this.resolve(t)),_.eachRight(t||this.__installed,this.__uninstall,this)},__install:function(t){var e=this.__options.plugindb[t];if(!e)throw"[three.install] Cannot install. '"+t+"' is not registered.";if(this.plugins[t])return console.warn("[three.install] "+t+" is already installed.");var n=e,r=new n(this.__options[t]||{},t);return this.plugins[t]=r,flag=r.install(this),this.__installed.push(r),this.trigger({type:"install",plugin:r}),flag},__uninstall:function(t,e){return plugin=_.isString(t)?this.plugins[t]:t,plugin?(t=plugin.__name,plugin.uninstall(this),this.__installed=_.without(this.__installed,plugin),delete this.plugins[t],void this.trigger({type:"uninstall",plugin:plugin})):console.warn("[three.uninstall] "+t+"' is not installed.")},__ready:function(){this.triggerOnce({type:"ready"})}},THREE.Binder.apply(THREE.Bootstrap.prototype),THREE.Bootstrap.Plugins={},THREE.Bootstrap.Aliases={},THREE.Bootstrap.Plugin=function(t){this.options=_.defaults(t||{},this.defaults)},THREE.Bootstrap.Plugin.prototype={listen:[],defaults:{},install:function(t){},uninstall:function(t){}},THREE.Binder.apply(THREE.Bootstrap.Plugin.prototype),THREE.Api.apply(THREE.Bootstrap.Plugin.prototype),THREE.Bootstrap.registerPlugin=function(t,e){var n=function(e){THREE.Bootstrap.Plugin.call(this,e),this.__name=t};n.prototype=_.extend(new THREE.Bootstrap.Plugin,e),THREE.Bootstrap.Plugins[t]=n},THREE.Bootstrap.unregisterPlugin=function(t){delete THREE.Bootstrap.Plugins[t]},THREE.Bootstrap.registerAlias=function(t,e){THREE.Bootstrap.Aliases[t]=e},THREE.Bootstrap.unregisterAlias=function(t){delete THREE.Bootstrap.Aliases[t]},THREE.Bootstrap.registerAlias("empty",["fallback","bind","renderer","size","fill","loop","time"]),THREE.Bootstrap.registerAlias("core",["empty","scene","camera","render","warmup"]),THREE.Bootstrap.registerAlias("VR",["core","cursor","fullscreen","render:vr"]),THREE.Bootstrap.registerPlugin("fallback",{defaults:{force:!1,fill:!0,begin:'
    ',end:"
    ",message:'This example requires WebGL
    Visit get.webgl.org for more info'},install:function(t){var e;try{if(e=document.createElement("canvas"),gl=e.getContext("webgl")||e.getContext("experimental-webgl"),!gl||this.options.force)throw"WebGL unavailable.";t.fallback=!1}catch(n){var r=this.options.message,i=this.options.begin,o=this.options.end,s=this.options.fill,a=document.createElement("div");for(a.innerHTML=i+r+o,this.children=[];a.childNodes.length>0;)this.children.push(a.firstChild),t.element.appendChild(a.firstChild);return s&&t.install("fill"),this.div=a,t.fallback=!0,!1}},uninstall:function(t){this.children&&(this.children.forEach(function(t){t.parentNode.removeChild(t)}),this.children=null),delete t.fallback}}),THREE.Bootstrap.registerPlugin("renderer",{defaults:{klass:THREE.WebGLRenderer,parameters:{depth:!0,stencil:!0,preserveDrawingBuffer:!0,antialias:!0}},listen:["resize"],install:function(t){var e=t.renderer=new this.options.klass(this.options.parameters);t.canvas=e.domElement,t.element.appendChild(e.domElement)},uninstall:function(t){t.element.removeChild(t.renderer.domElement),delete t.renderer,delete t.canvas},resize:function(t,e){var n=e.renderer,r=n.domElement;r&&"CANVAS"==r.tagName?n.setSize(t.renderWidth,t.renderHeight,!1):(n.setRenderSize&&n.setRenderSize(t.renderWidth,t.renderHeight),n.setSize(t.viewWidth,t.viewHeight,!1))}}),THREE.Bootstrap.registerPlugin("bind",{install:function(t){var e={three:t,window:window};t.bind=THREE.Binder.bind(t,e),t.unbind=THREE.Binder.unbind(t),t.bind("install:bind",this),t.bind("uninstall:unbind",this)},uninstall:function(t){t.unbind(this),delete t.bind,delete t.unbind},bind:function(t,e){var n=t.plugin,r=n.listen;r&&r.forEach(function(t){e.bind(t,n)})},unbind:function(t,e){e.unbind(t.plugin)}}),THREE.Bootstrap.registerPlugin("size",{defaults:{width:null,height:null,aspect:null,scale:1,maxRenderWidth:1/0,maxRenderHeight:1/0,devicePixelRatio:!0},listen:["window.resize:queue","element.resize:queue","this.change:queue","ready:resize","pre:pre"],install:function(t){t.Size=this.api({renderWidth:0,renderHeight:0,viewWidth:0,viewHeight:0}),this.resized=!1},uninstall:function(t){delete t.Size},queue:function(t,e){this.resized=!0},pre:function(t,e){this.resized&&(this.resized=!1,this.resize(t,e))},resize:function(t,e){var n,r,i,o,s,a,u,h,l,c=this.options,p=e.element,f=e.renderer,d=0,m=0;n=i=void 0===c.width||null==c.width?p.offsetWidth||p.innerWidth||0:c.width,r=o=void 0===c.height||null==c.height?p.offsetHeight||p.innerHeight||0:c.height,u=n/r,c.aspect&&(c.aspect>u?(r=Math.round(n/c.aspect),m=Math.floor((o-r)/2)):(n=Math.round(r*c.aspect),d=Math.floor((i-n)/2)),u=n/r),l=1,c.devicePixelRatio&&"undefined"!=typeof window&&(l=window.devicePixelRatio||1),s=Math.min(n*l*c.scale,c.maxRenderWidth),a=Math.min(r*l*c.scale,c.maxRenderHeight),raspect=s/a,raspect>u?s=Math.round(a*u):a=Math.round(s/u),l=a/r,h=f.domElement.style,h.width=n+"px",h.height=r+"px",h.marginLeft=d+"px",h.marginTop=m+"px",_.extend(e.Size,{renderWidth:s,renderHeight:a,viewWidth:n,viewHeight:r,aspect:u,pixelRatio:l}),e.trigger({type:"resize",renderWidth:s,renderHeight:a,viewWidth:n,viewHeight:r,aspect:u,pixelRatio:l})}}),THREE.Bootstrap.registerPlugin("fill",{defaults:{block:!0,body:!0,layout:!0},install:function(t){function e(t){var e=t.style.height;return"auto"==e||""==e}function n(t){return t.style.height="100%",t.style.margin=0,t.style.padding=0,t}if(this.options.body&&t.element==document.body&&(this.applied=[t.element,document.documentElement].filter(e).map(n)),this.options.block&&t.canvas&&(t.canvas.style.display="block",this.block=!0),this.options.layout&&t.element){var r=window.getComputedStyle(t.element);"static"==r.position&&(t.element.style.position="relative",this.layout=!0)}},uninstall:function(t){function e(t){return t.style.height="",t.style.margin="",t.style.padding="",t}this.applied&&(this.applied.map(e),delete this.applied),this.block&&t.canvas&&(t.canvas.style.display="",delete this.block),this.layout&&t.element&&(t.element.style.position="",delete this.layout)},change:function(t){this.uninstall(t),this.install(t)}}),THREE.Bootstrap.registerPlugin("loop",{defaults:{start:!0},listen:["ready"],install:function(t){this.running=!1,t.Loop=this.api({start:this.start.bind(this),stop:this.stop.bind(this),running:!1},t),this.events=["pre","update","render","post"].map(function(t){return{type:t}})},uninstall:function(t){this.stop(t)},ready:function(t,e){this.options.start&&this.start(e)},start:function(t){if(!this.running){t.Loop.running=this.running=!0;var e=t.trigger.bind(t),n=function(){this.running&&requestAnimationFrame(n),this.events.map(e)}.bind(this);requestAnimationFrame(n),t.trigger({type:"start"})}},stop:function(t){this.running&&(t.Loop.running=this.running=!1,t.trigger({type:"stop"}))}}),THREE.Bootstrap.registerPlugin("time",{defaults:{speed:1,warmup:0,timeout:1},listen:["pre:tick","this.change"],now:function(){return+new Date/1e3},install:function(t){t.Time=this.api({now:this.now(),clock:0,step:1/60,frames:0,time:0,delta:1/60,average:0,fps:0}),this.last=0,this.time=0,this.clock=0,this.wait=this.options.warmup,this.clockStart=0,this.timeStart=0},tick:function(t,e){var n=this.options.speed,r=this.options.timeout,i=e.Time,o=i.now=this.now(),s=this.last,a=this.time,u=this.clock;if(s){var h=i.delta=o-s,l=i.average||h;h>r&&(h=0);var c=h*n;a+=h,u+=c,i.frames>0&&(i.average=l+.1*(h-l),i.fps=1/l),i.step=c,i.clock=u-this.clockStart,i.time=a-this.timeStart,i.frames++,this.wait-->0&&(this.clockStart=u,this.timeStart=a,i.clock=0,i.step=1e-100)}this.last=o,this.clock=u,this.time=a},uninstall:function(t){delete t.Time}}),THREE.Bootstrap.registerPlugin("scene",{install:function(t){t.scene=new THREE.Scene},uninstall:function(t){delete t.scene}}),THREE.Bootstrap.registerPlugin("camera",{defaults:{near:.01,far:1e4,type:"perspective",fov:60,aspect:null,left:-1,right:1,bottom:-1,top:1,klass:null,parameters:null},listen:["resize","this.change"],install:function(t){t.Camera=this.api(),t.camera=null,this.aspect=1,this.change({},t)},uninstall:function(t){delete t.Camera,delete t.camera},change:function(t,e){var n=this.options,r=e.camera;if(!e.camera||t.changes.type||t.changes.klass){var i=n.klass||{perspective:THREE.PerspectiveCamera,orthographic:THREE.OrthographicCamera}[n.type]||THREE.Camera;e.camera=n.parameters?new i(n.parameters):new i}_.each(n,function(t,r){e.camera.hasOwnProperty(r)&&(e.camera[r]=n[r])}.bind(this)),this.update(e),r===e.camera||e.trigger({type:"camera",camera:e.camera})},resize:function(t,e){this.aspect=t.viewWidth/Math.max(1,t.viewHeight),this.update(e)},update:function(t){t.camera.aspect=this.options.aspect||this.aspect,t.camera.updateProjectionMatrix()}}),THREE.Bootstrap.registerPlugin("render",{listen:["render"],render:function(t,e){e.scene&&e.camera&&e.renderer.render(e.scene,e.camera)}}),THREE.Bootstrap.registerPlugin("warmup",{defaults:{delay:2},listen:["ready","post"],ready:function(t,e){e.renderer.domElement.style.visibility="hidden",this.frame=0,this.hidden=!0},post:function(t,e){this.hidden&&this.frame>=this.options.delay&&(e.renderer.domElement.style.visibility="visible",this.hidden=!1),this.frame++}}),THREE.Stats=function(){var t=Date.now(),e=t,n=0,r=1/0,i=0,o=0,s=1/0,a=0,u=0,h=0,l=document.createElement("div");l.id="stats",l.addEventListener("mousedown",function(t){t.preventDefault(),E(++h%2)},!1),l.style.cssText="width:80px;opacity:0.9;cursor:pointer";var c=document.createElement("div");c.id="fps",c.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#002",l.appendChild(c);var p=document.createElement("div");p.id="fpsText",p.style.cssText="color:#0ff;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px",p.innerHTML="FPS",c.appendChild(p);var f=document.createElement("div");for(f.id="fpsGraph",f.style.cssText="position:relative;width:74px;height:30px;background-color:#0ff",c.appendChild(f);74>f.children.length;){var d=document.createElement("span");d.style.cssText="width:1px;height:30px;float:left;background-color:#113",f.appendChild(d)}var m=document.createElement("div");m.id="ms",m.style.cssText="padding:0 0 3px 3px;text-align:left;background-color:#020;display:none",l.appendChild(m);var v=document.createElement("div");v.id="msText",v.style.cssText="color:#0f0;font-family:Helvetica,Arial,sans-serif;font-size:9px;font-weight:bold;line-height:15px",v.innerHTML="MS",m.appendChild(v);var g=document.createElement("div");for(g.id="msGraph",g.style.cssText="position:relative;width:74px;height:30px;background-color:#0f0",m.appendChild(g);74>g.children.length;)d=document.createElement("span"),d.style.cssText="width:1px;height:30px;float:left;background-color:#131",g.appendChild(d);var E=function(t){switch(h=t){case 0:c.style.display="block",m.style.display="none";break;case 1:c.style.display="none",m.style.display="block"}};return{REVISION:11,domElement:l,setMode:E,begin:function(){t=Date.now()},end:function(){var h=Date.now();n=h-t,r=Math.min(r,n),i=Math.max(i,n),v.textContent=n+" MS ("+r+"-"+i+")";var l=Math.min(30,30-30*(n/200));return g.appendChild(g.firstChild).style.height=l+"px",u++,h>e+1e3&&(o=Math.round(1e3*u/(h-e)),s=Math.min(s,o),a=Math.max(a,o),p.textContent=o+" FPS ("+s+"-"+a+")",l=Math.min(30,30-30*(o/100)),f.appendChild(f.firstChild).style.height=l+"px",e=h,u=0),h},update:function(){t=this.end()}}},THREE.DeviceOrientationControls=function(t){var e=this;this.object=t,this.object.rotation.reorder("YXZ"),this.freeze=!0,this.deviceOrientation={},this.screenOrientation=0;var n=function(t){e.deviceOrientation=t},r=function(){e.screenOrientation=window.orientation||0},i=function(){var t=new THREE.Vector3(0,0,1),e=new THREE.Euler,n=new THREE.Quaternion,r=new THREE.Quaternion(-Math.sqrt(.5),0,0,Math.sqrt(.5));return function(i,o,s,a,u){e.set(s,o,-a,"YXZ"),i.setFromEuler(e),i.multiply(r),i.multiply(n.setFromAxisAngle(t,-u))}}();this.connect=function(){r(),window.addEventListener("orientationchange",r,!1),window.addEventListener("deviceorientation",n,!1),e.freeze=!1},this.disconnect=function(){e.freeze=!0,window.removeEventListener("orientationchange",r,!1),window.removeEventListener("deviceorientation",n,!1)},this.update=function(){if(!e.freeze){var t=e.deviceOrientation.gamma?THREE.Math.degToRad(e.deviceOrientation.alpha):0,n=e.deviceOrientation.beta?THREE.Math.degToRad(e.deviceOrientation.beta):0,r=e.deviceOrientation.gamma?THREE.Math.degToRad(e.deviceOrientation.gamma):0,o=e.screenOrientation?THREE.Math.degToRad(e.screenOrientation):0;i(e.object.quaternion,t,n,r,o)}}},THREE.FirstPersonControls=function(t,e){function n(t,e){return function(){e.apply(t,arguments)}}this.object=t,this.target=new THREE.Vector3(0,0,0),this.domElement=void 0!==e?e:document,this.movementSpeed=1,this.lookSpeed=.005,this.lookVertical=!0,this.autoForward=!1,this.activeLook=!0,this.heightSpeed=!1,this.heightCoef=1,this.heightMin=0,this.heightMax=1,this.constrainVertical=!1,this.verticalMin=0,this.verticalMax=Math.PI,this.autoSpeedFactor=0,this.mouseX=0,this.mouseY=0,this.lat=0,this.lon=0,this.phi=0,this.theta=0,this.moveForward=!1,this.moveBackward=!1,this.moveLeft=!1,this.moveRight=!1,this.freeze=!1,this.mouseDragOn=!1,this.viewHalfX=0,this.viewHalfY=0,this.domElement!==document&&this.domElement.setAttribute("tabindex",-1),this.handleResize=function(){this.domElement===document?(this.viewHalfX=window.innerWidth/2,this.viewHalfY=window.innerHeight/2):(this.viewHalfX=this.domElement.offsetWidth/2,this.viewHalfY=this.domElement.offsetHeight/2)},this.onMouseDown=function(t){if(this.domElement!==document&&this.domElement.focus(),t.preventDefault(),t.stopPropagation(),this.activeLook)switch(t.button){case 0:this.moveForward=!0;break;case 2:this.moveBackward=!0}this.mouseDragOn=!0},this.onMouseUp=function(t){if(t.preventDefault(),t.stopPropagation(),this.activeLook)switch(t.button){case 0:this.moveForward=!1;break;case 2:this.moveBackward=!1}this.mouseDragOn=!1},this.onMouseMove=function(t){this.domElement===document?(this.mouseX=t.pageX-this.viewHalfX,this.mouseY=t.pageY-this.viewHalfY):(this.mouseX=t.pageX-this.domElement.offsetLeft-this.viewHalfX,this.mouseY=t.pageY-this.domElement.offsetTop-this.viewHalfY)},this.onKeyDown=function(t){switch(t.keyCode){case 38:case 87:this.moveForward=!0;break;case 37:case 65:this.moveLeft=!0;break;case 40:case 83:this.moveBackward=!0;break;case 39:case 68:this.moveRight=!0;break;case 82:this.moveUp=!0;break;case 70:this.moveDown=!0;break;case 81:this.freeze=!this.freeze}},this.onKeyUp=function(t){switch(t.keyCode){case 38:case 87:this.moveForward=!1;break;case 37:case 65:this.moveLeft=!1;break;case 40:case 83:this.moveBackward=!1;break;case 39:case 68:this.moveRight=!1;break;case 82:this.moveUp=!1;break;case 70:this.moveDown=!1}},this.update=function(t){if(!this.freeze){if(this.heightSpeed){var e=THREE.Math.clamp(this.object.position.y,this.heightMin,this.heightMax),n=e-this.heightMin;this.autoSpeedFactor=t*(n*this.heightCoef)}else this.autoSpeedFactor=0;var r=t*this.movementSpeed;(this.moveForward||this.autoForward&&!this.moveBackward)&&this.object.translateZ(-(r+this.autoSpeedFactor)),this.moveBackward&&this.object.translateZ(r),this.moveLeft&&this.object.translateX(-r),this.moveRight&&this.object.translateX(r),this.moveUp&&this.object.translateY(r),this.moveDown&&this.object.translateY(-r);var i=t*this.lookSpeed;this.activeLook||(i=0);var o=1;this.constrainVertical&&(o=Math.PI/(this.verticalMax-this.verticalMin)),this.lon+=this.mouseX*i,this.lookVertical&&(this.lat-=this.mouseY*i*o),this.lat=Math.max(-85,Math.min(85,this.lat)),this.phi=THREE.Math.degToRad(90-this.lat),this.theta=THREE.Math.degToRad(this.lon),this.constrainVertical&&(this.phi=THREE.Math.mapLinear(this.phi,0,Math.PI,this.verticalMin,this.verticalMax));var s=this.target,a=this.object.position;s.x=a.x+100*Math.sin(this.phi)*Math.cos(this.theta),s.y=a.y+100*Math.cos(this.phi),s.z=a.z+100*Math.sin(this.phi)*Math.sin(this.theta),this.object.lookAt(s)}},this.domElement.addEventListener("contextmenu",function(t){t.preventDefault()},!1),this.domElement.addEventListener("mousemove",n(this,this.onMouseMove),!1),this.domElement.addEventListener("mousedown",n(this,this.onMouseDown),!1),this.domElement.addEventListener("mouseup",n(this,this.onMouseUp),!1),this.domElement.addEventListener("keydown",n(this,this.onKeyDown),!1),this.domElement.addEventListener("keyup",n(this,this.onKeyUp),!1),this.handleResize()},THREE.OrbitControls=function(t,e){function n(){return 2*Math.PI/60/60*p.autoRotateSpeed}function r(){return Math.pow(.95,p.zoomSpeed)}function i(t){if(p.enabled!==!1){if(t.preventDefault(),0===t.button){if(p.noRotate===!0)return;C=A.ROTATE,d.set(t.clientX,t.clientY)}else if(1===t.button){if(p.noZoom===!0)return;C=A.DOLLY,T.set(t.clientX,t.clientY)}else if(2===t.button){if(p.noPan===!0)return;C=A.PAN,g.set(t.clientX,t.clientY)}document.documentElement.addEventListener("mousemove",o,!1),document.documentElement.addEventListener("mouseup",s,!1),p.dispatchEvent(O)}}function o(t){if(p.enabled!==!1){t.preventDefault();var e=p.domElement===document?p.domElement.body:p.domElement;if(C===A.ROTATE){if(p.noRotate===!0)return;m.set(t.clientX,t.clientY),v.subVectors(m,d),p.rotateLeft(2*Math.PI*v.x/e.clientWidth*p.rotateSpeed),p.rotateUp(2*Math.PI*v.y/e.clientHeight*p.rotateSpeed),d.copy(m)}else if(C===A.DOLLY){if(p.noZoom===!0)return;x.set(t.clientX,t.clientY),w.subVectors(x,T),w.y>0?p.dollyIn():p.dollyOut(),T.copy(x)}else if(C===A.PAN){if(p.noPan===!0)return;E.set(t.clientX,t.clientY),y.subVectors(E,g),p.pan(y.x,y.y),g.copy(E)}p.update()}}function s(){p.enabled!==!1&&(document.documentElement.removeEventListener("mousemove",o,!1),document.documentElement.removeEventListener("mouseup",s,!1),p.dispatchEvent(D),C=A.NONE)}function a(t){if(p.enabled!==!1&&p.noZoom!==!0){t.preventDefault(),t.stopPropagation();var e=0;void 0!==t.wheelDelta?e=t.wheelDelta:void 0!==t.detail&&(e=-t.detail),e>0?p.dollyOut():p.dollyIn(),p.update(),p.dispatchEvent(O),p.dispatchEvent(D)}}function u(t){if(p.enabled!==!1&&p.noKeys!==!0&&p.noPan!==!0)switch(t.keyCode){case p.keys.UP:p.pan(0,p.keyPanSpeed),p.update();break;case p.keys.BOTTOM:p.pan(0,-p.keyPanSpeed),p.update();break;case p.keys.LEFT:p.pan(p.keyPanSpeed,0),p.update();break;case p.keys.RIGHT:p.pan(-p.keyPanSpeed,0),p.update()}}function h(t){if(p.enabled!==!1){switch(t.touches.length){case 1:if(p.noRotate===!0)return;C=A.TOUCH_ROTATE,d.set(t.touches[0].pageX,t.touches[0].pageY);break;case 2:if(p.noZoom===!0)return;C=A.TOUCH_DOLLY;var e=t.touches[0].pageX-t.touches[1].pageX,n=t.touches[0].pageY-t.touches[1].pageY,r=Math.sqrt(e*e+n*n);T.set(0,r);break;case 3:if(p.noPan===!0)return;C=A.TOUCH_PAN,g.set(t.touches[0].pageX,t.touches[0].pageY);break;default:C=A.NONE}p.dispatchEvent(O)}}function l(t){if(p.enabled!==!1){t.preventDefault(),t.stopPropagation();var e=p.domElement===document?p.domElement.body:p.domElement;switch(t.touches.length){case 1:if(p.noRotate===!0)return;if(C!==A.TOUCH_ROTATE)return;m.set(t.touches[0].pageX,t.touches[0].pageY),v.subVectors(m,d),p.rotateLeft(2*Math.PI*v.x/e.clientWidth*p.rotateSpeed),p.rotateUp(2*Math.PI*v.y/e.clientHeight*p.rotateSpeed),d.copy(m),p.update();break;case 2:if(p.noZoom===!0)return;if(C!==A.TOUCH_DOLLY)return;var n=t.touches[0].pageX-t.touches[1].pageX,r=t.touches[0].pageY-t.touches[1].pageY,i=Math.sqrt(n*n+r*r);x.set(0,i),w.subVectors(x,T),w.y>0?p.dollyOut():p.dollyIn(),T.copy(x),p.update();break;case 3:if(p.noPan===!0)return;if(C!==A.TOUCH_PAN)return;E.set(t.touches[0].pageX,t.touches[0].pageY),y.subVectors(E,g),p.pan(y.x,y.y),g.copy(E),p.update();break;default:C=A.NONE}}}function c(){p.enabled!==!1&&(p.dispatchEvent(D),C=A.NONE)}this.object=t,this.domElement=void 0!==e?e:document,this.enabled=!0,this.target=new THREE.Vector3,this.center=this.target,this.noZoom=!1,this.zoomSpeed=1,this.minDistance=0,this.maxDistance=1/0,this.noRotate=!1,this.rotateSpeed=1,this.noPan=!1,this.keyPanSpeed=7,this.autoRotate=!1,this.autoRotateSpeed=2,this.minPolarAngle=0,this.maxPolarAngle=Math.PI,this.noKeys=!1,this.keys={LEFT:37,UP:38,RIGHT:39,BOTTOM:40};var p=this,f=1e-6,d=new THREE.Vector2,m=new THREE.Vector2,v=new THREE.Vector2,g=new THREE.Vector2,E=new THREE.Vector2,y=new THREE.Vector2,_=new THREE.Vector3,b=new THREE.Vector3,T=new THREE.Vector2,x=new THREE.Vector2,w=new THREE.Vector2,R=0,H=0,M=1,S=new THREE.Vector3,k=new THREE.Vector3,A={NONE:-1,ROTATE:0,DOLLY:1,PAN:2,TOUCH_ROTATE:3,TOUCH_DOLLY:4,TOUCH_PAN:5},C=A.NONE;this.target0=this.target.clone(),this.position0=this.object.position.clone();var P=(new THREE.Quaternion).setFromUnitVectors(t.up,new THREE.Vector3(0,1,0)),L=P.clone().inverse(),z={type:"change"},O={type:"start"},D={type:"end"};this.rotateLeft=function(t){void 0===t&&(t=n()),H-=t},this.rotateUp=function(t){void 0===t&&(t=n()),R-=t},this.panLeft=function(t){var e=this.object.matrix.elements;_.set(e[0],e[1],e[2]),_.multiplyScalar(-t),S.add(_)},this.panUp=function(t){var e=this.object.matrix.elements;_.set(e[4],e[5],e[6]),_.multiplyScalar(t),S.add(_)},this.pan=function(t,e){var n=p.domElement===document?p.domElement.body:p.domElement;if(void 0!==p.object.fov){var r=p.object.position,i=r.clone().sub(p.target),o=i.length();o*=Math.tan(p.object.fov/2*Math.PI/180),p.panLeft(2*t*o/n.clientHeight),p.panUp(2*e*o/n.clientHeight)}else void 0!==p.object.top?(p.panLeft(t*(p.object.right-p.object.left)/n.clientWidth),p.panUp(e*(p.object.top-p.object.bottom)/n.clientHeight)):console.warn("WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.")},this.dollyIn=function(t){void 0===t&&(t=r()),M/=t},this.dollyOut=function(t){void 0===t&&(t=r()),M*=t},this.update=function(){var t=this.object.position;b.copy(t).sub(this.target),b.applyQuaternion(P);var e=Math.atan2(b.x,b.z),r=Math.atan2(Math.sqrt(b.x*b.x+b.z*b.z),b.y);this.autoRotate&&this.rotateLeft(n()),e+=H,r+=R,r=Math.max(this.minPolarAngle,Math.min(this.maxPolarAngle,r)),r=Math.max(f,Math.min(Math.PI-f,r));var i=b.length()*M;i=Math.max(this.minDistance,Math.min(this.maxDistance,i)),this.target.add(S),b.x=i*Math.sin(r)*Math.sin(e),b.y=i*Math.cos(r),b.z=i*Math.sin(r)*Math.cos(e),b.applyQuaternion(L),t.copy(this.target).add(b),this.object.lookAt(this.target),H=0,R=0,M=1,S.set(0,0,0),k.distanceToSquared(this.object.position)>f&&(this.dispatchEvent(z),k.copy(this.object.position))},this.reset=function(){C=A.NONE,this.target.copy(this.target0),this.object.position.copy(this.position0),this.update()},this.domElement.addEventListener("contextmenu",function(t){t.preventDefault()},!1),this.domElement.addEventListener("mousedown",i,!1),this.domElement.addEventListener("mousewheel",a,!1),this.domElement.addEventListener("DOMMouseScroll",a,!1),this.domElement.addEventListener("touchstart",h,!1),this.domElement.addEventListener("touchend",c,!1),this.domElement.addEventListener("touchmove",l,!1),window.addEventListener("keydown",u,!1),this.update()},THREE.OrbitControls.prototype=Object.create(THREE.EventDispatcher.prototype),THREE.TrackballControls=function(t,e){function n(t){c.enabled!==!1&&(window.removeEventListener("keydown",n),v=m,m===p.NONE&&(t.keyCode!==c.keys[p.ROTATE]||c.noRotate?t.keyCode!==c.keys[p.ZOOM]||c.noZoom?t.keyCode!==c.keys[p.PAN]||c.noPan||(m=p.PAN):m=p.ZOOM:m=p.ROTATE))}function r(t){c.enabled!==!1&&(m=v,window.addEventListener("keydown",n,!1))}function i(t){c.enabled!==!1&&(t.preventDefault(),t.stopPropagation(),m===p.NONE&&(m=t.button),m!==p.ROTATE||c.noRotate?m!==p.ZOOM||c.noZoom?m!==p.PAN||c.noPan||(H.copy(C(t.pageX,t.pageY)),M.copy(H)):(T.copy(C(t.pageX,t.pageY)),x.copy(T)):(y.copy(P(t.pageX,t.pageY)),E.copy(y)),document.addEventListener("mousemove",o,!1),document.addEventListener("mouseup",s,!1),c.dispatchEvent(k))}function o(t){c.enabled!==!1&&(t.preventDefault(),t.stopPropagation(),m!==p.ROTATE||c.noRotate?m!==p.ZOOM||c.noZoom?m!==p.PAN||c.noPan||M.copy(C(t.pageX,t.pageY)):x.copy(C(t.pageX,t.pageY)):(E.copy(y),y.copy(P(t.pageX,t.pageY))))}function s(t){c.enabled!==!1&&(t.preventDefault(),t.stopPropagation(),m=p.NONE,document.removeEventListener("mousemove",o),document.removeEventListener("mouseup",s),c.dispatchEvent(A))}function a(t){if(c.enabled!==!1&&!c.noZoom){t.preventDefault(),t.stopPropagation();var e=0;t.wheelDelta?e=t.wheelDelta/40:t.detail&&(e=-t.detail/3),T.y+=.01*e,c.dispatchEvent(k),c.dispatchEvent(A)}}function u(t){if(c.enabled!==!1){switch(t.touches.length){case 1:m=p.TOUCH_ROTATE,y.copy(P(t.touches[0].pageX,t.touches[0].pageY)),E.copy(y);break;case 2:m=p.TOUCH_ZOOM_PAN;var e=t.touches[0].pageX-t.touches[1].pageX,n=t.touches[0].pageY-t.touches[1].pageY;R=w=Math.sqrt(e*e+n*n);var r=(t.touches[0].pageX+t.touches[1].pageX)/2,i=(t.touches[0].pageY+t.touches[1].pageY)/2;H.copy(C(r,i)),M.copy(H);break;default:m=p.NONE}c.dispatchEvent(k)}}function h(t){if(c.enabled!==!1)switch(t.preventDefault(),t.stopPropagation(),t.touches.length){case 1:E.copy(y),y.copy(P(t.touches[0].pageX,t.touches[0].pageY));break;case 2:var e=t.touches[0].pageX-t.touches[1].pageX,n=t.touches[0].pageY-t.touches[1].pageY;R=Math.sqrt(e*e+n*n);var r=(t.touches[0].pageX+t.touches[1].pageX)/2,i=(t.touches[0].pageY+t.touches[1].pageY)/2;M.copy(C(r,i));break;default:m=p.NONE}}function l(t){if(c.enabled!==!1){switch(t.touches.length){case 1:E.copy(y),y.copy(P(t.touches[0].pageX,t.touches[0].pageY));break;case 2:w=R=0;var e=(t.touches[0].pageX+t.touches[1].pageX)/2,n=(t.touches[0].pageY+t.touches[1].pageY)/2;M.copy(C(e,n)),H.copy(M)}m=p.NONE,c.dispatchEvent(A)}}var c=this,p={NONE:-1,ROTATE:0,ZOOM:1,PAN:2,TOUCH_ROTATE:3,TOUCH_ZOOM_PAN:4};this.object=t,this.domElement=void 0!==e?e:document,this.enabled=!0,this.screen={left:0,top:0,width:0,height:0},this.rotateSpeed=1,this.zoomSpeed=1.2,this.panSpeed=.3,this.noRotate=!1,this.noZoom=!1,this.noPan=!1,this.staticMoving=!1,this.dynamicDampingFactor=.2,this.minDistance=0,this.maxDistance=1/0,this.keys=[65,83,68],this.target=new THREE.Vector3;var f=1e-6,d=new THREE.Vector3,m=p.NONE,v=p.NONE,g=new THREE.Vector3,E=new THREE.Vector2,y=new THREE.Vector2,_=new THREE.Vector3,b=0,T=new THREE.Vector2,x=new THREE.Vector2,w=0,R=0,H=new THREE.Vector2,M=new THREE.Vector2;this.target0=this.target.clone(),this.position0=this.object.position.clone(),this.up0=this.object.up.clone();var S={type:"change"},k={type:"start"},A={type:"end"};this.handleResize=function(){if(this.domElement===document)this.screen.left=0,this.screen.top=0,this.screen.width=window.innerWidth,this.screen.height=window.innerHeight;else{var t=this.domElement.getBoundingClientRect(),e=this.domElement.ownerDocument.documentElement;this.screen.left=t.left+window.pageXOffset-e.clientLeft,this.screen.top=t.top+window.pageYOffset-e.clientTop,this.screen.width=t.width,this.screen.height=t.height}},this.handleEvent=function(t){"function"==typeof this[t.type]&&this[t.type](t)};var C=function(){var t=new THREE.Vector2;return function(e,n){return t.set((e-c.screen.left)/c.screen.width,(n-c.screen.top)/c.screen.height),t}}(),P=function(){var t=new THREE.Vector2;return function(e,n){return t.set((e-.5*c.screen.width-c.screen.left)/(.5*c.screen.width),(c.screen.height+2*(c.screen.top-n))/c.screen.width),t}}();this.rotateCamera=function(){var t,e=new THREE.Vector3,n=new THREE.Quaternion,r=new THREE.Vector3,i=new THREE.Vector3,o=new THREE.Vector3,s=new THREE.Vector3;return function(){s.set(y.x-E.x,y.y-E.y,0),t=s.length(),t?(g.copy(c.object.position).sub(c.target),r.copy(g).normalize(),i.copy(c.object.up).normalize(),o.crossVectors(i,r).normalize(),i.setLength(y.y-E.y),o.setLength(y.x-E.x),s.copy(i.add(o)),e.crossVectors(s,g).normalize(),t*=c.rotateSpeed,n.setFromAxisAngle(e,t),g.applyQuaternion(n),c.object.up.applyQuaternion(n),_.copy(e),b=t):!c.staticMoving&&b&&(b*=Math.sqrt(1-c.dynamicDampingFactor),g.copy(c.object.position).sub(c.target),n.setFromAxisAngle(_,b),g.applyQuaternion(n),c.object.up.applyQuaternion(n)),E.copy(y)}}(),this.zoomCamera=function(){var t;m===p.TOUCH_ZOOM_PAN?(t=w/R,w=R,g.multiplyScalar(t)):(t=1+(x.y-T.y)*c.zoomSpeed,1!==t&&t>0&&(g.multiplyScalar(t),c.staticMoving?T.copy(x):T.y+=(x.y-T.y)*this.dynamicDampingFactor))},this.panCamera=function(){var t=new THREE.Vector2,e=new THREE.Vector3,n=new THREE.Vector3;return function(){t.copy(M).sub(H),t.lengthSq()&&(t.multiplyScalar(g.length()*c.panSpeed),n.copy(g).cross(c.object.up).setLength(t.x), -n.add(e.copy(c.object.up).setLength(t.y)),c.object.position.add(n),c.target.add(n),c.staticMoving?H.copy(M):H.add(t.subVectors(M,H).multiplyScalar(c.dynamicDampingFactor)))}}(),this.checkDistances=function(){c.noZoom&&c.noPan||(g.lengthSq()>c.maxDistance*c.maxDistance&&c.object.position.addVectors(c.target,g.setLength(c.maxDistance)),g.lengthSq()f&&(c.dispatchEvent(S),d.copy(c.object.position))},this.reset=function(){m=p.NONE,v=p.NONE,c.target.copy(c.target0),c.object.position.copy(c.position0),c.object.up.copy(c.up0),g.subVectors(c.object.position,c.target),c.object.lookAt(c.target),c.dispatchEvent(S),d.copy(c.object.position)},this.domElement.addEventListener("contextmenu",function(t){t.preventDefault()},!1),this.domElement.addEventListener("mousedown",i,!1),this.domElement.addEventListener("mousewheel",a,!1),this.domElement.addEventListener("DOMMouseScroll",a,!1),this.domElement.addEventListener("touchstart",u,!1),this.domElement.addEventListener("touchend",l,!1),this.domElement.addEventListener("touchmove",h,!1),window.addEventListener("keydown",n,!1),window.addEventListener("keyup",r,!1),this.handleResize(),this.update()},THREE.TrackballControls.prototype=Object.create(THREE.EventDispatcher.prototype),THREE.TrackballControls.prototype.constructor=THREE.TrackballControls,THREE.VRControls=function(t,e){var n=1e-5,r=this.dummy=new THREE.Object3D;this.object=t,this.device=new THREE.DeviceOrientationControls(r,e),this.orbit=new THREE.OrbitControls(r,e),this.orbit.target.copy(t.position),this.orbit.target.z+=n,this.orbit.rotateSpeed=-.25,this.supported=!1;var i=function(t){this.supported=t&&t.alpha==+t.alpha,window.removeEventListener("deviceorientation",i,!1)}.bind(this);window.addEventListener("deviceorientation",i,!1)},THREE.VRControls.prototype.vr=function(t){this.vrstate=t},THREE.VRControls.prototype.update=function(t){var e=!1;this.vrstate&&this.vrstate.orientation?(e=!0,this.object.quaternion.copy(this.vrstate.orientation),this.object.position.copy(this.vrstate.position),this.device.object=this.dummy,this.orbit.object=this.dummy):this.vrstate&&this.supported?(this.device.freeze&&this.device.connect(),this.device.object=this.object,this.orbit.object=this.dummy,this.device.update(t)):(e=!0,this.device.object=this.dummy,this.orbit.object=this.object,this.orbit.update(t)),e&&!this.device.freeze&&this.device.disconnect()},THREE.Bootstrap.registerPlugin("stats",{listen:["pre","post"],install:function(t){var e=this.stats=new THREE.Stats,n=e.domElement.style;n.position="absolute",n.top=n.left=0,t.element.appendChild(e.domElement),t.stats=e},uninstall:function(t){document.body.removeChild(this.stats.domElement),delete t.stats},pre:function(t,e){this.stats.begin()},post:function(t,e){this.stats.end()}}),THREE.Bootstrap.registerPlugin("controls",{listen:["update","resize","camera","this.change"],defaults:{klass:null,parameters:{}},install:function(t){if(!this.options.klass)throw"Must provide class for `controls.klass`";t.controls=null,this._camera=t.camera||new THREE.PerspectiveCamera,this.change(null,t)},uninstall:function(t){delete t.controls},change:function(t,e){this.options.klass?((!t||t.changes.klass)&&(e.controls=new this.options.klass(this._camera,e.renderer.domElement)),_.extend(e.controls,this.options.parameters)):e.controls=null},update:function(t,e){var n=e.Time&&e.Time.delta||1/60,r=e.VR&&e.VR.state;e.controls.vr&&e.controls.vr(r),e.controls.update(n)},camera:function(t,e){e.controls.object=this._camera=t.camera},resize:function(t,e){e.controls.handleResize&&e.controls.handleResize()}}),THREE.Bootstrap.registerPlugin("cursor",{listen:["update","this.change","install:change","uninstall:change","element.mousemove","vr"],defaults:{cursor:null,hide:!1,timeout:3},install:function(t){this.timeout=this.options.timeout,this.element=t.element,this.change(null,t)},uninstall:function(t){delete t.controls},change:function(t,e){this.applyCursor(e)},mousemove:function(t,e){this.options.hide&&(this.applyCursor(e),this.timeout=+this.options.timeout||0)},update:function(t,e){var n=e.Time&&e.Time.delta||1/60;this.options.hide&&(this.timeout-=n,this.timeout<0&&this.applyCursor(e,"none"))},vr:function(t,e){this.hide=t.active&&!t.hmd.fake,this.applyCursor(e)},applyCursor:function(t,e){var n=t.controls?"move":"";e=e||this.options.cursor||n,this.hide&&(e="none"),this.cursor!=e&&(this.element.style.cursor=e)}}),THREE.Bootstrap.registerPlugin("fullscreen",{defaults:{key:"f"},listen:["ready","update"],install:function(t){t.Fullscreen=this.api({active:!1,toggle:this.toggle.bind(this)},t)},uninstall:function(t){delete t.Fullscreen},ready:function(t,e){document.body.addEventListener("keypress",function(t){this.options.key&&t.charCode==this.options.key.charCodeAt(0)&&this.toggle(e)}.bind(this));var n=function(){var t=!!(document.fullscreenElement||document.mozFullScreenElement||document.webkitFullscreenElement||document.msFullscreenElement);e.Fullscreen.active=this.active=t,e.trigger({type:"fullscreen",active:t})}.bind(this);document.addEventListener("fullscreenchange",n,!1),document.addEventListener("webkitfullscreenchange",n,!1),document.addEventListener("mozfullscreenchange",n,!1)},toggle:function(t){var e=t.canvas,n=t.VR&&t.VR.active?{vrDisplay:t.VR.hmd}:{};this.active?document.exitFullscreen?document.exitFullscreen():document.msExitFullscreen?document.msExitFullscreen():document.webkitExitFullscreen?document.webkitExitFullscreen():document.mozCancelFullScreen&&document.mozCancelFullScreen():e.requestFullScreen?e.requestFullScreen(n):e.msRequestFullScreen?e.msRequestFullscreen(n):e.webkitRequestFullscreen?e.webkitRequestFullscreen(n):e.mozRequestFullScreen&&e.mozRequestFullScreen(n)}}),THREE.Bootstrap.registerPlugin("vr",{defaults:{mode:"auto",device:null,fov:80},listen:["window.load","pre","render","resize","this.change"],install:function(t){t.VR=this.api({active:!1,devices:[],hmd:null,sensor:null,renderer:null,state:null},t)},uninstall:function(t){delete t.VR},mocks:function(t,e,n){var r=.03,i=function(t){return{left:{x:-r,y:0,z:0},right:{x:r,y:0,z:0}}[t]},o=function(r){var i=t.camera,o=i&&i.aspect||16/9,s=(e||i&&i.fov||n)/2,a=180*Math.atan(Math.tan(s*Math.PI/180)*o/2)/Math.PI,u=s;return{left:{rightDegrees:a,leftDegrees:a,downDegrees:u,upDegrees:u},right:{rightDegrees:a,leftDegrees:a,downDegrees:u,upDegrees:u}}[r]},s=function(){return{}};return[{fake:!0,force:1,deviceId:"emu",deviceName:"Emulated",getEyeTranslation:i,getRecommendedEyeFieldOfView:o},{force:2,getState:s}]},load:function(t,e){var n=function(t){this.callback(t,e)}.bind(this);navigator.getVRDevices?navigator.getVRDevices().then(n):navigator.mozGetVRDevices?navigator.mozGetVRDevices(n):(console.warn("No native VR support detected."),n(this.mocks(e,this.options.fov,this.defaults.fov),e))},callback:function(t,e){var n,r,i=window.HMDVRDevice||function(){},o=window.PositionSensorVRDevice||function(){};t=e.VR.devices=t||e.VR.devices;for(var s=this.options.device,a=0;a'),t.VR&&i.push(''),'
    '+i.join("\n")+"
    "},install:function(t){var e=this.ui=document.createElement("div");e.innerHTML=this.markup(t,this.options.theme,this.options.style),document.body.appendChild(e);var n=this.ui.fullscreen=e.querySelector("button.fullscreen");n&&t.bind([n,"click:goFullscreen"],this);var r=this.ui.vr=e.querySelector("button.vr");r&&t.VR&&(t.VR.set({mode:"2d"}),t.bind([r,"click:goVR"],this))},uninstall:function(t){document.body.removeChild(ui)},fullscreen:function(t,e){this.ui.style.display=t.active?"none":"block",t.active||e.VR&&e.VR.set({mode:"2d"})},goFullscreen:function(t,e){e.Fullscreen&&e.Fullscreen.toggle()},goVR:function(t,e){e.VR&&(e.VR.set({mode:"auto"}),e.Fullscreen.toggle())},uninstall:function(t){document.body.removeChild(this.ui)}}),THREE.VRRenderer=function(t,e){var n=this;n.initialize=function(){var t=e.getEyeTranslation("left");n.halfIPD=new THREE.Vector3(t.x,t.y,t.z).length(),n.fovLeft=e.getRecommendedEyeFieldOfView("left"),n.fovRight=e.getRecommendedEyeFieldOfView("right")},n.FovToNDCScaleOffset=function(t){var e=2/(t.leftTan+t.rightTan),n=(t.leftTan-t.rightTan)*e*.5,r=2/(t.upTan+t.downTan),i=(t.upTan-t.downTan)*r*.5;return{scale:[e,r],offset:[n,i]}},n.FovPortToProjection=function(t,e,r,i,o){r=void 0===r?!0:r,i=void 0===i?.01:i,o=void 0===o?1e4:o;var s=r?-1:1,a=t.elements,u=n.FovToNDCScaleOffset(e);a[0]=u.scale[0],a[1]=0,a[2]=u.offset[0]*s,a[3]=0,a[4]=0,a[5]=u.scale[1],a[6]=-u.offset[1]*s,a[7]=0,a[8]=0,a[9]=0,a[10]=o/(i-o)*-s,a[11]=o*i/(i-o),a[12]=0,a[13]=0,a[14]=s,a[15]=0,t.transpose()},n.FovToProjection=function(t,e,r,i,o){var s={upTan:Math.tan(e.upDegrees*Math.PI/180),downTan:Math.tan(e.downDegrees*Math.PI/180),leftTan:Math.tan(e.leftDegrees*Math.PI/180),rightTan:Math.tan(e.rightDegrees*Math.PI/180)};return n.FovPortToProjection(t,s,r,i,o)};var r=new THREE.Vector3,i=new THREE.PerspectiveCamera,o=new THREE.PerspectiveCamera;n.render=function(e,s){n.FovToProjection(i.projectionMatrix,n.fovLeft,!0,s.near,s.far),n.FovToProjection(o.projectionMatrix,n.fovRight,!0,s.near,s.far),r.set(n.halfIPD,0,0),r.applyQuaternion(s.quaternion),i.position.copy(s.position).sub(r),o.position.copy(s.position).add(r),i.quaternion.copy(s.quaternion),o.quaternion.copy(s.quaternion);var a=t.devicePixelRatio||1,u=t.domElement.width/2/a,h=t.domElement.height/a;t.enableScissorTest(!0),t.setViewport(0,0,u,h),t.setScissor(0,0,u,h),t.render(e,i),t.setViewport(u,0,u,h),t.setScissor(u,0,u,h),t.render(e,o)},n.initialize()},THREE.VRControls=function(t,e){var n=1e-5,r=this.dummy=new THREE.Object3D;this.object=t,this.device=new THREE.DeviceOrientationControls(r,e),this.orbit=new THREE.OrbitControls(r,e),this.orbit.target.copy(t.position),this.orbit.target.z+=n,this.orbit.rotateSpeed=-.25,this.supported=!1;var i=function(t){this.supported=t&&t.alpha==+t.alpha,window.removeEventListener("deviceorientation",i,!1)}.bind(this);window.addEventListener("deviceorientation",i,!1)},THREE.VRControls.prototype.vr=function(t){this.vrstate=t},THREE.VRControls.prototype.update=function(t){var e=!1;this.vrstate&&this.vrstate.orientation?(e=!0,this.object.quaternion.copy(this.vrstate.orientation),this.object.position.copy(this.vrstate.position),this.device.object=this.dummy,this.orbit.object=this.dummy):this.vrstate&&this.supported?(this.device.freeze&&this.device.connect(),this.device.object=this.object,this.orbit.object=this.dummy,this.device.update(t)):(e=!0,this.device.object=this.dummy,this.orbit.object=this.object,this.orbit.update(t)),e&&!this.device.freeze&&this.device.disconnect()},THREE.OrbitControls=function(t,e){function n(){return 2*Math.PI/60/60*p.autoRotateSpeed}function r(){return Math.pow(.95,p.zoomSpeed)}function i(t){if(p.enabled!==!1){if(t.preventDefault(),0===t.button){if(p.noRotate===!0)return;C=A.ROTATE,d.set(t.clientX,t.clientY)}else if(1===t.button){if(p.noZoom===!0)return;C=A.DOLLY,T.set(t.clientX,t.clientY)}else if(2===t.button){if(p.noPan===!0)return;C=A.PAN,g.set(t.clientX,t.clientY)}document.documentElement.addEventListener("mousemove",o,!1),document.documentElement.addEventListener("mouseup",s,!1),p.dispatchEvent(O)}}function o(t){if(p.enabled!==!1){t.preventDefault();var e=p.domElement===document?p.domElement.body:p.domElement;if(C===A.ROTATE){if(p.noRotate===!0)return;m.set(t.clientX,t.clientY),v.subVectors(m,d),p.rotateLeft(2*Math.PI*v.x/e.clientWidth*p.rotateSpeed),p.rotateUp(2*Math.PI*v.y/e.clientHeight*p.rotateSpeed),d.copy(m)}else if(C===A.DOLLY){if(p.noZoom===!0)return;x.set(t.clientX,t.clientY),w.subVectors(x,T),w.y>0?p.dollyIn():p.dollyOut(),T.copy(x)}else if(C===A.PAN){if(p.noPan===!0)return;E.set(t.clientX,t.clientY),y.subVectors(E,g),p.pan(y.x,y.y),g.copy(E)}p.update()}}function s(){p.enabled!==!1&&(document.documentElement.removeEventListener("mousemove",o,!1),document.documentElement.removeEventListener("mouseup",s,!1),p.dispatchEvent(D),C=A.NONE)}function a(t){if(p.enabled!==!1&&p.noZoom!==!0){t.preventDefault(),t.stopPropagation();var e=0;void 0!==t.wheelDelta?e=t.wheelDelta:void 0!==t.detail&&(e=-t.detail),e>0?p.dollyOut():p.dollyIn(),p.update(),p.dispatchEvent(O),p.dispatchEvent(D)}}function u(t){if(p.enabled!==!1&&p.noKeys!==!0&&p.noPan!==!0)switch(t.keyCode){case p.keys.UP:p.pan(0,p.keyPanSpeed),p.update();break;case p.keys.BOTTOM:p.pan(0,-p.keyPanSpeed),p.update();break;case p.keys.LEFT:p.pan(p.keyPanSpeed,0),p.update();break;case p.keys.RIGHT:p.pan(-p.keyPanSpeed,0),p.update()}}function h(t){if(p.enabled!==!1){switch(t.touches.length){case 1:if(p.noRotate===!0)return;C=A.TOUCH_ROTATE,d.set(t.touches[0].pageX,t.touches[0].pageY);break;case 2:if(p.noZoom===!0)return;C=A.TOUCH_DOLLY;var e=t.touches[0].pageX-t.touches[1].pageX,n=t.touches[0].pageY-t.touches[1].pageY,r=Math.sqrt(e*e+n*n);T.set(0,r);break;case 3:if(p.noPan===!0)return;C=A.TOUCH_PAN,g.set(t.touches[0].pageX,t.touches[0].pageY);break;default:C=A.NONE}p.dispatchEvent(O)}}function l(t){if(p.enabled!==!1){t.preventDefault(),t.stopPropagation();var e=p.domElement===document?p.domElement.body:p.domElement;switch(t.touches.length){case 1:if(p.noRotate===!0)return;if(C!==A.TOUCH_ROTATE)return;m.set(t.touches[0].pageX,t.touches[0].pageY),v.subVectors(m,d),p.rotateLeft(2*Math.PI*v.x/e.clientWidth*p.rotateSpeed),p.rotateUp(2*Math.PI*v.y/e.clientHeight*p.rotateSpeed),d.copy(m),p.update();break;case 2:if(p.noZoom===!0)return;if(C!==A.TOUCH_DOLLY)return;var n=t.touches[0].pageX-t.touches[1].pageX,r=t.touches[0].pageY-t.touches[1].pageY,i=Math.sqrt(n*n+r*r);x.set(0,i),w.subVectors(x,T),w.y>0?p.dollyOut():p.dollyIn(),T.copy(x),p.update();break;case 3:if(p.noPan===!0)return;if(C!==A.TOUCH_PAN)return;E.set(t.touches[0].pageX,t.touches[0].pageY),y.subVectors(E,g),p.pan(y.x,y.y),g.copy(E),p.update();break;default:C=A.NONE}}}function c(){p.enabled!==!1&&(p.dispatchEvent(D),C=A.NONE)}this.object=t,this.domElement=void 0!==e?e:document,this.enabled=!0,this.target=new THREE.Vector3,this.center=this.target,this.noZoom=!1,this.zoomSpeed=1,this.minDistance=0,this.maxDistance=1/0,this.noRotate=!1,this.rotateSpeed=1,this.noPan=!1,this.keyPanSpeed=7,this.autoRotate=!1,this.autoRotateSpeed=2,this.minPolarAngle=0,this.maxPolarAngle=Math.PI,this.noKeys=!1,this.keys={LEFT:37,UP:38,RIGHT:39,BOTTOM:40};var p=this,f=1e-6,d=new THREE.Vector2,m=new THREE.Vector2,v=new THREE.Vector2,g=new THREE.Vector2,E=new THREE.Vector2,y=new THREE.Vector2,_=new THREE.Vector3,b=new THREE.Vector3,T=new THREE.Vector2,x=new THREE.Vector2,w=new THREE.Vector2,R=0,H=0,M=1,S=new THREE.Vector3,k=new THREE.Vector3,A={NONE:-1,ROTATE:0,DOLLY:1,PAN:2,TOUCH_ROTATE:3,TOUCH_DOLLY:4,TOUCH_PAN:5},C=A.NONE;this.target0=this.target.clone(),this.position0=this.object.position.clone();var P=(new THREE.Quaternion).setFromUnitVectors(t.up,new THREE.Vector3(0,1,0)),L=P.clone().inverse(),z={type:"change"},O={type:"start"},D={type:"end"};this.rotateLeft=function(t){void 0===t&&(t=n()),H-=t},this.rotateUp=function(t){void 0===t&&(t=n()),R-=t},this.panLeft=function(t){var e=this.object.matrix.elements;_.set(e[0],e[1],e[2]),_.multiplyScalar(-t),S.add(_)},this.panUp=function(t){var e=this.object.matrix.elements;_.set(e[4],e[5],e[6]),_.multiplyScalar(t),S.add(_)},this.pan=function(t,e){var n=p.domElement===document?p.domElement.body:p.domElement;if(void 0!==p.object.fov){var r=p.object.position,i=r.clone().sub(p.target),o=i.length();o*=Math.tan(p.object.fov/2*Math.PI/180),p.panLeft(2*t*o/n.clientHeight),p.panUp(2*e*o/n.clientHeight)}else void 0!==p.object.top?(p.panLeft(t*(p.object.right-p.object.left)/n.clientWidth),p.panUp(e*(p.object.top-p.object.bottom)/n.clientHeight)):console.warn("WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.")},this.dollyIn=function(t){void 0===t&&(t=r()),M/=t},this.dollyOut=function(t){void 0===t&&(t=r()),M*=t},this.update=function(){var t=this.object.position;b.copy(t).sub(this.target),b.applyQuaternion(P);var e=Math.atan2(b.x,b.z),r=Math.atan2(Math.sqrt(b.x*b.x+b.z*b.z),b.y);this.autoRotate&&this.rotateLeft(n()),e+=H,r+=R,r=Math.max(this.minPolarAngle,Math.min(this.maxPolarAngle,r)),r=Math.max(f,Math.min(Math.PI-f,r));var i=b.length()*M;i=Math.max(this.minDistance,Math.min(this.maxDistance,i)),this.target.add(S),b.x=i*Math.sin(r)*Math.sin(e),b.y=i*Math.cos(r),b.z=i*Math.sin(r)*Math.cos(e),b.applyQuaternion(L),t.copy(this.target).add(b),this.object.lookAt(this.target),H=0,R=0,M=1,S.set(0,0,0),k.distanceToSquared(this.object.position)>f&&(this.dispatchEvent(z),k.copy(this.object.position))},this.reset=function(){C=A.NONE,this.target.copy(this.target0),this.object.position.copy(this.position0),this.update()},this.domElement.addEventListener("contextmenu",function(t){t.preventDefault()},!1),this.domElement.addEventListener("mousedown",i,!1),this.domElement.addEventListener("mousewheel",a,!1),this.domElement.addEventListener("DOMMouseScroll",a,!1),this.domElement.addEventListener("touchstart",h,!1),this.domElement.addEventListener("touchend",c,!1),this.domElement.addEventListener("touchmove",l,!1),window.addEventListener("keydown",u,!1),this.update()},THREE.OrbitControls.prototype=Object.create(THREE.EventDispatcher.prototype),THREE.DeviceOrientationControls=function(t){var e=this;this.object=t,this.object.rotation.reorder("YXZ"),this.freeze=!0,this.deviceOrientation={},this.screenOrientation=0;var n=function(t){e.deviceOrientation=t},r=function(){e.screenOrientation=window.orientation||0},i=function(){var t=new THREE.Vector3(0,0,1),e=new THREE.Euler,n=new THREE.Quaternion,r=new THREE.Quaternion(-Math.sqrt(.5),0,0,Math.sqrt(.5));return function(i,o,s,a,u){e.set(s,o,-a,"YXZ"),i.setFromEuler(e),i.multiply(r),i.multiply(n.setFromAxisAngle(t,-u))}}();this.connect=function(){r(),window.addEventListener("orientationchange",r,!1),window.addEventListener("deviceorientation",n,!1),e.freeze=!1},this.disconnect=function(){e.freeze=!0,window.removeEventListener("orientationchange",r,!1),window.removeEventListener("deviceorientation",n,!1)},this.update=function(){if(!e.freeze){var t=e.deviceOrientation.gamma?THREE.Math.degToRad(e.deviceOrientation.alpha):0,n=e.deviceOrientation.beta?THREE.Math.degToRad(e.deviceOrientation.beta):0,r=e.deviceOrientation.gamma?THREE.Math.degToRad(e.deviceOrientation.gamma):0,o=e.screenOrientation?THREE.Math.degToRad(e.screenOrientation):0;i(e.object.quaternion,t,n,r,o)}}},THREE.TrackballControls=function(t,e){function n(t){c.enabled!==!1&&(window.removeEventListener("keydown",n),v=m,m===p.NONE&&(t.keyCode!==c.keys[p.ROTATE]||c.noRotate?t.keyCode!==c.keys[p.ZOOM]||c.noZoom?t.keyCode!==c.keys[p.PAN]||c.noPan||(m=p.PAN):m=p.ZOOM:m=p.ROTATE))}function r(t){c.enabled!==!1&&(m=v,window.addEventListener("keydown",n,!1))}function i(t){c.enabled!==!1&&(t.preventDefault(),t.stopPropagation(),m===p.NONE&&(m=t.button),m!==p.ROTATE||c.noRotate?m!==p.ZOOM||c.noZoom?m!==p.PAN||c.noPan||(H.copy(C(t.pageX,t.pageY)),M.copy(H)):(T.copy(C(t.pageX,t.pageY)),x.copy(T)):(y.copy(P(t.pageX,t.pageY)),E.copy(y)),document.addEventListener("mousemove",o,!1),document.addEventListener("mouseup",s,!1),c.dispatchEvent(k))}function o(t){c.enabled!==!1&&(t.preventDefault(),t.stopPropagation(),m!==p.ROTATE||c.noRotate?m!==p.ZOOM||c.noZoom?m!==p.PAN||c.noPan||M.copy(C(t.pageX,t.pageY)):x.copy(C(t.pageX,t.pageY)):(E.copy(y),y.copy(P(t.pageX,t.pageY))))}function s(t){c.enabled!==!1&&(t.preventDefault(),t.stopPropagation(),m=p.NONE,document.removeEventListener("mousemove",o),document.removeEventListener("mouseup",s),c.dispatchEvent(A))}function a(t){if(c.enabled!==!1&&!c.noZoom){t.preventDefault(),t.stopPropagation();var e=0;t.wheelDelta?e=t.wheelDelta/40:t.detail&&(e=-t.detail/3),T.y+=.01*e,c.dispatchEvent(k),c.dispatchEvent(A)}}function u(t){if(c.enabled!==!1){switch(t.touches.length){case 1:m=p.TOUCH_ROTATE,y.copy(P(t.touches[0].pageX,t.touches[0].pageY)),E.copy(y);break;case 2:m=p.TOUCH_ZOOM_PAN;var e=t.touches[0].pageX-t.touches[1].pageX,n=t.touches[0].pageY-t.touches[1].pageY;R=w=Math.sqrt(e*e+n*n);var r=(t.touches[0].pageX+t.touches[1].pageX)/2,i=(t.touches[0].pageY+t.touches[1].pageY)/2;H.copy(C(r,i)),M.copy(H);break;default:m=p.NONE}c.dispatchEvent(k)}}function h(t){if(c.enabled!==!1)switch(t.preventDefault(),t.stopPropagation(),t.touches.length){case 1:E.copy(y),y.copy(P(t.touches[0].pageX,t.touches[0].pageY));break;case 2:var e=t.touches[0].pageX-t.touches[1].pageX,n=t.touches[0].pageY-t.touches[1].pageY;R=Math.sqrt(e*e+n*n);var r=(t.touches[0].pageX+t.touches[1].pageX)/2,i=(t.touches[0].pageY+t.touches[1].pageY)/2;M.copy(C(r,i));break;default:m=p.NONE}}function l(t){if(c.enabled!==!1){switch(t.touches.length){case 1:E.copy(y),y.copy(P(t.touches[0].pageX,t.touches[0].pageY));break;case 2:w=R=0;var e=(t.touches[0].pageX+t.touches[1].pageX)/2,n=(t.touches[0].pageY+t.touches[1].pageY)/2;M.copy(C(e,n)),H.copy(M)}m=p.NONE,c.dispatchEvent(A)}}var c=this,p={NONE:-1,ROTATE:0,ZOOM:1,PAN:2,TOUCH_ROTATE:3,TOUCH_ZOOM_PAN:4};this.object=t,this.domElement=void 0!==e?e:document,this.enabled=!0,this.screen={left:0,top:0,width:0,height:0},this.rotateSpeed=1,this.zoomSpeed=1.2,this.panSpeed=.3,this.noRotate=!1,this.noZoom=!1,this.noPan=!1,this.staticMoving=!1,this.dynamicDampingFactor=.2,this.minDistance=0,this.maxDistance=1/0,this.keys=[65,83,68],this.target=new THREE.Vector3;var f=1e-6,d=new THREE.Vector3,m=p.NONE,v=p.NONE,g=new THREE.Vector3,E=new THREE.Vector2,y=new THREE.Vector2,_=new THREE.Vector3,b=0,T=new THREE.Vector2,x=new THREE.Vector2,w=0,R=0,H=new THREE.Vector2,M=new THREE.Vector2;this.target0=this.target.clone(),this.position0=this.object.position.clone(),this.up0=this.object.up.clone();var S={type:"change"},k={type:"start"},A={type:"end"};this.handleResize=function(){if(this.domElement===document)this.screen.left=0,this.screen.top=0,this.screen.width=window.innerWidth,this.screen.height=window.innerHeight;else{var t=this.domElement.getBoundingClientRect(),e=this.domElement.ownerDocument.documentElement;this.screen.left=t.left+window.pageXOffset-e.clientLeft,this.screen.top=t.top+window.pageYOffset-e.clientTop,this.screen.width=t.width,this.screen.height=t.height}},this.handleEvent=function(t){"function"==typeof this[t.type]&&this[t.type](t)};var C=function(){var t=new THREE.Vector2;return function(e,n){return t.set((e-c.screen.left)/c.screen.width,(n-c.screen.top)/c.screen.height),t}}(),P=function(){var t=new THREE.Vector2;return function(e,n){return t.set((e-.5*c.screen.width-c.screen.left)/(.5*c.screen.width),(c.screen.height+2*(c.screen.top-n))/c.screen.width),t}}();this.rotateCamera=function(){var t,e=new THREE.Vector3,n=new THREE.Quaternion,r=new THREE.Vector3,i=new THREE.Vector3,o=new THREE.Vector3,s=new THREE.Vector3;return function(){s.set(y.x-E.x,y.y-E.y,0),t=s.length(),t?(g.copy(c.object.position).sub(c.target),r.copy(g).normalize(),i.copy(c.object.up).normalize(),o.crossVectors(i,r).normalize(),i.setLength(y.y-E.y),o.setLength(y.x-E.x),s.copy(i.add(o)),e.crossVectors(s,g).normalize(),t*=c.rotateSpeed,n.setFromAxisAngle(e,t),g.applyQuaternion(n),c.object.up.applyQuaternion(n),_.copy(e),b=t):!c.staticMoving&&b&&(b*=Math.sqrt(1-c.dynamicDampingFactor),g.copy(c.object.position).sub(c.target),n.setFromAxisAngle(_,b),g.applyQuaternion(n),c.object.up.applyQuaternion(n)),E.copy(y)}}(),this.zoomCamera=function(){var t;m===p.TOUCH_ZOOM_PAN?(t=w/R,w=R,g.multiplyScalar(t)):(t=1+(x.y-T.y)*c.zoomSpeed,1!==t&&t>0&&(g.multiplyScalar(t),c.staticMoving?T.copy(x):T.y+=(x.y-T.y)*this.dynamicDampingFactor))},this.panCamera=function(){var t=new THREE.Vector2,e=new THREE.Vector3,n=new THREE.Vector3;return function(){t.copy(M).sub(H),t.lengthSq()&&(t.multiplyScalar(g.length()*c.panSpeed),n.copy(g).cross(c.object.up).setLength(t.x),n.add(e.copy(c.object.up).setLength(t.y)),c.object.position.add(n),c.target.add(n),c.staticMoving?H.copy(M):H.add(t.subVectors(M,H).multiplyScalar(c.dynamicDampingFactor)))}}(),this.checkDistances=function(){c.noZoom&&c.noPan||(g.lengthSq()>c.maxDistance*c.maxDistance&&c.object.position.addVectors(c.target,g.setLength(c.maxDistance)),g.lengthSq()f&&(c.dispatchEvent(S),d.copy(c.object.position))},this.reset=function(){m=p.NONE,v=p.NONE,c.target.copy(c.target0),c.object.position.copy(c.position0),c.object.up.copy(c.up0),g.subVectors(c.object.position,c.target),c.object.lookAt(c.target),c.dispatchEvent(S),d.copy(c.object.position)},this.domElement.addEventListener("contextmenu",function(t){t.preventDefault()},!1),this.domElement.addEventListener("mousedown",i,!1),this.domElement.addEventListener("mousewheel",a,!1),this.domElement.addEventListener("DOMMouseScroll",a,!1),this.domElement.addEventListener("touchstart",u,!1),this.domElement.addEventListener("touchend",l,!1),this.domElement.addEventListener("touchmove",h,!1),window.addEventListener("keydown",n,!1),window.addEventListener("keyup",r,!1),this.handleResize(),this.update()},THREE.TrackballControls.prototype=Object.create(THREE.EventDispatcher.prototype),THREE.TrackballControls.prototype.constructor=THREE.TrackballControls,function t(e,n,r){function i(s,a){if(!n[s]){if(!e[s]){var u="function"==typeof require&&require;if(!a&&u)return u(s,!0);if(o)return o(s,!0);throw new Error("Cannot find module '"+s+"'")}var h=n[s]={exports:{}};e[s][0].call(h.exports,function(t){var n=e[s][1][t];return i(n?n:t)},h,h.exports,t,e,n,r)}return n[s].exports}for(var o="function"==typeof require&&require,s=0;s 0.0 ? 0.0 : 1.0; \n val = abs(val); \n float exponent = floor(log2(val)); \n float biased_exponent = exponent + 127.0; \n float fraction = ((val / exp2(exponent)) - 1.0) * 8388608.0; \n float t = biased_exponent / 2.0; \n float last_bit_of_biased_exponent = fract(t) * 2.0; \n float remaining_bits_of_biased_exponent = floor(t); \n float byte4 = extract_bits(fraction, 0.0, 8.0) / 255.0; \n float byte3 = extract_bits(fraction, 8.0, 16.0) / 255.0; \n float byte2 = (last_bit_of_biased_exponent * 128.0 + extract_bits(fraction, 16.0, 23.0)) / 255.0; \n float byte1 = (valuesign * 128.0 + remaining_bits_of_biased_exponent) / 255.0; \n return vec4(byte4, byte3, byte2, byte1); \n}\n","float.index.pack":"uniform vec4 indexModulus;\n\nvec4 getSample(vec4 xyzw);\nvec4 getIndex(vec4 xyzw);\n\nvec4 floatPackIndex(vec4 xyzw) {\n vec4 value = getSample(xyzw);\n vec4 index = getIndex(xyzw);\n\n vec4 offset = floor(index + .5) * indexModulus;\n vec2 sum2 = offset.xy + offset.zw;\n float sum = sum2.x + sum2.y;\n return vec4(value.xyz, sum);\n}","float.stretch":"vec4 getSample(vec4 xyzw);\n\nfloat floatStretch(vec4 xyzw, float channelIndex) {\n vec4 sample = getSample(xyzw);\n vec2 xy = channelIndex > 1.5 ? sample.zw : sample.xy;\n return mod(channelIndex, 2.0) > .5 ? xy.y : xy.x;\n}","fragment.clip.dashed":"varying float vClipStrokeWidth;\nvarying float vClipStrokeIndex;\nvarying vec3 vClipStrokeEven;\nvarying vec3 vClipStrokeOdd;\nvarying vec3 vClipStrokePosition;\n\nvoid clipStrokeFragment() {\n bool odd = mod(vClipStrokeIndex, 2.0) >= 1.0;\n\n vec3 tangent;\n if (odd) {\n tangent = vClipStrokeOdd;\n }\n else {\n tangent = vClipStrokeEven;\n }\n\n float travel = dot(vClipStrokePosition, normalize(tangent)) / vClipStrokeWidth;\n if (mod(travel, 16.0) > 8.0) {\n discard;\n }\n}\n","fragment.clip.dotted":"varying float vClipStrokeWidth;\nvarying float vClipStrokeIndex;\nvarying vec3 vClipStrokeEven;\nvarying vec3 vClipStrokeOdd;\nvarying vec3 vClipStrokePosition;\n\nvoid clipStrokeFragment() {\n bool odd = mod(vClipStrokeIndex, 2.0) >= 1.0;\n\n vec3 tangent;\n if (odd) {\n tangent = vClipStrokeOdd;\n }\n else {\n tangent = vClipStrokeEven;\n }\n\n float travel = dot(vClipStrokePosition, normalize(tangent)) / vClipStrokeWidth;\n if (mod(travel, 4.0) > 2.0) {\n discard;\n }\n}\n","fragment.clip.ends":"varying vec2 vClipEnds;\n\nvoid clipEndsFragment() {\n if (vClipEnds.x < 0.0 || vClipEnds.y < 0.0) discard;\n}\n","fragment.clip.proximity":"varying float vClipProximity;\n\nvoid clipProximityFragment() {\n if (vClipProximity >= 0.5) discard;\n}","fragment.color":"void setFragmentColor(vec4 color) {\n gl_FragColor = color;\n}","fragment.map.rgba":"vec4 fragmentRGBA(vec4 rgba, vec4 stpq) {\n return rgba;\n}","fragment.solid":"void setFragmentColor(vec4 color) {\n if (color.a < 1.0) discard;\n gl_FragColor = color;\n}","fragment.transparent":"void setFragmentColor(vec4 color) {\n if (color.a >= 1.0) discard;\n gl_FragColor = color;\n}","grid.position":"uniform vec4 gridPosition;\nuniform vec4 gridStep;\nuniform vec4 gridAxis;\n\nvec4 sampleData(vec2 xy);\n\nvec4 getGridPosition(vec4 xyzw) {\n vec4 onAxis = gridAxis * sampleData(vec2(xyzw.y, 0.0)).x;\n vec4 offAxis = gridStep * xyzw.x + gridPosition;\n return onAxis + offAxis;\n}\n","grow.position":"uniform float growScale;\nuniform vec4 growMask;\nuniform vec4 growAnchor;\n\nvec4 getSample(vec4 xyzw);\n\nvec4 getGrowSample(vec4 xyzw) {\n vec4 anchor = xyzw * growMask + growAnchor;\n\n vec4 position = getSample(xyzw);\n vec4 center = getSample(anchor);\n\n return mix(center, position, growScale);\n}","join.position":"uniform float joinStride;\nuniform float joinStrideInv;\n\nfloat getIndex(vec4 xyzw);\nvec4 getRest(vec4 xyzw);\nvec4 injectIndices(float a, float b);\n\nvec4 getJoinXYZW(vec4 xyzw) {\n\n float a = getIndex(xyzw);\n float b = a * joinStrideInv;\n\n float integer = floor(b);\n float fraction = b - integer;\n \n return injectIndices(fraction * joinStride, integer) + getRest(xyzw);\n}\n","label.alpha":"varying float vPixelSize;\n\nvec4 getLabelAlphaColor(vec4 color, vec4 sample) {\n float mask = clamp(sample.r * 1000.0, 0.0, 1.0);\n float alpha = (sample.r - .5) * vPixelSize + .5;\n float a = mask * alpha * color.a;\n if (a <= 0.0) discard;\n return vec4(color.xyz, a);\n}\n","label.map":"vec2 mapUV(vec4 uvwo, vec4 stpq) {\n return uvwo.xy;\n}\n","label.outline":"uniform float outlineExpand;\nuniform float outlineStep;\nuniform vec3 outlineColor;\n\nvarying float vPixelSize;\n\nconst float PIXEL_STEP = 255.0 / 16.0;\n\nvec4 getLabelOutlineColor(vec4 color, vec4 sample) {\n float ps = vPixelSize * PIXEL_STEP;\n float os = outlineStep;\n\n float sdf = sample.r - .5 + outlineExpand;\n vec2 sdfs = vec2(sdf, sdf + os);\n vec2 alpha = clamp(sdfs * ps + .5, 0.0, 1.0);\n\n if (alpha.y <= 0.0) {\n discard;\n }\n\n vec3 blend = color.xyz;\n if (alpha.y > alpha.x) {\n blend = sqrt(mix(outlineColor * outlineColor, blend * blend, alpha.x));\n }\n \n return vec4(blend, alpha.y * color.a);\n}\n","layer.position":"uniform vec4 layerScale;\nuniform vec4 layerBias;\n\nvec4 layerPosition(vec4 position, inout vec4 stpq) {\n return layerScale * position + layerBias;\n}\n","lerp.depth":"// External\nvec4 sampleData(vec4 xyzw);\n\nvec4 lerpDepth(vec4 xyzw) {\n float x = xyzw.z;\n float i = floor(x);\n float f = x - i;\n \n vec4 xyzw1 = vec4(xyzw.xy, i, xyzw.w);\n vec4 xyzw2 = vec4(xyzw.xy, i + 1.0, xyzw.w);\n \n vec4 a = sampleData(xyzw1);\n vec4 b = sampleData(xyzw2);\n\n return mix(a, b, f);\n}\n","lerp.height":"// External\nvec4 sampleData(vec4 xyzw);\n\nvec4 lerpHeight(vec4 xyzw) {\n float x = xyzw.y;\n float i = floor(x);\n float f = x - i;\n \n vec4 xyzw1 = vec4(xyzw.x, i, xyzw.zw);\n vec4 xyzw2 = vec4(xyzw.x, i + 1.0, xyzw.zw);\n \n vec4 a = sampleData(xyzw1);\n vec4 b = sampleData(xyzw2);\n\n return mix(a, b, f);\n}\n","lerp.items":"// External\nvec4 sampleData(vec4 xyzw);\n\nvec4 lerpItems(vec4 xyzw) {\n float x = xyzw.w;\n float i = floor(x);\n float f = x - i;\n \n vec4 xyzw1 = vec4(xyzw.xyz, i);\n vec4 xyzw2 = vec4(xyzw.xyz, i + 1.0);\n \n vec4 a = sampleData(xyzw1);\n vec4 b = sampleData(xyzw2);\n\n return mix(a, b, f);\n}\n","lerp.width":"// External\nvec4 sampleData(vec4 xyzw);\n\nvec4 lerpWidth(vec4 xyzw) {\n float x = xyzw.x;\n float i = floor(x);\n float f = x - i;\n \n vec4 xyzw1 = vec4(i, xyzw.yzw);\n vec4 xyzw2 = vec4(i + 1.0, xyzw.yzw);\n \n vec4 a = sampleData(xyzw1);\n vec4 b = sampleData(xyzw2);\n\n return mix(a, b, f);\n}\n","line.position":"// Units and calibration\nuniform float worldUnit;\nuniform float lineWidth;\nuniform float lineDepth;\nuniform float focusDepth;\n\n// General data index\nuniform vec4 geometryClip;\nattribute vec4 position4;\n\n// (Start/mid/end -1/0/1, top/bottom -1,1) \nattribute vec2 line;\n\n// 0...1 for round or bevel joins\n#ifdef LINE_JOIN_DETAIL\nattribute float joint;\n#else\nconst float joint = 0.0;\n#endif\n\n// Knock out excessively long line segments (e.g. for asymtpotes)\n#ifdef LINE_PROXIMITY\nuniform float lineProximity;\nvarying float vClipProximity;\n#endif\n\n// Ghetto line stroking (local only, not global)\n#ifdef LINE_STROKE\nvarying float vClipStrokeWidth;\nvarying float vClipStrokeIndex;\nvarying vec3 vClipStrokeEven;\nvarying vec3 vClipStrokeOdd;\nvarying vec3 vClipStrokePosition;\n#endif\n\n// External\nvec3 getPosition(vec4 xyzw, float canonical);\n\n// Clip line ends for arrows / decoration\n#ifdef LINE_CLIP\nuniform float clipRange;\nuniform vec2 clipStyle;\nuniform float clipSpace;\n\nattribute vec2 strip;\n\nvarying vec2 vClipEnds;\n\nvoid clipEnds(vec4 xyzw, vec3 center, vec3 pos) {\n\n // Sample end of line strip\n vec4 xyzwE = vec4(strip.y, xyzw.yzw);\n vec3 end = getPosition(xyzwE, 0.0);\n\n // Sample start of line strip\n vec4 xyzwS = vec4(strip.x, xyzw.yzw);\n vec3 start = getPosition(xyzwS, 0.0);\n\n // Measure length\n vec3 diff = end - start;\n float l = length(diff) * clipSpace;\n\n // Arrow length (=2.5x radius)\n float arrowSize = 1.25 * clipRange * lineWidth * worldUnit;\n\n vClipEnds = vec2(1.0);\n\n if (clipStyle.y > 0.0) {\n // Depth blend end\n float depth = focusDepth;\n if (lineDepth < 1.0) {\n float z = max(0.00001, -end.z);\n depth = mix(z, focusDepth, lineDepth);\n }\n \n // Absolute arrow length\n float size = arrowSize * depth;\n\n // Adjust clip range\n // Approach linear scaling with cubic ease the smaller we get\n float mini = clamp(1.0 - l / size * .333, 0.0, 1.0);\n float scale = 1.0 - mini * mini * mini; \n float invrange = 1.0 / (size * scale);\n \n // Clip end\n diff = normalize(end - center);\n float d = dot(end - pos, diff);\n vClipEnds.x = d * invrange - 1.0;\n }\n\n if (clipStyle.x > 0.0) {\n // Depth blend start\n float depth = focusDepth;\n if (lineDepth < 1.0) {\n float z = max(0.00001, -start.z);\n depth = mix(z, focusDepth, lineDepth);\n }\n \n // Absolute arrow length\n float size = arrowSize * depth;\n\n // Adjust clip range\n // Approach linear scaling with cubic ease the smaller we get\n float mini = clamp(1.0 - l / size * .333, 0.0, 1.0);\n float scale = 1.0 - mini * mini * mini; \n float invrange = 1.0 / (size * scale);\n \n // Clip start \n diff = normalize(center - start);\n float d = dot(pos - start, diff);\n vClipEnds.y = d * invrange - 1.0;\n }\n\n\n}\n#endif\n\n// Adjust left/center/right to be inside near/far z range\nconst float epsilon = 1e-5;\nvoid fixCenter(inout vec3 left, inout vec3 center, inout vec3 right) {\n if (center.z >= 0.0) {\n if (left.z < 0.0) {\n float d = (center.z + epsilon) / (center.z - left.z);\n center = mix(center, left, d);\n }\n else if (right.z < 0.0) {\n float d = (center.z + epsilon) / (center.z - right.z);\n center = mix(center, right, d);\n }\n }\n\n if (left.z >= 0.0) {\n if (center.z < 0.0) {\n float d = (left.z + epsilon) / (left.z - center.z);\n left = mix(left, center, d);\n }\n }\n\n if (right.z >= 0.0) {\n if (center.z < 0.0) {\n float d = (right.z + epsilon) / (right.z - center.z);\n right = mix(right, center, d);\n }\n }\n}\n\n// Sample the source data in an edge-aware manner\nvoid getLineGeometry(vec4 xyzw, float edge, out vec3 left, out vec3 center, out vec3 right) {\n vec4 delta = vec4(1.0, 0.0, 0.0, 0.0);\n\n center = getPosition(xyzw, 1.0);\n left = (edge > -0.5) ? getPosition(xyzw - delta, 0.0) : center;\n right = (edge < 0.5) ? getPosition(xyzw + delta, 0.0) : center;\n}\n\n// Calculate the position for a vertex along the line, including joins\nvec3 getLineJoin(float edge, bool odd, vec3 left, vec3 center, vec3 right, float width, float offset, float joint) {\n vec2 join = vec2(1.0, 0.0);\n\n fixCenter(left, center, right);\n\n vec4 a = vec4(left.xy, right.xy);\n vec4 b = a / vec4(left.zz, right.zz);\n\n vec2 l = b.xy;\n vec2 r = b.zw;\n vec2 c = center.xy / center.z;\n\n vec4 d = vec4(l, c) - vec4(c, r);\n float l1 = dot(d.xy, d.xy);\n float l2 = dot(d.zw, d.zw);\n\n if (l1 + l2 > 0.0) {\n \n if (edge > 0.5 || l2 == 0.0) {\n vec2 nl = normalize(d.xy);\n vec2 tl = vec2(nl.y, -nl.x);\n\n#ifdef LINE_PROXIMITY\n vClipProximity = 1.0;\n#endif\n\n#ifdef LINE_STROKE\n vClipStrokeEven = vClipStrokeOdd = normalize(left - center);\n#endif\n join = tl;\n }\n else if (edge < -0.5 || l1 == 0.0) {\n vec2 nr = normalize(d.zw);\n vec2 tr = vec2(nr.y, -nr.x);\n\n#ifdef LINE_PROXIMITY\n vClipProximity = 1.0;\n#endif\n\n#ifdef LINE_STROKE\n vClipStrokeEven = vClipStrokeOdd = normalize(center - right);\n#endif\n join = tr;\n }\n else {\n // Limit join stretch for tiny segments\n float lmin2 = min(l1, l2) / (width * width);\n\n // Hide line segment if ratio of leg lengths exceeds promixity threshold\n#ifdef LINE_PROXIMITY\n float lr = l1 / l2;\n float rl = l2 / l1;\n float ratio = max(lr, rl);\n float thresh = lineProximity + 1.0;\n vClipProximity = (ratio > thresh * thresh) ? 1.0 : 0.0;\n#endif\n\n // Calculate normals/tangents\n vec2 nl = normalize(d.xy);\n vec2 nr = normalize(d.zw);\n\n // Calculate tangents\n vec2 tl = vec2(nl.y, -nl.x);\n vec2 tr = vec2(nr.y, -nr.x);\n\n#ifdef LINE_PROXIMITY\n // Mix tangents according to leg lengths\n vec2 tc = normalize(mix(tl, tr, l1/(l1+l2)));\n#else\n // Average tangent\n vec2 tc = normalize(tl + tr);\n#endif\n \n // Miter join\n float cosA = dot(nl, tc);\n float sinA = max(0.1, abs(dot(tl, tc)));\n float factor = cosA / sinA;\n float scale = sqrt(1.0 + min(lmin2, factor * factor));\n\n // Stroke normals\n#ifdef LINE_STROKE\n vec3 stroke1 = normalize(left - center);\n vec3 stroke2 = normalize(center - right);\n\n if (odd) {\n vClipStrokeEven = stroke1;\n vClipStrokeOdd = stroke2;\n }\n else {\n vClipStrokeEven = stroke2;\n vClipStrokeOdd = stroke1;\n }\n#endif\n\n#ifdef LINE_JOIN_MITER\n // Apply straight up miter\n join = tc * scale;\n#endif\n\n#ifdef LINE_JOIN_ROUND\n // Slerp bevel join into circular arc\n float dotProduct = dot(nl, nr);\n float angle = acos(dotProduct);\n float sinT = sin(angle);\n join = (sin((1.0 - joint) * angle) * tl + sin(joint * angle) * tr) / sinT;\n#endif\n\n#ifdef LINE_JOIN_BEVEL\n // Direct bevel join between two flat ends\n float dotProduct = dot(nl, nr);\n join = mix(tl, tr, joint);\n#endif\n\n#ifdef LINE_JOIN_DETAIL\n // Check if on inside or outside of joint\n float crossProduct = nl.x * nr.y - nl.y * nr.x;\n if (offset * crossProduct < 0.0) {\n // For near-180-degree bends, correct back to a miter to avoid discontinuities\n float ratio = clamp(-dotProduct * 2.0 - 1.0, 0.0, 1.0);\n // Otherwise collapse the inside vertices into one.\n join = mix(tc * scale, join, ratio * ratio * ratio);\n }\n#endif\n\n }\n return vec3(join, 0.0);\n }\n else {\n return vec3(0.0);\n }\n\n}\n\n// Calculate final line position\nvec3 getLinePosition() {\n vec3 left, center, right, join;\n\n // left/center/right\n float edge = line.x;\n // up/down\n float offset = line.y;\n\n // Clip data\n vec4 p = min(geometryClip, position4);\n edge += max(0.0, position4.x - geometryClip.x);\n\n // Get position + adjacent neighbours\n getLineGeometry(p, edge, left, center, right);\n\n#ifdef LINE_STROKE\n // Set parameters for line stroke fragment shader\n vClipStrokePosition = center;\n vClipStrokeIndex = p.x;\n bool odd = mod(p.x, 2.0) >= 1.0;\n#else\n bool odd = true;\n#endif\n\n // Divide line width up/down\n float width = lineWidth * 0.5;\n\n float depth = focusDepth;\n if (lineDepth < 1.0) {\n // Depth blending\n float z = max(0.00001, -center.z);\n depth = mix(z, focusDepth, lineDepth);\n }\n width *= depth;\n\n // Convert to world units\n width *= worldUnit;\n\n // Calculate line join\n join = getLineJoin(edge, odd, left, center, right, width, offset, joint);\n vec3 pos = center + join * offset * width;\n\n#ifdef LINE_STROKE\n vClipStrokeWidth = width;\n#endif\n\n#ifdef LINE_CLIP\n clipEnds(p, center, pos);\n#endif\n\n return pos;\n}\n","map.2d.data":"uniform vec2 dataResolution;\nuniform vec2 dataPointer;\n\nvec2 map2DData(vec2 xy) {\n return (xy + dataPointer) * dataResolution;\n}\n","map.2d.data.wrap":"uniform vec2 dataResolution;\nuniform vec2 dataPointer;\n\nvec2 map2DData(vec2 xy) {\n return fract((xy + dataPointer) * dataResolution);\n}\n","map.xyzw.2dv":"void mapXyzw2DV(vec4 xyzw, out vec2 xy, out float z) {\n xy = xyzw.xy;\n z = xyzw.z;\n}\n\n","map.xyzw.align":"vec4 alignXYZW(vec4 xyzw) {\n return floor(xyzw + .5);\n}\n\n","map.xyzw.texture":"uniform float textureItems;\nuniform float textureHeight;\n\nvec2 mapXyzwTexture(vec4 xyzw) {\n \n float x = xyzw.x;\n float y = xyzw.y;\n float z = xyzw.z;\n float i = xyzw.w;\n \n return vec2(i, y) + vec2(x, z) * vec2(textureItems, textureHeight);\n}\n\n","mesh.fragment.color":"varying vec4 vColor;\n\nvec4 getColor() {\n return vColor;\n}\n","mesh.fragment.map":"#ifdef POSITION_STPQ\nvarying vec4 vSTPQ;\n#endif\n#ifdef POSITION_U\nvarying float vU;\n#endif\n#ifdef POSITION_UV\nvarying vec2 vUV;\n#endif\n#ifdef POSITION_UVW\nvarying vec3 vUVW;\n#endif\n#ifdef POSITION_UVWO\nvarying vec4 vUVWO;\n#endif\n\nvec4 getSample(vec4 uvwo, vec4 stpq);\n\nvec4 getMapColor() {\n #ifdef POSITION_STPQ\n vec4 stpq = vSTPQ;\n #else\n vec4 stpq = vec4(0.0);\n #endif\n\n #ifdef POSITION_U\n vec4 uvwo = vec4(vU, 0.0, 0.0, 0.0);\n #endif\n #ifdef POSITION_UV\n vec4 uvwo = vec4(vUV, 0.0, 0.0);\n #endif\n #ifdef POSITION_UVW\n vec4 uvwo = vec4(vUVW, 0.0);\n #endif\n #ifdef POSITION_UVWO\n vec4 uvwo = vec4(vUVWO);\n #endif\n\n return getSample(uvwo, stpq);\n}\n","mesh.fragment.mask":"varying float vMask;\n\nfloat ease(float t) {\n t = clamp(t, 0.0, 1.0);\n return t * t * (3.0 - 2.0 * t);\n}\n\nvec4 maskColor() {\n if (vMask <= 0.0) discard;\n return vec4(vec3(1.0), ease(vMask));\n}\n","mesh.fragment.material":"#ifdef POSITION_STPQ\nvarying vec4 vSTPQ;\n#endif\n#ifdef POSITION_U\nvarying float vU;\n#endif\n#ifdef POSITION_UV\nvarying vec2 vUV;\n#endif\n#ifdef POSITION_UVW\nvarying vec3 vUVW;\n#endif\n#ifdef POSITION_UVWO\nvarying vec4 vUVWO;\n#endif\n\nvec4 getSample(vec4 rgba, vec4 stpq);\n\nvec4 getMaterialColor(vec4 rgba) {\n vec4 stpq = vec4(0.0);\n\n #ifdef POSITION_U\n stpq.x = vU;\n #endif\n #ifdef POSITION_UV\n stpq.xy = vUV;\n #endif\n #ifdef POSITION_UVW\n stpq.xyz = vUVW;\n #endif\n #ifdef POSITION_UVWO\n stpq = vUVWO;\n #endif\n\n #ifdef POSITION_STPQ\n stpq = vSTPQ;\n #endif\n\n return getSample(rgba, stpq);\n}\n","mesh.fragment.shaded":"varying vec3 vNormal;\nvarying vec3 vLight;\nvarying vec3 vPosition;\n\nvec3 offSpecular(vec3 color) {\n vec3 c = 1.0 - color;\n return 1.0 - c * c;\n}\n\nvec4 getShadedColor(vec4 rgba) {\n \n vec3 color = rgba.xyz;\n vec3 color2 = offSpecular(rgba.xyz);\n\n vec3 normal = normalize(vNormal);\n vec3 light = normalize(vLight);\n vec3 position = normalize(vPosition);\n \n float side = gl_FrontFacing ? -1.0 : 1.0;\n float cosine = side * dot(normal, light);\n float diffuse = mix(max(0.0, cosine), .5 + .5 * cosine, .1);\n \n vec3 halfLight = normalize(light + position);\n float cosineHalf = max(0.0, side * dot(normal, halfLight));\n float specular = pow(cosineHalf, 16.0);\n \n return vec4(color * (diffuse * .9 + .05) + .25 * color2 * specular, rgba.a);\n}\n","mesh.fragment.texture":"","mesh.gamma.in":"vec4 getGammaInColor(vec4 rgba) {\n return vec4(rgba.rgb * rgba.rgb, rgba.a);\n}\n","mesh.gamma.out":"vec4 getGammaOutColor(vec4 rgba) {\n return vec4(sqrt(rgba.rgb), rgba.a);\n}\n","mesh.map.uvwo":"vec4 mapUVWO(vec4 uvwo, vec4 stpq) {\n return uvwo;\n}\n","mesh.position":"uniform vec4 geometryClip;\nattribute vec4 position4;\n\n// External\nvec3 getPosition(vec4 xyzw, float canonical);\n\nvec3 getMeshPosition() {\n vec4 p = min(geometryClip, position4);\n return getPosition(p, 1.0);\n}\n","mesh.vertex.color":"attribute vec4 position4;\nuniform vec4 geometryClip;\nvarying vec4 vColor;\n\n// External\nvec4 getSample(vec4 xyzw);\n\nvoid vertexColor() {\n vec4 p = min(geometryClip, position4);\n vColor = getSample(p);\n}\n","mesh.vertex.mask":"attribute vec4 position4;\nuniform vec4 geometryResolution;\nuniform vec4 geometryClip;\nvarying float vMask;\n\n// External\nfloat getSample(vec4 xyzw);\n\nvoid maskLevel() {\n vec4 p = min(geometryClip, position4);\n vMask = getSample(p * geometryResolution);\n}\n","mesh.vertex.position":"uniform vec4 geometryResolution;\n\n#ifdef POSITION_STPQ\nvarying vec4 vSTPQ;\n#endif\n#ifdef POSITION_U\nvarying float vU;\n#endif\n#ifdef POSITION_UV\nvarying vec2 vUV;\n#endif\n#ifdef POSITION_UVW\nvarying vec3 vUVW;\n#endif\n#ifdef POSITION_UVWO\nvarying vec4 vUVWO;\n#endif\n\n// External\nvec3 getPosition(vec4 xyzw, in vec4 stpqIn, out vec4 stpqOut);\n\nvec3 getMeshPosition(vec4 xyzw, float canonical) {\n vec4 stpqOut, stpqIn = xyzw * geometryResolution;\n vec3 xyz = getPosition(xyzw, stpqIn, stpqOut);\n\n #ifdef POSITION_MAP\n if (canonical > 0.5) {\n #ifdef POSITION_STPQ\n vSTPQ = stpqOut;\n #endif\n #ifdef POSITION_U\n vU = stpqOut.x;\n #endif\n #ifdef POSITION_UV\n vUV = stpqOut.xy;\n #endif\n #ifdef POSITION_UVW\n vUVW = stpqOut.xyz;\n #endif\n #ifdef POSITION_UVWO\n vUVWO = stpqOut;\n #endif\n }\n #endif\n return xyz;\n}\n","move.position":"uniform float transitionEnter;\nuniform float transitionExit;\nuniform vec4 transitionScale;\nuniform vec4 transitionBias;\nuniform float transitionSkew;\nuniform float transitionActive;\n\nuniform vec4 moveFrom;\nuniform vec4 moveTo;\n\nfloat ease(float t) {\n t = clamp(t, 0.0, 1.0);\n return 1.0 - (2.0 - t) * t;\n}\n\nvec4 getTransitionPosition(vec4 xyzw, inout vec4 stpq) {\n if (transitionActive < 0.5) return xyzw;\n\n float enter = transitionEnter;\n float exit = transitionExit;\n float skew = transitionSkew;\n vec4 scale = transitionScale;\n vec4 bias = transitionBias;\n\n float factor = 1.0 + skew;\n float offset = dot(vec4(1.0), stpq * scale + bias);\n\n float a1 = ease(enter * factor - offset);\n float a2 = ease(exit * factor + offset - skew);\n\n return xyzw + a1 * moveFrom + a2 * moveTo;\n}","object.mask.default":"vec4 getMask(vec4 xyzw) {\n return vec4(1.0);\n}","point.alpha.circle":"varying float vPixelSize;\n\nfloat getDiscAlpha(float mask) {\n // Approximation: 1 - x*x is approximately linear around x = 1 with slope 2\n return vPixelSize * (1.0 - mask);\n // return vPixelSize * 2.0 * (1.0 - sqrt(mask));\n}\n","point.alpha.circle.hollow":"varying float vPixelSize;\n\nfloat getDiscHollowAlpha(float mask) {\n return vPixelSize * (0.5 - 2.0 * abs(sqrt(mask) - .75));\n}\n","point.alpha.generic":"varying float vPixelSize;\n\nfloat getGenericAlpha(float mask) {\n return vPixelSize * 2.0 * (1.0 - mask);\n}\n","point.alpha.generic.hollow":"varying float vPixelSize;\n\nfloat getGenericHollowAlpha(float mask) {\n return vPixelSize * (0.5 - 2.0 * abs(mask - .75));\n}\n","point.edge":"varying vec2 vSprite;\n\nfloat getSpriteMask(vec2 xy);\nfloat getSpriteAlpha(float mask);\n\nvoid setFragmentColorFill(vec4 color) {\n float mask = getSpriteMask(vSprite);\n if (mask > 1.0) {\n discard;\n }\n float alpha = getSpriteAlpha(mask);\n if (alpha >= 1.0) {\n discard;\n }\n gl_FragColor = vec4(color.rgb, alpha * color.a);\n}\n","point.fill":"varying vec2 vSprite;\n\nfloat getSpriteMask(vec2 xy);\nfloat getSpriteAlpha(float mask);\n\nvoid setFragmentColorFill(vec4 color) {\n float mask = getSpriteMask(vSprite);\n if (mask > 1.0) {\n discard;\n }\n float alpha = getSpriteAlpha(mask);\n if (alpha < 1.0) {\n discard;\n }\n gl_FragColor = color;\n}\n\n","point.mask.circle":"varying float vPixelSize;\n\nfloat getCircleMask(vec2 uv) {\n return dot(uv, uv);\n}\n","point.mask.diamond":"varying float vPixelSize;\n\nfloat getDiamondMask(vec2 uv) {\n vec2 a = abs(uv);\n return a.x + a.y;\n}\n","point.mask.down":"varying float vPixelSize;\n\nfloat getTriangleDownMask(vec2 uv) {\n uv.y += .25;\n return max(uv.y, abs(uv.x) * .866 - uv.y * .5 + .6);\n}\n","point.mask.left":"varying float vPixelSize;\n\nfloat getTriangleLeftMask(vec2 uv) {\n uv.x += .25;\n return max(uv.x, abs(uv.y) * .866 - uv.x * .5 + .6);\n}\n","point.mask.right":"varying float vPixelSize;\n\nfloat getTriangleRightMask(vec2 uv) {\n uv.x -= .25;\n return max(-uv.x, abs(uv.y) * .866 + uv.x * .5 + .6);\n}\n","point.mask.square":"varying float vPixelSize;\n\nfloat getSquareMask(vec2 uv) {\n vec2 a = abs(uv);\n return max(a.x, a.y);\n}\n","point.mask.up":"varying float vPixelSize;\n\nfloat getTriangleUpMask(vec2 uv) {\n uv.y -= .25;\n return max(-uv.y, abs(uv.x) * .866 + uv.y * .5 + .6);\n}\n","point.position":"uniform float pointDepth;\n\nuniform float pixelUnit;\nuniform float renderScale;\nuniform float renderScaleInv;\nuniform float focusDepth;\n\nuniform vec4 geometryClip;\nattribute vec4 position4;\nattribute vec2 sprite;\n\nvarying vec2 vSprite;\nvarying float vPixelSize;\n\nconst float pointScale = POINT_SHAPE_SCALE;\n\n// External\nfloat getPointSize(vec4 xyzw);\nvec3 getPosition(vec4 xyzw, float canonical);\n\nvec3 getPointPosition() {\n vec4 p = min(geometryClip, position4);\n vec3 center = getPosition(p, 1.0);\n\n // Depth blending\n // TODO: orthographic camera\n // Workaround: set depth = 0\n float z = -center.z;\n float depth = mix(z, focusDepth, pointDepth);\n \n // Match device/unit mapping \n // Sprite goes from -1..1, width = 2.\n float pointSize = getPointSize(p);\n float size = pointScale * pointSize * pixelUnit * .5;\n float depthSize = depth * size;\n \n // Pad sprite by half a pixel to make the anti-aliasing straddle the pixel edge\n // Note: pixelsize measures radius\n float pixelSize = .5 * (pointDepth > 0.0 ? depthSize / z : size);\n float paddedSize = pixelSize + 0.5;\n float padFactor = paddedSize / pixelSize;\n\n vPixelSize = paddedSize;\n vSprite = sprite;\n\n return center + vec3(sprite * depthSize * renderScaleInv * padFactor, 0.0);\n}\n","point.size.uniform":"uniform float pointSize;\n\nfloat getPointSize(vec4 xyzw) {\n return pointSize;\n}","point.size.varying":"uniform float pointSize;\n\nvec4 getSample(vec4 xyzw);\n\nfloat getPointSize(vec4 xyzw) {\n return pointSize * getSample(xyzw).x;\n}","polar.position":"uniform float polarBend;\nuniform float polarFocus;\nuniform float polarAspect;\nuniform float polarHelix;\n\nuniform mat4 viewMatrix;\n\nvec4 getPolarPosition(vec4 position, inout vec4 stpq) {\n if (polarBend > 0.0) {\n\n if (polarBend < 0.001) {\n // Factor out large addition/subtraction of polarFocus\n // to avoid numerical error\n // sin(x) ~ x\n // cos(x) ~ 1 - x * x / 2\n vec2 pb = position.xy * polarBend;\n float ppbbx = pb.x * pb.x;\n return viewMatrix * vec4(\n position.x * (1.0 - polarBend + (pb.y * polarAspect)),\n position.y * (1.0 - .5 * ppbbx) - (.5 * ppbbx) * polarFocus / polarAspect,\n position.z + position.x * polarHelix * polarBend,\n 1.0\n );\n }\n else {\n vec2 xy = position.xy * vec2(polarBend, polarAspect);\n float radius = polarFocus + xy.y;\n return viewMatrix * vec4(\n sin(xy.x) * radius,\n (cos(xy.x) * radius - polarFocus) / polarAspect,\n position.z + position.x * polarHelix * polarBend,\n 1.0\n );\n }\n }\n else {\n return viewMatrix * vec4(position.xyz, 1.0);\n }\n}","project.position":"uniform float styleZBias;\nuniform float styleZIndex;\n\nvoid setPosition(vec3 position) {\n vec4 pos = projectionMatrix * vec4(position, 1.0);\n\n // Apply relative Z bias\n float bias = (1.0 - styleZBias / 32768.0);\n pos.z *= bias;\n \n // Apply large scale Z index changes\n if (styleZIndex > 0.0) {\n float z = pos.z / pos.w;\n pos.z = ((z + 1.0) / (styleZIndex + 1.0) - 1.0) * pos.w;\n }\n \n gl_Position = pos;\n}","project.readback":"// This is three.js' global uniform, missing from fragment shaders.\nuniform mat4 projectionMatrix;\n\nvec4 readbackPosition(vec3 position, vec4 stpq) {\n vec4 pos = projectionMatrix * vec4(position, 1.0);\n vec3 final = pos.xyz / pos.w;\n if (final.z < -1.0) {\n return vec4(0.0, 0.0, 0.0, -1.0);\n }\n else {\n return vec4(final, -position.z);\n }\n}\n","raw.position.scale":"uniform vec4 geometryScale;\nattribute vec4 position4;\n\nvec4 getRawPositionScale() {\n return geometryScale * position4;\n}\n","repeat.position":"uniform vec4 repeatModulus;\n\nvec4 getRepeatXYZW(vec4 xyzw) {\n return mod(xyzw + .5, repeatModulus) - .5;\n}\n","resample.padding":"uniform vec4 resampleBias;\n\nvec4 resamplePadding(vec4 xyzw) {\n return xyzw + resampleBias;\n}","resample.relative":"uniform vec4 resampleFactor;\n\nvec4 resampleRelative(vec4 xyzw) {\n return xyzw * resampleFactor;\n}","reveal.mask":"uniform float transitionEnter;\nuniform float transitionExit;\nuniform vec4 transitionScale;\nuniform vec4 transitionBias;\nuniform float transitionSkew;\nuniform float transitionActive;\n\nfloat getTransitionSDFMask(vec4 stpq) {\n if (transitionActive < 0.5) return 1.0;\n\n float enter = transitionEnter;\n float exit = transitionExit;\n float skew = transitionSkew;\n vec4 scale = transitionScale;\n vec4 bias = transitionBias;\n\n float factor = 1.0 + skew;\n float offset = dot(vec4(1.0), stpq * scale + bias);\n\n vec2 d = vec2(enter, exit) * factor + vec2(-offset, offset - skew);\n if (exit == 1.0) return d.x;\n if (enter == 1.0) return d.y;\n return min(d.x, d.y);\n}","root.position":"vec3 getRootPosition(vec4 position, in vec4 stpqIn, out vec4 stpqOut) {\n stpqOut = stpqIn; // avoid inout confusion\n return position.xyz;\n}","sample.2d":"uniform sampler2D dataTexture;\n\nvec4 sample2D(vec2 uv) {\n return texture2D(dataTexture, uv);\n}\n","scale.position":"uniform vec4 scaleAxis;\nuniform vec4 scaleOffset;\n\nvec4 sampleData(float x);\n\nvec4 getScalePosition(vec4 xyzw) {\n return scaleAxis * sampleData(xyzw.x).x + scaleOffset;\n}\n","screen.map.stpq":"uniform vec4 remapSTPQScale;\n\nvec4 screenMapSTPQ(vec4 xyzw, out vec4 stpq) {\n stpq = xyzw * remapSTPQScale;\n return xyzw;\n}\n","screen.map.xy":"uniform vec2 remapUVScale;\n\nvec4 screenMapXY(vec4 uvwo, vec4 stpq) {\n return vec4(floor(remapUVScale * uvwo.xy), 0.0, 0.0);\n}\n","screen.map.xyzw":"uniform vec2 remapUVScale;\nuniform vec2 remapModulus;\nuniform vec2 remapModulusInv;\n\nvec4 screenMapXYZW(vec4 uvwo, vec4 stpq) {\n vec2 st = floor(remapUVScale * uvwo.xy);\n vec2 xy = st * remapModulusInv;\n vec2 ixy = floor(xy);\n vec2 fxy = xy - ixy;\n vec2 zw = fxy * remapModulus;\n return vec4(ixy.x, zw.y, ixy.y, zw.x);\n}\n","screen.pass.uv":"vec2 screenPassUV(vec4 uvwo, vec4 stpq) {\n return uvwo.xy;\n}\n","screen.position":"void setScreenPosition(vec4 position) {\n gl_Position = vec4(position.xy * 2.0 - 1.0, 0.5, 1.0);\n}\n","slice.position":"uniform vec4 sliceOffset;\n\nvec4 getSliceOffset(vec4 xyzw) {\n return xyzw + sliceOffset;\n}\n","spherical.position":"uniform float sphericalBend;\nuniform float sphericalFocus;\nuniform float sphericalAspectX;\nuniform float sphericalAspectY;\nuniform float sphericalScaleY;\n\nuniform mat4 viewMatrix;\n\nvec4 getSphericalPosition(vec4 position, inout vec4 stpq) {\n if (sphericalBend > 0.0001) {\n\n vec3 xyz = position.xyz * vec3(sphericalBend, sphericalBend / sphericalAspectY * sphericalScaleY, sphericalAspectX);\n float radius = sphericalFocus + xyz.z;\n float cosine = cos(xyz.y) * radius;\n\n return viewMatrix * vec4(\n sin(xyz.x) * cosine,\n sin(xyz.y) * radius * sphericalAspectY,\n (cos(xyz.x) * cosine - sphericalFocus) / sphericalAspectX,\n 1.0\n );\n }\n else {\n return viewMatrix * vec4(position.xyz, 1.0);\n }\n}", -"split.position":"uniform float splitStride;\n\nvec2 getIndices(vec4 xyzw);\nvec4 getRest(vec4 xyzw);\nvec4 injectIndex(float v);\n\nvec4 getSplitXYZW(vec4 xyzw) {\n vec2 uv = getIndices(xyzw);\n float offset = uv.x + uv.y * splitStride;\n return injectIndex(offset) + getRest(xyzw);\n}\n","spread.position":"uniform vec4 spreadOffset;\nuniform mat4 spreadMatrix;\n\n// External\nvec4 getSample(vec4 xyzw);\n\nvec4 getSpreadSample(vec4 xyzw) {\n vec4 sample = getSample(xyzw);\n return sample + spreadMatrix * (spreadOffset + xyzw);\n}\n","sprite.fragment":"varying vec2 vSprite;\n\nvec4 getSample(vec2 xy);\n\nvec4 getSpriteColor() {\n return getSample(vSprite);\n}","sprite.position":"uniform vec2 spriteOffset;\nuniform float spriteScale;\nuniform float spriteDepth;\nuniform float spriteSnap;\n\nuniform vec2 renderOdd;\nuniform float renderScale;\nuniform float renderScaleInv;\nuniform float pixelUnit;\nuniform float focusDepth;\n\nuniform vec4 geometryClip;\nattribute vec4 position4;\nattribute vec2 sprite;\n\nvarying float vPixelSize;\n\n// External\nvec3 getPosition(vec4 xyzw, float canonical);\nvec4 getSprite(vec4 xyzw);\n\nvec3 getSpritePosition() {\n // Clip points\n vec4 p = min(geometryClip, position4);\n float diff = length(position4 - p);\n if (diff > 0.0) {\n return vec3(0.0, 0.0, 1000.0);\n }\n\n // Make sprites\n vec3 center = getPosition(p, 1.0);\n vec4 atlas = getSprite(p);\n\n // Sprite goes from -1..1, width = 2.\n // -1..1 -> -0.5..0.5\n vec2 halfSprite = sprite * .5;\n vec2 halfFlipSprite = vec2(halfSprite.x, -halfSprite.y);\n\n#ifdef POSITION_UV\n // Assign UVs\n vUV = atlas.xy + atlas.zw * (halfFlipSprite + .5);\n#endif\n\n // Depth blending\n // TODO: orthographic camera\n // Workaround: set depth = 0\n float depth = focusDepth, z;\n z = -center.z;\n if (spriteDepth < 1.0) {\n depth = mix(z, focusDepth, spriteDepth);\n }\n \n // Match device/unit mapping \n float size = pixelUnit * spriteScale;\n float depthSize = depth * size;\n\n // Calculate pixelSize for anti-aliasing\n float pixelSize = (spriteDepth > 0.0 ? depthSize / z : size);\n vPixelSize = pixelSize;\n\n // Position sprite\n vec2 atlasOdd = fract(atlas.zw / 2.0);\n vec2 offset = (spriteOffset + halfSprite * atlas.zw) * depthSize;\n if (spriteSnap > 0.5) {\n // Snap to pixel (w/ epsilon shift to avoid jitter)\n return vec3(((floor(center.xy / center.z * renderScale + 0.001) + renderOdd + atlasOdd) * center.z + offset) * renderScaleInv, center.z);\n }\n else {\n // Place directly\n return center + vec3(offset * renderScaleInv, 0.0);\n }\n\n}\n","stereographic.position":"uniform float stereoBend;\n\nuniform mat4 viewMatrix;\n\nvec4 getStereoPosition(vec4 position, inout vec4 stpq) {\n if (stereoBend > 0.0001) {\n\n vec3 pos = position.xyz;\n float r = length(pos);\n float z = r + pos.z;\n vec3 project = vec3(pos.xy / z, r);\n \n vec3 lerped = mix(pos, project, stereoBend);\n\n return viewMatrix * vec4(lerped, 1.0);\n }\n else {\n return viewMatrix * vec4(position.xyz, 1.0);\n }\n}","stereographic4.position":"uniform float stereoBend;\nuniform vec4 basisScale;\nuniform vec4 basisOffset;\nuniform mat4 viewMatrix;\nuniform vec2 view4D;\n\nvec4 getStereographic4Position(vec4 position, inout vec4 stpq) {\n \n vec4 transformed;\n if (stereoBend > 0.0001) {\n\n float r = length(position);\n float w = r + position.w;\n vec4 project = vec4(position.xyz / w, r);\n \n transformed = mix(position, project, stereoBend);\n }\n else {\n transformed = position;\n }\n\n vec4 pos4 = transformed * basisScale - basisOffset;\n vec3 xyz = (viewMatrix * vec4(pos4.xyz, 1.0)).xyz;\n return vec4(xyz, pos4.w * view4D.y + view4D.x);\n}\n","stpq.sample.2d":"varying vec2 vST;\n\nvec4 getSample(vec2 st);\n\nvec4 getSTSample() {\n return getSample(vST);\n}\n","stpq.xyzw.2d":"varying vec2 vUV;\n\nvoid setRawUV(vec4 xyzw) {\n vUV = xyzw.xy;\n}\n","strip.position.normal":"uniform vec4 geometryClip;\nattribute vec4 position4;\nattribute vec3 strip;\n\n// External\nvec3 getPosition(vec4 xyzw, float canonical);\n\nvarying vec3 vNormal;\nvarying vec3 vLight;\nvarying vec3 vPosition;\n\nvoid getStripGeometry(vec4 xyzw, vec3 strip, out vec3 pos, out vec3 normal) {\n vec3 a, b, c;\n\n a = getPosition(xyzw, 1.0);\n b = getPosition(vec4(xyzw.xyz, strip.x), 0.0);\n c = getPosition(vec4(xyzw.xyz, strip.y), 0.0);\n\n normal = normalize(cross(c - a, b - a)) * strip.z;\n \n pos = a;\n}\n\nvec3 getStripPositionNormal() {\n vec3 center, normal;\n\n vec4 p = min(geometryClip, position4);\n\n getStripGeometry(p, strip, center, normal);\n vNormal = normal;\n vLight = normalize((viewMatrix * vec4(1.0, 2.0, 2.0, 0.0)).xyz);\n vPosition = -center;\n\n return center;\n}\n","style.color":"uniform vec3 styleColor;\nuniform float styleOpacity;\n\nvec4 getStyleColor() {\n return vec4(styleColor, styleOpacity);\n}\n","subdivide.depth":"uniform float subdivideBevel;\n\n// External\nvec4 sampleData(vec4 xyzw);\n\nvec4 subdivideDepth(vec4 xyzw) {\n float x = xyzw.z;\n float i = floor(x);\n float f = x - i;\n\n float minf = subdivideBevel * min(f, 1.0 - f);\n float g = (f > 0.5) ? 1.0 - minf : (f < 0.5) ? minf : 0.5;\n\n return sampleData(vec4(xyzw.xy, i + g, xyzw.w));\n}\n","subdivide.depth.lerp":"uniform float subdivideBevel;\n\n// External\nvec4 sampleData(vec4 xyzw);\n\nvec4 subdivideDepthLerp(vec4 xyzw) {\n float x = xyzw.z;\n float i = floor(x);\n float f = x - i;\n\n float minf = subdivideBevel * min(f, 1.0 - f);\n float g = (f > 0.5) ? 1.0 - minf : (f < 0.5) ? minf : 0.5;\n\n vec4 xyzw1 = vec4(xyzw.xy, i, xyzw.w);\n vec4 xyzw2 = vec4(xyzw.xy, i + 1.0, xyzw.w);\n \n vec4 a = sampleData(xyzw1);\n vec4 b = sampleData(xyzw2);\n\n return mix(a, b, g);\n}\n","subdivide.height":"uniform float subdivideBevel;\n\n// External\nvec4 sampleData(vec4 xyzw);\n\nvec4 subdivideHeight(vec4 xyzw) {\n float x = xyzw.y;\n float i = floor(x);\n float f = x - i;\n\n float minf = subdivideBevel * min(f, 1.0 - f);\n float g = (f > 0.5) ? 1.0 - minf : (f < 0.5) ? minf : 0.5;\n\n return sampleData(vec4(xyzw.x, i + g, xyzw.zw));\n}\n","subdivide.height.lerp":"uniform float subdivideBevel;\n\n// External\nvec4 sampleData(vec4 xyzw);\n\nvec4 subdivideHeightLerp(vec4 xyzw) {\n float x = xyzw.y;\n float i = floor(x);\n float f = x - i;\n\n float minf = subdivideBevel * min(f, 1.0 - f);\n float g = (f > 0.5) ? 1.0 - minf : (f < 0.5) ? minf : 0.5;\n\n vec4 xyzw1 = vec4(xyzw.x, i, xyzw.zw);\n vec4 xyzw2 = vec4(xyzw.x, i + 1.0, xyzw.zw);\n \n vec4 a = sampleData(xyzw1);\n vec4 b = sampleData(xyzw2);\n\n return mix(a, b, g);\n}\n","subdivide.items":"uniform float subdivideBevel;\n\n// External\nvec4 sampleData(vec4 xyzw);\n\nvec4 subdivideItems(vec4 xyzw) {\n float x = xyzw.w;\n float i = floor(x);\n float f = x - i;\n\n float minf = subdivideBevel * min(f, 1.0 - f);\n float g = (f > 0.5) ? 1.0 - minf : (f < 0.5) ? minf : 0.5;\n\n return sampleData(vec4(xyzw.xyz, i + g));\n}\n","subdivide.items.lerp":"uniform float subdivideBevel;\n\n// External\nvec4 sampleData(vec4 xyzw);\n\nvec4 subdivideItemsLerp(vec4 xyzw) {\n float x = xyzw.w;\n float i = floor(x);\n float f = x - i;\n\n float minf = subdivideBevel * min(f, 1.0 - f);\n float g = (f > 0.5) ? 1.0 - minf : (f < 0.5) ? minf : 0.5;\n\n vec4 xyzw1 = vec4(xyzw.xyz, i);\n vec4 xyzw2 = vec4(xyzw.xyz, i + 1.0);\n \n vec4 a = sampleData(xyzw1);\n vec4 b = sampleData(xyzw2);\n\n return mix(a, b, g);\n}\n","subdivide.width":"uniform float subdivideBevel;\n\n// External\nvec4 sampleData(vec4 xyzw);\n\nvec4 subdivideWidth(vec4 xyzw) {\n float x = xyzw.x;\n float i = floor(x);\n float f = x - i;\n\n float minf = subdivideBevel * min(f, 1.0 - f);\n float g = (f > 0.5) ? 1.0 - minf : (f < 0.5) ? minf : 0.5;\n\n return sampleData(vec4(i + g, xyzw.yzw));\n}\n","subdivide.width.lerp":"uniform float subdivideBevel;\n\n// External\nvec4 sampleData(vec4 xyzw);\n\nvec4 subdivideWidthLerp(vec4 xyzw) {\n float x = xyzw.x;\n float i = floor(x);\n float f = x - i;\n\n float minf = subdivideBevel * min(f, 1.0 - f);\n float g = (f > 0.5) ? 1.0 - minf : (f < 0.5) ? minf : 0.5;\n\n vec4 xyzw1 = vec4(i, xyzw.yzw);\n vec4 xyzw2 = vec4(i + 1.0, xyzw.yzw);\n \n vec4 a = sampleData(xyzw1);\n vec4 b = sampleData(xyzw2);\n\n return mix(a, b, g);\n}\n","surface.mask.hollow":"attribute vec4 position4;\n\nfloat getSurfaceHollowMask(vec4 xyzw) {\n vec4 df = abs(fract(position4) - .5);\n vec2 df2 = min(df.xy, df.zw);\n float df3 = min(df2.x, df2.y);\n return df3;\n}","surface.position":"uniform vec4 geometryClip;\nuniform vec4 geometryResolution;\nuniform vec4 mapSize;\n\nattribute vec4 position4;\n\n// External\nvec3 getPosition(vec4 xyzw, float canonical);\n\nvec3 getSurfacePosition() {\n vec4 p = min(geometryClip, position4);\n vec3 xyz = getPosition(p, 1.0);\n\n // Overwrite UVs\n#ifdef POSITION_UV\n#ifdef POSITION_UV_INT\n vUV = -.5 + (position4.xy * geometryResolution.xy) * mapSize.xy;\n#else\n vUV = position4.xy * geometryResolution.xy;\n#endif\n#endif\n\n return xyz;\n}\n","surface.position.normal":"uniform vec4 mapSize;\nuniform vec4 geometryResolution;\nuniform vec4 geometryClip;\nattribute vec4 position4;\nattribute vec2 surface;\n\n// External\nvec3 getPosition(vec4 xyzw, float canonical);\n\nvoid getSurfaceGeometry(vec4 xyzw, float edgeX, float edgeY, out vec3 left, out vec3 center, out vec3 right, out vec3 up, out vec3 down) {\n vec4 deltaX = vec4(1.0, 0.0, 0.0, 0.0);\n vec4 deltaY = vec4(0.0, 1.0, 0.0, 0.0);\n\n /*\n // high quality, 5 tap\n center = getPosition(xyzw, 1.0);\n left = (edgeX > -0.5) ? getPosition(xyzw - deltaX, 0.0) : center;\n right = (edgeX < 0.5) ? getPosition(xyzw + deltaX, 0.0) : center;\n down = (edgeY > -0.5) ? getPosition(xyzw - deltaY, 0.0) : center;\n up = (edgeY < 0.5) ? getPosition(xyzw + deltaY, 0.0) : center;\n */\n \n // low quality, 3 tap\n center = getPosition(xyzw, 1.0);\n left = center;\n down = center;\n right = (edgeX < 0.5) ? getPosition(xyzw + deltaX, 0.0) : (2.0 * center - getPosition(xyzw - deltaX, 0.0));\n up = (edgeY < 0.5) ? getPosition(xyzw + deltaY, 0.0) : (2.0 * center - getPosition(xyzw - deltaY, 0.0));\n}\n\nvec3 getSurfaceNormal(vec3 left, vec3 center, vec3 right, vec3 up, vec3 down) {\n vec3 dx = right - left;\n vec3 dy = up - down;\n vec3 n = cross(dy, dx);\n if (length(n) > 0.0) {\n return normalize(n);\n }\n return vec3(0.0, 1.0, 0.0);\n}\n\nvarying vec3 vNormal;\nvarying vec3 vLight;\nvarying vec3 vPosition;\n\nvec3 getSurfacePositionNormal() {\n vec3 left, center, right, up, down;\n\n vec4 p = min(geometryClip, position4);\n\n getSurfaceGeometry(p, surface.x, surface.y, left, center, right, up, down);\n vNormal = getSurfaceNormal(left, center, right, up, down);\n vLight = normalize((viewMatrix * vec4(1.0, 2.0, 2.0, 0.0)).xyz); // hardcoded directional light\n vPosition = -center;\n\n#ifdef POSITION_UV\n#ifdef POSITION_UV_INT\n vUV = -.5 + (position4.xy * geometryResolution.xy) * mapSize.xy;\n#else\n vUV = position4.xy * geometryResolution.xy;\n#endif\n#endif\n \n return center;\n}\n","ticks.position":"uniform float worldUnit;\nuniform float focusDepth;\nuniform float tickSize;\nuniform float tickEpsilon;\nuniform vec3 tickNormal;\nuniform vec2 tickStrip;\n\nvec4 getSample(vec4 xyzw);\n\nvec3 transformPosition(vec4 position, in vec4 stpqIn, out vec4 stpqOut);\n\nvec3 getTickPosition(vec4 xyzw, in vec4 stpqIn, out vec4 stpqOut) {\n float epsilon = tickEpsilon;\n\n // determine tick direction\n float leftX = max(tickStrip.x, xyzw.y - 1.0);\n float rightX = min(tickStrip.y, xyzw.y + 1.0);\n \n vec4 left = getSample(vec4(leftX, xyzw.zw, 0.0));\n vec4 right = getSample(vec4(rightX, xyzw.zw, 0.0));\n vec4 diff = right - left;\n\n vec3 normal = cross(normalize(diff.xyz + vec3(diff.w)), tickNormal);\n float bias = max(0.0, 1.0 - length(normal) * 2.0);\n normal = mix(normal, tickNormal.yzx, bias * bias);\n \n // transform (point) and (point + delta)\n vec4 center = getSample(vec4(xyzw.yzw, 0.0));\n vec4 delta = vec4(normal, 0.0) * epsilon;\n\n vec4 a = center;\n vec4 b = center + delta;\n\n vec4 _;\n vec3 c = transformPosition(a, stpqIn, stpqOut);\n vec3 d = transformPosition(b, stpqIn, _);\n \n // sample on either side to create line\n float line = xyzw.x - .5;\n vec3 mid = c;\n vec3 side = normalize(d - c);\n\n return mid + side * line * tickSize * worldUnit * focusDepth;\n}\n","transform3.position":"uniform mat4 transformMatrix;\n\nvec4 transformPosition(vec4 position, inout vec4 stpq) {\n return transformMatrix * vec4(position.xyz, 1.0);\n}\n","transform4.position":"uniform mat4 transformMatrix;\nuniform vec4 transformOffset;\n\nvec4 transformPosition(vec4 position, inout vec4 stpq) {\n return transformMatrix * position + transformOffset;\n}\n","view.position":"// Implicit three.js uniform\n// uniform mat4 viewMatrix;\n\nvec4 getViewPosition(vec4 position, inout vec4 stpq) {\n return (viewMatrix * vec4(position.xyz, 1.0));\n}\n"}},{}],2:[function(t,e,n){function r(t){return function(e){return s(e,i(t))}}function i(t){for(var e in t)o(t,e)&&(t[e]=Function("return function(node, attr) { return node."+t[e]+" }"),t[e]=t[e]());return t}function o(t,e){return t.hasOwnProperty(e)&&"string"==typeof t[e]}function s(t,e){function n(t){var e;return"comma"===t.type?void y.unshift(g=[]):"op"===t.type||"any-child"===t.type?(g.unshift(v[t.data]),void g.unshift(i())):(g[0]=g[0]||i(),e=g[0],"!"===t.type?void(e.subject=y[0].subject=!0):void e.push("class"===t.type?o(t.type,t.data):"attr"===t.type?s(t):":"===t.type||"::"===t.type?m(t):"*"===t.type?Boolean:h(t.type,t.data)))}function r(t,e){function n(t){for(var e;t.length;)e=t.shift(),-1===u.indexOf(e)&&u.push(e)}var r,i,o,s,u;o=t,u=[];for(var h=0,l=y.length;l>h;++h){g=y[h],r=a,i=g.length,t=o,s=[];for(var c=0;i>c&&(t=r(t,g[c],s),t);c+=2)r=g[c+1];if(c>=i){if(e)return!0;n(g.subject?s:[o])}}return e?!1:u.length?1===u.length?u[0]:u:!1}function i(){function t(e,n){for(var r=0,i=t.bits.length;i>r;++r)if(!t.bits[r](e))return!1;return t.subject&&n.push(e),!0}return t.bits=[],t.subject=!1,t.push=function(e){t.bits.push(e)},t}function o(t,n){return function(r){var i=e[t](r);return i=Array.isArray(i)?i:i?i.toString().split(/\s+/):[],i.indexOf(n)>=0}}function s(t){return t.data.lhs?c(e.attr,t.data.lhs,t.data.cmp,t.data.rhs):c(e.attr,t.data)}function h(t,n){return function(r){return e[t](r)==n}}function l(t,n,r){do t=e.parent(t);while(t&&!n(t,r));return t}function p(t,n,r){return t=e.parent(t),t&&n(t,r)?t:null}function f(t,n,r){var i,o=e.parent(t),s=0;i=e.children(o);for(var a=0,u=i.length;u>a;++a)if(i[a]===t){s=a;break}return i[s-1]&&n(i[s-1],r)?i[s-1]:null}function d(t,n,r){var i,o=e.parent(t);i=e.children(o);for(var s=0,a=i.length;a>s;++s){if(i[s]===t)return null;if(n(i[s],r))return i[s]}return null}function m(t){return u(e,t.data)}var v,g,E=T(),y=[[]];return g=y[0],v={"":l,">":p,"+":f,"~":d},E.on("data",n).end(t),r}function a(t,e,n){return e(t,n)?t:null}function u(t,e){switch(e){case"empty":return d(t);case"first-child":return p(t);case"last-child":return f(t);case"root":return m(t)}return 0===e.indexOf("contains")?v(t,e.slice(9,-1)):0===e.indexOf("any")?l(t,e.slice(4,-1)):0===e.indexOf("not")?h(t,e.slice(4,-1)):function(){return!1}}function h(t,e){function n(t){return!r(t,!0)}var r=s(e,t);return n}function l(t,e){var n=s(e,t);return n}function c(t,e,n,r){return function(i){var o=t(i,e);return n?1===n.length?o==r:void 0===o||null===o?!1:x[n.charAt(0)](o,r):!!o}}function p(t){return function(e){return t.children(t.parent(e))[0]===e}}function f(t){return function(e){var n=t.children(t.parent(e));return n[n.length-1]===e}}function d(t){return function(e){return 0===t.children(e).length}}function m(t){return function(e){return!t.parent(e)}}function v(t,e){return function(n){return-1!==t.contents(n).indexOf(e)}}function g(t,e){return t.slice(t.length-e.length)===e}function E(t,e){return t.slice(0,e.length)===e}function y(t,e){return t.indexOf(e)>-1}function _(t,e){return t.split(/\s+/).indexOf(e)>-1}function b(t,e){return t.split("-").indexOf(e)>-1}e.exports=r;var T=t("./tokenizer"),x={$:g,"^":E,"*":y,"~":_,"|":b}},{"./tokenizer":4}],3:[function(t,e,n){(function(r){function i(t,e,n){function i(){for(;h.length&&!c.paused;){var t=h.shift();if(null===t)return c.emit("end");c.emit("data",t)}}function s(){c.writable=!1,e.call(c),!c.readable&&c.autoDestroy&&c.destroy()}t=t||function(t){this.queue(t)},e=e||function(){this.queue(null)};var a=!1,u=!1,h=[],l=!1,c=new o;return c.readable=c.writable=!0,c.paused=!1,c.autoDestroy=!(n&&n.autoDestroy===!1),c.write=function(e){return t.call(this,e),!c.paused},c.queue=c.push=function(t){return l?c:(null===t&&(l=!0),h.push(t),i(),c)},c.on("end",function(){c.readable=!1,!c.writable&&c.autoDestroy&&r.nextTick(function(){c.destroy()})}),c.end=function(t){return a?void 0:(a=!0,arguments.length&&c.write(t),s(),c)},c.destroy=function(){return u?void 0:(u=!0,a=!0,h.length=0,c.writable=c.readable=!1,c.emit("close"),c)},c.pause=function(){return c.paused?void 0:(c.paused=!0,c)},c.resume=function(){return c.paused&&(c.paused=!1,c.emit("resume")),i(),c.paused||c.emit("drain"),c},c}var o=t("stream");n=e.exports=i,i.through=i}).call(this,t("1YiZ5S"))},{"1YiZ5S":10,stream:12}],4:[function(t,e,n){function r(){function t(t){for(j=j.concat(t.split("")),L=j.length;L>G&&(B=j[G++]);)switch(I){case p:n();break;case a:w();break;case f:x();break;case s:M();break;case u:S();break;case h:k();break;case c:case l:R();break;case o:H();break;case _:case E:case d:A()}j=j.slice(G)}function e(e){arguments.length&&t(e),V.length&&P.queue(C())}function n(){switch(!0){case"#"===B:I=_;break;case"."===B:I=d;break;case":"===B:I=c;break;case"["===B:I=s;break;case"!"===B:r();break;case"*"===B:b();break;case","===B:T();break;case/[>\+~]/.test(B):I=f;break;case/\s/.test(B):I=a;break;case/[\w\d\-_]/.test(B):I=E,--G}}function r(){I=g,V=["!"],P.queue(C()),I=p}function b(){I=y,V=["*"],P.queue(C()),I=p}function T(){I=m,V=[","],P.queue(C()),I=p}function x(){return/[>\+~]/.test(B)?V.push(B):void(/\s/.test(B)||(P.queue(C()),I=p,--G))}function w(){if(!/\s/.test(B)){if(/[>\+~]/.test(B))return--G,I=f;P.queue(C()),I=p,--G}}function R(){if(F=I,A(!0),I===p){if("("===B)return D=V.join(""),I=o,V.length=0,O=1,void++G;I=c,P.queue(C()),I=p}}function H(){if(0!==V.length||z||!(z=/['"]/.test(B)?B:null)){if(z)return N||B!==z?"\\"===B?void(N?V.push(B):N=!0):(N=!1,void V.push(B)):void(z=null);V.push(B),"("===B?++O:")"===B&&--O,O||(V.pop(),P.queue({type:F,data:D+"("+V.join("")+")"}),I=p,D=F=U=null,V.length=0)}}function M(){if(A(!0),I===p){if("]"===B)return I=v,P.queue(C()),void(I=p);D=V.join(""),V.length=0,I=u}}function S(){return/[=~|$^*]/.test(B)&&V.push(B),2===V.length||"="===B?(U=V.join(""),V.length=0,I=h,void(z=null)):void 0}function k(){if(V.length||z||!(z=/['"]/.test(B)?B:null)){if(z)return N||B!==z?"\\"===B?(N&&V.push(B),void(N=!N)):(N=!1,void V.push(B)):void(z=null);A(!0),I===p&&(P.queue({type:v,data:{lhs:D,rhs:V.join(""),cmp:U}}),I=p,D=F=U=null,V.length=0)}}function A(t){return/[^\d\w\-_]/.test(B)&&!N?void("\\"===B?N=!0:(!t&&P.queue(C()),I=p,--G)):(N=!1,void V.push(B))}function C(){var t=V.join("");return V.length=0,{type:I,data:t}}var P,L,z,O,D,F,U,B,N=!1,V=[],I=p,j=[],G=0;return P=i(t,e)}e.exports=r;var i=t("through"),o="pseudo-start",s="attr-start",a="any-child",u="attr-comp",h="attr-end",l="::",c=":",p="(ready)",f="op",d="class",m="comma",v="attr",g="!",E="tag",y="*",_="id"},{through:3}],5:[function(t,e,n){function r(t,e,n){if(!(this instanceof r))return new r(t,e,n);var i=typeof t;if("base64"===e&&"string"===i)for(t=S(t);t.length%4!==0;)t+="=";var o;if("number"===i)o=A(t);else if("string"===i)o=r.byteLength(t,e);else{if("object"!==i)throw new Error("First argument needs to be a number, array or string.");o=A(t.length)}var s;r._useTypedArrays?s=r._augment(new Uint8Array(o)):(s=this,s.length=o,s._isBuffer=!0);var a;if(r._useTypedArrays&&"number"==typeof t.byteLength)s._set(t);else if(P(t))for(a=0;o>a;a++)r.isBuffer(t)?s[a]=t.readUInt8(a):s[a]=t[a];else if("string"===i)s.write(t,0,e);else if("number"===i&&!r._useTypedArrays&&!n)for(a=0;o>a;a++)s[a]=0;return s}function i(t,e,n,i){n=Number(n)||0;var o=t.length-n;i?(i=Number(i),i>o&&(i=o)):i=o;var s=e.length;j(s%2===0,"Invalid hex string"),i>s/2&&(i=s/2);for(var a=0;i>a;a++){var u=parseInt(e.substr(2*a,2),16);j(!isNaN(u),"Invalid hex string"),t[n+a]=u}return r._charsWritten=2*a,a}function o(t,e,n,i){var o=r._charsWritten=U(z(e),t,n,i);return o}function s(t,e,n,i){var o=r._charsWritten=U(O(e),t,n,i);return o}function a(t,e,n,r){return s(t,e,n,r)}function u(t,e,n,i){var o=r._charsWritten=U(F(e),t,n,i);return o}function h(t,e,n,i){var o=r._charsWritten=U(D(e),t,n,i);return o}function l(t,e,n){return 0===e&&n===t.length?G.fromByteArray(t):G.fromByteArray(t.slice(e,n))}function c(t,e,n){var r="",i="";n=Math.min(t.length,n);for(var o=e;n>o;o++)t[o]<=127?(r+=B(i)+String.fromCharCode(t[o]),i=""):i+="%"+t[o].toString(16);return r+B(i)}function p(t,e,n){var r="";n=Math.min(t.length,n);for(var i=e;n>i;i++)r+=String.fromCharCode(t[i]);return r}function f(t,e,n){return p(t,e,n)}function d(t,e,n){var r=t.length;(!e||0>e)&&(e=0),(!n||0>n||n>r)&&(n=r);for(var i="",o=e;n>o;o++)i+=L(t[o]);return i}function m(t,e,n){for(var r=t.slice(e,n),i="",o=0;o=i)){var o;return n?(o=t[e],i>e+1&&(o|=t[e+1]<<8)):(o=t[e]<<8,i>e+1&&(o|=t[e+1])),o}}function g(t,e,n,r){r||(j("boolean"==typeof n,"missing or invalid endian"),j(void 0!==e&&null!==e,"missing offset"),j(e+3=i)){var o;return n?(i>e+2&&(o=t[e+2]<<16),i>e+1&&(o|=t[e+1]<<8),o|=t[e],i>e+3&&(o+=t[e+3]<<24>>>0)):(i>e+1&&(o=t[e+1]<<16),i>e+2&&(o|=t[e+2]<<8),i>e+3&&(o|=t[e+3]),o+=t[e]<<24>>>0),o}}function E(t,e,n,r){r||(j("boolean"==typeof n,"missing or invalid endian"),j(void 0!==e&&null!==e,"missing offset"),j(e+1=i)){var o=v(t,e,n,!0),s=32768&o;return s?-1*(65535-o+1):o}}function y(t,e,n,r){r||(j("boolean"==typeof n,"missing or invalid endian"),j(void 0!==e&&null!==e,"missing offset"),j(e+3=i)){var o=g(t,e,n,!0),s=2147483648&o;return s?-1*(4294967295-o+1):o}}function _(t,e,n,r){return r||(j("boolean"==typeof n,"missing or invalid endian"),j(e+3=o))for(var s=0,a=Math.min(o-n,2);a>s;s++)t[n+s]=(e&255<<8*(r?s:1-s))>>>8*(r?s:1-s)}function x(t,e,n,r,i){i||(j(void 0!==e&&null!==e,"missing value"),j("boolean"==typeof r,"missing or invalid endian"),j(void 0!==n&&null!==n,"missing offset"),j(n+3=o))for(var s=0,a=Math.min(o-n,4);a>s;s++)t[n+s]=e>>>8*(r?s:3-s)&255}function w(t,e,n,r,i){i||(j(void 0!==e&&null!==e,"missing value"),j("boolean"==typeof r,"missing or invalid endian"),j(void 0!==n&&null!==n,"missing offset"),j(n+1=o||(e>=0?T(t,e,n,r,i):T(t,65535+e+1,n,r,i))}function R(t,e,n,r,i){i||(j(void 0!==e&&null!==e,"missing value"),j("boolean"==typeof r,"missing or invalid endian"),j(void 0!==n&&null!==n,"missing offset"),j(n+3=o||(e>=0?x(t,e,n,r,i):x(t,4294967295+e+1,n,r,i))}function H(t,e,n,r,i){i||(j(void 0!==e&&null!==e,"missing value"),j("boolean"==typeof r,"missing or invalid endian"),j(void 0!==n&&null!==n,"missing offset"),j(n+3=o||W.write(t,e,n,r,23,4)}function M(t,e,n,r,i){i||(j(void 0!==e&&null!==e,"missing value"),j("boolean"==typeof r,"missing or invalid endian"),j(void 0!==n&&null!==n,"missing offset"),j(n+7=o||W.write(t,e,n,r,52,8)}function S(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}function k(t,e,n){return"number"!=typeof t?n:(t=~~t,t>=e?e:t>=0?t:(t+=e,t>=0?t:0))}function A(t){return t=~~Math.ceil(+t),0>t?0:t}function C(t){return(Array.isArray||function(t){return"[object Array]"===Object.prototype.toString.call(t)})(t)}function P(t){return C(t)||r.isBuffer(t)||t&&"object"==typeof t&&"number"==typeof t.length}function L(t){return 16>t?"0"+t.toString(16):t.toString(16)}function z(t){for(var e=[],n=0;n=r)e.push(t.charCodeAt(n));else{var i=n;r>=55296&&57343>=r&&n++;for(var o=encodeURIComponent(t.slice(i,n+1)).substr(1).split("%"),s=0;s>8,r=e%256,i.push(r),i.push(n);return i}function F(t){return G.toByteArray(t)}function U(t,e,n,r){for(var i=0;r>i&&!(i+n>=e.length||i>=t.length);i++)e[i+n]=t[i];return i}function B(t){try{return decodeURIComponent(t)}catch(e){return String.fromCharCode(65533)}}function N(t,e){j("number"==typeof t,"cannot write a non-number as a number"),j(t>=0,"specified a negative value for writing an unsigned value"),j(e>=t,"value is larger than maximum value for type"),j(Math.floor(t)===t,"value has a fractional component")}function V(t,e,n){j("number"==typeof t,"cannot write a non-number as a number"),j(e>=t,"value larger than maximum allowed value"),j(t>=n,"value smaller than minimum allowed value"),j(Math.floor(t)===t,"value has a fractional component")}function I(t,e,n){j("number"==typeof t,"cannot write a non-number as a number"),j(e>=t,"value larger than maximum allowed value"),j(t>=n,"value smaller than minimum allowed value")}function j(t,e){if(!t)throw new Error(e||"Failed assertion")}var G=t("base64-js"),W=t("ieee754");n.Buffer=r,n.SlowBuffer=r,n.INSPECT_MAX_BYTES=50,r.poolSize=8192,r._useTypedArrays=function(){try{var t=new ArrayBuffer(0),e=new Uint8Array(t);return e.foo=function(){return 42},42===e.foo()&&"function"==typeof e.subarray}catch(n){return!1}}(),r.isEncoding=function(t){switch(String(t).toLowerCase()){case"hex":case"utf8":case"utf-8":case"ascii":case"binary":case"base64":case"raw":case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return!0;default:return!1}},r.isBuffer=function(t){return!(null===t||void 0===t||!t._isBuffer)},r.byteLength=function(t,e){var n;switch(t+="",e||"utf8"){case"hex":n=t.length/2;break;case"utf8":case"utf-8":n=z(t).length;break;case"ascii":case"binary":case"raw":n=t.length;break;case"base64":n=F(t).length;break;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":n=2*t.length;break;default:throw new Error("Unknown encoding")}return n},r.concat=function(t,e){if(j(C(t),"Usage: Buffer.concat(list, [totalLength])\nlist should be an Array."),0===t.length)return new r(0);if(1===t.length)return t[0];var n;if("number"!=typeof e)for(e=0,n=0;nc&&(n=c)):n=c,r=String(r||"utf8").toLowerCase();var p;switch(r){case"hex":p=i(this,t,e,n);break;case"utf8":case"utf-8":p=o(this,t,e,n);break;case"ascii":p=s(this,t,e,n);break;case"binary":p=a(this,t,e,n);break;case"base64":p=u(this,t,e,n);break;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":p=h(this,t,e,n);break;default:throw new Error("Unknown encoding")}return p},r.prototype.toString=function(t,e,n){var r=this;if(t=String(t||"utf8").toLowerCase(),e=Number(e)||0,n=void 0!==n?Number(n):n=r.length,n===e)return"";var i;switch(t){case"hex":i=d(r,e,n);break;case"utf8":case"utf-8":i=c(r,e,n);break;case"ascii":i=p(r,e,n);break;case"binary":i=f(r,e,n);break;case"base64":i=l(r,e,n);break;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":i=m(r,e,n);break;default:throw new Error("Unknown encoding")}return i},r.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}},r.prototype.copy=function(t,e,n,i){var o=this;if(n||(n=0),i||0===i||(i=this.length),e||(e=0),i!==n&&0!==t.length&&0!==o.length){j(i>=n,"sourceEnd < sourceStart"),j(e>=0&&e=0&&n=0&&i<=o.length,"sourceEnd out of bounds"),i>this.length&&(i=this.length),t.length-es||!r._useTypedArrays)for(var a=0;s>a;a++)t[a+e]=this[a+n];else t._set(this.subarray(n,n+s),e)}},r.prototype.slice=function(t,e){var n=this.length;if(t=k(t,n,0),e=k(e,n,n),r._useTypedArrays)return r._augment(this.subarray(t,e));for(var i=e-t,o=new r(i,void 0,!0),s=0;i>s;s++)o[s]=this[s+t];return o},r.prototype.get=function(t){return console.log(".get() is deprecated. Access using array indexes instead."),this.readUInt8(t)},r.prototype.set=function(t,e){return console.log(".set() is deprecated. Access using array indexes instead."),this.writeUInt8(t,e)},r.prototype.readUInt8=function(t,e){return e||(j(void 0!==t&&null!==t,"missing offset"),j(t=this.length?void 0:this[t]},r.prototype.readUInt16LE=function(t,e){return v(this,t,!0,e)},r.prototype.readUInt16BE=function(t,e){return v(this,t,!1,e)},r.prototype.readUInt32LE=function(t,e){return g(this,t,!0,e)},r.prototype.readUInt32BE=function(t,e){return g(this,t,!1,e)},r.prototype.readInt8=function(t,e){if(e||(j(void 0!==t&&null!==t,"missing offset"),j(t=this.length)){var n=128&this[t];return n?-1*(255-this[t]+1):this[t]}},r.prototype.readInt16LE=function(t,e){return E(this,t,!0,e)},r.prototype.readInt16BE=function(t,e){return E(this,t,!1,e)},r.prototype.readInt32LE=function(t,e){return y(this,t,!0,e)},r.prototype.readInt32BE=function(t,e){return y(this,t,!1,e)},r.prototype.readFloatLE=function(t,e){return _(this,t,!0,e)},r.prototype.readFloatBE=function(t,e){return _(this,t,!1,e)},r.prototype.readDoubleLE=function(t,e){return b(this,t,!0,e)},r.prototype.readDoubleBE=function(t,e){return b(this,t,!1,e)},r.prototype.writeUInt8=function(t,e,n){n||(j(void 0!==t&&null!==t,"missing value"),j(void 0!==e&&null!==e,"missing offset"),j(e=this.length||(this[e]=t)},r.prototype.writeUInt16LE=function(t,e,n){T(this,t,e,!0,n)},r.prototype.writeUInt16BE=function(t,e,n){T(this,t,e,!1,n)},r.prototype.writeUInt32LE=function(t,e,n){x(this,t,e,!0,n)},r.prototype.writeUInt32BE=function(t,e,n){x(this,t,e,!1,n)},r.prototype.writeInt8=function(t,e,n){n||(j(void 0!==t&&null!==t,"missing value"),j(void 0!==e&&null!==e,"missing offset"),j(e=this.length||(t>=0?this.writeUInt8(t,e,n):this.writeUInt8(255+t+1,e,n))},r.prototype.writeInt16LE=function(t,e,n){w(this,t,e,!0,n)},r.prototype.writeInt16BE=function(t,e,n){ -w(this,t,e,!1,n)},r.prototype.writeInt32LE=function(t,e,n){R(this,t,e,!0,n)},r.prototype.writeInt32BE=function(t,e,n){R(this,t,e,!1,n)},r.prototype.writeFloatLE=function(t,e,n){H(this,t,e,!0,n)},r.prototype.writeFloatBE=function(t,e,n){H(this,t,e,!1,n)},r.prototype.writeDoubleLE=function(t,e,n){M(this,t,e,!0,n)},r.prototype.writeDoubleBE=function(t,e,n){M(this,t,e,!1,n)},r.prototype.fill=function(t,e,n){if(t||(t=0),e||(e=0),n||(n=this.length),"string"==typeof t&&(t=t.charCodeAt(0)),j("number"==typeof t&&!isNaN(t),"value is not a number"),j(n>=e,"end < start"),n!==e&&0!==this.length){j(e>=0&&e=0&&n<=this.length,"end out of bounds");for(var r=e;n>r;r++)this[r]=t}},r.prototype.inspect=function(){for(var t=[],e=this.length,r=0;e>r;r++)if(t[r]=L(this[r]),r===n.INSPECT_MAX_BYTES){t[r+1]="...";break}return""},r.prototype.toArrayBuffer=function(){if("undefined"!=typeof Uint8Array){if(r._useTypedArrays)return new r(this).buffer;for(var t=new Uint8Array(this.length),e=0,n=t.length;n>e;e+=1)t[e]=this[e];return t.buffer}throw new Error("Buffer.toArrayBuffer not supported in this browser")};var q=r.prototype;r._augment=function(t){return t._isBuffer=!0,t._get=t.get,t._set=t.set,t.get=q.get,t.set=q.set,t.write=q.write,t.toString=q.toString,t.toLocaleString=q.toString,t.toJSON=q.toJSON,t.copy=q.copy,t.slice=q.slice,t.readUInt8=q.readUInt8,t.readUInt16LE=q.readUInt16LE,t.readUInt16BE=q.readUInt16BE,t.readUInt32LE=q.readUInt32LE,t.readUInt32BE=q.readUInt32BE,t.readInt8=q.readInt8,t.readInt16LE=q.readInt16LE,t.readInt16BE=q.readInt16BE,t.readInt32LE=q.readInt32LE,t.readInt32BE=q.readInt32BE,t.readFloatLE=q.readFloatLE,t.readFloatBE=q.readFloatBE,t.readDoubleLE=q.readDoubleLE,t.readDoubleBE=q.readDoubleBE,t.writeUInt8=q.writeUInt8,t.writeUInt16LE=q.writeUInt16LE,t.writeUInt16BE=q.writeUInt16BE,t.writeUInt32LE=q.writeUInt32LE,t.writeUInt32BE=q.writeUInt32BE,t.writeInt8=q.writeInt8,t.writeInt16LE=q.writeInt16LE,t.writeInt16BE=q.writeInt16BE,t.writeInt32LE=q.writeInt32LE,t.writeInt32BE=q.writeInt32BE,t.writeFloatLE=q.writeFloatLE,t.writeFloatBE=q.writeFloatBE,t.writeDoubleLE=q.writeDoubleLE,t.writeDoubleBE=q.writeDoubleBE,t.fill=q.fill,t.inspect=q.inspect,t.toArrayBuffer=q.toArrayBuffer,t}},{"base64-js":6,ieee754:7}],6:[function(t,e,n){var r="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";!function(t){"use strict";function e(t){var e=t.charCodeAt(0);return e===s||e===c?62:e===a||e===p?63:u>e?-1:u+10>e?e-u+26+26:l+26>e?e-l:h+26>e?e-h+26:void 0}function n(t){function n(t){h[c++]=t}var r,i,s,a,u,h;if(t.length%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var l=t.length;u="="===t.charAt(l-2)?2:"="===t.charAt(l-1)?1:0,h=new o(3*t.length/4-u),s=u>0?t.length-4:t.length;var c=0;for(r=0,i=0;s>r;r+=4,i+=3)a=e(t.charAt(r))<<18|e(t.charAt(r+1))<<12|e(t.charAt(r+2))<<6|e(t.charAt(r+3)),n((16711680&a)>>16),n((65280&a)>>8),n(255&a);return 2===u?(a=e(t.charAt(r))<<2|e(t.charAt(r+1))>>4,n(255&a)):1===u&&(a=e(t.charAt(r))<<10|e(t.charAt(r+1))<<4|e(t.charAt(r+2))>>2,n(a>>8&255),n(255&a)),h}function i(t){function e(t){return r.charAt(t)}function n(t){return e(t>>18&63)+e(t>>12&63)+e(t>>6&63)+e(63&t)}var i,o,s,a=t.length%3,u="";for(i=0,s=t.length-a;s>i;i+=3)o=(t[i]<<16)+(t[i+1]<<8)+t[i+2],u+=n(o);switch(a){case 1:o=t[t.length-1],u+=e(o>>2),u+=e(o<<4&63),u+="==";break;case 2:o=(t[t.length-2]<<8)+t[t.length-1],u+=e(o>>10),u+=e(o>>4&63),u+=e(o<<2&63),u+="="}return u}var o="undefined"!=typeof Uint8Array?Uint8Array:Array,s="+".charCodeAt(0),a="/".charCodeAt(0),u="0".charCodeAt(0),h="a".charCodeAt(0),l="A".charCodeAt(0),c="-".charCodeAt(0),p="_".charCodeAt(0);t.toByteArray=n,t.fromByteArray=i}("undefined"==typeof n?this.base64js={}:n)},{}],7:[function(t,e,n){n.read=function(t,e,n,r,i){var o,s,a=8*i-r-1,u=(1<>1,l=-7,c=n?i-1:0,p=n?-1:1,f=t[e+c];for(c+=p,o=f&(1<<-l)-1,f>>=-l,l+=a;l>0;o=256*o+t[e+c],c+=p,l-=8);for(s=o&(1<<-l)-1,o>>=-l,l+=r;l>0;s=256*s+t[e+c],c+=p,l-=8);if(0===o)o=1-h;else{if(o===u)return s?NaN:(f?-1:1)*(1/0);s+=Math.pow(2,r),o-=h}return(f?-1:1)*s*Math.pow(2,o-r)},n.write=function(t,e,n,r,i,o){var s,a,u,h=8*o-i-1,l=(1<>1,p=23===i?Math.pow(2,-24)-Math.pow(2,-77):0,f=r?0:o-1,d=r?1:-1,m=0>e||0===e&&0>1/e?1:0;for(e=Math.abs(e),isNaN(e)||e===1/0?(a=isNaN(e)?1:0,s=l):(s=Math.floor(Math.log(e)/Math.LN2),e*(u=Math.pow(2,-s))<1&&(s--,u*=2),e+=s+c>=1?p/u:p*Math.pow(2,1-c),e*u>=2&&(s++,u/=2),s+c>=l?(a=0,s=l):s+c>=1?(a=(e*u-1)*Math.pow(2,i),s+=c):(a=e*Math.pow(2,c-1)*Math.pow(2,i),s=0));i>=8;t[n+f]=255&a,f+=d,a/=256,i-=8);for(s=s<0;t[n+f]=255&s,f+=d,s/=256,h-=8);t[n+f-d]|=128*m}},{}],8:[function(t,e,n){function r(){this._events=this._events||{},this._maxListeners=this._maxListeners||void 0}function i(t){return"function"==typeof t}function o(t){return"number"==typeof t}function s(t){return"object"==typeof t&&null!==t}function a(t){return void 0===t}e.exports=r,r.EventEmitter=r,r.prototype._events=void 0,r.prototype._maxListeners=void 0,r.defaultMaxListeners=10,r.prototype.setMaxListeners=function(t){if(!o(t)||0>t||isNaN(t))throw TypeError("n must be a positive number");return this._maxListeners=t,this},r.prototype.emit=function(t){var e,n,r,o,u,h;if(this._events||(this._events={}),"error"===t&&(!this._events.error||s(this._events.error)&&!this._events.error.length)){if(e=arguments[1],e instanceof Error)throw e;throw TypeError('Uncaught, unspecified "error" event.')}if(n=this._events[t],a(n))return!1;if(i(n))switch(arguments.length){case 1:n.call(this);break;case 2:n.call(this,arguments[1]);break;case 3:n.call(this,arguments[1],arguments[2]);break;default:for(r=arguments.length,o=new Array(r-1),u=1;r>u;u++)o[u-1]=arguments[u];n.apply(this,o)}else if(s(n)){for(r=arguments.length,o=new Array(r-1),u=1;r>u;u++)o[u-1]=arguments[u];for(h=n.slice(),r=h.length,u=0;r>u;u++)h[u].apply(this,o)}return!0},r.prototype.addListener=function(t,e){var n;if(!i(e))throw TypeError("listener must be a function");if(this._events||(this._events={}),this._events.newListener&&this.emit("newListener",t,i(e.listener)?e.listener:e),this._events[t]?s(this._events[t])?this._events[t].push(e):this._events[t]=[this._events[t],e]:this._events[t]=e,s(this._events[t])&&!this._events[t].warned){var n;n=a(this._maxListeners)?r.defaultMaxListeners:this._maxListeners,n&&n>0&&this._events[t].length>n&&(this._events[t].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[t].length),"function"==typeof console.trace&&console.trace())}return this},r.prototype.on=r.prototype.addListener,r.prototype.once=function(t,e){function n(){this.removeListener(t,n),r||(r=!0,e.apply(this,arguments))}if(!i(e))throw TypeError("listener must be a function");var r=!1;return n.listener=e,this.on(t,n),this},r.prototype.removeListener=function(t,e){var n,r,o,a;if(!i(e))throw TypeError("listener must be a function");if(!this._events||!this._events[t])return this;if(n=this._events[t],o=n.length,r=-1,n===e||i(n.listener)&&n.listener===e)delete this._events[t],this._events.removeListener&&this.emit("removeListener",t,e);else if(s(n)){for(a=o;a-->0;)if(n[a]===e||n[a].listener&&n[a].listener===e){r=a;break}if(0>r)return this;1===n.length?(n.length=0,delete this._events[t]):n.splice(r,1),this._events.removeListener&&this.emit("removeListener",t,e)}return this},r.prototype.removeAllListeners=function(t){var e,n;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[t]&&delete this._events[t],this;if(0===arguments.length){for(e in this._events)"removeListener"!==e&&this.removeAllListeners(e);return this.removeAllListeners("removeListener"),this._events={},this}if(n=this._events[t],i(n))this.removeListener(t,n);else for(;n.length;)this.removeListener(t,n[n.length-1]);return delete this._events[t],this},r.prototype.listeners=function(t){var e;return e=this._events&&this._events[t]?i(this._events[t])?[this._events[t]]:this._events[t].slice():[]},r.listenerCount=function(t,e){var n;return n=t._events&&t._events[e]?i(t._events[e])?1:t._events[e].length:0}},{}],9:[function(t,e,n){"function"==typeof Object.create?e.exports=function(t,e){t.super_=e,t.prototype=Object.create(e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(t,e){t.super_=e;var n=function(){};n.prototype=e.prototype,t.prototype=new n,t.prototype.constructor=t}},{}],10:[function(t,e,n){function r(){}var i=e.exports={};i.nextTick=function(){var t="undefined"!=typeof window&&window.setImmediate,e="undefined"!=typeof window&&window.postMessage&&window.addEventListener;if(t)return function(t){return window.setImmediate(t)};if(e){var n=[];return window.addEventListener("message",function(t){var e=t.source;if((e===window||null===e)&&"process-tick"===t.data&&(t.stopPropagation(),n.length>0)){var r=n.shift();r()}},!0),function(t){n.push(t),window.postMessage("process-tick","*")}}return function(t){setTimeout(t,0)}}(),i.title="browser",i.browser=!0,i.env={},i.argv=[],i.on=r,i.addListener=r,i.once=r,i.off=r,i.removeListener=r,i.removeAllListeners=r,i.emit=r,i.binding=function(t){throw new Error("process.binding is not supported")},i.cwd=function(){return"/"},i.chdir=function(t){throw new Error("process.chdir is not supported")}},{}],11:[function(t,e,n){function r(t){return this instanceof r?(a.call(this,t),u.call(this,t),t&&t.readable===!1&&(this.readable=!1),t&&t.writable===!1&&(this.writable=!1),this.allowHalfOpen=!0,t&&t.allowHalfOpen===!1&&(this.allowHalfOpen=!1),void this.once("end",i)):new r(t)}function i(){if(!this.allowHalfOpen&&!this._writableState.ended){var t=this;s(function(){t.end()})}}e.exports=r;var o=t("inherits"),s=t("process/browser.js").nextTick,a=t("./readable.js"),u=t("./writable.js");o(r,a),r.prototype.write=u.prototype.write,r.prototype.end=u.prototype.end,r.prototype._write=u.prototype._write},{"./readable.js":15,"./writable.js":17,inherits:9,"process/browser.js":13}],12:[function(t,e,n){function r(){i.call(this)}e.exports=r;var i=t("events").EventEmitter,o=t("inherits");o(r,i),r.Readable=t("./readable.js"),r.Writable=t("./writable.js"),r.Duplex=t("./duplex.js"),r.Transform=t("./transform.js"),r.PassThrough=t("./passthrough.js"),r.Stream=r,r.prototype.pipe=function(t,e){function n(e){t.writable&&!1===t.write(e)&&h.pause&&h.pause()}function r(){h.readable&&h.resume&&h.resume()}function o(){l||(l=!0,t.end())}function s(){l||(l=!0,"function"==typeof t.destroy&&t.destroy())}function a(t){if(u(),0===i.listenerCount(this,"error"))throw t}function u(){h.removeListener("data",n),t.removeListener("drain",r),h.removeListener("end",o),h.removeListener("close",s),h.removeListener("error",a),t.removeListener("error",a),h.removeListener("end",u),h.removeListener("close",u),t.removeListener("close",u)}var h=this;h.on("data",n),t.on("drain",r),t._isStdio||e&&e.end===!1||(h.on("end",o),h.on("close",s));var l=!1;return h.on("error",a),t.on("error",a),h.on("end",u),h.on("close",u),t.on("close",u),t.emit("pipe",h),t}},{"./duplex.js":11,"./passthrough.js":14,"./readable.js":15,"./transform.js":16,"./writable.js":17,events:8,inherits:9}],13:[function(t,e,n){var r=e.exports={};r.nextTick=function(){var t="undefined"!=typeof window&&window.setImmediate,e="undefined"!=typeof window&&window.postMessage&&window.addEventListener;if(t)return function(t){return window.setImmediate(t)};if(e){var n=[];return window.addEventListener("message",function(t){var e=t.source;if((e===window||null===e)&&"process-tick"===t.data&&(t.stopPropagation(),n.length>0)){var r=n.shift();r()}},!0),function(t){n.push(t),window.postMessage("process-tick","*")}}return function(t){setTimeout(t,0)}}(),r.title="browser",r.browser=!0,r.env={},r.argv=[],r.binding=function(t){throw new Error("process.binding is not supported")},r.cwd=function(){return"/"},r.chdir=function(t){throw new Error("process.chdir is not supported")}},{}],14:[function(t,e,n){function r(t){return this instanceof r?void i.call(this,t):new r(t)}e.exports=r;var i=t("./transform.js"),o=t("inherits");o(r,i),r.prototype._transform=function(t,e,n){n(null,t)}},{"./transform.js":16,inherits:9}],15:[function(t,e,n){(function(n){function r(e,n){e=e||{};var r=e.highWaterMark;this.highWaterMark=r||0===r?r:16384,this.highWaterMark=~~this.highWaterMark,this.buffer=[],this.length=0,this.pipes=null,this.pipesCount=0,this.flowing=!1,this.ended=!1,this.endEmitted=!1,this.reading=!1,this.calledRead=!1,this.sync=!0,this.needReadable=!1,this.emittedReadable=!1,this.readableListening=!1,this.objectMode=!!e.objectMode,this.defaultEncoding=e.defaultEncoding||"utf8",this.ranOut=!1,this.awaitDrain=0,this.readingMore=!1,this.decoder=null,this.encoding=null,e.encoding&&(x||(x=t("string_decoder").StringDecoder),this.decoder=new x(e.encoding),this.encoding=e.encoding)}function i(t){return this instanceof i?(this._readableState=new r(t,this),this.readable=!0,void R.call(this)):new i(t)}function o(t,e,n,r,i){var o=h(e,n);if(o)t.emit("error",o);else if(null===n||void 0===n)e.reading=!1,e.ended||l(t,e);else if(e.objectMode||n&&n.length>0)if(e.ended&&!i){var a=new Error("stream.push() after EOF");t.emit("error",a)}else if(e.endEmitted&&i){var a=new Error("stream.unshift() after end event");t.emit("error",a)}else!e.decoder||i||r||(n=e.decoder.write(n)),e.length+=e.objectMode?1:n.length,i?e.buffer.unshift(n):(e.reading=!1,e.buffer.push(n)),e.needReadable&&c(t),f(t,e);else i||(e.reading=!1);return s(e)}function s(t){return!t.ended&&(t.needReadable||t.length=k)t=k;else{t--;for(var e=1;32>e;e<<=1)t|=t>>e;t++}return t}function u(t,e){return 0===e.length&&e.ended?0:e.objectMode?0===t?0:1:isNaN(t)||null===t?e.flowing&&e.buffer.length?e.buffer[0].length:e.length:0>=t?0:(t>e.highWaterMark&&(e.highWaterMark=a(t)),t>e.length?e.ended?e.length:(e.needReadable=!0,0):t)}function h(t,e){var n=null;return H.isBuffer(e)||"string"==typeof e||null===e||void 0===e||t.objectMode||n||(n=new TypeError("Invalid non-string/buffer chunk")),n}function l(t,e){if(e.decoder&&!e.ended){var n=e.decoder.end();n&&n.length&&(e.buffer.push(n),e.length+=e.objectMode?1:n.length)}e.ended=!0,e.length>0?c(t):_(t)}function c(t){var e=t._readableState;e.needReadable=!1,e.emittedReadable||(e.emittedReadable=!0,e.sync?M(function(){p(t)}):p(t))}function p(t){t.emit("readable")}function f(t,e){e.readingMore||(e.readingMore=!0,M(function(){d(t,e)}))}function d(t,e){for(var n=e.length;!e.reading&&!e.flowing&&!e.ended&&e.length0)return;return 0===r.pipesCount?(r.flowing=!1,void(w.listenerCount(t,"data")>0&&E(t))):void(r.ranOut=!0)}function g(){this._readableState.ranOut&&(this._readableState.ranOut=!1,v(this))}function E(t,e){var n=t._readableState;if(n.flowing)throw new Error("Cannot switch to old mode now.");var r=e||!1,i=!1;t.readable=!0,t.pipe=R.prototype.pipe,t.on=t.addListener=R.prototype.on,t.on("readable",function(){i=!0;for(var e;!r&&null!==(e=t.read());)t.emit("data",e);null===e&&(i=!1,t._readableState.needReadable=!0)}),t.pause=function(){r=!0,this.emit("pause")},t.resume=function(){r=!1,i?M(function(){t.emit("readable")}):this.read(0),this.emit("resume")},t.emit("readable")}function y(t,e){var n,r=e.buffer,i=e.length,o=!!e.decoder,s=!!e.objectMode;if(0===r.length)return null;if(0===i)n=null;else if(s)n=r.shift();else if(!t||t>=i)n=o?r.join(""):H.concat(r,i),r.length=0;else if(th&&t>u;h++){var a=r[0],c=Math.min(t-u,a.length);o?n+=a.slice(0,c):a.copy(n,u,0,c),c0)throw new Error("endReadable called on non-empty stream");!e.endEmitted&&e.calledRead&&(e.ended=!0,M(function(){e.endEmitted||0!==e.length||(e.endEmitted=!0,t.readable=!1,t.emit("end"))}))}function b(t,e){for(var n=0,r=t.length;r>n;n++)e(t[n],n)}function T(t,e){for(var n=0,r=t.length;r>n;n++)if(t[n]===e)return n;return-1}e.exports=i,i.ReadableState=r;var x,w=t("events").EventEmitter,R=t("./index.js"),H=t("buffer").Buffer,M=t("process/browser.js").nextTick,S=t("inherits");S(i,R),i.prototype.push=function(t,e){var n=this._readableState;return"string"!=typeof t||n.objectMode||(e=e||n.defaultEncoding,e!==n.encoding&&(t=new H(t,e),e="")),o(this,n,t,e,!1)},i.prototype.unshift=function(t){var e=this._readableState;return o(this,e,t,"",!0)},i.prototype.setEncoding=function(e){x||(x=t("string_decoder").StringDecoder),this._readableState.decoder=new x(e),this._readableState.encoding=e};var k=8388608;i.prototype.read=function(t){var e=this._readableState;e.calledRead=!0;var n=t;if(("number"!=typeof t||t>0)&&(e.emittedReadable=!1),0===t&&e.needReadable&&(e.length>=e.highWaterMark||e.ended))return c(this),null;if(t=u(t,e),0===t&&e.ended)return 0===e.length&&_(this),null;var r=e.needReadable;e.length-t<=e.highWaterMark&&(r=!0),(e.ended||e.reading)&&(r=!1),r&&(e.reading=!0,e.sync=!0,0===e.length&&(e.needReadable=!0),this._read(e.highWaterMark),e.sync=!1),r&&!e.reading&&(t=u(n,e));var i;return i=t>0?y(t,e):null,null===i&&(e.needReadable=!0,t=0),e.length-=t,0!==e.length||e.ended||(e.needReadable=!0),e.ended&&!e.endEmitted&&0===e.length&&_(this),i},i.prototype._read=function(t){this.emit("error",new Error("not implemented"))},i.prototype.pipe=function(t,e){function r(t){t===l&&o()}function i(){t.end()}function o(){t.removeListener("close",a),t.removeListener("finish",u),t.removeListener("drain",d),t.removeListener("error",s),t.removeListener("unpipe",r),l.removeListener("end",i),l.removeListener("end",o),(!t._writableState||t._writableState.needDrain)&&d()}function s(e){h(),0===E&&0===w.listenerCount(t,"error")&&t.emit("error",e)}function a(){t.removeListener("finish",u),h()}function u(){t.removeListener("close",a),h()}function h(){l.unpipe(t)}var l=this,c=this._readableState;switch(c.pipesCount){case 0:c.pipes=t;break;case 1:c.pipes=[c.pipes,t];break;default:c.pipes.push(t)}c.pipesCount+=1;var p=(!e||e.end!==!1)&&t!==n.stdout&&t!==n.stderr,f=p?i:o;c.endEmitted?M(f):l.once("end",f),t.on("unpipe",r);var d=m(l);t.on("drain",d);var E=w.listenerCount(t,"error");return t.once("error",s),t.once("close",a),t.once("finish",u),t.emit("pipe",l),c.flowing||(this.on("readable",g),c.flowing=!0,M(function(){v(l)})),t},i.prototype.unpipe=function(t){var e=this._readableState;if(0===e.pipesCount)return this;if(1===e.pipesCount)return t&&t!==e.pipes?this:(t||(t=e.pipes),e.pipes=null,e.pipesCount=0,this.removeListener("readable",g),e.flowing=!1,t&&t.emit("unpipe",this),this);if(!t){var n=e.pipes,r=e.pipesCount;e.pipes=null,e.pipesCount=0,this.removeListener("readable",g),e.flowing=!1;for(var i=0;r>i;i++)n[i].emit("unpipe",this);return this}var i=T(e.pipes,t);return-1===i?this:(e.pipes.splice(i,1),e.pipesCount-=1,1===e.pipesCount&&(e.pipes=e.pipes[0]),t.emit("unpipe",this),this)},i.prototype.on=function(t,e){var n=R.prototype.on.call(this,t,e);if("data"!==t||this._readableState.flowing||E(this),"readable"===t&&this.readable){var r=this._readableState;r.readableListening||(r.readableListening=!0,r.emittedReadable=!1,r.needReadable=!0,r.reading?r.length&&c(this,r):this.read(0))}return n},i.prototype.addListener=i.prototype.on,i.prototype.resume=function(){E(this),this.read(0),this.emit("resume")},i.prototype.pause=function(){E(this,!0),this.emit("pause")},i.prototype.wrap=function(t){var e=this._readableState,n=!1,r=this;t.on("end",function(){if(e.decoder&&!e.ended){var t=e.decoder.end();t&&t.length&&r.push(t)}r.push(null)}),t.on("data",function(i){if(e.decoder&&(i=e.decoder.write(i)),i&&(e.objectMode||i.length)){var o=r.push(i);o||(n=!0,t.pause())}});for(var i in t)"function"==typeof t[i]&&"undefined"==typeof this[i]&&(this[i]=function(e){return function(){return t[e].apply(t,arguments)}}(i));var o=["error","close","destroy","pause","resume"];return b(o,function(e){t.on(e,function(t){return r.emit.apply(r,e,t)})}),r._read=function(e){n&&(n=!1,t.resume())},r},i._fromList=y}).call(this,t("1YiZ5S"))},{"./index.js":12,"1YiZ5S":10,buffer:5,events:8,inherits:9,"process/browser.js":13,string_decoder:18}],16:[function(t,e,n){function r(t,e){this.afterTransform=function(t,n){return i(e,t,n)},this.needTransform=!1,this.transforming=!1,this.writecb=null,this.writechunk=null}function i(t,e,n){var r=t._transformState;r.transforming=!1;var i=r.writecb;if(!i)return t.emit("error",new Error("no writecb in Transform class"));r.writechunk=null,r.writecb=null,null!==n&&void 0!==n&&t.push(n),i&&i(e);var o=t._readableState;o.reading=!1,(o.needReadable||o.length=this.charLength-this.charReceived?this.charLength-this.charReceived:t.length;if(t.copy(this.charBuffer,this.charReceived,n,r),this.charReceived+=r-n,n=r,this.charReceived=55296&&56319>=i)){if(this.charReceived=this.charLength=0,r==t.length)return e;t=t.slice(r,t.length);break}this.charLength+=this.surrogateSize,e=""}var o=this.detectIncompleteChar(t),s=t.length;this.charLength&&(t.copy(this.charBuffer,0,t.length-o,s),this.charReceived=o,s-=o),e+=t.toString(this.encoding,0,s);var s=e.length-1,i=e.charCodeAt(s);if(i>=55296&&56319>=i){var a=this.surrogateSize;return this.charLength+=a,this.charReceived+=a,this.charBuffer.copy(this.charBuffer,a,0,a),this.charBuffer.write(e.charAt(e.length-1),this.encoding),e.substring(0,s)}return e},u.prototype.detectIncompleteChar=function(t){for(var e=t.length>=3?3:t.length;e>0;e--){var n=t[t.length-e];if(1==e&&n>>5==6){this.charLength=2;break}if(2>=e&&n>>4==14){this.charLength=3;break}if(3>=e&&n>>3==30){this.charLength=4;break}}return e},u.prototype.end=function(t){var e="";if(t&&t.length&&(e=this.write(t)),this.charReceived){var n=this.charReceived,r=this.charBuffer,i=this.encoding;e+=r.slice(0,n).toString(i)}return e}},{buffer:5}],19:[function(t,e,n){var r=[].indexOf||function(t){for(var e=0,n=this.length;n>e;e++)if(e in this&&this[e]===t)return e;return-1};n.setOrigin=function(t,e,n){var i,o,s,a;return+e===e&&(e=[e]),o=r.call(e,1)>=0?0:n.x,s=r.call(e,2)>=0?0:n.y,a=r.call(e,3)>=0?0:n.z,i=r.call(e,4)>=0?0:n.w,t.set(o,s,a,i)},n.addOrigin=function(){var t;return t=new THREE.Vector4,function(e,r,i){return n.setOrigin(t,r,i),e.add(t)}}(),n.setDimension=function(t,e){var n,r,i,o;return r=1===e?1:0,i=2===e?1:0,o=3===e?1:0,n=4===e?1:0,t.set(r,i,o,n)},n.setDimensionNormal=function(t,e){var n,r,i,o;return r=1===e?1:0,i=2===e?1:0,o=3===e?1:0,n=4===e?1:0,t.set(i,o+r,n,0)},n.recenterAxis=function(){var t;return t=[0,0],function(e,n,r,i){var o,s,a,u,h,l;return null==i&&(i=0),r>0&&(h=e,l=e+n,o=Math.max(Math.abs(h),Math.abs(l)),s=o*i,u=Math.min(h,l),a=Math.max(h,l),e=u+(-o+s-u)*r,n=a+(o+s-a)*r-e),t[0]=e,t[1]=n,t}}()},{}],20:[function(t,e,n){var r;n.getSizes=r=function(t){var e,n;for(n=[],e=t;"string"!=typeof e&&null!=(null!=e?e.length:void 0);)n.push(e.length),e=e[0];return n},n.getDimensions=function(t,e){var n,i,o,s,a,u,h,l,c,p,f,d,m,v,g;return null==e&&(e={}),a=e.items,n=e.channels,g=e.width,s=e.height,i=e.depth,o={},t&&t.length?(v=r(t),l=v.length,o.channels=1!==n&&v.length>1?v.pop():n,o.items=1!==a&&v.length>1?v.pop():a,o.width=1!==g&&v.length>1?v.pop():g,o.height=1!==s&&v.length>1?v.pop():s,o.depth=1!==i&&v.length>1?v.pop():i,u=l,1===n&&u++,1===a&&u>1&&u++,1===g&&u>2&&u++,1===s&&u>3&&u++,h=null!=(c=v.pop())?c:1,1>=u&&(h/=null!=(p=o.channels)?p:1),2>=u&&(h/=null!=(f=o.items)?f:1),3>=u&&(h/=null!=(d=o.width)?d:1),4>=u&&(h/=null!=(m=o.height)?m:1),h=Math.floor(h),null==o.width&&(o.width=h,h=1),null==o.height&&(o.height=h,h=1),null==o.depth&&(o.depth=h,h=1),o):{items:a,channels:n,width:null!=g?g:0,height:null!=s?s:0,depth:null!=i?i:0}},n.repeatCall=function(t,e){switch(e){case 0:return function(){return!0};case 1:return function(){return t()};case 2:return function(){return t(),t()};case 3:return function(){return t(),t(),t(),t()};case 4:return function(){return t(),t(),t(),t()};case 6:return function(){return t(),t(),t(),t(),t(),t()};case 8:return function(){return t(),t(),t(),t(),t(),t()}}},n.makeEmitter=function(t,e,n){var r,i,o,s;for(r=function(){switch(n){case 0:return function(){return!0};case 1:return function(e){return e(t())};case 2:return function(e){return e(t(),t())};case 3:return function(e){return e(t(),t(),t())};case 4:return function(e){return e(t(),t(),t(),t())};case 6:return function(e){return e(t(),t(),t(),t(),t(),t())};case 8:return function(e){return e(t(),t(),t(),t(),t(),t(),t(),t())}}}(),o=null;e>0;)i=Math.min(e,8),s=function(){switch(i){case 1:return function(t){return r(t)};case 2:return function(t){return r(t),r(t)};case 3:return function(t){return r(t),r(t),r(t)};case 4:return function(t){return r(t),r(t),r(t),r(t)};case 5:return function(t){return r(t),r(t),r(t),r(t),r(t)};case 6:return function(t){return r(t),r(t),r(t),r(t),r(t),r(t)};case 7:return function(t){return r(t),r(t),r(t),r(t),r(t),r(t),r(t)};case 8:return function(t){return r(t),r(t),r(t),r(t),r(t),r(t),r(t),r(t)}}}(),o=null!=o?function(t,e){return function(n){return t(n),e(n)}}(s,o):s,e-=i;return s=null!=o?o:function(){return!0},s.reset=t.reset,s.rebind=t.rebind,s},n.getThunk=function(t){var e,n,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S;switch(H=r(t),d=H.length,e=H.pop(),n=H.pop(),i=H.pop(),o=H.pop(),s=!1,d){case 0:S=function(){return 0},S.reset=function(){};break;case 1:h=0,S=function(){return t[h++]},S.reset=function(){return h=0};break;case 2:h=l=0,a=null!=(m=t[l])?m:[],S=function(){var n,r;return r=a[h++],h===e&&(h=0,l++,a=null!=(n=t[l])?n:[]),r},S.reset=function(){var e;h=l=0,a=null!=(e=t[l])?e:[]};break;case 3:h=l=c=0,R=null!=(v=t[c])?v:[],a=null!=(g=R[l])?g:[],S=function(){var r,i,o;return o=a[h++],h===e&&(h=0,l++,l===n&&(l=0,c++,R=null!=(r=t[c])?r:[]), -a=null!=(i=R[l])?i:[]),o},S.reset=function(){var e,n;h=l=c=0,R=null!=(e=t[c])?e:[],a=null!=(n=R[l])?n:[]};break;case 4:h=l=c=p=0,M=null!=(E=t[p])?E:[],R=null!=(y=M[c])?y:[],a=null!=(_=R[l])?_:[],S=function(){var r,o,s,u;return u=a[h++],h===e&&(h=0,l++,l===n&&(l=0,c++,c===i&&(c=0,p++,M=null!=(r=t[p])?r:[]),R=null!=(o=M[c])?o:[]),a=null!=(s=R[l])?s:[]),u},S.reset=function(){var e,n,r;h=l=c=p=0,M=null!=(e=t[p])?e:[],R=null!=(n=M[c])?n:[],a=null!=(r=R[l])?r:[]};break;case 5:h=l=c=p=f=0,u=null!=(b=t[f])?b:[],M=null!=(T=u[p])?T:[],R=null!=(x=M[c])?x:[],a=null!=(w=R[l])?w:[],S=function(){var r,s,d,m,v;return v=a[h++],h===e&&(h=0,l++,l===n&&(l=0,c++,c===i&&(c=0,p++,p===o&&(p=0,f++,u=null!=(r=t[f])?r:[]),M=null!=(s=u[p])?s:[]),R=null!=(d=M[c])?d:[]),a=null!=(m=R[l])?m:[]),v},S.reset=function(){var e,n,r,i;h=l=c=p=f=0,u=null!=(e=t[f])?e:[],M=null!=(n=u[p])?n:[],R=null!=(r=M[c])?r:[],a=null!=(i=R[l])?i:[]}}return S.rebind=function(o){return t=o,H=r(t),H.length&&(e=H.pop()),H.length&&(n=H.pop()),H.length&&(i=H.pop()),H.length?o=H.pop():void 0},S},n.getStreamer=function(t,e,n,r){var i,o,s,a,u,h,l,c,p;return l=u=h=0,c=function(){return l=e*n*r,u=h=0},o=function(){return h},s=function(){return 0>=l-u},p=function(){switch(n){case 1:return function(t){u+=t,h+=t};case 2:return function(t){u+=2*t,h+=t};case 3:return function(t){u+=3*t,h+=t};case 4:return function(t){u+=4*t,h+=t}}}(),i=function(){switch(n){case 1:return function(e){e(t[u++]),++h};case 2:return function(e){e(t[u++],t[u++]),++h};case 3:return function(e){e(t[u++],t[u++],t[u++]),++h};case 4:return function(e){e(t[u++],t[u++],t[u++],t[u++]),++h}}}(),a=function(){switch(n){case 1:return function(e){t[u++]=e,++h};case 2:return function(e,n){t[u++]=e,t[u++]=n,++h};case 3:return function(e,n,r){t[u++]=e,t[u++]=n,t[u++]=r,++h};case 4:return function(e,n,r,i){t[u++]=e,t[u++]=n,t[u++]=r,t[u++]=i,++h}}}(),i.reset=c,a.reset=c,c(),{emit:a,consume:i,skip:p,count:o,done:s,reset:c}},n.getLerpEmitter=function(t,e){var n,r,i,o,s,a,u,h,l,c,p;return p=new Float32Array(4096),s=a=.5,u=h=l=c=0,r=function(t,e,n,r){return l++,p[u++]=t*s,p[u++]=e*s,p[u++]=n*s,p[u++]=r*s},i=function(t,e,n,r){return c++,p[h++]+=t*a,p[h++]+=e*a,p[h++]+=n*a,p[h++]+=r*a},n=Math.max(t.length,e.length),o=3>=n?function(n,o,s){var a,f,d,m,v,g;for(u=h=l=c=0,t(r,o,s),e(i,o,s),d=Math.min(l,c),f=0,g=[],a=m=0,v=d;v>=0?v>m:m>v;a=v>=0?++m:--m)g.push(n(p[f++],p[f++],p[f++],p[f++]));return g}:5>=n?function(n,o,s,a,f){var d,m,v,g,E,y;for(u=h=l=c=0,t(r,o,s,a,f),e(i,o,s,a,f),v=Math.min(l,c),m=0,y=[],d=g=0,E=v;E>=0?E>g:g>E;d=E>=0?++g:--g)y.push(n(p[m++],p[m++],p[m++],p[m++]));return y}:7>=n?function(n,o,s,a,f,d,m){var v,g,E,y,_;for(u=h=l=c=0,t(r,o,s,a,f,d,m),e(i,o,s,a,f,d,m),g=Math.min(l,c),v=0,_=[],m=E=0,y=g;y>=0?y>E:E>y;m=y>=0?++E:--E)_.push(n(p[v++],p[v++],p[v++],p[v++]));return _}:9>=n?function(n,o,s,a,f,d,m,v,g){var E,y,_,b;for(u=h=l=c=0,t(r,o,s,a,f,d,m,v,g),e(i,o,s,a,f,d,m,v,g),E=Math.min(l,c),g=0,b=[],v=y=0,_=E;_>=0?_>y:y>_;v=_>=0?++y:--y)b.push(n(p[g++],p[g++],p[g++],p[g++]));return b}:function(n,o,s,a,f,d,m,v,g,E,y){var _,b,T,x;for(u=h=0,t(r,o,s,a,f,d,m,v,g,E,y),e(i,o,s,a,f,d,m,v,g,E,y),_=Math.min(l,c),g=0,x=[],v=b=0,T=_;T>=0?T>b:b>T;v=T>=0?++b:--b)x.push(n(p[g++],p[g++],p[g++],p[g++]));return x},o.lerp=function(t){var e;return e=[1-t,t],s=e[0],a=e[1],e},o},n.getLerpThunk=function(t,e){var r,i,o,s,a,u;return i=n.getSizes(t).reduce(function(t,e){return t*e}),o=n.getSizes(e).reduce(function(t,e){return t*e}),r=Math.min(i,o),a=n.getThunk(t),u=n.getThunk(e),s=new Float32Array(r),s.lerp=function(t){var e,n,i,o;for(a.reset(),u.reset(),i=0,o=[];r>i;)e=a(),n=u(),o.push(s[i++]=e+(n-e)*t);return o},s}},{}],21:[function(t,e,n){var r,i;i=Math.PI,r={clamp:function(t,e,n){return Math.max(e,Math.min(n,t))},cosine:function(t){return.5-.5*Math.cos(r.clamp(t,0,1)*i)},binary:function(t){return+(t>=.5)},hold:function(t){return+(t>=1)}},e.exports=r},{}],22:[function(t,e,n){var r,i,o,s,a,u=[].indexOf||function(t){for(var e=0,n=this.length;n>e;e++)if(e in this&&this[e]===t)return e;return-1};i="xyzw".split(""),r={0:-1,x:0,y:1,z:2,w:3},o=function(t){return t===""+t&&(t=t.split("")),t===+t&&(t=[t]),t},a=function(t){return t===+t&&(t="vec"+t),"vec1"===t&&(t="float"),t},s=function(t){return t=""+t,t.indexOf(".")<0?t+=".0":void 0},n.mapByte2FloatOffset=function(t){var e;return null==t&&(t=4),e=s(t),"vec4 float2ByteIndex(vec4 xyzw, out float channelIndex) {\n float relative = xyzw.w / "+e+";\n float w = floor(relative);\n channelIndex = (relative - w) * "+e+";\n return vec4(xyzw.xyz, w);\n}"},n.sample2DArray=function(t){var e,n;return n=function(t,e){var r,i;return t===e?i="return texture2D(dataTextures["+t+"], uv);":(r=Math.ceil(t+(e-t)/2),i="if (z < "+(r-.5)+") {\n "+n(t,r-1)+"\n}\nelse {\n "+n(r,e)+"\n}"),i=i.replace(/\n/g,"\n ")},e=n(0,t-1),"uniform sampler2D dataTextures["+t+"];\n\nvec4 sample2DArray(vec2 uv, float z) {\n "+e+"\n}"},n.binaryOperator=function(t,e,n){return t=a(t),null!=n?t+" binaryOperator("+t+" a) {\n return a "+e+" "+n+";\n}":t+" binaryOperator("+t+" a, "+t+" b) {\n return a "+e+" b;\n}"},n.extendVec=function(t,e,r){var i,o,u,h;return null==r&&(r=0),t>e?n.truncateVec(t,e):(o=e-t,t=a(t),e=a(e),r=s(r),u=function(){h=[];for(var t=0;o>=0?o>=t:t>=o;o>=0?t++:t--)h.push(t);return h}.apply(this).map(function(t){return t?r:"v"}),i=u.join(","),e+" extendVec("+t+" v) { return "+e+"("+i+"); }")},n.truncateVec=function(t,e){var r;return e>t?n.extendVec(t,e):(r="."+"xyzw".substr(0,e),t=a(t),e=a(e),e+" truncateVec("+t+" v) { return v"+r+"; }")},n.injectVec4=function(t){var e,n,i,s,a,u,h;for(h=["0.0","0.0","0.0","0.0"],t=o(t),t=t.map(function(t){return t===""+t?r[t]:t}),i=s=0,a=t.length;a>s;i=++s)n=t[i],h[n]=["a","b","c","d"][i];return u=h.slice(0,4).join(", "),e=["float a","float b","float c","float d"].slice(0,t.length),"vec4 inject("+e+") {\n return vec4("+u+");\n}"},n.swizzleVec4=function(t,e){var n,i;for(null==e&&(e=null),n=["0.0","xyzw.x","xyzw.y","xyzw.z","xyzw.w"],null==e&&(e=t.length),t=o(t),t=t.map(function(t){var e;return e=+t,u.call([0,1,2,3,4],e)>=0&&(t=+t),t===""+t&&(t=r[t]+1),n[t]});t.lengths;e=++s)u=t[e],l=i[e],n=r[u],c[n]="xyzw."+l;return h=c.join(", "),"vec4 invertSwizzle(vec4 xyzw) {\n return vec4("+h+");\n}"},n.identity=function(t){var e;return e=[].slice.call(arguments),e.length>1?(e=e.map(function(t,e){return["inout",t,String.fromCharCode(97+e)].join(" ")}),e=e.join(", "),"void identity("+e+") { }"):t+" identity("+t+" x) {\n return x;\n}"},n.constant=function(t,e){return t+" constant() {\n return "+e+";\n}"},n.toType=a},{}],23:[function(t,e,n){n.Axis=t("./axis"),n.Data=t("./data"),n.Ease=t("./ease"),n.GLSL=t("./glsl"),n.JS=t("./js"),n.Pretty=t("./pretty"),n.Three=t("./three"),n.Ticks=t("./ticks"),n.VDOM=t("./vdom")},{"./axis":19,"./data":20,"./ease":21,"./glsl":22,"./js":24,"./pretty":25,"./three":26,"./ticks":27,"./vdom":28}],24:[function(t,e,n){n.merge=function(){var t,e,n,r,i,o;for(o={},t=0,n=arguments.length;n>t;t++){r=arguments[t];for(e in r)i=r[e],o[e]=i}return o},n.clone=function(t){return JSON.parse(JSON.serialize(t))},n.parseQuoted=function(t){var e,n,r,i,o,s,a,u,h,l;for(e="",l=function(t){return t=t.replace(/\\/g,"")},a=function(t){return e.length&&s.push(l(e)),e=null!=t?t:""},t=t.split(/(?=(?:\\.|["' ,]))/g),u=!1,s=[],i=0,o=t.length;o>i;i++)switch(r=t[i],n=r[0],h=r.slice(1),n){case'"':case"'":if(u)u===n?(u=!1,a(h)):e+=r;else{if(""!==e)throw new Error("ParseError: String `"+t+"` does not contain comma-separated quoted tokens.");u=n,e+=h}break;case" ":case",":u?e+=r:a(h);break;default:e+=r}return a(),s}},{}],25:[function(t,e,n){var r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E;r=5,i=1e-4,o=function(t,e){return Math.abs(t/e-Math.round(t/e))i?"-":"",i+=n):"1"!==n&&(i+=r?""+n:"*"+n),i+"/"+e},u=[{1:1},{1:1,"τ":2*Math.PI},{1:1,"π":Math.PI},{1:1,"τ":2*Math.PI,"π":Math.PI},{1:1,e:Math.E},{1:1,"τ":2*Math.PI,e:Math.E},{1:1,"π":Math.PI,e:Math.E},{1:1,"τ":2*Math.PI,"π":Math.PI,e:Math.E}],c=[[420,[2,3,5,7]],[88200,[2,3,5,7]],[60060,[2,3,5,7,11,13]],[861764,[2,17,19,23,29]],[65536,[2]],[1e6,[2,5]]],g=function(t){var e,n,o,a,p,f,d,m,v,g;return t&&(e=t.cache,o=t.compact,v=t.tau,d=t.pi,a=t.e,g=t.threshold,m=t.precision),o=+!!(null!=o?o:!0),v=+!!(null!=v?v:!0),d=+!!(null!=d?d:!0),a=+!!(null!=a?a:!0),e=+!!(null!=e?e:!0),g=+(null!=g?g:i),m=+(null!=m?m:r),p=v+2*d+4*a,n=p+g+m,f=e?{}:null,function(t){var e,n,i,a,d,m,v,g,E,y,_,b,T,x,w,R,H,M;if(null!=f){if(null!=(n=f[t]))return n;if(t===Math.round(t))return f[t]=""+t}w=""+t,e=w.length+w.indexOf(".")+2,b=function(t){var n;return n=t.length,e>=n?(w=""+t,e=n):void 0},H=u[p];for(g in H)if(d=H[g],s(t/d))b(""+l(t/d,1,g,o));else for(m=0,E=c.length;E>m;m++)if(M=c[m],a=M[0],_=M[1],x=t/d*a,s(x)){for(v=0,y=_.length;y>v;v++)for(R=_[v];s(T=x/R)&&s(i=a/R);)x=T,a=i;b(""+h(t/d,a,g,o));break}return(""+t).length>r&&b(""+t.toPrecision(r)),null!=f&&(f[t]=w),w}},E=function(t,e){return null==e&&(e="info"),t=v(t),console[e].apply(console,t)},v=function(t){var e,n,r,i,o,s,a,u;return a="color:rgb(128,0,128)",n="color:rgb(144,64,0)",s="color:rgb(0,0,192)",i="color:rgb(0,70,156)",u="color:inherit",o=!1,r=0,e=[],t=t.replace(/(\\[<={}> "'])|(=>|[<={}> "'])/g,function(t,h,l){var c;return(null!=h?h.length:void 0)?h:o&&'"'!==l&&"'"!==l?l:r&&'"'!==l&&"'"!==l&&"{"!==l&&"}"!==l?l:c=function(){switch(l){case"<":return e.push(a),"%c<";case">":return e.push(a),e.push(u),"%c>%c";case" ":return e.push(n)," %c";case"=":case"=>":return e.push(a),"%c"+l;case'"':case"'":return o=!o,o?(e.push(r?n:s),l+"%c"):(e.push(r?i:a),"%c"+l);case"{":return 0===r++?(e.push(i),"%c"+l):l;case"}":return 0===--r?(e.push(a),l+"%c"):l;default:return l}}()}),[t].concat(e)},m=function(t,e){return d(t,e,"=")},f=function(t,e){return d(t,e,"=>")},d=function(){var t;return t=g({compact:!1}),function(e,n,r){var i,o,s;return i=function(t){return t===""+ +t||t.match(/^[A-Za-z_][A-Za-z0-9]*$/)?t:JSON.stringify(t)},s=function(t){return t.match('\n*"')?t:"{"+t+"}"},o=function(e){var n,r;if(e instanceof Array)return"["+e.map(o).join(", ")+"]";switch(typeof e){case"string":return e.match("\n")?'"\n'+e+'"\n':'"'+e+'"';case"function":return e=""+e,e.match("\n"),e=e.replace(/^function (\([^)]+\))/,"$1 =>"),e=e.replace(/^(\([^)]+\)) =>\s*{\s*return\s*([^}]+)\s*;\s*}/,"$1 => $2");case"number":return t(e);default:return null!=e&&e!==!!e?null!=e._up?o(e.map(function(t){return t})):e.toMarkup?e.toString():"{"+function(){var t;t=[];for(n in e)r=e[n],e.hasOwnProperty(n)&&t.push(i(n)+": "+o(r));return t}().join(", ")+"}":""+JSON.stringify(e)}},[e,r,s(o(n))].join("")}}(),a=function(t){return t=t.replace(/&/g,"&"),t=t.replace(/",t=a(t),r=0,i=n.length;i>r;r++)e=n[r],t=t.replace(/%([a-z])/,function(t,e){var r;switch(r=n.shift(),e){case"c":return'';default:return a(r)}});return o+=t,o+=""},e.exports={markup:v,number:g,print:E,format:p,JSX:{prop:m,bind:f}}},{}],26:[function(t,e,n){n.paramToGL=function(t,e){return e===THREE.RepeatWrapping?t.REPEAT:e===THREE.ClampToEdgeWrapping?t.CLAMP_TO_EDGE:e===THREE.MirroredRepeatWrapping?t.MIRRORED_REPEAT:e===THREE.NearestFilter?t.NEAREST:e===THREE.NearestMipMapNearestFilter?t.NEAREST_MIPMAP_NEAREST:e===THREE.NearestMipMapLinearFilter?t.NEAREST_MIPMAP_LINEAR:e===THREE.LinearFilter?t.LINEAR:e===THREE.LinearMipMapNearestFilter?t.LINEAR_MIPMAP_NEAREST:e===THREE.LinearMipMapLinearFilter?t.LINEAR_MIPMAP_LINEAR:e===THREE.UnsignedByteType?t.UNSIGNED_BYTE:e===THREE.UnsignedShort4444Type?t.UNSIGNED_SHORT_4_4_4_4:e===THREE.UnsignedShort5551Type?t.UNSIGNED_SHORT_5_5_5_1:e===THREE.UnsignedShort565Type?t.UNSIGNED_SHORT_5_6_5:e===THREE.ByteType?t.BYTE:e===THREE.ShortType?t.SHORT:e===THREE.UnsignedShortType?t.UNSIGNED_SHORT:e===THREE.IntType?t.INT:e===THREE.UnsignedIntType?t.UNSIGNED_INT:e===THREE.FloatType?t.FLOAT:e===THREE.AlphaFormat?t.ALPHA:e===THREE.RGBFormat?t.RGB:e===THREE.RGBAFormat?t.RGBA:e===THREE.LuminanceFormat?t.LUMINANCE:e===THREE.LuminanceAlphaFormat?t.LUMINANCE_ALPHA:e===THREE.AddEquation?t.FUNC_ADD:e===THREE.SubtractEquation?t.FUNC_SUBTRACT:e===THREE.ReverseSubtractEquation?t.FUNC_REVERSE_SUBTRACT:e===THREE.ZeroFactor?t.ZERO:e===THREE.OneFactor?t.ONE:e===THREE.SrcColorFactor?t.SRC_COLOR:e===THREE.OneMinusSrcColorFactor?t.ONE_MINUS_SRC_COLOR:e===THREE.SrcAlphaFactor?t.SRC_ALPHA:e===THREE.OneMinusSrcAlphaFactor?t.ONE_MINUS_SRC_ALPHA:e===THREE.DstAlphaFactor?t.DST_ALPHA:e===THREE.OneMinusDstAlphaFactor?t.ONE_MINUS_DST_ALPHA:e===THREE.DstColorFactor?t.DST_COLOR:e===THREE.OneMinusDstColorFactor?t.ONE_MINUS_DST_COLOR:e===THREE.SrcAlphaSaturateFactor?t.SRC_ALPHA_SATURATE:0},n.paramToArrayStorage=function(t){switch(t){case THREE.UnsignedByteType:return Uint8Array;case THREE.ByteType:return Int8Array;case THREE.ShortType:return Int16Array;case THREE.UnsignedShortType:return Uint16Array;case THREE.IntType:return Int32Array;case THREE.UnsignedIntType:return Uint32Array;case THREE.FloatType:return Float32Array}},n.swizzleToEulerOrder=function(t){return t.map(function(t){return["","X","Y","Z"][t]}).join("")},n.transformComposer=function(){var t,e,r,i,o;return t=new THREE.Euler,r=new THREE.Quaternion,e=new THREE.Vector3,i=new THREE.Vector3,o=new THREE.Matrix4,function(s,a,u,h,l,c){return null==c&&(c="XYZ"),null!=a?(c instanceof Array&&(c=n.swizzleToEulerOrder(c)),t.setFromVector3(a,c),r.setFromEuler(t)):r.set(0,0,0,1),null!=u&&r.multiply(u),null!=s?e.copy(s):e.set(0,0,0),null!=h?i.copy(h):i.set(1,1,1),o.compose(e,r,i),null!=l&&o.multiplyMatrices(o,l),o}}},{}],27:[function(t,e,n){var r,i,o,s,a;o=function(t,e,n,r,i,o,s,a,u,h){var l,c,p,f,d,m,v,g,E,y;return null==h&&(h=!0),n||(n=10),r||(r=1),i||(i=10),o||(o=1),v=e-t,d=v/n,h?(r||(r=1),i||(i=10),m=r*Math.pow(i,Math.floor(Math.log(d/r)/Math.log(i))),p=i%2===0?[i/2,1,.5]:i%3===0?[i/3,1,1/3]:[1],E=function(){var t,e,n;for(n=[],t=0,e=p.length;e>t;t++)c=p[t],n.push(m*c);return n}(),l=1/0,g=E.reduce(function(t,e){var n;return c=e/d,n=Math.max(c,1/c),l>n?(l=n,e):t},m),g*=o,t=Math.ceil(t/g+ +!s)*g,e=(Math.floor(e/g)-+!a)*g,n=Math.ceil((e-t)/g),y=function(){var e,r,i;for(i=[],f=e=0,r=n;r>=0?r>=e:e>=r;f=r>=0?++e:--e)i.push(t+f*g);return i}(),u||(y=y.filter(function(t){return 0!==t})),y):(y=function(){var e,r,i;for(i=[],f=e=0,r=n;r>=0?r>=e:e>=r;f=r>=0?++e:--e)i.push(t+f*d);return i}(),s||y.shift(),a||y.pop(),u||(y=y.filter(function(t){return 0!==t})),y)},s=function(t,e,n,r,i,o,s,a,u,h){throw new Error("Log ticks not yet implemented.")},r=0,i=1,a=function(t,e,n,a,u,h,l,c,p,f,d){switch(t){case r:return o(e,n,a,u,h,l,c,p,f,d);case i:return s(e,n,a,u,h,l,c,p,f,d)}},n.make=a,n.linear=o,n.log=s},{}],28:[function(t,e,n){var r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b;for(r=[],l=0,i={},a=function(){return{id:l++,type:null,props:null,children:null,rendered:null,instance:null}},h=function(t){var e,n,i,o;for(t*=2,t=Math.max(0,r.length-t),o=[],e=n=0,i=t;i>=0?i>n:n>i;e=i>=0?++n:--n)o.push(r.push(a()));return o},u=function(t,e,n){var i;return i=r.length?r.pop():a(),i.type=null!=t?t:"div",i.props=null!=e?e:null,i.children=null!=n?n:null,i},g=function(t){var e,n,i,o;if(t.type&&(n=t.children,t.type=t.props=t.children=t.instance=null,r.push(t),null!=n))for(i=0,o=n.length;o>i;i++)e=n[i],g(e)},o=function(t,e,n,r,s){var a,h,l,c,p,f,d,v,g,E,T,x,w,R,H,M,S,k,A,C,P,L,z,O,D,F,U,B,N,V;if(null!=t){if(null==e)return m(t,r,s);if(t instanceof Node){if(F=t===e)return}else F=typeof t==typeof e&&null!==e&&null!==t&&t.type===e.type;if(F){if(t.instance=e.instance,B=(null!=(P=t.type)?P.isComponentClass:void 0)?t.type:i[t.type],A=null!=e?e.props:void 0,H=t.props,c=null!=(L=null!=e?e.children:void 0)?L:null,R=t.children,null!=H&&(H.children=R),null!=B){if(f=n._COMPONENT_DIRTY,null!=A!=(null!=H)&&(f=!0),c!==R&&(f=!0),null!=A&&null!=H){if(!f)for(E in A)H.hasOwnProperty(E)||(f=!0);if(!f)for(E in H)V=H[E],(C=A[E])!==V&&(f=!0)}if(f){p=e.instance,null==t.props&&(t.props={}),z=p.defaultProps;for(g in z)N=z[g],null==(a=t.props)[g]&&(a[g]=N);t.props.children=t.children,"function"==typeof p.willReceiveProps&&p.willReceiveProps(t.props),U=n._COMPONENT_FORCE||(null!=(O="function"==typeof p.shouldUpdate?p.shouldUpdate(t.props):void 0)?O:!0),U&&(M=p.getNextState(),"function"==typeof p.willUpdate&&p.willUpdate(t.props,M)),S=p.props,k=p.applyNextState(),p.props=t.props,p.children=t.children,U&&(t=t.rendered="function"==typeof p.render?p.render(u,t.props,t.children):void 0,o(t,e.rendered,n,r,s),"function"==typeof p.didUpdate&&p.didUpdate(S,k))}return}if(null!=A)for(E in A)H.hasOwnProperty(E)||b(n,E,A[E]);if(null!=H)for(E in H)V=H[E],(C=A[E])!==V&&"children"!==E&&y(n,E,V,C);if(null!=R)if("string"==(D=typeof R)||"number"===D)R!==c&&(n.textContent=R);else if(null!=R.type)o(R,c,n.childNodes[0],n,0);else if(l=n.childNodes,null!=c)for(d=v=0,x=R.length;x>v;d=++v)h=R[d],o(h,c[d],l[d],n,d);else for(d=T=0,w=R.length;w>T;d=++T)h=R[d],o(h,null,l[d],n,d);else null!=c&&(_(null,n),n.innerHTML="");return}return _(e.instance,n),n.remove(),m(t,r,s)}return null!=e?(_(e.instance,n),e.node.remove()):void 0},m=function(t,e,n){var r,o,s,a,h,l,c,p,f,d,v,g,E,_,b,T,x,w,R,H;if(null==n&&(n=0),w=(null!=(g=t.type)?g.isComponentClass:void 0)?t.type:i[t.type],t instanceof Node)v=t;else{if(null!=w){if(h=(null!=(E=t.type)?E.isComponentClass:void 0)?t.type:i[t.type],!h)return t=t.rendered=u("noscript"),v=m(t,e,n);t.instance=a=new h(e),null==t.props&&(t.props={}),_=a.defaultProps;for(p in _)R=_[p],null==(r=t.props)[p]&&(r[p]=R);return t.props.children=t.children,a.props=t.props,a.children=t.children,a.setState("function"==typeof a.getInitialState?a.getInitialState():void 0),"function"==typeof a.willMount&&a.willMount(),t=t.rendered="function"==typeof a.render?a.render(u,t.props,t.children):void 0,v=m(t,e,n),"function"==typeof a.didMount&&a.didMount(t),v._COMPONENT=a,v}if("string"==(b=typeof t)||"number"===b)v=document.createTextNode(t);else{v=document.createElement(t.type),T=t.props;for(f in T)H=T[f],y(v,f,H)}if(s=t.children,null!=s)if("string"==(x=typeof s)||"number"===x)v.textContent=s;else if(null!=s.type)m(s,v,0);else for(l=c=0,d=s.length;d>c;l=++c)o=s[l],m(o,v,l)}return e.insertBefore(v,e.childNodes[n]),v},_=function(t,e){var n,r,i,o,s,a;if(t){"function"==typeof t.willUnmount&&t.willUnmount();for(i in t)delete t[i]}for(s=e.childNodes,a=[],r=0,o=s.length;o>r;r++)n=s[r],_(n._COMPONENT,n),a.push(delete n._COMPONENT);return a},v=function(t){var e,n,r,i;if("undefined"==typeof document)return!0;if(null!=document.documentElement.style[t])return t;for(t=t[0].toUpperCase()+t.slice(1),i=["webkit","moz","ms","o"],e=0,n=i.length;n>e;e++)if(r=i[e],null!=document.documentElement.style[r+t])return r+t},d={},E=["transform"],c=0,f=E.length;f>c;c++)p=E[c],d[p]=v(p);y=function(t,e,n,r){var i,o,s;{if("style"!==e)return null!=t[e]?void(t[e]=n):void(t instanceof Node&&t.setAttribute(e,n));for(i in n)s=n[i],(null!=r?r[i]:void 0)!==s&&(t.style[null!=(o=d[i])?o:i]=s)}},b=function(t,e,n){var r,i,o;if("style"!==e)null!=t[e]&&(t[e]=void 0),t instanceof Node&&t.removeAttribute(e);else for(r in n)o=n[r],t.style[null!=(i=d[r])?i:r]=""},s=function(t){var e,n,r,i,o;r={willMount:"componentWillMount",didMount:"componentDidMount",willReceiveProps:"componentWillReceiveProps",shouldUpdate:"shouldComponentUpdate",willUpdate:"componentWillUpdate",didUpdate:"componentDidUpdate",willUnmount:"componentWillUnmount"};for(n in r)i=r[n],null==t[n]&&(t[n]=t[i]);return e=function(){function e(e,n,r,i){var o,s,a,u;this.props=null!=n?n:{},this.state=null!=r?r:null,this.children=null!=i?i:null,o=function(t,e){return"function"==typeof t?t.bind(e):t};for(s in t)u=t[s],this[s]=o(u,this);a=null,this.setState=function(t){null==a&&(a=t?null!=a?a:{}:null);for(s in t)u=t[s],a[s]=u;e._COMPONENT_DIRTY=!0},this.forceUpdate=function(){var t,n;for(e._COMPONENT_FORCE=e._COMPONENT_DIRTY=!0,t=e,n=[];t=t.parentNode;)t._COMPONENT?n.push(t._COMPONENT_FORCE=!0):n.push(void 0);return n},this.getNextState=function(){return a},this.applyNextState=function(){var t,n;return e._COMPONENT_FORCE=e._COMPONENT_DIRTY=!1,t=this.state,n=[null,a],a=n[0],this.state=n[1],t}}return e}(),e.isComponentClass=!0,e.prototype.defaultProps=null!=(o="function"==typeof t.getDefaultProps?t.getDefaultProps():void 0)?o:{},e},e.exports={element:u,recycle:g,apply:o,hint:h,Types:i,createClass:s}},{}],29:[function(t,e,n){var r,i,o,s,a,u,h,l;i=t("./model"),o=t("./overlay"),s=t("./primitives"),a=t("./render"),u=t("./shaders"),h=t("./stage"),l=t("./util"),r=function(){function t(t,e,n){var r;null==e&&(e=null),null==n&&(n=null),this.canvas=r=t.domElement,this.element=null,this.shaders=new u.Factory(u.Snippets),this.renderables=new a.Factory(a.Classes,t,this.shaders),this.overlays=new o.Factory(o.Classes,r),this.scene=this.renderables.make("scene",{scene:e}),this.camera=this.defaultCamera=null!=n?n:new THREE.PerspectiveCamera,this.attributes=new i.Attributes(s.Types,this),this.primitives=new s.Factory(s.Types,this),this.root=this.primitives.make("root"),this.model=new i.Model(this.root),this.guard=new i.Guard,this.controller=new h.Controller(this.model,this.primitives),this.animator=new h.Animator(this),this.api=new h.API(this),this.speed=1,this.time={now:+new Date/1e3,time:0,delta:0,clock:0,step:0}}return t.Namespace={Model:i,Overlay:o,Primitives:s,Render:a,Shaders:u,Stage:h,Util:l,DOM:l.VDOM},t.prototype.init=function(){return this.scene.inject(),this.overlays.inject(),this},t.prototype.destroy=function(){return this.scene.unject(),this.overlays.unject(),this},t.prototype.resize=function(t){return null==t&&(t={}),null==t.renderWidth&&(t.renderWidth=null!=t.viewWidth?t.viewWidth:t.viewWidth=1280),null==t.renderHeight&&(t.renderHeight=null!=t.viewHeight?t.viewHeight:t.viewHeight=720),null==t.pixelRatio&&(t.pixelRatio=t.renderWidth/Math.max(1e-6,t.viewWidth)),null==t.aspect&&(t.aspect=t.viewWidth/Math.max(1e-6,t.viewHeight)),this.root.controller.resize(t),this},t.prototype.frame=function(t){return this.pre(t),this.update(),this.render(),this.post(),this},t.prototype.pre=function(t){var e;return t||(t={now:+new Date/1e3,time:0,delta:0,clock:0,step:0},t.delta=null!=this.time.now?t.now-this.time.now:0,t.delta>1&&(t.delta=1/60),t.step=t.delta*this.speed,t.time=this.time.time+t.delta,t.clock=this.time.clock+t.step),this.time=t,"function"==typeof(e=this.root.controller).pre&&e.pre(),this},t.prototype.update=function(){var t;return this.animator.update(),this.attributes.compute(),this.guard.iterate({step:function(t){return function(){var e;return e=t.attributes.digest(),e||(e=t.model.digest())}}(this),last:function(){return{attribute:this.attributes.getLastTrigger(),model:this.model.getLastTrigger()}}}),"function"==typeof(t=this.root.controller).update&&t.update(),this.camera=this.root.controller.getCamera(),this.speed=this.root.controller.getSpeed(),this},t.prototype.render=function(){var t;return"function"==typeof(t=this.root.controller).render&&t.render(),this.scene.render(),this},t.prototype.post=function(){var t;return"function"==typeof(t=this.root.controller).post&&t.post(),this},t.prototype.setWarmup=function(t){return this.scene.warmup(t),this},t.prototype.getPending=function(){return this.scene.pending.length},t}(),e.exports=r},{"./model":34,"./overlay":40,"./primitives":43,"./render":149,"./shaders":164,"./stage":169,"./util":175}],30:[function(t,e,n){var r,i,o,s,a;o=function(t){var e,n;return n=THREE.Bootstrap(t),n.fallback||(n.Time||n.install("time"),n.MathBox||n.install(["mathbox","splash"])),null!=(e=n.mathbox)?e:n},window.π=Math.PI,window.τ=2*π,window.e=Math.E,window.MathBox=n,window.mathBox=n.mathBox=o,n.version="0.0.5",n.Context=r=t("./context"),s=r.Namespace;for(i in s)a=s[i],n[i]=a;t("./splash"),THREE.Bootstrap.registerPlugin("mathbox",{defaults:{init:!0,warmup:2,inspect:!0,splash:!0},listen:["ready","pre","update","post","resize"],install:function(t){var e;return e=!1,this.first=!0,t.MathBox={init:function(n){return function(i){var o,s;if(!e)return e=!0,s=(null!=i?i.scene:void 0)||n.options.scene||t.scene,o=(null!=i?i.camera:void 0)||n.options.camera||t.camera,n.context=new r(t.renderer,s,o),n.context.api.three=t.three=t,n.context.api.mathbox=t.mathbox=n.context.api,n.context.api.start=function(){return t.Loop.start()},n.context.api.stop=function(){return t.Loop.stop()},n.context.init(),n.context.resize(t.Size),n.context.setWarmup(n.options.warmup),n.pending=0,n.warm=!n.options.warmup,console.log("MathBox²",MathBox.version),t.trigger({type:"mathbox/init",version:MathBox.version,context:n.context})}}(this),destroy:function(n){return function(){return e?(e=!1,t.trigger({type:"mathbox/destroy",context:n.context}),n.context.destroy(),delete t.mathbox,delete n.context.api.three,delete n.context):void 0}}(this),object:function(t){return function(){var e;return null!=(e=t.context)?e.scene.root:void 0}}(this)}},uninstall:function(t){return t.MathBox.destroy(),delete t.MathBox},ready:function(t,e){return this.options.init?(e.MathBox.init(),setTimeout(function(t){return function(){return t.options.inspect?t.inspect(e):void 0}}(this))):void 0},inspect:function(t){return this.context.api.inspect(),this.options.warmup?void 0:this.info(t)},info:function(t){var e,n;return e=function(t){var e;for(e=[];t>=1e3;)e.unshift(("000"+t%1e3).slice(-3)),t=Math.floor(t/1e3);return e.unshift(t),e.join(",")},n=t.renderer.info.render,console.log("Geometry ",e(n.faces)+" faces ",e(n.vertices)+" vertices ",e(n.calls)+" draw calls ")},resize:function(t,e){var n;return null!=(n=this.context)?n.resize(e.Size):void 0},pre:function(t,e){var n;return null!=(n=this.context)?n.pre(e.Time):void 0},update:function(t,e){var n,r,i,o;return null!=(r=this.context)&&r.update(),(n=null!=(i=this.context)?i.camera:void 0)&&n!==e.camera&&(e.camera=n),e.Time.set({speed:this.context.speed}),this.progress(this.context.getPending(),e),null!=(o=this.context)?o.render():void 0},post:function(t,e){var n;return null!=(n=this.context)?n.post():void 0},progress:function(t,e){var n,r,i;if(t||this.pending)return r=Math.max(t+this.options.warmup,this.pending),n=r-t,i=r,e.trigger({type:"mathbox/progress",current:r-t,total:r}),0===t&&(r=0),this.pending=r,n===i&&!this.warm&&(this.warm=!0,this.options.inspect)?this.info(e):void 0}})},{"./context":29,"./splash":165}],31:[function(t,e,n){var r,i,o;r=function(){function t(t,e){this.context=e,this.traits=t.Traits,this.types=t.Types,this.pending=[],this.bound=[],this.last=null}return t.prototype.make=function(t){return{"enum":"function"==typeof t["enum"]?t["enum"]():void 0,type:"function"==typeof t.uniform?t.uniform():void 0,value:t.make()}},t.prototype.apply=function(t,e){return new i(t,e,this)},t.prototype.bind=function(t){return this.bound.push(t)},t.prototype.unbind=function(t){var e;return this.bound=function(){var n,r,i,o;for(i=this.bound,o=[],n=0,r=i.length;r>n;n++)e=i[n],e!==t&&o.push(e);return o}.call(this)},t.prototype.queue=function(t,e,n,r){return this.lastObject=e,this.lastKey=n,this.lastValue=r,this.pending.push(t)},t.prototype.invoke=function(t){return t(this.context.time.clock,this.context.time.step)},t.prototype.compute=function(){var t,e,n,r;if(this.bound.length)for(r=this.bound,e=0,n=r.length;n>e;e++)t=r[e],this.invoke(t)},t.prototype.digest=function(){var t,e,n,r,i;if(!this.pending.length)return!1;for(i=[this.pending,[]],e=i[0],this.pending=i[1],n=0,r=e.length;r>n;n++)(t=e[n])();return!0},t.prototype.getTrait=function(t){return this.traits[t]},t.prototype.getLastTrigger=function(){return this.lastObject.toString()+" - "+this.lastKey+"=`"+this.lastValue+"`"},t}(),o=function(t){var e,n,r;n={};for(e in t)r=t[e],n[e]=r;return n},i=function(){function t(t,e,n){var r,i,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S,k,A,C,P,L,z,O,D,F,U,B,N,V,I,j,G,W,q,X,Y,Z,K,Q,J,$,tt,et,nt,rt,it;for(J=e.traits,I=e.props,w=e.finals,H=e.freeform,m=this,null!=t.props&&null!=t.expr&&null!=t.orig&&null!=t.computed&&null!=t.attributes&&(N=o(t.props),U=o(t.expr),B=t.orig(),F=t.computed(),null!=(j=t.attributes)&&j.dispose()),R={},V={},z={},Y=function(t){var e;return null!=(e=z[t])?e:t},v=function(e,n){if(z[n])throw new Error(t.toString()+" - Duplicate property `"+n+"`");return z[n]=e},M=function(t){var e,n,r;return null!=(e=null!=(n=m[t])?n.value:void 0)?e:null!=(r=m[Y(t)])?r.value:void 0},W=function(e,n,r,o){var s,h,l,p,f;if(e=Y(e),null==(s=m[e])){if(!H)throw new Error(t.toString()+" - Setting unknown property `"+e+"={"+n+"}`");s=m[e]={"short":e,type:null,last:null,value:null},nt[e]=function(t){return t}}if(!r){if(a[e])throw new Error(t.toString()+" - Can't set bound property `"+e+"={"+n+"}`");if(i[e])throw new Error(t.toString()+" - Can't set computed property `"+e+"={"+n+"}`");if(u[e])throw new Error(t.toString()+" - Can't set final property `"+e+"={"+n+"}`")}return p=!0,f=et(e,n,s.last,function(){return p=!1,null}),p&&(h=[f,s.value],s.value=h[0],s.last=h[1],l=s["short"],R[l]=f,r||(V[l]=n),o||_(e,s.value,s.last)||c(e,n)),p},d=function(t,e,n){return t=Y(t),W(t,e,!0,n),u[t]=!0},x={},r={},s={},a={},i={},u={},l=function(e,o,h){var l,c;if(null==h&&(h=!1),e=Y(e),"function"!=typeof o)throw new Error(t.toString()+" - Expression `"+e+"=>{"+x+"}` is not a function");if(a[e])throw new Error(t.toString()+" - Property `"+e+"=>{"+x+"}` is already bound");if(i[e])throw new Error(t.toString()+" - Property `"+e+"` is computed");if(u[e])throw new Error(t.toString()+" - Property `"+e+"` is final");return l=h?i:a,l[e]=o,c=null!=m[e]?m[e]["short"]:e,h||(x[c]=o),s[e]=o,o=o.bind(t),r[e]=function(n,r){var i,s;return(i=null!=(s=t.clock)?s.getTime():void 0)&&(n=i.clock,r=i.step),t.set(e,o(n,r),!0)},n.bind(r[e])},$=function(t,e){var o;return null==e&&(e=!1),t=Y(t),o=e?i:a,o[t]?(n.unbind(r[t]),delete r[t],delete o[t],null!=m[t]&&(t=m[t]["short"]),delete x[t]):void 0},b=function(t,e){var n;return t=Y(t),null!=(n="function"==typeof s[t]?s[t](e,0):void 0)?n:m[t].value},t.expr=x,t.props=R,t.evaluate=function(t,e){var n;if(null!=t)return b(t,e);n={};for(t in I)n[t]=b(t,e);return n},t.get=function(t){return null!=t?M(t):R},t.set=function(t,e,n,r){var i;if("string"==typeof t)W(t,e,n,r);else{r=n,n=e,i=t;for(t in i)e=i[t],W(t,e,n,r)}},t.bind=function(t,e,n){var r;if("string"==typeof t)l(t,e,n);else{n=e,r=t;for(t in r)e=r[t],l(t,e,n)}},t.unbind=function(t,e){var n;if("string"==typeof t)$(t,e);else{e=x,n=t;for(t in n)$(t,e)}},t.attribute=function(t){return null!=t?m[Y(t)]:m},t.orig=function(t){return null!=t?V[Y(t)]:o(V)},t.computed=function(t){return null!=t?i[Y(t)]:o(i)},L={},nt={},y={},_=function(t,e,n){return y[t](e,n)},et=function(t,e,n,r){return nt[t](e,n,r)},t.validate=function(e,n){var r,i;return e=Y(e),r=L[e],null!=r&&(i=r()),i=et(e,n,i,function(){throw new Error(t.toString()+" - Invalid value `"+e+"={"+n+"}`")})},E=!1,f={},K={},p={},Z={},S=function(t){return t.split(".")[0]},c=function(e,r){var i;return E||(E=!0,n.queue(g,t,e,r)),i=S(e),f[e]=!0,K[i]=!0},T={type:"change",changed:null,touched:null},g=function(){var e,n,r;T.changed=f,T.touched=K,f=p,K=Z,p=T.changed,Z=T.touched,E=!1;for(e in f)f[e]=!1;for(e in K)K[e]=!1;T.type="change",t.trigger(T),n=[];for(r in T.touched)T.type="change:"+r,n.push(t.trigger(T));return n},q=function(t){var e,n;return e=t.split(/\./g),n=e.pop(),e.pop(),e.unshift(n),e.reduce(function(t,e){return t+e.charAt(0).toUpperCase()+e.substring(1)})},h=function(t,e){var n,r,i,o,s,a,u,h;s=[];for(r in e)u=e[r],r=[t,r].join("."),a=q(r),m[r]=n={T:u,ns:t,"short":a,"enum":"function"==typeof u["enum"]?u["enum"]():void 0,type:"function"==typeof u.uniform?u.uniform():void 0,last:u.make(),value:h=u.make()},v(r,a),R[a]=h,L[r]=u.make,nt[r]=null!=(i=u.validate)?i:function(t){return t; -},s.push(y[r]=null!=(o=u.equals)?o:function(t,e){return t===e});return s},P=[],it={},k=0,C=J.length;C>k;k++)Q=J[k],G=Q.split(":"),Q=G[0],D=G[1],O=D?[D,Q].join("."):Q,X=n.getTrait(Q),P.push(Q),null!=X&&h(O,X);if(null!=I)for(D in I)X=I[D],h(D,X);if(tt=P.filter(function(t,e){return P.indexOf(t)===e}),t.traits=tt,null!=N&&t.set(N,!0,!0),null!=w)for(A in w)rt=w[A],d(A,rt,!0);null!=B&&t.set(B,!1,!0),null!=F&&t.bind(F,!0),null!=U&&t.bind(U,!1),this.dispose=function(){for(A in i)$(A,!0);for(A in a)$(A,!1);return I={},delete t.attributes,delete t.get,delete t.set}}return t}(),e.exports=r},{}],32:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;i=t("./node"),r=function(t){function e(t,n,r,i,o,s){e.__super__.constructor.call(this,t,n,r,i,o,s),this.children=[],this.on("reindex",function(t){return function(e){var n,r,i,o,s;for(o=t.children,s=[],r=0,i=o.length;i>r;r++)n=o[r],s.push(n.trigger(e));return s}}(this))}return o(e,t),e.prototype.add=function(t){var e;return null!=(e=t.parent)&&e.remove(t),t._index(this.children.length,this),this.children.push(t),t._added(this)},e.prototype.remove=function(t){var e,n,r,i,o,s;if((null!=(o=t.children)?o.length:void 0)&&t.empty(),n=this.children.indexOf(t),-1!==n)for(this.children.splice(n,1),t._index(null),t._removed(this),s=this.children,e=r=0,i=s.length;i>r;e=++r)t=s[e],e>=n&&t._index(e)},e.prototype.empty=function(){var t,e,n,r;for(t=this.children.slice().reverse(),e=0,n=t.length;n>e;e++)r=t[e],this.remove(r)},e}(i),e.exports=r},{"./node":36}],33:[function(t,e,n){var r;r=function(){function t(t){this.limit=null!=t?t:10}return t.prototype.iterate=function(t){var e,n,r,i;for(i=t.step,e=t.last,n=this.limit;r=i();)if(!--n)throw console.warn("Last iteration","function"==typeof e?e():void 0),new Error("Exceeded iteration limit.");return null},t}(),e.exports=r},{}],34:[function(t,e,n){n.Attributes=t("./attributes"),n.Group=t("./group"),n.Guard=t("./guard"),n.Model=t("./model"),n.Node=t("./node")},{"./attributes":31,"./group":32,"./guard":33,"./model":35,"./node":36}],35:[function(t,e,n){var r,i,o,s,a,u,h,l,c,p=[].indexOf||function(t){for(var e=0,n=this.length;n>e;e++)if(e in this&&this[e]===t)return e;return-1};l=t("cssauron"),r="*",s=/^#([A-Za-z0-9_])$/,o=/^\.([A-Za-z0-9_]+)$/,u=/^\[([A-Za-z0-9_]+)\]$/,h=/^[A-Za-z0-9_]+$/,i=/^<([0-9]+|<*)$/,c=null,a=function(){function t(t){var e,n,r,i,o,s,a,u,h,p,f,d,m,v,g,E,y,_,b,T,x,w;this.root=t,this.root.model=this,this.root.root=this.root,this.ids={},this.classes={},this.traits={},this.types={},this.nodes=[],this.watchers=[],this.fire=!1,this.lastNode=null,this.event={type:"update"},null==c&&(c=l({tag:"type",id:"id","class":"classes.join(' ')",parent:"parent",children:"children",attr:"traits.hash[attr]"})),e=function(t){return function(t){return u(t.node)}}(this),v=function(t){return function(t){return p(t.node)}}(this),this.root.on("add",e),this.root.on("remove",v),u=function(t){return function(t){return i(t),a(t),s(t),t.on("change:node",w),w(null,t,!0),f(t)}}(this),p=function(t){return function(t){return y(t),T(t),b(t),E(t.id,t),g(t.classes,t),t.off("change:node",w),f(t)}}(this),m=function(t){return function(e){var n,r,i,o;for(i=t.watchers,n=0,r=i.length;r>n;n++)o=i[n],o.match=o.matcher(e);return null}}(this),h=function(t){return function(e){var n,r,i,o,s;for(o=t.watchers,r=0,i=o.length;i>r;r++)s=o[r],n=s.fire||(s.fire=s.match!==s.matcher(e)),n&&(t.lastNode=e),t.fire||(t.fire=n);return null}}(this),f=function(t){return function(e){var n,r,i,o,s;for(o=t.watchers,r=0,i=o.length;i>r;r++)s=o[r],n=s.fire||(s.fire=s.matcher(e)),n&&(t.lastNode=e),t.fire||(t.fire=n);return null}}(this),this.digest=function(t){return function(){var e,n,r,i;if(!t.fire)return!1;for(r=t.watchers.slice(),e=0,n=r.length;n>e;e++)i=r[e],i.fire&&(i.fire=!1,i.handler());return t.fire=!1,!0}}(this),w=function(t){return function(t,e,i){var o,s,a,u,l,c,p,f;return o=i||t.changed["node.id"],s=i||t.changed["node.classes"],c=!1,o&&(u=e.get("node.id"),u!==e.id&&(i||m(e),c=!0,null!=e.id&&E(e.id,e),r(u,e))),s&&(a=null!=(p=e.get("node.classes"))?p:[],l=a.join(","),l!==(null!=(f=e.classes)?f.klass:void 0)&&(a=a.slice(),i||c||m(e),c=!0,null!=e.classes&&g(e.classes,e),n(a,e),e.classes=a,e.classes.klass=l)),!i&&c&&h(e),null}}(this),o=function(t,e,n){var r,i,o,s,a;if(null!=e){for(r=0,o=e.length;o>r;r++)i=e[r],s=null!=(a=t[i])?a:[],s.push(n),t[i]=s;return null}},_=function(t,e,n){var r,i,o,s,a;if(null!=e){for(r=0,s=e.length;s>r;r++)o=e[r],a=t[o],i=a.indexOf(n),i>=0&&a.splice(i,1),0===a.length&&delete t[o];return null}},d=function(t){var e,n,r,i,o;if(t.length>0){for(e=t.hash={},o=[],n=0,i=t.length;i>n;n++)r=t[n],o.push(e[r]=!0);return o}},x=function(t){return delete t.hash},r=function(t){return function(e,n){if(t.ids[e])throw new Error("Duplicate node id `"+e+"`");return null!=e&&(t.ids[e]=[n]),n.id=null!=e?e:n._id}}(this),E=function(t){return function(e,n){return null!=e&&delete t.ids[e],n.id=n._id}}(this),n=function(t){return function(e,n){return o(t.classes,e,n),null!=e?d(e):void 0}}(this),g=function(t){return function(e,n){return _(t.classes,e,n),null!=e?x(e):void 0}}(this),i=function(t){return function(e){return t.nodes.push(e)}}(this),y=function(t){return function(e){return t.nodes.splice(t.nodes.indexOf(e),1)}}(this),a=function(t){return function(e){return o(t.types,[e.type],e)}}(this),T=function(t){return function(e){return _(t.types,[e.type],e)}}(this),s=function(t){return function(e){return o(t.traits,e.traits,e),d(e.traits)}}(this),b=function(t){return function(e){return _(t.traits,e.traits,e),x(e.traits)}}(this),u(this.root),this.root.trigger({type:"added"})}return t.prototype.filter=function(t,e){var n,r,i,o,s;for(i=this._matcher(e),s=[],n=0,r=t.length;r>n;n++)o=t[n],i(o)&&s.push(o);return s},t.prototype.ancestry=function(t,e){var n,r,i,o,s;for(o=[],n=0,r=t.length;r>n;n++)for(i=t[n],s=i.parent;null!=s;){if(p.call(e,s)>=0){o.push(i);break}s=s.parent}return o},t.prototype.select=function(t,e){var n;return n=this._select(t),null!=e&&(n=this.ancestry(n,e)),n.sort(function(t,e){return e.order-t.order}),n},t.prototype.watch=function(t,e){var n;return e.unwatch=function(t){return function(){return t.unwatch(e)}}(this),e.watcher=n={selector:t,handler:e,matcher:this._matcher(t),match:!1,fire:!1},this.watchers.push(n),this.select(t)},t.prototype.unwatch=function(t){var e;return e=t.watcher,null!=e?(this.watchers.splice(this.watchers.indexOf(e),1),delete t.unwatch,delete t.watcher):void 0},t.prototype._simplify=function(t){var e,n,a,l,c,p,f,d,m,v,g,E;return t=t.replace(/^\s+/,""),t=t.replace(/\s+$/,""),a=e=t===r,a||(a=l=null!=(p=t.match(s))?p[1]:void 0),a||(a=c=null!=(f=t.match(o))?f[1]:void 0),a||(a=g=null!=(d=t.match(u))?d[1]:void 0),a||(a=E=null!=(m=t.match(h))?m[0]:void 0),a||(a=n=null!=(v=t.match(i))?v[0]:void 0),[e,l,c,g,E,n]},t.prototype._matcher=function(t){var e,n,r,i,o,s,a;if(o=this._simplify(t),e=o[0],r=o[1],i=o[2],s=o[3],a=o[4],n=o[5],e)return function(t){return!0};if(r)return function(t){return t.id===r};if(i)return function(t){var e,n;return null!=(e=t.classes)&&null!=(n=e.hash)?n[i]:void 0};if(s)return function(t){var e,n;return null!=(e=t.traits)&&null!=(n=e.hash)?n[s]:void 0};if(a)return function(t){return t.type===a};if(n)throw"Auto-link matcher unsupported";return c(t)},t.prototype._select=function(t){var e,n,r,i,o,s,a,u,h,l;return i=this._simplify(t),e=i[0],n=i[1],r=i[2],h=i[3],l=i[4],e?this.nodes:n?null!=(o=this.ids[n])?o:[]:r?null!=(s=this.classes[r])?s:[]:h?null!=(a=this.traits[h])?a:[]:l?null!=(u=this.types[l])?u:[]:this.filter(this.nodes,t)},t.prototype.getRoot=function(){return this.root},t.prototype.getLastTrigger=function(){return this.lastNode.toString()},t}(),e.exports=a},{cssauron:2}],36:[function(t,e,n){var r,i,o,s;o=t("../util"),s=0,i=function(){function t(t,e,n,r,i,o){this.type=t,this._id=(++s).toString(),this.configure(i,o),this.parent=this.root=this.path=this.index=null,this.set(e,!0,!0),this.set(n,!1,!0),this.bind(r,!1)}return t.prototype.configure=function(t,e){var n,r,i,o,s,a,u,h,l,c,p,f;return f=t.traits,i=t.props,n=t.finals,r=t.freeform,null==f&&(f=null!=(o=null!=(s=this._config)?s.traits:void 0)?o:[]),null==i&&(i=null!=(a=null!=(u=this._config)?u.props:void 0)?a:{}),null==n&&(n=null!=(h=null!=(l=this._config)?l.finals:void 0)?h:{}),null==r&&(r=null!=(c=null!=(p=this._config)?p.freeform:void 0)?c:!1),this._config={traits:f,props:i,finals:n,freeform:r},this.attributes=e.apply(this,this._config)},t.prototype.dispose=function(){return this.attributes.dispose(),this.attributes=null},t.prototype._added=function(t){var e;return this.parent=t,this.root=t.root,e={type:"add",node:this,parent:this.parent},this.root&&this.root.trigger(e),e.type="added",this.trigger(e)},t.prototype._removed=function(){var t;return t={type:"remove",node:this},this.root&&this.root.trigger(t),t.type="removed",this.trigger(t),this.root=this.parent=null},t.prototype._index=function(t,e){var n,r;return null==e&&(e=this.parent),this.index=t,this.path=n=null!=t?(null!=(r=null!=e?e.path:void 0)?r:[]).concat([t]):null,this.order=null!=n?this._encode(n):1/0,null!=this.root?this.trigger({type:"reindex"}):void 0},t.prototype._encode=function(t){var e,n,r,i,o,s,a,u,h,l,c;for(a=3,l=function(t){return a/(t+a)},h=function(t){return n+(e-n)*t},e=1+1/a,n=0,o=0,u=t.length;u>o;o++)s=t[o],r=l(s+1),i=l(s+2),c=[h(r),h(i)],e=c[0],n=c[1];return e},t.prototype.toString=function(){var t,e,n,r,i,o,s;return t=null!=(r=this.id)?r:this._id,s=null!=(i=this.type)?i:"node",n=s,n+="#"+t,(null!=(o=this.classes)?o.length:void 0)&&(n+="."+this.classes.join(".")),null!=this.children?(e=this.children.length)?"<"+n+">…("+e+")…":"<"+n+">":"<"+n+" />"},t.prototype.toMarkup=function(t,e){var n,r,i,s,a,u,h,l,c,p,f,d,m,v,g,E;null==t&&(t=null),null==e&&(e=""),t&&"function"!=typeof t&&(t=null!=(f=null!=(d=this.root)?d.model._matcher(t):void 0)?f:function(){return!0}),g=null!=(m=this.type)?m:"node",a=this.expr,l={id:this._id},v="function"==typeof this.orig?this.orig():void 0;for(u in v)E=v[u],l[u]=E;return c=function(){var t;t=[];for(u in l)E=l[u],this.expr[u]||t.push(o.Pretty.JSX.prop(u,E));return t}.call(this),a=function(){var t;t=[];for(u in a)E=a[u],t.push(o.Pretty.JSX.bind(u,E));return t}(),n=[""],c.length&&(n=n.concat(c)),a.length&&(n=n.concat(a)),n=n.join(" "),r=e,p=function(e){return function(){var n,i;return(null!=(i=e.children)?i.length:void 0)?n=e.children.map(function(e){return e.toMarkup(t,r)}).filter(function(t){return null!=t&&t.length}).join("\n"):""}}(this),t&&!t(this)?p():null!=this.children?(h="<"+g+n+">",s="",r=e+" ",i=p(),i.length&&(i="\n"+i+"\n"+e),null==i&&(i=""),e+h+i+s):e+"<"+g+n+" />"},t.prototype.print=function(t,e){return o.Pretty.print(this.toMarkup(t),e)},t}(),r=t("../util/binder"),r.apply(i.prototype),e.exports=i},{"../util":175,"../util/binder":171}],37:[function(t,e,n){var r;r={dom:t("./dom")},e.exports=r},{"./dom":38}],38:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("./overlay"),o=t("../util").VDOM,r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.prototype.el=o.element,e.prototype.hint=o.hint,e.prototype.apply=o.apply,e.prototype.recycle=o.recycle,e.prototype.init=function(t){return this.last=null},e.prototype.dispose=function(){return this.unmount(),e.__super__.dispose.apply(this,arguments)},e.prototype.mount=function(){var t;return t=document.createElement("div"),t.classList.add("mathbox-overlay"),this.element.appendChild(t),this.overlay=t},e.prototype.unmount=function(t){return this.overlay.parentNode&&this.element.removeChild(this.overlay),this.overlay=null},e.prototype.render=function(t){var e,n,r,i,o,s;this.overlay||this.mount(),("string"==(s=typeof t)||"number"===s)&&(t=this.el("div",null,t)),t instanceof Array&&(t=this.el("div",null,t)),n="div"===t.type,e=this.last,i=this.overlay,r=n?i:i.childNodes[0],o=n?i.parentNode:i,!e&&r&&(e=this.el("div")),this.apply(t,e,r,o,0),this.last=t,null!=e&&this.recycle(e)},e}(i),e.exports=r},{"../util":175,"./overlay":41}],39:[function(t,e,n){var r;r=function(){function t(t,e){var n;this.classes=t,this.canvas=e,n=document.createElement("div"),n.classList.add("mathbox-overlays"),this.div=n}return t.prototype.inject=function(){var t;if(t=this.canvas.parentNode,!t)throw new Error("Canvas not inserted into document.");return t.insertBefore(this.div,this.canvas)},t.prototype.unject=function(){var t;return t=this.div.parentNode,t.removeChild(this.div)},t.prototype.getTypes=function(){return Object.keys(this.classes)},t.prototype.make=function(t,e){return new this.classes[t](this.div,e)},t}(),e.exports=r},{}],40:[function(t,e,n){n.Factory=t("./factory"),n.Classes=t("./classes"),n.Overlay=t("./overlay")},{"./classes":37,"./factory":39,"./overlay":41}],41:[function(t,e,n){var r;r=function(){function t(t,e){this.element=t,"function"==typeof this.init&&this.init(e)}return t.prototype.dispose=function(){},t}(),e.exports=r},{}],42:[function(t,e,n){var r,i;i=t("../util"),r=function(){function t(t,e){this.context=e,this.classes=t.Classes,this.helpers=t.Helpers}return t.prototype.getTypes=function(){return Object.keys(this.classes)},t.prototype.make=function(t,e,n){var r,i,o;if(null==e&&(e={}),null==n&&(n=null),r=this.classes[t],null==r)throw new Error("Unknown primitive class `"+t+"`");return i=new r.model(t,r.defaults,e,n,r,this.context.attributes),o=new r(i,this.context,this.helpers),i},t}(),e.exports=r},{"../util":175}],43:[function(t,e,n){n.Factory=t("./factory"),n.Primitive=t("./primitive"),n.Types=t("./types")},{"./factory":42,"./primitive":44,"./types":72}],44:[function(t,e,n){var r,i,o,s=[].indexOf||function(t){for(var e=0,n=this.length;n>e;e++)if(e in this&&this[e]===t)return e;return-1};i=t("../model"),o=function(){function t(t,e,n){this.node=t,this._context=e,this._renderables=this._context.renderables,this._attributes=this._context.attributes,this._shaders=this._context.shaders,this._overlays=this._context.overlays,this._animator=this._context.animator,this._types=this._attributes.types,this.node.controller=this,this.node.on("added",function(t){return function(e){return t._added()}}(this)),this.node.on("removed",function(t){return function(e){return t._removed()}}(this)),this.node.on("change",function(t){return function(e){return t._root?t.change(e.changed,e.touched):void 0}}(this)),this.reconfigure(),this._get=this.node.get.bind(this.node),this._helpers=n(this,this.node.traits),this._handlers={inherit:{},listen:[],watch:[],compute:[]},this._root=this._parent=null,this.init()}return t.Node=i.Node,t.Group=i.Group,t.model=t.Node,t.defaults=null,t.traits=null,t.props=null,t.finals=null,t.freeform=!1,t.prototype.is=function(t){return this.traits.hash[t]},t.prototype.init=function(){},t.prototype.make=function(){},t.prototype.made=function(){},t.prototype.unmake=function(t){},t.prototype.unmade=function(){},t.prototype.change=function(t,e,n){},t.prototype.refresh=function(){return this.change({},{},!0)},t.prototype.rebuild=function(){return this._root?(this._removed(!0),this._added()):void 0},t.prototype.reconfigure=function(t){return null!=t&&this.node.configure(t,this._attributes),this.traits=this.node.traits,this.props=this.node.props},t.prototype._added=function(){var t,e,n,r,i,o;this._parent=null!=(r=this.node.parent)?r.controller:void 0,this._root=null!=(i=this.node.root)?i.controller:void 0,this.node.clock=null!=(o=this._inherit("clock"))?o:this._root;try{try{return this.make(),this.refresh(),this.made()}catch(e){throw t=e,this.node.print("warn"),console.error(t),t}}catch(n){t=n;try{return this._removed()}catch(s){}}},t.prototype._removed=function(t){return null==t&&(t=!1),this.unmake(t),this._unlisten(),this._unattach(),this._uncompute(),this._root=null,this._parent=null,this.unmade(t)},t.prototype._listen=function(t,e,n,r){var i,o,s;if(null==r&&(r=this),t instanceof Array)for(i=0,o=t.length;o>i;i++)return s=t[i],this.__listen(s,e,n,r);return this.__listen(t,e,n,r)},t.prototype.__listen=function(t,e,n,r){var i;return null==r&&(r=this),"string"==typeof t&&(t=this._inherit(t)),null!=t&&(i=n.bind(r),i.node=this.node,t.on(e,i),this._handlers.listen.push([t,e,i])),t},t.prototype._unlisten=function(){var t,e,n,r,i,o,s;if(this._handlers.listen.length){for(i=this._handlers.listen,e=0,n=i.length;n>e;e++)o=i[e],r=o[0],s=o[1],t=o[2],r.off(s,t);return this._handlers.listen=[]}},t.prototype._inherit=function(t){var e,n;return e=this._handlers.inherit[t],void 0!==e?e:this._handlers.inherit[t]=null!=(n=this._parent)?n._find(null!=t?t:null):void 0},t.prototype._find=function(t){var e;return this.is(t)?this:null!=(e=this._parent)?e._find(t):void 0},t.prototype._uninherit=function(){return this._handlers.inherit={}},t.prototype._attach=function(t,e,n,r,o,a,u){var h,l,c,p,f;return null==r&&(r=this),null==o&&(o=this),null==a&&(a=!1),null==u&&(u=!1),h=function(t){return null!=t&&s.call(t.traits,e)>=0?t:void 0},c=function(t){return null!=t?t.controller:void 0},l=function(t){var e,n,r,i;if(null==t)return t;for(r=[],e=0,n=t.length;n>e;e++)i=t[e],i instanceof Array?r=r.concat(i):r.push(i);return r},f=function(t){return function(s){var c,p,d,m,v,g,E,y;if("object"==typeof s){if(d=s,null!=d?d._up:void 0)return s=u?d._targets:[d[0]];if(d instanceof Array)return s=u?l(d.map(f)):f(d[0]);if(d instanceof i.Node)return[d]}else if("string"==typeof s&&"<"===s[0]){for(c=0,(p=s.match(/^<([0-9])+$/))&&(c=+p[1]-1),s.match(/^<+$/)&&(c=+s.length-1),m=[],g=o.node;g&&(v=g.parent);)if(g=v.children[g.index-1],g||m.length||(g=v),d=null,h(g)&&(d=g),null!=d&&c--<=0&&m.push(d),!u&&m.length)return m;if(u&&m.length)return m}else if("string"==typeof s)if(y=n.bind(r),t._handlers.watch.push(y),E=t._root.watch(s,y),u){if(m=E.filter(h),m.length)return m}else if(h(E[0])&&(d=E[0]),null!=d)return[d];if(!a)throw console.warn(t.node.toMarkup()),new Error(t.node.toString()+" - Could not find "+e+" `"+s+"`");return u?[]:null}}(this),p=l(f(t)),u?null!=p?p.map(c):null:null!=p?c(p[0]):null},t.prototype._unattach=function(){var t,e,n,r;if(this._handlers.watch.length){for(n=this._handlers.watch,t=0,e=n.length;e>t;t++)r=n[t],null!=r&&r.unwatch();return this._handlers.watch=[]}},t.prototype._compute=function(t,e){return this._handlers.compute.push(t),this.node.bind(t,e,!0)},t.prototype._uncompute=function(){var t,e,n,r;if(this._handlers.compute.length){for(r=this._handlers.compute,t=0,n=r.length;n>t;t++)e=r[t],this.node.unbind(e,!0);return this._handlers.compute=[]}},t}(),r=t("../util/binder"),r.apply(o.prototype),e.exports=o},{"../model":34,"../util/binder":171}],45:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;i=t("./parent"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","object","entity","visible","active"],e.prototype.make=function(){return this._helpers.visible.make(),this._helpers.active.make()},e.prototype.unmake=function(){return this._helpers.visible.unmake(),this._helpers.active.unmake()},e}(i),e.exports=r},{"./parent":47}],46:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty,a=[].indexOf||function(t){for(var e=0,n=this.length;n>e;e++)if(e in this&&this[e]===t)return e;return-1};i=t("./parent"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","bind"],e.prototype.make=function(){return this._helpers.bind.make([{to:"inherit.source",trait:"node"}])},e.prototype.unmake=function(){return this._helpers.bind.unmake()},e.prototype._find=function(t){return this.bind.source&&a.call(this.props.traits,t)>=0?this.bind.source._inherit(t):e.__super__._find.apply(this,arguments)},e}(i),e.exports=r},{"./parent":47}],47:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;i=t("../../primitive"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.model=i.Group,e.traits=["node"],e}(i),e.exports=r},{"../../primitive":44}],48:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./parent"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","root","clock","scene","vertex","unit"],e.prototype.init=function(){return this.size=null,this.cameraEvent={type:"root.camera"},this.preEvent={type:"root.pre"},this.updateEvent={type:"root.update"},this.renderEvent={type:"root.render"},this.postEvent={type:"root.post"},this.clockEvent={type:"clock.tick"},this.camera=null},e.prototype.make=function(){return this._helpers.unit.make()},e.prototype.unmake=function(){return this._helpers.unit.unmake()},e.prototype.change=function(t,e,n){return t["root.camera"]||n?(this._unattach(),this._attach(this.props.camera,"camera",this.setCamera,this,this,!0),this.setCamera()):void 0},e.prototype.adopt=function(t){var e,n,r,i,o;for(i=t.renders,o=[],e=0,n=i.length;n>e;e++)r=i[e],o.push(this._context.scene.add(r));return o},e.prototype.unadopt=function(t){var e,n,r,i,o;for(i=t.renders,o=[],e=0,n=i.length;n>e;e++)r=i[e],o.push(this._context.scene.remove(r));return o},e.prototype.select=function(t){return this.node.model.select(t)},e.prototype.watch=function(t,e){return this.node.model.watch(t,e)},e.prototype.unwatch=function(t){return this.node.model.unwatch(t)},e.prototype.resize=function(t){return this.size=t,this.trigger({type:"root.resize",size:t})},e.prototype.getSize=function(){return this.size},e.prototype.getSpeed=function(){return this.props.speed},e.prototype.getUnit=function(){return this._helpers.unit.get()},e.prototype.getUnitUniforms=function(){return this._helpers.unit.uniforms()},e.prototype.pre=function(){return this.getCamera().updateProjectionMatrix(),this.trigger(this.clockEvent),this.trigger(this.preEvent)},e.prototype.update=function(){return this.trigger(this.updateEvent)},e.prototype.render=function(){return this.trigger(this.renderEvent)},e.prototype.post=function(){return this.trigger(this.postEvent)},e.prototype.setCamera=function(){var t,e;return t=null!=(e=this.select(this.props.camera)[0])?e.controller:void 0,this.camera!==t?(this.camera=t,this.trigger({type:"root.camera"})):void 0},e.prototype.getCamera=function(){var t,e;return null!=(t=null!=(e=this.camera)?e.getCamera():void 0)?t:this._context.defaultCamera},e.prototype.getTime=function(){return this._context.time},e.prototype.vertex=function(t,e){return 2===e?t.pipe("view.position"):3===e?t.pipe("root.position"):t},e}(r),e.exports=i},{"../../../util":175,"./parent":47}],49:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("../../primitive"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","source","index"],e.prototype.made=function(){return this.trigger({type:"source.rebuild"})},e.prototype.indexShader=function(t){return t.pipe(o.GLSL.identity("vec4"))},e.prototype.sourceShader=function(t){return t.pipe(o.GLSL.identity("vec4"))},e.prototype.getDimensions=function(){return{items:1,width:1,height:1,depth:1}},e.prototype.getActiveDimensions=function(){return this.getDimensions()},e.prototype.getIndexDimensions=function(){return this.getActiveDimensions()},e.prototype.getFutureDimensions=function(){return this.getActiveDimensions()},e}(r),e.exports=i},{"../../../util":175,"../../primitive":44}],50:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./parent"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","unit"],e.prototype.make=function(){return this._helpers.unit.make()},e.prototype.unmake=function(){return this._helpers.unit.unmake()},e.prototype.getUnit=function(){return this._helpers.unit.get()},e.prototype.getUnitUniforms=function(){return this._helpers.unit.uniforms()},e}(r),e.exports=i},{"../../../util":175,"./parent":47}],51:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("../../primitive"),o=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","camera"],e.prototype.init=function(){},e.prototype.make=function(){var t;return t=this._context.defaultCamera,this.camera=this.props.proxy?t:t.clone(),this.euler=new THREE.Euler,this.quat=new THREE.Quaternion},e.prototype.unmake=function(){},e.prototype.getCamera=function(){return this.camera},e.prototype.change=function(t,e,n){var r,i,s,a,u,h,l,c;return t["camera.position"]||t["camera.quaternion"]||t["camera.rotation"]||t["camera.lookAt"]||t["camera.up"]||t["camera.fov"]||n?(h=this.props,a=h.position,u=h.quaternion,l=h.rotation,s=h.lookAt,c=h.up,i=h.fov,r=h.aspect,null!=a&&this.camera.position.copy(a),(null!=u||null!=l||null!=s)&&(null!=s?this.camera.lookAt(s):this.camera.quaternion.set(0,0,0,1),null!=l&&(this.euler.setFromVector3(l,o.Three.swizzleToEulerOrder(this.props.eulerOrder)),this.quat.setFromEuler(this.euler),this.camera.quaternion.multiply(this.quat)),null!=u&&this.camera.quaternion.multiply(u)),null!=i&&null!=this.camera.fov&&(this.camera.fov=i),null!=c&&this.camera.up.copy(c),this.camera.updateMatrix()):void 0},e}(i),e.exports=r},{"../../../util":175,"../../primitive":44}],52:[function(t,e,n){var r;r={axis:t("./draw/axis"),face:t("./draw/face"),grid:t("./draw/grid"),line:t("./draw/line"),point:t("./draw/point"),strip:t("./draw/strip"),surface:t("./draw/surface"),ticks:t("./draw/ticks"),vector:t("./draw/vector"),view:t("./view/view"),cartesian:t("./view/cartesian"),cartesian4:t("./view/cartesian4"),polar:t("./view/polar"),spherical:t("./view/spherical"),stereographic:t("./view/stereographic"),stereographic4:t("./view/stereographic4"),transform:t("./transform/transform3"),transform4:t("./transform/transform4"),vertex:t("./transform/vertex"),fragment:t("./transform/fragment"),layer:t("./transform/layer"),mask:t("./transform/mask"),array:t("./data/array"),interval:t("./data/interval"),matrix:t("./data/matrix"),area:t("./data/area"),voxel:t("./data/voxel"),volume:t("./data/volume"),scale:t("./data/scale"),html:t("./overlay/html"),dom:t("./overlay/dom"),text:t("./text/text"),format:t("./text/format"),label:t("./text/label"),retext:t("./text/retext"),clamp:t("./operator/clamp"),grow:t("./operator/grow"),join:t("./operator/join"),lerp:t("./operator/lerp"),memo:t("./operator/memo"),readback:t("./operator/readback"),resample:t("./operator/resample"),repeat:t("./operator/repeat"),swizzle:t("./operator/swizzle"),spread:t("./operator/spread"),split:t("./operator/split"),slice:t("./operator/slice"),subdivide:t("./operator/subdivide"),transpose:t("./operator/transpose"),group:t("./base/group"),inherit:t("./base/inherit"),root:t("./base/root"),unit:t("./base/unit"),shader:t("./shader/shader"),camera:t("./camera/camera"),rtt:t("./rtt/rtt"),compose:t("./rtt/compose"),clock:t("./time/clock"),now:t("./time/now"),move:t("./present/move"),play:t("./present/play"),present:t("./present/present"),reveal:t("./present/reveal"),slide:t("./present/slide"),step:t("./present/step")},e.exports=r},{"./base/group":45,"./base/inherit":46,"./base/root":48,"./base/unit":50,"./camera/camera":51,"./data/area":53,"./data/array":54,"./data/interval":57,"./data/matrix":58,"./data/scale":59,"./data/volume":60,"./data/voxel":61,"./draw/axis":62,"./draw/face":63,"./draw/grid":64,"./draw/line":65,"./draw/point":66,"./draw/strip":67,"./draw/surface":68,"./draw/ticks":69,"./draw/vector":70,"./operator/clamp":73,"./operator/grow":74,"./operator/join":75,"./operator/lerp":76,"./operator/memo":77,"./operator/readback":79,"./operator/repeat":80,"./operator/resample":81,"./operator/slice":82,"./operator/split":83,"./operator/spread":84,"./operator/subdivide":85,"./operator/swizzle":86,"./operator/transpose":87,"./overlay/dom":88,"./overlay/html":89,"./present/move":90,"./present/play":91,"./present/present":92,"./present/reveal":93,"./present/slide":94,"./present/step":95,"./rtt/compose":98,"./rtt/rtt":99,"./shader/shader":100,"./text/format":101,"./text/label":102,"./text/retext":103,"./text/text":104,"./time/clock":105,"./time/now":106,"./transform/fragment":108,"./transform/layer":109,"./transform/mask":110,"./transform/transform3":112,"./transform/transform4":113,"./transform/vertex":114,"./view/cartesian":116,"./view/cartesian4":117,"./view/polar":118,"./view/spherical":119,"./view/stereographic":120,"./view/stereographic4":121,"./view/view":122}],53:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("./matrix"),o=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","buffer","active","data","source","index","matrix","texture","raw","span:x","span:y","area","sampler:x","sampler:y"],e.prototype.updateSpan=function(){var t,e,n,r,i,o,s,a,u,h,l,c,p;return n=this.props.axes,p=this.props.width,r=this.props.height,t=this.props.centeredX,e=this.props.centeredY,s=this.props.paddingX,a=this.props.paddingY,u=this._helpers.span.get("x.",n[0]),h=this._helpers.span.get("y.",n[1]),this.aX=u.x,this.aY=h.x,l=u.y-u.x,c=h.y-h.x,p+=2*s,r+=2*a,t?(i=1/Math.max(1,p),this.aX+=l*i/2):i=1/Math.max(1,p-1),e?(o=1/Math.max(1,r),this.aY+=c*o/2):o=1/Math.max(1,r-1),this.bX=l*i,this.bY=c*o,this.aX+=s*this.bX,this.aY+=a*this.bY},e.prototype.callback=function(t){return this.updateSpan(),this.last===t?this._callback:(this.last=t,t.length<=5?this._callback=function(e){return function(n,r,i){var o,s;return o=e.aX+e.bX*r,s=e.aY+e.bY*i,t(n,o,s,r,i)}}(this):this._callback=function(e){return function(n,r,i){var o,s;return o=e.aX+e.bX*r,s=e.aY+e.bY*i,t(n,o,s,r,i,e.bufferClock,e.bufferStep)}}(this))},e.prototype.make=function(){return e.__super__.make.apply(this,arguments),this._helpers.span.make(),this._listen(this,"span.range",this.updateSpan)},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),this._helpers.span.unmake()},e}(i),e.exports=r},{"../../../util":175,"./matrix":58}],54:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("./buffer"),o=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","buffer","active","data","source","index","array","texture","raw"],e.prototype.init=function(){return this.buffer=this.spec=null,this.space={width:0,history:0},this.used={width:0},this.storage="arrayBuffer",this.passthrough=function(t,e){return t(e,0,0,0)},e.__super__.init.apply(this,arguments)},e.prototype.sourceShader=function(t){ -var e;return e=this.getDimensions(),this.alignShader(e,t),this.buffer.shader(t)},e.prototype.getDimensions=function(){return{items:this.items,width:this.space.width,height:this.space.history,depth:1}},e.prototype.getActiveDimensions=function(){return{items:this.items,width:this.used.width,height:this.buffer.getFilled(),depth:1}},e.prototype.getFutureDimensions=function(){return{items:this.items,width:this.used.width,height:this.space.history,depth:1}},e.prototype.getRawDimensions=function(){return{items:this.items,width:space.width,height:1,depth:1}},e.prototype.make=function(){var t,n,r,i,s,a,u,h,l,c,p,f,d,m;return e.__super__.make.apply(this,arguments),u=null!=(h=this.minFilter)?h:this.props.minFilter,a=null!=(l=this.magFilter)?l:this.props.magFilter,d=null!=(c=this.type)?c:this.props.type,m=this.props.width,i=this.props.history,p=this.props.bufferWidth,t=this.props.channels,s=this.props.items,r=this.spec={channels:t,items:s,width:m},this.items=r.items,this.channels=r.channels,n=this.props.data,r=o.Data.getDimensions(n,r),f=this.space,f.width=Math.max(p,r.width||1),f.history=i,this.buffer=this._renderables.make(this.storage,{width:f.width,history:f.history,channels:t,items:s,minFilter:u,magFilter:a,type:d})},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),this.buffer?(this.buffer.dispose(),this.buffer=this.spec=null):void 0},e.prototype.change=function(t,e,n){var r;if(e.texture||t["history.history"]||t["buffer.channels"]||t["buffer.items"]||t["array.bufferWidth"])return this.rebuild();if(this.buffer)return t["array.width"]&&(r=this.props.width,r>this.space.width)?this.rebuild():t["data.map"]||t["data.data"]||t["data.resolve"]||t["data.expr"]||n?this.buffer.setCallback(this.emitter()):void 0},e.prototype.callback=function(t){return t.length<=2?t:function(e){return function(n,r){return t(n,r,e.bufferClock,e.bufferStep)}}(this)},e.prototype.update=function(){var t,e,n,r,i;if(this.buffer)return t=this.props.data,r=this.space,i=this.used,n=i.width,e=this.buffer.getFilled(),this.syncBuffer(function(e){return function(n){var s,a,u;return null!=t?(a=o.Data.getDimensions(t,e.spec),a.width>r.width?(n(),e.rebuild()):(i.width=a.width,e.buffer.setActive(i.width),"function"==typeof(s=e.buffer.callback).rebind&&s.rebind(t),e.buffer.update())):(u=e.spec.width||1,e.buffer.setActive(u),u=e.buffer.update(),i.width=u)}}(this)),i.width!==n||e!==this.buffer.getFilled()?this.trigger({type:"source.resize"}):void 0},e}(i),e.exports=r},{"../../../util":175,"./buffer":55}],55:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("./data"),o=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","buffer","active","data","source","index","texture"],e.prototype.init=function(){return this.bufferSlack=0,this.bufferFrames=0,this.bufferTime=0,this.bufferDelta=0,this.bufferClock=0,this.bufferStep=0,e.__super__.init.apply(this,arguments)},e.prototype.make=function(){return e.__super__.make.apply(this,arguments),this.clockParent=this._inherit("clock")},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments)},e.prototype.rawBuffer=function(){return this.buffer},e.prototype.emitter=function(){var t,n,r;return r=this.props,t=r.channels,n=r.items,e.__super__.emitter.call(this,t,n)},e.prototype.change=function(t,e,n){var r;return t["buffer.fps"]||n?(r=this.props.fps,this.bufferSlack=r?.5/r:0):void 0},e.prototype.syncBuffer=function(t){var e,n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b;if(this.buffer&&(d=this.props,c=d.live,i=d.fps,a=d.hurry,l=d.limit,f=d.realtime,p=d.observe,r=this.buffer.getFilled(),!r||c)){if(b=this.clockParent.getTime(),null!=i){for(g=this.bufferSlack,E=b.step/b.delta,n=f?b.delta:b.step,o=1/i,y=f&&p?E*o:o,this.bufferSlack=Math.min(l/i,g+n),this.bufferDelta=n,this.bufferStep=y,s=Math.min(a,Math.floor(g*i)),r||(s=Math.max(1,s)),_=!1,e=function(){return _=!0},v=[],u=h=0,m=s;(m>=0?m>h:h>m)&&(this.bufferTime+=n,this.bufferClock+=y,!_);u=m>=0?++h:--h)t(e,this.bufferFrames++,u,s),v.push(this.bufferSlack-=o);return v}return this.bufferTime=b.time,this.bufferDelta=b.delta,this.bufferClock=b.clock,this.bufferStep=b.step,t(function(){},this.bufferFrames++,0,1)}},e.prototype.alignShader=function(t,e){var n,r,i,o,s,a;return a=this.props,i=a.minFilter,r=a.magFilter,n=a.aligned,o=t.items>1&&t.width>1||t.height>1&&t.depth>1,!n&&o?(s=i===this.node.attributes["texture.minFilter"]["enum"].nearest&&r===this.node.attributes["texture.magFilter"]["enum"].nearest,s||console.warn(this.node.toString()+" - Cannot use linear min/magFilter with 3D/4D sampling"),e.pipe("map.xyzw.align")):void 0},e}(i),e.exports=r},{"../../../util":175,"./data":56}],56:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("../base/source"),o=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","data","source","index","entity","active"],e.prototype.init=function(){return this.dataEmitter=null,this.dataSizes=null},e.prototype.emitter=function(t,e){var n,r,i,s,a,u,h,l;return r=this.props.data,n=this.props.bind,s=this.props.expr,null!=r?(a=this.dataSizes,h=o.Data.getSizes(r),a&&a.length===h.length||(l=o.Data.getThunk(r),this.dataEmitter=this.callback(o.Data.makeEmitter(l,e,t)),this.dataSizes=h),i=this.dataEmitter):"undefined"!=typeof u&&null!==u?(u=this._inherit("resolve"),i=this.callback(u.callback(n))):i=null!=s?this.callback(s):this.callback(this.passthrough),i},e.prototype.callback=function(t){return null!=t?t:function(){}},e.prototype.update=function(){},e.prototype.make=function(){return this._helpers.active.make(),this.first=!0,this._listen("root","root.update",function(t){return function(){return(t.isActive||t.first)&&t.update(),t.first=!1}}(this))},e.prototype.unmake=function(){return this._helpers.active.unmake(),this.dataEmitter=null,this.dataSizes=null},e}(i),e.exports=r},{"../../../util":175,"../base/source":49}],57:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;o=t("./array"),i=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","buffer","active","data","source","index","texture","array","span","interval","sampler","raw"],e.prototype.updateSpan=function(){var t,e,n,r,i,o,s;return e=this.props.axis,s=this.props.width,t=this.props.centered,r=this.props.padding,i=this._helpers.span.get("",e),s+=2*r,this.a=i.x,o=i.y-i.x,t?(n=1/Math.max(1,s),this.a+=o*n/2):n=1/Math.max(1,s-1),this.b=o*n,this.a+=r*this.b},e.prototype.callback=function(t){return this.updateSpan(),this.last===t?this._callback:(this.last=t,t.length<=3?this._callback=function(e){return function(n,r){var i;return i=e.a+e.b*r,t(n,i,r)}}(this):this._callback=function(e){return function(n,r){var i;return i=e.a+e.b*r,t(n,i,r,e.bufferClock,e.bufferStep)}}(this))},e.prototype.make=function(){return e.__super__.make.apply(this,arguments),this._helpers.span.make(),this._listen(this,"span.range",this.updateSpan)},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),this._helpers.span.unmake()},e}(o),e.exports=r},{"../../../util":175,"./array":54}],58:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./buffer"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","buffer","active","data","source","index","texture","matrix","raw"],e.prototype.init=function(){return this.buffer=this.spec=null,this.space={width:0,height:0,history:0},this.used={width:0,height:0},this.storage="matrixBuffer",this.passthrough=function(t,e,n){return t(e,n,0,0)},e.__super__.init.apply(this,arguments)},e.prototype.sourceShader=function(t){var e;return e=this.getDimensions(),this.alignShader(e,t),this.buffer.shader(t)},e.prototype.getDimensions=function(){return{items:this.items,width:this.space.width,height:this.space.height,depth:this.space.history}},e.prototype.getActiveDimensions=function(){return{items:this.items,width:this.used.width,height:this.used.height,depth:this.buffer.getFilled()}},e.prototype.getFutureDimensions=function(){return{items:this.items,width:this.used.width,height:this.used.height,depth:this.space.history}},e.prototype.getRawDimensions=function(){return{items:this.items,width:this.space.width,height:this.space.height,depth:1}},e.prototype.make=function(){var t,n,r,i,s,a,u,h,l,c,p,f,d,m,v,g;return e.__super__.make.apply(this,arguments),h=null!=(l=this.minFilter)?l:this.props.minFilter,u=null!=(c=this.magFilter)?c:this.props.magFilter,v=null!=(p=this.type)?p:this.props.type,g=this.props.width,i=this.props.height,s=this.props.history,f=this.props.bufferWidth,d=this.props.bufferHeight,t=this.props.channels,a=this.props.items,r=this.spec={channels:t,items:a,width:g,height:i},this.items=r.items,this.channels=r.channels,n=this.props.data,r=o.Data.getDimensions(n,r),m=this.space,m.width=Math.max(f,r.width||1),m.height=Math.max(d,r.height||1),m.history=s,this.buffer=this._renderables.make(this.storage,{width:m.width,height:m.height,history:m.history,channels:t,items:a,minFilter:h,magFilter:u,type:v})},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),this.buffer?(this.buffer.dispose(),this.buffer=this.spec=null):void 0},e.prototype.change=function(t,e,n){var r,i;if(e.texture||t["matrix.history"]||t["buffer.channels"]||t["buffer.items"]||t["matrix.bufferWidth"]||t["matrix.bufferHeight"])return this.rebuild();if(this.buffer)return t["matrix.width"]&&(i=this.props.width,i>this.space.width)?this.rebuild():t["matrix.height"]&&(r=this.props.height,r>this.space.height)?this.rebuild():t["data.map"]||t["data.data"]||t["data.resolve"]||t["data.expr"]||n?this.buffer.setCallback(this.emitter()):void 0},e.prototype.callback=function(t){return t.length<=3?t:function(e){return function(n,r,i){return t(n,r,i,e.bufferClock,e.bufferStep)}}(this)},e.prototype.update=function(){var t,e,n,r,i,s;if(this.buffer)return t=this.props.data,r=this.space,i=this.used,s=i.width,n=i.height,e=this.buffer.getFilled(),this.syncBuffer(function(e){return function(n){var s,a,u,h,l,c;return null!=t?(u=o.Data.getDimensions(t,e.spec),u.width>r.width||u.height>r.height?(n(),e.rebuild()):(i.width=u.width,i.height=u.height,e.buffer.setActive(i.width,i.height),"function"==typeof(a=e.buffer.callback).rebind&&a.rebind(t),e.buffer.update())):(c=e.spec.width||1,h=e.spec.height||1,e.buffer.setActive(c,h),l=e.buffer.update(),i.width=s=c,i.height=Math.ceil(l/s),1===i.height?i.width=l:void 0)}}(this)),i.width!==s||i.height!==n||e!==this.buffer.getFilled()?this.trigger({type:"source.resize"}):void 0},e}(r),e.exports=i},{"../../../util":175,"./buffer":55}],59:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("../base/source"),o=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","source","index","interval","span","scale","raw","origin"],e.prototype.init=function(){return this.used=this.space=this.scaleAxis=this.sampler=null},e.prototype.rawBuffer=function(){return this.buffer},e.prototype.sourceShader=function(t){return t.pipe(this.sampler)},e.prototype.getDimensions=function(){return{items:1,width:this.space,height:1,depth:1}},e.prototype.getActiveDimensions=function(){return{items:1,width:this.used,height:this.buffer.getFilled(),depth:1}},e.prototype.getRawDimensions=function(){return this.getDimensions()},e.prototype.make=function(){var t,e,n;return this.space=n=this._helpers.scale.divide(""),this.buffer=this._renderables.make("dataBuffer",{width:n,channels:1,items:1}),e={scaleAxis:this._attributes.make(this._types.vec4()),scaleOffset:this._attributes.make(this._types.vec4())},this.scaleAxis=e.scaleAxis.value,this.scaleOffset=e.scaleOffset.value,t=this.sampler=this._shaders.shader(),t.require(this.buffer.shader(this._shaders.shader(),1)),t.pipe("scale.position",e),this._helpers.span.make(),this._listen(this,"span.range",this.updateRanges)},e.prototype.unmake=function(){return this.scaleAxis=null,this._helpers.span.unmake()},e.prototype.change=function(t,e,n){return t["scale.divide"]?this.rebuild():e.view||e.interval||e.span||e.scale||n?this.updateRanges():void 0},e.prototype.updateRanges=function(){var t,e,n,r,i,s,a,u;return u=this.used,s=this.props,t=s.axis,r=s.origin,i=this._helpers.span.get("",t),n=i.x,e=i.y,a=this._helpers.scale.generate("",this.buffer,n,e),o.Axis.setDimension(this.scaleAxis,t),o.Axis.setOrigin(this.scaleOffset,t,r),this.used=a.length,this.used!==u?this.trigger({type:"source.resize"}):void 0},e}(i),e.exports=r},{"../../../util":175,"../base/source":49}],60:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;o=t("./voxel"),r=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","buffer","active","data","source","index","texture","voxel","span:x","span:y","span:z","volume","sampler:x","sampler:y","sampler:z","raw"],e.prototype.updateSpan=function(){var t,e,n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E;return i=this.props.axes,E=this.props.width,o=this.props.height,r=this.props.depth,t=this.props.centeredX,e=this.props.centeredY,n=this.props.centeredZ,h=this.props.paddingX,l=this.props.paddingY,c=this.props.paddingZ,p=this._helpers.span.get("x.",i[0]),f=this._helpers.span.get("y.",i[1]),d=this._helpers.span.get("z.",i[2]),this.aX=p.x,this.aY=f.x,this.aZ=d.x,m=p.y-p.x,v=f.y-f.x,g=d.y-d.x,E+=2*h,o+=2*l,r+=2*c,t?(s=1/Math.max(1,E),this.aX+=m*s/2):s=1/Math.max(1,E-1),e?(a=1/Math.max(1,o),this.aY+=v*a/2):a=1/Math.max(1,o-1),n?(u=1/Math.max(1,r),this.aZ+=g*u/2):u=1/Math.max(1,r-1),this.bX=m*s,this.bY=v*a,this.bZ=g*u,this.aX+=this.bX*h,this.aY+=this.bY*l,this.aZ+=this.bZ*l},e.prototype.callback=function(t){return this.updateSpan(),this.last===t?this._callback:(this.last=t,t.length<=7?this._callback=function(e){return function(n,r,i,o){var s,a,u;return s=e.aX+e.bX*r,a=e.aY+e.bY*i,u=e.aZ+e.bZ*o,t(n,s,a,u,r,i,o)}}(this):this._callback=function(e){return function(n,r,i,o){var s,a,u;return s=e.aX+e.bX*r,a=e.aY+e.bY*i,u=e.aZ+e.bZ*o,t(n,s,a,u,r,i,o,e.bufferClock,e.bufferStep)}}(this))},e.prototype.make=function(){return e.__super__.make.apply(this,arguments),this._helpers.span.make(),this._listen(this,"span.range",this.updateSpan)},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),this._helpers.span.unmake()},e}(o),e.exports=i},{"../../../util":175,"./voxel":61}],61:[function(t,e,n){var r,i,o,s=function(t,e){return function(){return t.apply(e,arguments)}},a=function(t,e){function n(){this.constructor=t}for(var r in e)u.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},u={}.hasOwnProperty;r=t("./buffer"),i=t("../../../util"),o=function(t){function e(){return this.update=s(this.update,this),e.__super__.constructor.apply(this,arguments)}return a(e,t),e.traits=["node","buffer","active","data","source","index","texture","voxel","raw"],e.prototype.init=function(){return this.buffer=this.spec=null,this.space={width:0,height:0,depth:0},this.used={width:0,height:0,depth:0},this.storage="voxelBuffer",this.passthrough=function(t,e,n,r){return t(e,n,r,0)},e.__super__.init.apply(this,arguments)},e.prototype.sourceShader=function(t){var e;return e=this.getDimensions(),this.alignShader(e,t),this.buffer.shader(t)},e.prototype.getDimensions=function(){return{items:this.items,width:this.space.width,height:this.space.height,depth:this.space.depth}},e.prototype.getActiveDimensions=function(){return{items:this.items,width:this.used.width,height:this.used.height,depth:this.used.depth*this.buffer.getFilled()}},e.prototype.getRawDimensions=function(){return this.getDimensions()},e.prototype.make=function(){var t,n,r,o,s,a,u,h,l,c,p,f,d,m,v,g,E;return e.__super__.make.apply(this,arguments),h=null!=(l=this.minFilter)?l:this.props.minFilter,u=null!=(c=this.magFilter)?c:this.props.magFilter,g=null!=(p=this.type)?p:this.props.type,E=this.props.width,s=this.props.height,r=this.props.depth,f=this.props.bufferWidth,d=this.props.bufferHeight,m=this.props.bufferDepth,t=this.props.channels,a=this.props.items,o=this.spec={channels:t,items:a,width:E,height:s,depth:r},this.items=o.items,this.channels=o.channels,n=this.props.data,o=i.Data.getDimensions(n,o),v=this.space,v.width=Math.max(f,o.width||1),v.height=Math.max(d,o.height||1),v.depth=Math.max(m,o.depth||1),this.buffer=this._renderables.make(this.storage,{width:v.width,height:v.height,depth:v.depth,channels:t,items:a,minFilter:h,magFilter:u,type:g})},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),this.buffer?(this.buffer.dispose(),this.buffer=this.spec=null):void 0},e.prototype.change=function(t,e,n){var r,i,o;if(e.texture||t["buffer.channels"]||t["buffer.items"]||t["voxel.bufferWidth"]||t["voxel.bufferHeight"]||t["voxel.bufferDepth"])return this.rebuild();if(this.buffer)return t["voxel.width"]&&(o=this.props.width,o>this.space.width)?this.rebuild():t["voxel.height"]&&(i=this.props.height,i>this.space.height)?this.rebuild():t["voxel.depth"]&&(r=this.props.depth,r>this.space.depth)?this.rebuild():t["data.map"]||t["data.data"]||t["data.resolve"]||t["data.expr"]||n?this.buffer.setCallback(this.emitter()):void 0},e.prototype.callback=function(t){return t.length<=4?t:function(e){return function(n,r,i,o){return t(n,r,i,o,e.bufferClock,e.bufferStep)}}(this)},e.prototype.update=function(){var t,e,n,r,o,s,a;if(this.buffer)return e=this.props.data,o=this.space,s=this.used,a=s.width,r=s.height,t=s.depth,n=this.buffer.getFilled(),this.syncBuffer(function(t){return function(n){var r,a,u,h,l,c,p,f;return null!=e?(l=i.Data.getDimensions(e,t.spec),l.width>o.width||l.height>o.height||l.depth>o.depth?(n(),t.rebuild()):(s.width=l.width,s.height=l.height,s.depth=l.depth,t.buffer.setActive(s.width,s.height,s.depth),"function"==typeof(u=t.buffer.callback).rebind&&u.rebind(e),t.buffer.update())):(f=t.spec.width||1,c=t.spec.height||1,h=t.spec.depth||1,t.buffer.setActive(f,c,h),p=t.buffer.update(),s.width=a=f,s.height=r=c,s.depth=Math.ceil(p/a/r),1===s.depth&&(s.height=Math.ceil(p/a),1===s.height)?s.width=p:void 0)}}(this)),s.width!==a||s.height!==r||s.depth!==t||n!==this.buffer.getFilled()?this.trigger({type:"source.resize"}):void 0},e}(r),e.exports=o},{"../../../util":175,"./buffer":55}],62:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("../../primitive"),o=t("../../../util"),r=function(t){function e(t,n,r){e.__super__.constructor.call(this,t,n,r),this.axisPosition=this.axisStep=this.resolution=this.line=this.arrows=null}return s(e,t),e.traits=["node","object","visible","style","line","axis","span","interval","arrow","position","origin","shade"],e.defaults={end:!0,zBias:-1},e.prototype.make=function(){var t,e,n,r,i,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b;return c={axisPosition:this._attributes.make(this._types.vec4()),axisStep:this._attributes.make(this._types.vec4())},this.axisPosition=c.axisPosition.value,this.axisStep=c.axisStep.value,l=this._shaders.shader(),l.pipe("axis.position",c),l=this._helpers.position.pipeline(l),E=this._helpers.style.uniforms(),a=this._helpers.line.uniforms(),t=this._helpers.arrow.uniforms(),b=this._inherit("unit").getUnitUniforms(),r=this.props.detail,m=r+1,this.resolution=1/r,p=this.props,v=p.start,i=p.end,f=this.props,g=f.stroke,s=f.join,u=this._helpers.object.mask(),h=this._helpers.shade.pipeline()||!1,d=this.props,n=d.crossed,e=d.axis,!n&&null!=u&&e>1&&(y=["x000","y000","z000","w000"][e],u=this._helpers.position.swizzle(u,y)),_=o.JS.merge(t,a,E,b),this.line=this._renderables.make("line",{uniforms:_,samples:m,position:l,clip:v||i,stroke:g,join:s,mask:u,material:h}),this.arrows=[],v&&this.arrows.push(this._renderables.make("arrow",{uniforms:_,flip:!0,samples:m,position:l,mask:u,material:h})),i&&this.arrows.push(this._renderables.make("arrow",{uniforms:_,samples:m,position:l,mask:u,material:h})),this._helpers.visible.make(),this._helpers.object.make(this.arrows.concat([this.line])),this._helpers.span.make(),this._listen(this,"span.range",this.updateRanges)},e.prototype.unmake=function(){return this._helpers.visible.unmake(),this._helpers.object.unmake(),this._helpers.span.unmake()},e.prototype.change=function(t,e,n){return t["axis.detail"]||t["line.stroke"]||t["line.join"]||t["axis.crossed"]||t["interval.axis"]&&this.props.crossed?this.rebuild():e.interval||e.span||e.view||n?this.updateRanges():void 0},e.prototype.updateRanges=function(){var t,e,n,r,i,s;return s=this.props,t=s.axis,r=s.origin,i=this._helpers.span.get("",t),n=i.x,e=i.y,o.Axis.setDimension(this.axisPosition,t).multiplyScalar(n),o.Axis.setDimension(this.axisStep,t).multiplyScalar((e-n)*this.resolution),o.Axis.addOrigin(this.axisPosition,t,r)},e}(i),e.exports=r},{"../../../util":175,"../../primitive":44}],63:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("../../primitive"),o=t("../../../util"),r=function(t){function e(t,n,r){e.__super__.constructor.call(this,t,n,r),this.face=null}return s(e,t),e.traits=["node","object","visible","style","line","mesh","face","geometry","position","bind","shade"],e.prototype.resize=function(){var t,e,n,r,i,o;if(null!=this.bind.points)return e=this.bind.points.getActiveDimensions(),r=e.items,o=e.width,n=e.height,t=e.depth,this.face&&this.face.geometry.clip(o,n,t,r),this.line&&this.line.geometry.clip(r,o,n,t),null!=this.bind.map&&(i=this.bind.map.getActiveDimensions(),this.face)?this.face.geometry.map(i.width,i.height,i.depth,i.items):void 0},e.prototype.make=function(){var t,e,n,r,i,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H;return this._helpers.bind.make([{to:"geometry.points",trait:"source"},{to:"geometry.colors",trait:"source"},{to:"mesh.map",trait:"source"}]),null!=this.bind.points?(v=this.bind.points.sourceShader(this._shaders.shader()),v=this._helpers.position.pipeline(v),b=this._helpers.style.uniforms(),c=this._helpers.line.uniforms(),w=this._inherit("unit").getUnitUniforms(),H={},H.styleZBias=this._attributes.make(this._types.number()),this.wireZBias=H.styleZBias,n=this.bind.points.getDimensions(),a=n.items,R=n.width,s=n.height,e=n.depth,g=this.props,h=g.line,y=g.shaded,i=g.fill,_=g.stroke,u=g.join,this.bind.colors&&(t=this._shaders.shader(),this.bind.colors.sourceShader(t)),f=this._helpers.object.mask(),p=this._helpers.shade.map(null!=(E=this.bind.map)?E.sourceShader(this._shaders.shader()):void 0),d=this._helpers.shade.pipeline(),r=d||y,l=d||!1,m=[],h&&(T=this._shaders.shader(),T.pipe(o.GLSL.swizzleVec4("yzwx")),T.pipe(v),x=o.JS.merge(w,c,b,H),this.line=this._renderables.make("line",{uniforms:x,samples:a,strips:R,ribbons:s,layers:e,position:T,color:t,stroke:_,join:u,material:l,mask:f,closed:!0}),m.push(this.line)),i&&(x=o.JS.merge(w,b,{}),this.face=this._renderables.make("face",{uniforms:x,width:R,height:s,depth:e,items:a,position:v,color:t,material:r,mask:f,map:p}),m.push(this.face)),this._helpers.visible.make(),this._helpers.object.make(m)):void 0},e.prototype.made=function(){return this.resize()},e.prototype.unmake=function(){return this._helpers.bind.unmake(),this._helpers.visible.unmake(),this._helpers.object.unmake(),this.face=this.line=null},e.prototype.change=function(t,e,n){var r,i,o,s;return t["geometry.points"]||e.mesh?this.rebuild():t["style.zBias"]||t["mesh.lineBias"]||n?(o=this.props,r=o.fill,s=o.zBias,i=o.lineBias,this.wireZBias.value=s+(r?i:0)):void 0},e}(i),e.exports=r},{"../../../util":175,"../../primitive":44}],64:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("../../primitive"),o=t("../../../util"),r=function(t){function e(t,n,r){e.__super__.constructor.call(this,t,n,r),this.axes=null}return s(e,t),e.traits=["node","object","visible","style","line","grid","area","position","origin","shade","axis:x","axis:y","scale:x","scale:y","span:x","span:y"],e.defaults={width:1,zBias:-2},e.prototype.make=function(){var t,e,n,r,i,s,a,u,h,l,c,p,f;return u=this._helpers.object.mask(),h=this._helpers.shade.pipeline()||!1,e=function(t){return function(e,n,i){var s,a,l,c,f,d,m,v,g,E,y,_,b,T;return a=t._get(e+"axis.detail"),g=a+1,v=1/a,E=t._helpers.scale.divide(n),s=t._renderables.make("dataBuffer",{width:E,channels:1}),m={gridPosition:t._attributes.make(t._types.vec4()),gridStep:t._attributes.make(t._types.vec4()),gridAxis:t._attributes.make(t._types.vec4())},T={gridPosition:m.gridPosition.value,gridStep:m.gridStep.value,gridAxis:m.gridAxis.value},f=d=t._shaders.shader(),null!=i&&null!=u&&(u=t._helpers.position.swizzle(u,i)),f.require(s.shader(t._shaders.shader(),2)),f.pipe("grid.position",m),d=t._helpers.position.pipeline(f),y=t._helpers.style.uniforms(),c=t._helpers.line.uniforms(),b=t._inherit("unit").getUnitUniforms(),_=o.JS.merge(c,y,b),l=t._renderables.make("line",{uniforms:_,samples:g,strips:E,position:d,stroke:p,join:r,mask:u,material:h}),{first:e,second:n,resolution:v,samples:g,line:l,buffer:s,values:T}}}(this),l=this.props,i=l.lineX,s=l.lineY,n=l.crossed,t=l.axes,f=["0000","x000","y000","z000","w000"][t[1]],c=this.props,p=c.stroke,r=c.join,this.axes=[],i&&this.axes.push(e("x.","y.",null)),s&&this.axes.push(e("y.","x.",n?null:f)),a=function(){var t,n,r,i;for(r=this.axes,i=[],t=0,n=r.length;n>t;t++)e=r[t],i.push(e.line);return i}.call(this),this._helpers.visible.make(),this._helpers.object.make(a),this._helpers.span.make(),this._listen(this,"span.range",this.updateRanges)},e.prototype.unmake=function(){var t,e,n,r;for(this._helpers.visible.unmake(),this._helpers.object.unmake(),this._helpers.span.unmake(),r=this.axes,e=0,n=r.length;n>e;e++)t=r[e],t.buffer.dispose();return this.axes=null},e.prototype.change=function(t,e,n){return t["x.axis.detail"]||t["y.axis.detail"]||t["x.axis.factor"]||t["y.axis.factor"]||t["grid.lineX"]||t["grid.lineY"]||t["line.stroke"]||t["line.join"]||t["grid.crossed"]||t["grid.axes"]&&this.props.crossed?this.rebuild():e.x||e.y||e.area||e.grid||e.view||n?this.updateRanges():void 0},e.prototype.updateRanges=function(){var t,e,n,r,i,s,a,u,h;return e=function(e){return function(n,r,s,a,u){var h,l,c,p,f,d,m,v,g,E,y;return l=u.first,g=u.second,m=u.resolution,v=u.samples,c=u.line,h=u.buffer,y=u.values,f=s.x,p=s.y,o.Axis.setDimension(y.gridPosition,n).multiplyScalar(f),o.Axis.setDimension(y.gridStep,n).multiplyScalar((p-f)*m),o.Axis.addOrigin(y.gridPosition,t,i),f=a.x,p=a.y,E=e._helpers.scale.generate(g,h,f,p),o.Axis.setDimension(y.gridAxis,r),d=E.length,c.geometry.clip(v,d,1,1)}}(this),u=this.props,t=u.axes,i=u.origin,s=this._helpers.span.get("x.",t[0]),a=this._helpers.span.get("y.",t[1]),h=this.props,n=h.lineX,r=h.lineY,n&&e(t[0],t[1],s,a,this.axes[0]),r?e(t[1],t[0],a,s,this.axes[+n]):void 0},e}(i),e.exports=r},{"../../../util":175,"../../primitive":44}],65:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("../../primitive"),o=t("../../../util"),r=function(t){function e(t,n,r){e.__super__.constructor.call(this,t,n,r),this.line=this.arrows=null}return s(e,t),e.traits=["node","object","visible","style","line","arrow","geometry","position","bind","shade"],e.prototype.resize=function(){var t,e,n,r,i,o,s,a,u,h;if(null!=this.bind.points){for(e=this.bind.points.getActiveDimensions(),u=e.width,h=e.height,a=e.depth,r=e.items,this.line.geometry.clip(u,h,a,r),o=this.arrows,s=[],n=0,i=o.length;i>n;n++)t=o[n],s.push(t.geometry.clip(u,h,a,r));return s}},e.prototype.make=function(){var t,e,n,r,i,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b;return this._helpers.bind.make([{to:"geometry.points",trait:"source"},{to:"geometry.colors",trait:"source"}]),null!=this.bind.points?(l=this._shaders.shader(),l=this.bind.points.sourceShader(l),l=this._helpers.position.pipeline(l),y=this._helpers.style.uniforms(),a=this._helpers.line.uniforms(),t=this._helpers.arrow.uniforms(),b=this._inherit("unit").getUnitUniforms(),p=this.props,v=p.start,r=p.end,f=this.props,E=f.stroke,i=f.join,c=f.proximity,this.proximity=c,n=this.bind.points.getDimensions(),m=n.width,g=n.height,d=n.depth,s=n.items,this.bind.colors&&(e=this._shaders.shader(),this.bind.colors.sourceShader(e)),u=this._helpers.object.mask(),h=this._helpers.shade.pipeline()||!1,_=o.JS.merge(t,a,y,b),this.line=this._renderables.make("line",{uniforms:_,samples:m,strips:g,ribbons:d,layers:s,position:l,color:e,clip:v||r,stroke:E,join:i,proximity:c,mask:u,material:h}),this.arrows=[],v&&this.arrows.push(this._renderables.make("arrow",{uniforms:_,flip:!0,samples:m,strips:g,ribbons:d,layers:s,position:l,color:e,mask:u,material:h})),r&&this.arrows.push(this._renderables.make("arrow",{uniforms:_,samples:m,strips:g,ribbons:d,layers:s,position:l,color:e,mask:u,material:h})),this._helpers.visible.make(),this._helpers.object.make(this.arrows.concat([this.line]))):void 0},e.prototype.made=function(){return this.resize()},e.prototype.unmake=function(){return this._helpers.bind.unmake(),this._helpers.visible.unmake(),this._helpers.object.unmake(),this.line=this.arrows=null},e.prototype.change=function(t,e,n){return t["geometry.points"]||t["line.stroke"]||t["line.join"]||t["arrow.start"]||t["arrow.end"]?this.rebuild():t["line.proximity"]&&null!=this.proximity!=(null!=this.props.proximity)?this.rebuild():void 0},e}(i),e.exports=r},{"../../../util":175,"../../primitive":44}],66:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("../../primitive"),o=t("../../../util"),r=function(t){function e(t,n,r){e.__super__.constructor.call(this,t,n,r),this.point=null}return s(e,t),e.traits=["node","object","visible","style","point","geometry","position","bind","shade"],e.prototype.resize=function(){var t,e,n,r,i;if(null!=this.bind.points)return e=this.bind.points.getActiveDimensions(),r=e.items,i=e.width,n=e.height,t=e.depth,this.point.geometry.clip(i,n,t,r)},e.prototype.make=function(){var t,e,n,r,i,s,a,u,h,l,c,p,f,d,m,v,g;return this._helpers.bind.make([{to:"geometry.points",trait:"source"},{to:"geometry.colors",trait:"source"},{to:"point.sizes",trait:"source"}]),null!=this.bind.points?(c=this._shaders.shader(),c=this.bind.points.sourceShader(c),c=this._helpers.position.pipeline(c),n=this.bind.points.getDimensions(),s=n.items,g=n.width,i=n.height,e=n.depth,d=this._helpers.style.uniforms(),l=this._helpers.point.uniforms(),v=this._inherit("unit").getUnitUniforms(),this.bind.colors&&(t=this._shaders.shader(),this.bind.colors.sourceShader(t)),this.bind.sizes&&(f=this._shaders.shader(),this.bind.sizes.sourceShader(f)), -a=this._helpers.object.mask(),u=this._helpers.shade.pipeline()||!1,p=this.props.shape,r=this.props.fill,h=this.props.optical,m=o.JS.merge(v,l,d),this.point=this._renderables.make("point",{uniforms:m,width:g,height:i,depth:e,items:s,position:c,color:t,size:f,shape:p,optical:h,fill:r,mask:a,material:u}),this._helpers.visible.make(),this._helpers.object.make([this.point])):void 0},e.prototype.made=function(){return this.resize()},e.prototype.unmake=function(){return this._helpers.bind.unmake(),this._helpers.visible.unmake(),this._helpers.object.unmake(),this.point=null},e.prototype.change=function(t,e,n){return t["geometry.points"]||t["point.shape"]||t["point.fill"]?this.rebuild():void 0},e}(i),e.exports=r},{"../../../util":175,"../../primitive":44}],67:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("../../primitive"),o=t("../../../util"),i=function(t){function e(t,n,r){e.__super__.constructor.call(this,t,n,r),this.strip=null}return s(e,t),e.traits=["node","object","visible","style","line","mesh","strip","geometry","position","bind","shade"],e.prototype.resize=function(){var t,e,n,r,i,o;if(null!=this.bind.points)return e=this.bind.points.getActiveDimensions(),r=e.items,o=e.width,n=e.height,t=e.depth,this.strip&&this.strip.geometry.clip(o,n,t,r),this.line&&this.line.geometry.clip(r,o,n,t),null!=this.bind.map&&(i=this.bind.map.getActiveDimensions(),this.strip)?this.strip.geometry.map(i.width,i.height,i.depth,i.items):void 0},e.prototype.make=function(){var t,e,n,r,i,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H;return this._helpers.bind.make([{to:"geometry.points",trait:"source"},{to:"geometry.colors",trait:"source"},{to:"mesh.map",trait:"source"}]),null!=this.bind.points?(v=this._shaders.shader(),v=this.bind.points.sourceShader(v),v=this._helpers.position.pipeline(v),b=this._helpers.style.uniforms(),c=this._helpers.line.uniforms(),w=this._inherit("unit").getUnitUniforms(),h=this.props.line,y=this.props.shaded,i=this.props.fill,H={},H.styleZBias=this._attributes.make(this._types.number()),this.wireZBias=H.styleZBias,n=this.bind.points.getDimensions(),a=n.items,R=n.width,s=n.height,e=n.depth,g=this.props,h=g.line,y=g.shaded,i=g.fill,_=g.stroke,u=g.join,this.bind.colors&&(t=this._shaders.shader(),t=this.bind.colors.sourceShader(t)),f=this._helpers.object.mask(),p=this._helpers.shade.map(null!=(E=this.bind.map)?E.sourceShader(this._shaders.shader()):void 0),d=this._helpers.shade.pipeline(),r=d||y,l=d||!1,m=[],h&&(T=this._shaders.shader(),T.pipe(o.GLSL.swizzleVec4("yzwx")),T.pipe(v),x=o.JS.merge(w,c,b,H),this.line=this._renderables.make("line",{uniforms:x,samples:a,strips:R,ribbons:s,layers:e,position:T,color:t,stroke:_,join:u,mask:f,material:l}),m.push(this.line)),i&&(x=o.JS.merge(b,{}),this.strip=this._renderables.make("strip",{uniforms:x,width:R,height:s,depth:e,items:a,position:v,color:t,material:r}),m.push(this.strip)),this._helpers.visible.make(),this._helpers.object.make(m)):void 0},e.prototype.made=function(){return this.resize()},e.prototype.unmake=function(){return this._helpers.bind.unmake(),this._helpers.visible.unmake(),this._helpers.object.unmake(),this.strip=null},e.prototype.change=function(t,e,n){var r,i,o,s;return t["geometry.points"]||e.mesh?this.rebuild():t["style.zBias"]||t["mesh.lineBias"]||n?(o=this.props,r=o.fill,s=o.zBias,i=o.lineBias,this.wireZBias.value=s+(r?i:0)):void 0},e}(r),e.exports=i},{"../../../util":175,"../../primitive":44}],68:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("../../primitive"),o=t("../../../util"),i=function(t){function e(t,n,r){e.__super__.constructor.call(this,t,n,r),this.lineX=this.lineY=this.surface=null}return s(e,t),e.traits=["node","object","visible","style","line","mesh","geometry","surface","position","grid","bind","shade"],e.defaults={lineX:!1,lineY:!1},e.prototype.resize=function(){var t,e,n,r,i,o;if(null!=this.bind.points)return e=this.bind.points.getActiveDimensions(),o=e.width,n=e.height,t=e.depth,r=e.items,this.surface&&this.surface.geometry.clip(o,n,t,r),this.lineX&&this.lineX.geometry.clip(o,n,t,r),this.lineY&&this.lineY.geometry.clip(n,o,t,r),null!=this.bind.map&&(i=this.bind.map.getActiveDimensions(),this.surface)?this.surface.geometry.map(i.width,i.height,i.depth,i.items):void 0},e.prototype.make=function(){var t,e,n,r,i,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S,k,A,C,P,L,z,O;return this._helpers.bind.make([{to:"geometry.points",trait:"source"},{to:"geometry.colors",trait:"source"},{to:"mesh.map",trait:"source"}]),null!=this.bind.points?(_=this._shaders.shader(),_=this.bind.points.sourceShader(_),_=this._helpers.position.pipeline(_),M=this._helpers.style.uniforms(),z=this._helpers.style.uniforms(),f=this._helpers.line.uniforms(),S=this._helpers.surface.uniforms(),P=this._inherit("unit").getUnitUniforms(),z.styleColor=this._attributes.make(this._types.color()),z.styleZBias=this._attributes.make(this._types.number()),this.wireColor=z.styleColor.value,this.wireZBias=z.styleZBias,this.wireScratch=new THREE.Color,s=this.bind.points.getDimensions(),L=s.width,h=s.height,i=s.depth,l=s.items,T=this.props,R=T.shaded,u=T.fill,d=T.lineX,m=T.lineY,t=T.closedX,e=T.closedY,H=T.stroke,c=T.join,b=T.proximity,r=T.crossed,y=[],this.proximity=b,this.bind.colors&&(n=this._shaders.shader(),this.bind.colors.sourceShader(n)),g=this._helpers.object.mask(),v=this._helpers.shade.map(null!=(x=this.bind.map)?x.sourceShader(this._shaders.shader()):void 0),E=this._helpers.shade.pipeline(),a=E||R,p=E||!1,w=this._helpers.position,k=w.swizzle,A=w.swizzle2,C=o.JS.merge(P,f,M,z),O=d||m?-50:0,d&&(this.lineX=this._renderables.make("line",{uniforms:C,samples:L,strips:h,ribbons:i,layers:l,position:_,color:n,zUnits:-O,stroke:H,join:c,mask:g,material:p,proximity:b,closed:t||closed}),y.push(this.lineX)),m&&(this.lineY=this._renderables.make("line",{uniforms:C,samples:h,strips:L,ribbons:i,layers:l,position:A(_,"yxzw","yxzw"),color:k(n,"yxzw"),zUnits:-O,stroke:H,join:c,mask:k(g,r?"xyzw":"yxzw"),material:p,proximity:b,closed:e||closed}),y.push(this.lineY)),u&&(C=o.JS.merge(P,S,M),this.surface=this._renderables.make("surface",{uniforms:C,width:L,height:h,surfaces:i,layers:l,position:_,color:n,zUnits:O,stroke:H,material:a,mask:g,map:v,intUV:!0,closedX:t||closed,closedY:e||closed}),y.push(this.surface)),this._helpers.visible.make(),this._helpers.object.make(y)):void 0},e.prototype.made=function(){return this.resize()},e.prototype.unmake=function(){return this._helpers.bind.unmake(),this._helpers.visible.unmake(),this._helpers.object.unmake(),this.lineX=this.lineY=this.surface=null},e.prototype.change=function(t,e,n){var r,i,o,s,a,u;return t["geometry.points"]||t["mesh.shaded"]||t["mesh.fill"]||t["line.stroke"]||t["line.join"]||e.grid?this.rebuild():((t["style.color"]||t["style.zBias"]||t["mesh.fill"]||t["mesh.lineBias"]||n)&&(a=this.props,o=a.fill,i=a.color,u=a.zBias,s=a.lineBias,this.wireZBias.value=u+(o?s:0),this.wireColor.copy(i),o&&(r=this.wireScratch,r.setRGB(i.x,i.y,i.z),r.convertGammaToLinear().multiplyScalar(.75).convertLinearToGamma(),this.wireColor.x=r.r,this.wireColor.y=r.g,this.wireColor.z=r.b)),t["line.proximity"]&&null!=this.proximity!=(null!=this.props.proximity)?this.rebuild():void 0)},e}(r),e.exports=i},{"../../../util":175,"../../primitive":44}],69:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("../../primitive"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","object","visible","style","line","ticks","geometry","position","bind","shade"],e.prototype.init=function(){return this.tickStrip=this.line=null},e.prototype.resize=function(){var t,e,n,r,i;if(null!=this.bind.points)return e=this.bind.points.getActiveDimensions(),t=+(e.items>0),i=e.width*t,r=e.height*t,n=e.depth*t,this.line.geometry.clip(2,i,r,n),this.tickStrip.set(0,i-1)},e.prototype.make=function(){var t,e,n,r,i,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_;return this._helpers.bind.make([{to:"geometry.points",trait:"source"},{to:"geometry.colors",trait:"source"}]),null!=this.bind.points?(v=this._helpers.style.uniforms(),i=this._helpers.line.uniforms(),_=this._inherit("unit").getUnitUniforms(),y=o.JS.merge(i,v,_),l={tickEpsilon:this.node.attributes["ticks.epsilon"],tickSize:this.node.attributes["ticks.size"],tickNormal:this.node.attributes["ticks.normal"],tickStrip:this._attributes.make(this._types.vec2(0,0)),worldUnit:y.worldUnit,focusDepth:y.focusDepth},this.tickStrip=l.tickStrip.value,u=h=this._shaders.shader(),u.require(this.bind.points.sourceShader(this._shaders.shader())),u.require(this._helpers.position.pipeline(this._shaders.shader())),u.pipe("ticks.position",l),c=this.props,m=c.stroke,n=c.join,e=this.bind.points.getDimensions(),d=e.width,f=e.height,r=e.depth,this.bind.colors&&(t=this._shaders.shader(),this.bind.colors.sourceShader(t)),s=this._helpers.object.mask(),a=this._helpers.shade.pipeline()||!1,p=this._helpers.position,g=p.swizzle,E=p.swizzle2,this.line=this._renderables.make("line",{uniforms:y,samples:2,strips:d,ribbons:f,layers:r,position:h,color:t,stroke:m,join:n,mask:g(s,"yzwx"),material:a}),this._helpers.visible.make(),this._helpers.object.make([this.line])):void 0},e.prototype.made=function(){return this.resize()},e.prototype.unmake=function(){return this.line=null,this._helpers.visible.unmake(),this._helpers.object.unmake()},e.prototype.change=function(t,e,n){return t["geometry.points"]||t["line.stroke"]||t["line.join"]?this.rebuild():void 0},e}(r),e.exports=i},{"../../../util":175,"../../primitive":44}],70:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("../../primitive"),i=t("../../../util"),o=function(t){function e(t,n,r){e.__super__.constructor.call(this,t,n,r),this.line=this.arrows=null}return s(e,t),e.traits=["node","object","visible","style","line","arrow","geometry","position","bind","shade"],e.prototype.resize=function(){var t,e,n,r,i,o,s,a,u,h;if(null!=this.bind.points){for(e=this.bind.points.getActiveDimensions(),u=e.items,h=e.width,a=e.height,r=e.depth,this.line.geometry.clip(u,h,a,r),o=this.arrows,s=[],n=0,i=o.length;i>n;n++)t=o[n],s.push(t.geometry.clip(u,h,a,r));return s}},e.prototype.make=function(){var t,e,n,r,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w;return this._helpers.bind.make([{to:"geometry.points",trait:"source"},{to:"geometry.colors",trait:"source"}]),null!=this.bind.points?(l=this._shaders.shader(),this.bind.points.sourceShader(l),this._helpers.position.pipeline(l),_=this._helpers.style.uniforms(),a=this._helpers.line.uniforms(),t=this._helpers.arrow.uniforms(),w=this._inherit("unit").getUnitUniforms(),p=this.props,g=p.start,r=p.end,f=this.props,y=f.stroke,o=f.join,c=f.proximity,this.proximity=c,n=this.bind.points.getDimensions(),v=n.items,E=n.width,m=n.height,s=n.depth,this.bind.colors&&(e=this._shaders.shader(),this.bind.colors.sourceShader(e)),u=this._helpers.object.mask(),h=this._helpers.shade.pipeline()||!1,d=this._helpers.position,b=d.swizzle,T=d.swizzle2,l=T(l,"yzwx","yzwx"),e=b(e,"yzwx"),u=b(u,"yzwx"),h=b(h,"yzwx"),x=i.JS.merge(t,a,_,w),this.line=this._renderables.make("line",{uniforms:x,samples:v,ribbons:m,strips:E,layers:s,position:l,color:e,clip:g||r,stroke:y,join:o,proximity:c,mask:u,material:h}),this.arrows=[],g&&this.arrows.push(this._renderables.make("arrow",{uniforms:x,flip:!0,samples:v,ribbons:m,strips:E,layers:s,position:l,color:e,mask:u,material:h})),r&&this.arrows.push(this._renderables.make("arrow",{uniforms:x,samples:v,ribbons:m,strips:E,layers:s,position:l,color:e,mask:u,material:h})),this._helpers.visible.make(),this._helpers.object.make(this.arrows.concat([this.line]))):void 0},e.prototype.made=function(){return this.resize()},e.prototype.unmake=function(){return this._helpers.bind.unmake(),this._helpers.visible.unmake(),this._helpers.object.unmake(),this.line=this.arrows=null},e.prototype.change=function(t,e,n){return t["geometry.points"]||t["line.stroke"]||t["line.join"]||t["arrow.start"]||t["arrow.end"]?this.rebuild():t["line.proximity"]&&null!=this.proximity!=(null!=this.props.proximity)?this.rebuild():void 0},e}(r),e.exports=o},{"../../../util":175,"../../primitive":44}],71:[function(t,e,n){var r,i,o,s=[].indexOf||function(t){for(var e=0,n=this.length;n>e;e++)if(e in this&&this[e]===t)return e;return-1};r=t("../../util"),i=t("./view/view"),o={bind:{make:function(t){var e,n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E;for(null==this.bind&&(this.bind={}),null==this.bound&&(this.bound=[]),r=0,s=t.length;s>r;r++){if(f=t[r],v=f.to,g=f.trait,l=f.optional,E=f.unique,u=f.multiple,e=f.callback,null==e&&(e=this.rebuild),h=v.split(/\./g).pop(),p=this._get(v),d=null,null!=p)for(m=this,n=!1;!n;)m=d=this._attach(p,g,e,this,m,l,u),i=E&&(null==d||this.bound.indexOf(d)<0),n=u||l||!E||i;if(null!=d)if(null!=this.resize&&this._listen(d,"source.resize",this.resize),e&&this._listen(d,"source.rebuild",e),u)for(o=0,a=d.length;a>o;o++)c=d[o],this.bound.push(c);else this.bound.push(d);this.bind[h]=d}return null},unmake:function(){return this.bind?(delete this.bind,delete this.bound):void 0}},span:{make:function(){return this.spanView=this._inherit("view"),this._listen("view","view.range",function(t){return function(){return t.trigger({type:"span.range"})}}(this))},unmake:function(){return delete this.spanView},get:function(){var t;return t=new THREE.Vector2(-1,1),function(e,n){var r,i,o;return r=this._get(e+"span.range"),null!=r?r:null!=(i=null!=(o=this.spanView)?o.axis(n):void 0)?i:t}}()},scale:{divide:function(t){var e,n;return e=this._get(t+"scale.divide"),n=this._get(t+"scale.factor"),Math.round(2.5*e/n)},generate:function(t,e,n,i){var o,s,a,u,h,l,c,p,f,d;return h=this._get(t+"scale.mode"),s=this._get(t+"scale.divide"),f=this._get(t+"scale.unit"),o=this._get(t+"scale.base"),u=this._get(t+"scale.factor"),c=this._get(t+"scale.start"),a=this._get(t+"scale.end"),d=this._get(t+"scale.zero"),l=this._get(t+"scale.nice"),p=r.Ticks.make(h,n,i,s,f,o,u,c,a,d,l),e.copy(p),p}},style:{uniforms:function(){return{styleColor:this.node.attributes["style.color"],styleOpacity:this.node.attributes["style.opacity"],styleZBias:this.node.attributes["style.zBias"],styleZIndex:this.node.attributes["style.zIndex"]}}},arrow:{uniforms:function(){var t,e,n,r,i;return r=this.props.start,t=this.props.end,n=this._attributes.make(this._types.number(1.25/(r+t))),i=this._attributes.make(this._types.vec2(+r,+t)),e=this.node.attributes["arrow.size"],{clipStyle:i,clipRange:e,clipSpace:n,arrowSpace:n,arrowSize:e}}},point:{uniforms:function(){return{pointSize:this.node.attributes["point.size"],pointDepth:this.node.attributes["point.depth"]}}},line:{uniforms:function(){return{lineWidth:this.node.attributes["line.width"],lineDepth:this.node.attributes["line.depth"],lineProximity:this.node.attributes["line.proximity"]}}},surface:{uniforms:function(){return{}}},shade:{pipeline:function(t){var e,n,r;if(!this._inherit("fragment"))return t;for(null==t&&(t=this._shaders.shader()),n=e=0;2>=e;n=++e)t=null!=(r=this._inherit("fragment"))?r.fragment(t,n):void 0;return t.pipe("fragment.map.rgba"),t},map:function(t){return t?t=this._shaders.shader().pipe("mesh.map.uvwo").pipe(t):t}},position:{pipeline:function(t){var e,n,r;if(!this._inherit("vertex"))return t;for(null==t&&(t=this._shaders.shader()),n=e=0;3>=e;n=++e)t=null!=(r=this._inherit("vertex"))?r.vertex(t,n):void 0;return t},swizzle:function(t,e){return t?this._shaders.shader().pipe(r.GLSL.swizzleVec4(e)).pipe(t):void 0},swizzle2:function(t,e,n){return t?this._shaders.shader().split().pipe(r.GLSL.swizzleVec4(e)).next().pipe(r.GLSL.swizzleVec4(n)).join().pipe(t):void 0}},visible:{make:function(){var t,e,n,r;return t={type:"visible.change"},n=null,this.setVisible=function(t){return null!=t&&(n=t),e()},e=function(e){return function(){var i,o,s;return i=e.isVisible,s=null!=(o=null!=n?n:e._get("object.visible"))?o:!0,"undefined"!=typeof r&&null!==r&&s&&(s=r.isVisible),e.isVisible=s,i!==e.isVisible?e.trigger(t):void 0}}(this),r=this._inherit("visible"),r&&this._listen(r,"visible.change",e),this.is("object")&&this._listen(this.node,"change:object",e),e()},unmake:function(){return delete this.isVisible}},active:{make:function(){var t,e,n,r;return n={type:"active.change"},t=null,this.setActive=function(e){return null!=e&&(t=e),r()},r=function(r){return function(){var i,o,s;return i=r.isActive,s=null!=(o=null!=t?t:r._get("entity.active"))?o:!0,"undefined"!=typeof e&&null!==e&&s&&(s=e.isActive),r.isActive=s,i!==r.isActive?r.trigger(n):void 0}}(this),e=this._inherit("active"),e&&this._listen(e,"active.change",r),this.is("entity")&&this._listen(this.node,"change:entity",r),r()},unmake:function(){return delete this.isActive}},object:{make:function(t){var e,n,r,i,o,a,u,h,l,c,p,f,d,m;for(this.objects=null!=t?t:[],this.renders=this.objects.reduce(function(t,e){return t.concat(e.renders)},[]),u=this._inherit("scene"),c=e=f=null,n=s.call(this.traits,"style")>=0,c=1,e=THREE.NormalBlending,m=!0,d=!0,n&&(c=this.props.opacity,e=this.props.blending,f=this.props.zOrder,m=this.props.zWrite,d=this.props.zTest),h=function(t){return function(n){var r,i;return r=n.changed,i=null,r["style.opacity"]&&(i=c=t.props.opacity),r["style.blending"]&&(i=e=t.props.blending),r["style.zOrder"]&&(i=f=t.props.zOrder),r["style.zWrite"]&&(i=m=t.props.zWrite),r["style.zTest"]&&(i=d=t.props.zTest),null!=i?l():void 0}}(this),i=null,l=function(t){return function(){var r,i,o,s,a,u,h,l,p,v,g,E,y,_,b,T;if(l=null!=f?-f:t.node.order,T=(null!=(p=t.isVisible)?p:!0)&&c>0){if(n){for(v=t.objects,y=[],r=0,s=v.length;s>r;r++)h=v[r],h.show(1>c,e,l),y.push(h.depth(m,d));return y}for(g=t.objects,_=[],i=0,a=g.length;a>i;i++)h=g[i],_.push(h.show(!0,e,l));return _}for(E=t.objects,b=[],o=0,u=E.length;u>o;o++)h=E[o],b.push(h.hide());return b}}(this),this._listen(this.node,"change:style",h),this._listen(this.node,"reindex",l),this._listen(this,"visible.change",l),p=this.objects,r=0,o=p.length;o>r;r++)a=p[r],u.adopt(a);return l()},unmake:function(t){var e,n,r,i,o,s,a,u,h;if(null==t&&(t=!0),this.objects){for(s=this._inherit("scene"),a=this.objects,e=0,r=a.length;r>e;e++)o=a[e],s.unadopt(o);if(t){for(u=this.objects,h=[],n=0,i=u.length;i>n;n++)o=u[n],h.push(o.dispose());return h}}},mask:function(){var t,e;if(t=this._inherit("mask"))return e=t.mask(e)}},unit:{make:function(){var t,e,n,r,i,o,s,a,u,h,l,c,p,f,d,m,v;return v=Math.PI,this.unitUniforms={renderScaleInv:h=this._attributes.make(this._types.number(1)),renderScale:u=this._attributes.make(this._types.number(1)),renderAspect:o=this._attributes.make(this._types.number(1)),renderWidth:l=this._attributes.make(this._types.number(0)),renderHeight:s=this._attributes.make(this._types.number(0)),viewWidth:d=this._attributes.make(this._types.number(0)),viewHeight:f=this._attributes.make(this._types.number(0)),pixelRatio:r=this._attributes.make(this._types.number(1)),pixelUnit:i=this._attributes.make(this._types.number(1)),worldUnit:m=this._attributes.make(this._types.number(1)),focusDepth:e=this._attributes.make(this._types.number(1)),renderOdd:a=this._attributes.make(this._types.vec2())},p=new THREE.Vector3,t=new THREE.Vector3,n=function(n){return function(){var g,E,y,_,b,T,x,w,R,H,M,S,k,A;if(null!=(k="undefined"!=typeof c&&null!==c?c.getSize():void 0))return v=Math.PI,S=n.props.scale,_=n.props.fov,y=null!=(H=n.props.focus)?H:n.inherit("unit").props.focus,T=null===S,w=1,(g="undefined"!=typeof c&&null!==c?c.getCamera():void 0)&&(x=g.projectionMatrix,p.set(0,-.5,1).applyProjection(x),t.set(0,.5,1).applyProjection(x),p.sub(t),w=p.y),E=k.renderHeight/k.viewHeight,b=null!=_?w*Math.tan(_*v/360):1,R=T?E:k.renderHeight/S*b,M=k.renderHeight*w/2,A=R/M,d.value=k.viewWidth,f.value=k.viewHeight,l.value=k.renderWidth,s.value=k.renderHeight,o.value=k.aspect,u.value=M,h.value=1/M,r.value=E,i.value=R,m.value=A,e.value=y,a.value.set(k.renderWidth%2,k.renderHeight%2).multiplyScalar(.5)}}(this),c=this.is("root")?this:this._inherit("root"),this._listen(c,"root.update",n),n()},unmake:function(){return delete this.unitUniforms},get:function(){var t,e,n,r;n={},e=this.unitUniforms;for(t in e)r=e[t],n[t]=r.value;return n},uniforms:function(){return this.unitUniforms}}},e.exports=function(t,e){var n,r,i,s,a,u,h;for(n={},r=0,s=e.length;s>r;r++)if(h=e[r],u=o[h]){n[h]={};for(i in u)a=u[i],n[h][i]=a.bind(t)}return n}},{"../../util":175,"./view/view":122}],72:[function(t,e,n){var r;r=t("../../model"),n.Classes=t("./classes"),n.Types=t("./types"),n.Traits=t("./traits"),n.Helpers=t("./helpers")},{"../../model":34,"./classes":52,"./helpers":71,"./traits":107,"./types":115}],73:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;i=t("./operator"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","bind","operator","source","index","clamp"],e.prototype.indexShader=function(t){return t.pipe(this.operator),e.__super__.indexShader.call(this,t)},e.prototype.sourceShader=function(t){return t.pipe(this.operator),e.__super__.sourceShader.call(this,t)},e.prototype.make=function(){var t,n;return e.__super__.make.apply(this,arguments),null!=this.bind.source?(n={clampLimit:this._attributes.make(this._types.vec4())},this.clampLimit=n.clampLimit,t=this._shaders.shader(),t.pipe("clamp.position",n),this.operator=t):void 0},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments)},e.prototype.resize=function(){var t;return null!=this.bind.source&&(t=this.bind.source.getActiveDimensions(),this.clampLimit.value.set(t.width-1,t.height-1,t.depth-1,t.items-1)),e.__super__.resize.apply(this,arguments)},e.prototype.change=function(t,e,n){return e.operator||e.clamp?this.rebuild():void 0},e}(i),e.exports=r},{"./operator":78}],74:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;i=t("./operator"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","bind","operator","source","index","grow"],e.prototype.sourceShader=function(t){return t.pipe(this.operator)},e.prototype.make=function(){var t,n;return e.__super__.make.apply(this,arguments),null!=this.bind.source?(n={growScale:this.node.attributes["grow.scale"],growMask:this._attributes.make(this._types.vec4()),growAnchor:this._attributes.make(this._types.vec4())},this.growMask=n.growMask.value,this.growAnchor=n.growAnchor.value,t=this._shaders.shader(),t.require(this.bind.source.sourceShader(this._shaders.shader())),t.pipe("grow.position",n),this.operator=t):void 0},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments)},e.prototype.resize=function(){return this.update(),e.__super__.resize.apply(this,arguments)},e.prototype.update=function(){var t,e,n,r,i,o,s,a,u;for(e=this.bind.source.getFutureDimensions(),a=["width","height","depth","items"],s=function(t,e){return((t||1)-1)*(.5-.5*e)},u=[],n=r=0,o=a.length;o>r;n=++r)i=a[n],t=this.props[i],this.growMask.setComponent(n,+(null==t)),u.push(this.growAnchor.setComponent(n,null!=t?s(e[i],t):0));return u},e.prototype.change=function(t,e,n){return e.operator?this.rebuild():e.grow?this.update():void 0},e}(i),e.exports=r},{"./operator":78}],75:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("./operator"),o=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","bind","operator","source","index","join"],e.prototype.indexShader=function(t){return t.pipe(this.operator),e.__super__.indexShader.call(this,t)},e.prototype.sourceShader=function(t){return t.pipe(this.operator),e.__super__.sourceShader.call(this,t)},e.prototype.getDimensions=function(){return this._resample(this.bind.source.getDimensions())},e.prototype.getActiveDimensions=function(){return this._resample(this.bind.source.getActiveDimensions())},e.prototype.getFutureDimensions=function(){return this._resample(this.bind.source.getFutureDimensions())},e.prototype.getIndexDimensions=function(){return this._resample(this.bind.source.getIndexDimensions())},e.prototype._resample=function(t){var e,n,r,i,o,s,a,u,h,l,c,p,f,d,m,v;for(l=this.order,e=this.axis,p=this.overlap,u=this.length,v=this.stride,s=["width","height","depth","items"],h=l.map(function(t){return s[t-1]}),i=l.indexOf(e),m=function(){var e,r,i;for(i=[],e=0,r=h.length;r>e;e++)n=h[e],i.push(t[n]);return i}(),f=(null!=(d=m[i+1])?d:1)*v,m.splice(i,2,f),m=m.slice(0,3),m.push(1),c={},r=o=0,a=h.length;a>o;r=++o)n=h[r],c[n]=m[r];return c},e.prototype.make=function(){var t,n,r,i,s,a,u,h,l,c,p,f,d;return e.__super__.make.apply(this,arguments),null!=this.bind.source?(u=this.props.order,t=this.props.axis,h=this.props.overlap,l=u.join(""),null==t&&(t=u[0]),r=l.indexOf(t),c=l.replace(t,"00").substring(0,4),i=[null,"width","height","depth","items"],a=i[t],n=this.bind.source.getDimensions(),s=n[a],h=Math.min(s-1,h),p=s-h,d={joinStride:this._attributes.make(this._types.number(p)),joinStrideInv:this._attributes.make(this._types.number(1/p))},f=this._shaders.shader(),f.require(o.GLSL.swizzleVec4(t,1)),f.require(o.GLSL.swizzleVec4(c,4)),f.require(o.GLSL.injectVec4([r,r+1])),f.pipe("join.position",d),f.pipe(o.GLSL.invertSwizzleVec4(u)),this.operator=f,this.order=u,this.axis=t,this.overlap=h,this.length=s,this.stride=p):void 0},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments)},e.prototype.change=function(t,e,n){return e.join||e.operator?this.rebuild():void 0},e}(i),e.exports=r},{"../../../util":175,"./operator":78}],76:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("./operator"),o=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","bind","operator","source","index","lerp","sampler:x","sampler:y","sampler:z","sampler:w"],e.prototype.indexShader=function(t){return t.pipe(this.indexer),e.__super__.indexShader.call(this,t)},e.prototype.sourceShader=function(t){return t.pipe(this.operator)},e.prototype.getDimensions=function(){return this._resample(this.bind.source.getDimensions())},e.prototype.getActiveDimensions=function(){return this._resample(this.bind.source.getActiveDimensions())},e.prototype.getFutureDimensions=function(){return this._resample(this.bind.source.getFutureDimensions())},e.prototype.getIndexDimensions=function(){return this._resample(this.bind.source.getIndexDimensions())},e.prototype._resample=function(t){var e,n,r;return r=this.resampled,e=this.centered,n=this.padding,this.relativeSize?(e.items||t.items--,e.width||t.width--,e.height||t.height--,e.depth||t.depth--,null!=r.items&&(t.items*=r.items),null!=r.width&&(t.width*=r.width),null!=r.height&&(t.height*=r.height),null!=r.depth&&(t.depth*=r.depth),e.items||t.items++,e.width||t.width++,e.height||t.height++,e.depth||t.depth++,t.items-=2*n.items,t.width-=2*n.width,t.height-=2*n.height,t.depth-=2*n.depth):(null!=r.items&&(t.items=r.items),null!=r.width&&(t.width=r.width),null!=r.height&&(t.height=r.height),null!=r.depth&&(t.depth=r.depth)),t.items=Math.max(0,Math.floor(t.items)),t.width=Math.max(0,Math.floor(t.width)),t.height=Math.max(0,Math.floor(t.height)),t.depth=Math.max(0,Math.floor(t.depth)),t},e.prototype.make=function(){var t,n,r,i,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R;if(e.__super__.make.apply(this,arguments),null!=this.bind.source){for(v=this.props,T=v.size,h=v.items,R=v.width,i=v.height,r=v.depth,y=T===this.node.attributes["lerp.size"]["enum"].relative,this.resampled={},null!=h&&(this.resampled.items=h),null!=R&&(this.resampled.width=R),null!=i&&(this.resampled.height=i),null!=r&&(this.resampled.depth=r),this.centered={},this.centered.items=this.props.centeredW,this.centered.width=this.props.centeredX,this.centered.height=this.props.centeredY,this.centered.depth=this.props.centeredZ,this.padding={},this.padding.items=this.props.paddingW,this.padding.width=this.props.paddingX,this.padding.height=this.props.paddingY,this.padding.depth=this.props.paddingZ,m=this._shaders.shader(),u=this._shaders.shader(),x={resampleFactor:this._attributes.make(this._types.vec4(0,0,0,0)),resampleBias:this._attributes.make(this._types.vec4(0,0,0,0))},this.resampleFactor=x.resampleFactor,this.resampleBias=x.resampleBias,_=null!=h||null!=R||null!=i||null!=r,m.pipe("resample.padding",x),w=[],t=!1,g=["width","height","depth","items"],s=l=0,f=g.length;f>l;s=++l)p=g[s],n=this.centered[p],t||(t=n),w[s]=n?"0.5":"0.0";for(t&&_&&(w="vec4("+w+")",m.pipe(o.GLSL.binaryOperator(4,"+",vec4)),u.pipe(o.GLSL.binaryOperator(4,"+",vec4))),_?(m.pipe("resample.relative",x),u.pipe("resample.relative",x)):(m.pipe(o.GLSL.identity("vec4")),u.pipe(o.GLSL.identity("vec4"))),t&&_&&(m.pipe(o.GLSL.binaryOperator(4,"-",w)),u.pipe(o.GLSL.binaryOperator(4,"-",w))),b=this.bind.source.sourceShader(this._shaders.shader()),E=["width","height","depth","items"],s=c=0,d=E.length;d>c;s=++c)p=E[s],a="lerp."+p,null!=this.props[p]&&(b=this._shaders.shader().require(b),b.pipe(a,x));return m.pipe(b),this.operator=m,this.indexer=u,this.relativeSize=y}},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),this.operator=null},e.prototype.resize=function(){var t,n,r,i,o,s,a,u,h,l,c,p,f,d,m;if(null!=this.bind.source)return s=this.bind.source.getActiveDimensions(),m=this.getActiveDimensions(),t=function(t){return function(e){var n,r,i;return n=t.centered[e],r=t.padding[e],m[e]+=2*r,i=n?s[e]/Math.max(1,m[e]):Math.max(1,s[e]-1)/Math.max(1,m[e]-1),[i,r]}}(this),u=t("width"),d=u[0],o=u[1],h=t("height"),p=h[0],r=h[1],l=t("depth"),a=l[0],n=l[1],c=t("items"),f=c[0],i=c[1],this.resampleFactor.value.set(d,p,a,f),this.resampleBias.value.set(o,r,n,i),e.__super__.resize.apply(this,arguments)},e.prototype.change=function(t,e,n){return e.operator||e.lerp||e.sampler?this.rebuild():void 0},e}(i),e.exports=r},{"../../../util":175,"./operator":78}],77:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("./operator"),o=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","bind","active","operator","source","index","texture","memo"],e.prototype.sourceShader=function(t){return this.memo.shaderAbsolute(t,1)},e.prototype.make=function(){var t,n,r,i,o,s,a,u,h,l;return e.__super__.make.apply(this,arguments),null!=this.bind.source?(this._helpers.active.make(),this._listen("root","root.update",function(t){return function(){return t.isActive?t.update():void 0}}(this)),u=this.props,s=u.minFilter,o=u.magFilter,h=u.type,n=this.bind.source.getDimensions(),i=n.items,l=n.width,r=n.height,t=n.depth,this.memo=this._renderables.make("memo",{items:i,width:l,height:r,depth:t,minFilter:s,magFilter:o,type:h}),a=this._shaders.shader(),this.bind.source.sourceShader(a),this.compose=this._renderables.make("memoScreen",{map:a,items:i,width:l, -height:r,depth:t}),this.memo.adopt(this.compose),this.objects=[this.compose],this.renders=this.compose.renders):void 0},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),null!=this.bind.source?(this._helpers.active.unmake(),this.memo.unadopt(this.compose),this.memo.dispose(),this.memo=this.compose=null):void 0},e.prototype.update=function(){var t;return null!=(t=this.memo)?t.render():void 0},e.prototype.resize=function(){var t,n,r,i;if(null!=this.bind.source)return n=this.bind.source.getActiveDimensions(),i=n.width,r=n.height,t=n.depth,this.compose.cover(i,r,t),e.__super__.resize.apply(this,arguments)},e.prototype.change=function(t,e,n){return e.texture||e.operator?this.rebuild():void 0},e}(i),e.exports=r},{"../../../util":175,"./operator":78}],78:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;i=t("../base/source"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","bind","operator","source","index"],e.prototype.indexShader=function(t){var e;return null!=(e=this.bind.source)&&"function"==typeof e.indexShader?e.indexShader(t):void 0},e.prototype.sourceShader=function(t){var e;return null!=(e=this.bind.source)&&"function"==typeof e.sourceShader?e.sourceShader(t):void 0},e.prototype.getDimensions=function(){return this.bind.source.getDimensions()},e.prototype.getFutureDimensions=function(){return this.bind.source.getFutureDimensions()},e.prototype.getActiveDimensions=function(){return this.bind.source.getActiveDimensions()},e.prototype.getIndexDimensions=function(){return this.bind.source.getIndexDimensions()},e.prototype.init=function(){return this.sourceSpec=[{to:"operator.source",trait:"source"}]},e.prototype.make=function(){return e.__super__.make.apply(this,arguments),this._helpers.bind.make(this.sourceSpec)},e.prototype.made=function(){return this.resize(),e.__super__.made.apply(this,arguments)},e.prototype.unmake=function(){return this._helpers.bind.unmake()},e.prototype.resize=function(t){return this.trigger({type:"source.resize"})},e}(i),e.exports=r},{"../base/source":49}],79:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("../../primitive"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","bind","operator","readback","entity","active"],e.finals={channels:4},e.prototype.init=function(){return this.emitter=this.root=null,this.active={}},e.prototype.make=function(){var t,n,r,i,o,s,a,u,h,l;return e.__super__.make.apply(this,arguments),this._compute("readback.data",function(t){return function(){var e;return null!=(e=t.readback)?e.data:void 0}}(this)),this._compute("readback.items",function(t){return function(){var e;return null!=(e=t.readback)?e.items:void 0}}(this)),this._compute("readback.width",function(t){return function(){var e;return null!=(e=t.readback)?e.width:void 0}}(this)),this._compute("readback.height",function(t){return function(){var e;return null!=(e=t.readback)?e.height:void 0}}(this)),this._compute("readback.depth",function(t){return function(){var e;return null!=(e=t.readback)?e.depth:void 0}}(this)),this._helpers.bind.make([{to:"operator.source",trait:"source"}]),null!=this.bind.source?(s=this.props,h=s.type,t=s.channels,r=s.expr,this.root=this._inherit("root"),this._listen("root","root.update",this.update),a=this.bind.source.getDimensions(),o=a.items,l=a.width,i=a.height,n=a.depth,u=this.bind.source.sourceShader(this._shaders.shader()),this.readback=this._renderables.make("readback",{map:u,items:o,width:l,height:i,depth:n,channels:t,type:h}),null!=r&&this.readback.setCallback(r),this._helpers.active.make()):void 0},e.prototype.unmake=function(){return null!=this.readback&&(this.readback.dispose(),this.readback=null,this.root=null,this.emitter=null,this.active={}),this._helpers.active.unmake(),this._helpers.bind.unmake()},e.prototype.update=function(){var t;if(null!=this.readback)return this.isActive&&(this.readback.update(null!=(t=this.root)?t.getCamera():void 0),this.readback.post(),null!=this.props.expr)?this.readback.iterate():void 0},e.prototype.resize=function(){var t,e,n,r,i,o,s,a;if(null!=this.readback)return r=this.bind.source.getActiveDimensions(),n=r.items,a=r.width,e=r.height,t=r.depth,this.readback.setActive(n,a,e,t),this.strideI=i=n,this.strideJ=o=i*a,this.strideK=s=o*e},e.prototype.change=function(t,e,n){return t["readback.type"]?this.rebuild():t["readback.expr"]&&this.readback?this.readback.setCallback(this.props.expr):void 0},e}(r),e.exports=i},{"../../../util":175,"../../primitive":44}],80:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;r=t("./operator"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","bind","operator","source","index","repeat"],e.prototype.indexShader=function(t){return t.pipe(this.operator),e.__super__.indexShader.call(this,t)},e.prototype.sourceShader=function(t){return t.pipe(this.operator),e.__super__.sourceShader.call(this,t)},e.prototype.getDimensions=function(){return this._resample(this.bind.source.getDimensions())},e.prototype.getActiveDimensions=function(){return this._resample(this.bind.source.getActiveDimensions())},e.prototype.getFutureDimensions=function(){return this._resample(this.bind.source.getFutureDimensions())},e.prototype.getIndexDimensions=function(){return this._resample(this.bind.source.getIndexDimensions())},e.prototype._resample=function(t){var e;return e=this.resample,{items:e.items*t.items,width:e.width*t.width,height:e.height*t.height,depth:e.depth*t.depth}},e.prototype.make=function(){var t,n;return e.__super__.make.apply(this,arguments),null!=this.bind.source?(this.resample={},n={repeatModulus:this._attributes.make(this._types.vec4())},this.repeatModulus=n.repeatModulus,t=this._shaders.shader(),t.pipe("repeat.position",n),this.operator=t):void 0},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments)},e.prototype.resize=function(){var t;return null!=this.bind.source&&(t=this.bind.source.getActiveDimensions(),this.repeatModulus.value.set(t.width,t.height,t.depth,t.items)),e.__super__.resize.apply(this,arguments)},e.prototype.change=function(t,e,n){var r,i,o,s,a;if(e.operator||e.repeat)return this.rebuild();if(n){for(s=["items","width","height","depth"],a=[],r=0,o=s.length;o>r;r++)i=s[r],a.push(this.resample[i]=this.props[i]);return a}},e}(r),e.exports=i},{"./operator":78}],81:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./operator"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","bind","operator","source","index","resample","sampler:x","sampler:y","sampler:z","sampler:w","include"],e.prototype.indexShader=function(t){return t.pipe(this.indexer),e.__super__.indexShader.call(this,t)},e.prototype.sourceShader=function(t){return t.pipe(this.operator)},e.prototype.getDimensions=function(){return this._resample(this.bind.source.getDimensions())},e.prototype.getActiveDimensions=function(){return this._resample(this.bind.source.getActiveDimensions())},e.prototype.getFutureDimensions=function(){return this._resample(this.bind.source.getFutureDimensions())},e.prototype.getIndexDimensions=function(){return this._resample(this.bind.source.getIndexDimensions())},e.prototype._resample=function(t){var e,n,r;return r=this.resampled,e=this.centered,n=this.padding,this.relativeSize?(e.items||t.items--,e.width||t.width--,e.height||t.height--,e.depth||t.depth--,null!=r.items&&(t.items*=r.items),null!=r.width&&(t.width*=r.width),null!=r.height&&(t.height*=r.height),null!=r.depth&&(t.depth*=r.depth),e.items||t.items++,e.width||t.width++,e.height||t.height++,e.depth||t.depth++,t.items-=2*n.items,t.width-=2*n.width,t.height-=2*n.height,t.depth-=2*n.depth):(null!=r.items&&(t.items=r.items),null!=r.width&&(t.width=r.width),null!=r.height&&(t.height=r.height),null!=r.depth&&(t.depth=r.depth)),t.items=Math.max(0,Math.floor(t.items)),t.width=Math.max(0,Math.floor(t.width)),t.height=Math.max(0,Math.floor(t.height)),t.depth=Math.max(0,Math.floor(t.depth)),t},e.prototype.make=function(){var t,n,r,i,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M;if(e.__super__.make.apply(this,arguments),null!=this.bind.source){for(this._helpers.bind.make([{to:"include.shader",trait:"shader",optional:!0}]),m=this.props,h=m.indices,r=m.channels,T=this.bind.shader,v=this.props,b=v.sample,x=v.size,l=v.items,M=v.width,s=v.height,i=v.depth,E=b===this.node.attributes["resample.sample"]["enum"].relative,y=x===this.node.attributes["resample.size"]["enum"].relative,this.resampled={},null!=l&&(this.resampled.items=l),null!=M&&(this.resampled.width=M),null!=s&&(this.resampled.height=s),null!=i&&(this.resampled.depth=i),this.centered={},this.centered.items=this.props.centeredW,this.centered.width=this.props.centeredX,this.centered.height=this.props.centeredY,this.centered.depth=this.props.centeredZ,this.padding={},this.padding.items=this.props.paddingW,this.padding.width=this.props.paddingX,this.padding.height=this.props.paddingY,this.padding.depth=this.props.paddingZ,d=this._shaders.shader(),u=this._shaders.shader(),w=[null,this._types.number,this._types.vec2,this._types.vec3,this._types.vec4][h],R={dataSize:this._attributes.make(w(0,0,0,0)),dataResolution:this._attributes.make(w(0,0,0,0)),targetSize:this._attributes.make(w(0,0,0,0)),targetResolution:this._attributes.make(w(0,0,0,0)),resampleFactor:this._attributes.make(this._types.vec4(0,0,0,0)),resampleBias:this._attributes.make(this._types.vec4(0,0,0,0))},this.dataResolution=R.dataResolution,this.dataSize=R.dataSize,this.targetResolution=R.targetResolution,this.targetSize=R.targetSize,this.resampleFactor=R.resampleFactor,this.resampleBias=R.resampleBias,_=null!=l||null!=M||null!=s||null!=i,d.pipe("resample.padding",R),H=[],t=!1,g=["width","height","depth","items"],a=c=0,f=g.length;f>c;a=++c)p=g[a],n=this.centered[p],t||(t=n),H[a]=n?"0.5":"0.0";return t&&(H="vec4("+H+")",d.pipe(o.GLSL.binaryOperator(4,"+",vec4)),_&&u.pipe(o.GLSL.binaryOperator(4,"+",vec4))),E&&(_?(d.pipe("resample.relative",R),u.pipe("resample.relative",R)):u.pipe(o.GLSL.identity("vec4"))),null!=T?(4!==h&&d.pipe(o.GLSL.truncateVec(4,h)),d.callback(),4!==h&&d.pipe(o.GLSL.extendVec(h,4)),t&&d.pipe(o.GLSL.binaryOperator(4,"-",H)),d.pipe(this.bind.source.sourceShader(this._shaders.shader())),4!==r&&d.pipe(o.GLSL.truncateVec(4,r)),d.join(),null!=this.bind.shader&&d.pipe(this.bind.shader.shaderBind(R)),4!==r&&d.pipe(o.GLSL.extendVec(r,4))):(t&&d.pipe(o.GLSL.binaryOperator(4,"-",H)),d.pipe(this.bind.source.sourceShader(this._shaders.shader()))),t&&_&&u.pipe(o.GLSL.binaryOperator(4,"-",H)),this.operator=d,this.indexer=u,this.indices=h,this.relativeSample=E,this.relativeSize=y}},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),this.operator=null},e.prototype.resize=function(){var t,n,r,i,o,s,a,u,h,l,c,p,f,d,m;if(null!=this.bind.source)return s=this.bind.source.getActiveDimensions(),m=this.getActiveDimensions(),t=function(t){return function(e){var n,r,i;return n=t.centered[e],r=t.padding[e],m[e]+=2*r,i=n?s[e]/Math.max(1,m[e]):Math.max(1,s[e]-1)/Math.max(1,m[e]-1),[i,r]}}(this),u=t("width"),d=u[0],o=u[1],h=t("height"),p=h[0],r=h[1],l=t("depth"),a=l[0],n=l[1],c=t("items"),f=c[0],i=c[1],1===this.indices?(this.dataResolution.value=1/s.width,this.targetResolution.value=1/m.width,this.dataSize.value=s.width,this.targetSize.value=m.width):(this.dataResolution.value.set(1/s.width,1/s.height,1/s.depth,1/s.items),this.targetResolution.value.set(1/m.width,1/m.height,1/m.depth,1/m.items),this.dataSize.value.set(s.width,s.height,s.depth,s.items),this.targetSize.value.set(m.width,m.height,m.depth,m.items)),this.resampleFactor.value.set(d,p,a,f),this.resampleBias.value.set(o,r,n,i),e.__super__.resize.apply(this,arguments)},e.prototype.change=function(t,e,n){return e.operator||e.resample||e.sampler||e.include?this.rebuild():void 0},e}(r),e.exports=i},{"../../../util":175,"./operator":78}],82:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./operator"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","bind","operator","source","index","slice"],e.prototype.getDimensions=function(){return this._resample(this.bind.source.getDimensions())},e.prototype.getActiveDimensions=function(){return this._resample(this.bind.source.getActiveDimensions())},e.prototype.getFutureDimensions=function(){return this._resample(this.bind.source.getFutureDimensions())},e.prototype.getIndexDimensions=function(){return this._resample(this.bind.source.getIndexDimensions())},e.prototype.sourceShader=function(t){return t.pipe("slice.position",this.uniforms),this.bind.source.sourceShader(t)},e.prototype._resolve=function(t,e){var n,r,i,o,s;return o=this.props[t],n=e[t],null==o?[0,n]:(i=function(t,e){return 0>t?e+t:t},s=i(Math.round(o.x),n),r=i(Math.round(o.y),n),r=Math.max(s,r),[s,r-s])},e.prototype._resample=function(t){return t.width=this._resolve("width",t)[1],t.height=this._resolve("height",t)[1],t.depth=this._resolve("depth",t)[1],t.items=this._resolve("items",t)[1],t},e.prototype.make=function(){return e.__super__.make.apply(this,arguments),null!=this.bind.source?this.uniforms={sliceOffset:this._attributes.make(this._types.vec4())}:void 0},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments)},e.prototype.resize=function(){var t;if(null!=this.bind.source)return t=this.bind.source.getActiveDimensions(),this.uniforms.sliceOffset.value.set(this._resolve("width",t)[0],this._resolve("height",t)[0],this._resolve("depth",t)[0],this._resolve("items",t)[0]),e.__super__.resize.apply(this,arguments)},e.prototype.change=function(t,e,n){return e.operator?this.rebuild():e.slice?this.resize():void 0},e}(r),e.exports=i},{"../../../util":175,"./operator":78}],83:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./operator"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","bind","operator","source","index","split"],e.prototype.indexShader=function(t){return t.pipe(this.operator),e.__super__.indexShader.call(this,t)},e.prototype.sourceShader=function(t){return t.pipe(this.operator),e.__super__.sourceShader.call(this,t)},e.prototype.getDimensions=function(){return this._resample(this.bind.source.getDimensions())},e.prototype.getActiveDimensions=function(){return this._resample(this.bind.source.getActiveDimensions())},e.prototype.getFutureDimensions=function(){return this._resample(this.bind.source.getFutureDimensions())},e.prototype.getIndexDimensions=function(){return this._resample(this.bind.source.getIndexDimensions())},e.prototype._resample=function(t){var e,n,r,i,o,s,a,u,h,l,c,p,f,d,m;for(l=this.order,e=this.axis,p=this.overlap,u=this.length,m=this.stride,s=["width","height","depth","items"],h=l.map(function(t){return s[t-1]}),i=l.indexOf(e),d=function(){var e,r,i;for(i=[],e=0,r=h.length;r>e;e++)n=h[e],i.push(t[n]);return i}(),f=Math.floor((d[i]-p)/m),d.splice(i,1,u,f),d=d.slice(0,4),c={},r=o=0,a=h.length;a>o;r=++o)n=h[r],c[n]=d[r];return c},e.prototype.make=function(){var t,n,r,i,s,a,u,h,l,c,p,f;return e.__super__.make.apply(this,arguments),null!=this.bind.source?(i=this.props.order,t=this.props.axis,s=this.props.overlap,r=this.props.length,a=i.join(""),null==t&&(t=i[0]),n=a.indexOf(t),l=a[n]+(null!=(u=a[n+1])?u:0),h=a.replace(l[1],"").replace(l[0],"0")+"0",s=Math.min(r-1,s),c=r-s,f={splitStride:this._attributes.make(this._types.number(c))},p=this._shaders.shader(),p.require(o.GLSL.swizzleVec4(l,2)),p.require(o.GLSL.swizzleVec4(h,4)),p.require(o.GLSL.injectVec4(n)),p.pipe("split.position",f),p.pipe(o.GLSL.invertSwizzleVec4(i)),this.operator=p,this.order=i,this.axis=t,this.overlap=s,this.length=r,this.stride=c):void 0},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments)},e.prototype.change=function(t,e,n){return t["split.axis"]||t["split.order"]||e.operator?this.rebuild():void 0},e}(r),e.exports=i},{"../../../util":175,"./operator":78}],84:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;r=t("./operator"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","bind","operator","source","index","spread"],e.prototype.sourceShader=function(t){return t.pipe(this.operator)},e.prototype.make=function(){var t,n;return e.__super__.make.apply(this,arguments),null!=this.bind.source?(n={spreadMatrix:this._attributes.make(this._types.mat4()),spreadOffset:this._attributes.make(this._types.vec4())},this.spreadMatrix=n.spreadMatrix,this.spreadOffset=n.spreadOffset,t=this._shaders.shader(),t.require(this.bind.source.sourceShader(this._shaders.shader())),t.pipe("spread.position",n),this.operator=t):void 0},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments)},e.prototype.resize=function(){return this.update(),e.__super__.resize.apply(this,arguments)},e.prototype.update=function(){var t,e,n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y;for(r=this.bind.source.getFutureDimensions(),c=this.spreadMatrix.value,i=c.elements,f=["width","height","depth","items"],t=["alignWidth","alignHeight","alignDepth","alignItems"],g=this.props.unit,E=this.node.attributes["spread.unit"]["enum"],l=function(){switch(g){case E.relative:return function(t,e,n,o){return i[4*e+n]=o/Math.max(1,r[t]-1)};case E.absolute:return function(t,e,n,r){return i[4*e+n]=r}}}(),m=[],o=s=0,h=f.length;h>s;o=++s)u=f[o],v=this.props[u],e=this.props[t[o]],null!=v?(n=null!=(d=r[u])?d:1,p=-(n-1)*(.5-.5*e)):p=0,this.spreadOffset.value.setComponent(o,p),m.push(function(){var t,e,n;for(n=[],a=t=0;3>=t;a=++t)y=null!=(e=null!=v?v.getComponent(a):void 0)?e:0,n.push(i[4*o+a]=l(u,o,a,y));return n}());return m},e.prototype.change=function(t,e,n){return e.operator?this.rebuild():e.spread?this.update():void 0},e}(r),e.exports=i},{"./operator":78}],85:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./operator"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","bind","operator","source","index","subdivide"],e.prototype.indexShader=function(t){return t.pipe(this.indexer),e.__super__.indexShader.call(this,t)},e.prototype.sourceShader=function(t){return t.pipe(this.operator)},e.prototype.getDimensions=function(){return this._resample(this.bind.source.getDimensions())},e.prototype.getActiveDimensions=function(){return this._resample(this.bind.source.getActiveDimensions())},e.prototype.getFutureDimensions=function(){return this._resample(this.bind.source.getFutureDimensions())},e.prototype.getIndexDimensions=function(){return this._resample(this.bind.source.getIndexDimensions())},e.prototype._resample=function(t){var e;return e=this.resampled,t.items--,t.width--,t.height--,t.depth--,null!=e.items&&(t.items*=e.items),null!=e.width&&(t.width*=e.width),null!=e.height&&(t.height*=e.height),null!=e.depth&&(t.depth*=e.depth),t.items++,t.width++,t.height++,t.depth++,t},e.prototype.make=function(){var t,n,r,i,s,a,u,h,l,c,p,f,d,m,v,g,E,y;if(e.__super__.make.apply(this,arguments),null!=this.bind.source){for(f=this.props,g=f.size,a=f.items,y=f.width,n=f.height,t=f.depth,c=f.lerp,this.resampled={},null!=a&&(this.resampled.items=a),null!=y&&(this.resampled.width=y),null!=n&&(this.resampled.height=n),null!=t&&(this.resampled.depth=t),p=this._shaders.shader(),s=this._shaders.shader(),E={resampleFactor:this._attributes.make(this._types.vec4(0,0,0,0)),subdivideBevel:this.node.attributes["subdivide.bevel"]},this.resampleFactor=E.resampleFactor,this.resampleBias=E.resampleBias,m=null!=a||null!=y||null!=n||null!=t,m?(p.pipe("resample.relative",E),s.pipe("resample.relative",E)):(p.pipe(o.GLSL.identity("vec4")),s.pipe(o.GLSL.identity("vec4"))),v=this.bind.source.sourceShader(this._shaders.shader()),c=c?".lerp":"",d=["width","height","depth","items"],r=u=0,l=d.length;l>u;r=++u)h=d[r],i="subdivide."+h+c,null!=this.props[h]&&(v=this._shaders.shader().require(v),v.pipe(i,E));return p.pipe(v),this.operator=p,this.indexer=s}},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),this.operator=null},e.prototype.resize=function(){var t,n,r,i,o,s,a;if(null!=this.bind.source)return n=this.bind.source.getActiveDimensions(),a=this.getActiveDimensions(),t=function(t){return Math.max(1,n[t]-1)/Math.max(1,a[t]-1)},s=t("width"),i=t("height"),r=t("depth"),o=t("items"),this.resampleFactor.value.set(s,i,r,o),e.__super__.resize.apply(this,arguments)},e.prototype.change=function(t,e,n){return e.operator||e.subdivide?this.rebuild():void 0},e}(r),e.exports=i},{"../../../util":175,"./operator":78}],86:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./operator"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","bind","operator","source","index","swizzle"],e.prototype.sourceShader=function(t){return t=e.__super__.sourceShader.call(this,t),this.swizzler&&t.pipe(this.swizzler),t},e.prototype.make=function(){var t;return e.__super__.make.apply(this,arguments),null!=this.bind.source?(t=this.props.order,"1234"!==t.join()?this.swizzler=o.GLSL.swizzleVec4(t,4):void 0):void 0},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),this.swizzler=null},e.prototype.change=function(t,e,n){return e.swizzle||e.operator?this.rebuild():void 0},e}(r),e.exports=i},{"../../../util":175,"./operator":78}],87:[function(t,e,n){var r,i,o,s,a=function(t,e){function n(){this.constructor=t}for(var r in e)u.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},u={}.hasOwnProperty;r=t("./operator"),o=t("../../../util"),s={1:"width",2:"height",3:"depth",4:"items"},i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return a(e,t),e.traits=["node","bind","operator","source","index","transpose"],e.prototype.indexShader=function(t){return this.swizzler&&t.pipe(this.swizzler),e.__super__.indexShader.call(this,t)},e.prototype.sourceShader=function(t){return this.swizzler&&t.pipe(this.swizzler),e.__super__.sourceShader.call(this,t)},e.prototype.getDimensions=function(){return this._remap(this.transpose,this.bind.source.getDimensions())},e.prototype.getActiveDimensions=function(){return this._remap(this.transpose,this.bind.source.getActiveDimensions())},e.prototype.getFutureDimensions=function(){return this._remap(this.transpose,this.bind.source.getFutureDimensions())},e.prototype.getIndexDimensions=function(){return this._remap(this.transpose,this.bind.source.getIndexDimensions())},e.prototype._remap=function(t,e){var n,r,i,o,a,u;for(o={},r=i=0;3>=i;r=++i)n=s[r+1],u=s[t[r]],o[n]=null!=(a=e[u])?a:1;return o},e.prototype.make=function(){var t;return e.__super__.make.apply(this,arguments),null!=this.bind.source?(t=this.props.order,"1234"!==t.join()&&(this.swizzler=o.GLSL.invertSwizzleVec4(t)),this.transpose=t,this.trigger({type:"source.rebuild"})):void 0},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),this.swizzler=null},e.prototype.change=function(t,e,n){return e.transpose||e.operator?this.rebuild():void 0},e}(r),e.exports=i},{"../../../util":175,"./operator":78}],88:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("../../primitive"),o=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","bind","object","visible","overlay","dom","attach","position"],e.prototype.init=function(){return this.emitter=this.root=null,this.active={}},e.prototype.make=function(){var t,n,r,i,o,s,a,u,h;return e.__super__.make.apply(this,arguments),this._helpers.bind.make([{to:"dom.html",trait:"html"},{to:"dom.points",trait:"source"}]),null!=this.bind.points&&null!=this.bind.html?(this.root=this._inherit("root"),this._listen("root","root.update",this.update),this._listen("root","root.post",this.post),s=this.bind.points.getDimensions(),r=this.bind.html.getDimensions(),o=Math.min(s.items,r.items),h=Math.min(s.width,r.width),n=Math.min(s.height,r.height),t=Math.min(s.depth,r.depth),a=this.bind.points.sourceShader(this._shaders.shader()),a=this._helpers.position.pipeline(a),u=this._shaders.shader({globals:["projectionMatrix"]}),u.pipe("project.readback"),a.pipe(u),i=this._shaders.shader(),this.readback=this._renderables.make("readback",{map:a,indexer:i,items:o,width:h,height:n,depth:t,channels:4,stpq:!0}),this.dom=this._overlays.make("dom"),this.dom.hint(o*h*n*t*2),this.readback.setCallback(this.emitter=this.callback(this.bind.html.nodes())),this._helpers.visible.make()):void 0},e.prototype.unmake=function(){return null!=this.readback&&(this.readback.dispose(),this.dom.dispose(),this.readback=this.dom=null,this.root=null,this.emitter=null,this.active={}),this._helpers.bind.unmake(),this._helpers.visible.unmake()},e.prototype.update=function(){var t;if(null!=this.readback)return this.props.visible?(this.readback.update(null!=(t=this.root)?t.getCamera():void 0),this.readback.post(),this.readback.iterate()):void 0},e.prototype.post=function(){return null!=this.readback?this.dom.render(this.isVisible?this.emitter.nodes():[]):void 0},e.prototype.callback=function(t){var e,n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x;return _=this._inherit("unit").getUnitUniforms(),b=_.viewWidth,u=_.viewHeight,e=this.node.attributes["dom.attributes"],d=this.node.attributes["dom.size"],x=this.node.attributes["dom.zoom"],r=this.node.attributes["dom.color"],p=this.node.attributes["dom.outline"],f=this.node.attributes["dom.pointerEvents"],c=this.node.attributes["overlay.opacity"],T=this.node.attributes["overlay.zIndex"],l=this.node.attributes["attach.offset"],o=this.node.attributes["attach.depth"],m=this.node.attributes["attach.snap"],s=this.dom.el,h=[],y=null,n=null,v=g=E=0,i="",a=function(r,i,a,p,f,d,_,T){var w,R,H,M,S,k,A,C,P,L,z,O,D,F,U,B;k=T+v*f+g*d+E*_,H=t[k],M=0>p,A=1/p,S=1+(A-1)*o.value,D=M?0:S,C=+l.value.x*D,P=+l.value.y*D,U=(r+1)*b.value*.5+C,B=(i-1)*u.value*.5+P,U/=x.value,B/=x.value,m.value&&(U=Math.round(U),B=Math.round(B)),R=Math.min(.999,M?0:c.value),L={className:n,style:{transform:"translate3d("+U+"px, "+-B+"px, "+(1-p)+"px) translate(-50%, -50%) scale("+D+","+D+")",opacity:R}};for(_ in y)F=y[_],L.style[_]=F;if(w=e.value,null!=w){O=w.style;for(_ in w)F=w[_],"style"!==_&&"className"!==_&&(L[_]=F);if(null!=O)for(_ in O)F=O[_],L.style[_]=F}return L.className+=" "+(null!=(z=null!=w?w.className:void 0)?z:"mathbox-label"),h.push(s("div",L,H))},a.reset=function(t){return function(){var e,o,s;return h=[],s=[t.strideI,t.strideJ,t.strideK],v=s[0],g=s[1],E=s[2],e=r.value,o=function(t){return Math.floor(255*t)},i=e?"rgb("+[o(e.x),o(e.y),o(e.z)]+")":"",n="mathbox-outline-"+Math.round(p.value),y={},e&&(y.color=i),y.fontSize=d.value+"px",1!==x.value&&(y.zoom=x.value),T.value>0&&(y.zIndex=T.value),f.value?y.pointerEvents="auto":void 0}}(this),a.nodes=function(){return h},a},e.prototype.resize=function(){var t,e,n,r,i,o,s,a,u;if(null!=this.readback)return i=this.bind.points.getActiveDimensions(),n=this.bind.html.getActiveDimensions(),r=Math.min(i.items,n.items),u=Math.min(i.width,n.width),e=Math.min(i.height,n.height),t=Math.min(i.depth,n.depth),this.readback.setActive(r,u,e,t),this.strideI=o=n.items,this.strideJ=s=o*n.width,this.strideK=a=s*n.height},e.prototype.change=function(t,e,n){return t["dom.html"]||t["dom.points"]?this.rebuild():void 0},e}(i),e.exports=r},{"../../../util":175,"../../primitive":44}],89:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;o=t("../data/voxel"),i=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","buffer","active","data","voxel","html"],e.finals={channels:1},e.prototype.init=function(){return e.__super__.init.apply(this,arguments),this.storage="pushBuffer"},e.prototype.make=function(){var t,n,r,i,o;return e.__super__.make.apply(this,arguments),i=this.getDimensions(),r=i.items,o=i.width,n=i.height,t=i.depth,this.dom=this._overlays.make("dom"),this.dom.hint(r*o*n*t)},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),null!=this.dom?(this.dom.dispose(),this.dom=null):void 0},e.prototype.update=function(){return e.__super__.update.apply(this,arguments)},e.prototype.change=function(t,n,r){return n.html?this.rebuild():e.__super__.change.call(this,t,n,r)},e.prototype.nodes=function(){return this.buffer.read()},e.prototype.callback=function(t){var e;return e=this.dom.el,t.length<=6?function(n,r,i,o,s){return t(n,e,r,i,o,s)}:function(n){return function(r,i,o,s,a){return t(r,e,i,o,s,a,n.bufferClock,n.bufferStep)}}(this)},e}(o),e.exports=r},{"../../../util":175,"../data/voxel":61}],90:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;i=t("./transition"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","transition","vertex","move","visible","active"],e.prototype.make=function(){var t,n,r;e.__super__.make.apply(this,arguments),n={moveFrom:this.node.attributes["move.from"],moveTo:this.node.attributes["move.to"]};for(t in n)r=n[t],this.uniforms[t]=r},e.prototype.vertex=function(t,e){var n,r;return e===this.props.pass&&t.pipe("move.position",this.uniforms),null!=(n=null!=(r=this._inherit("vertex"))?r.vertex(t,e):void 0)?n:t},e}(i),e.exports=r},{"./transition":97}],91:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;i=t("./track"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","track","trigger","play","bind"],e.prototype.init=function(){return e.__super__.init.apply(this,arguments),this.skew=null,this.start=null},e.prototype.reset=function(t){return null==t&&(t=!0),this.skew=t?0:null,this.start=null},e.prototype.make=function(){var t;return e.__super__.make.apply(this,arguments), -this._listen("slide","slide.step",function(t){return function(e){var n;return n=t.props.trigger,null!=n&&e.index===n?t.reset():null!=n&&0===e.index?t.reset(!1):void 0}}(this)),this.props.trigger&&null!=this._inherit("slide")||this.reset(),t=this._inherit("clock"),this._listen(t,"clock.tick",function(e){return function(){var n,r,i,o,s,a,u,h,l,c,p,f;return l=e.props,i=l.from,f=l.to,c=l.speed,a=l.pace,n=l.delay,h=l.realtime,p=t.getTime(),null!=e.skew?(o=h?p.time:p.clock,r=h?p.delta:p.step,u=c/a,null==e.start&&(e.start=o),e.skew+=r*(u-1),s=Math.max(0,o-e.start+e.skew-n*u),e.props.loop&&(s%=f-i),e.playhead=Math.min(f,i+s)):e.playhead=0,e.update()}}(this))},e.prototype.update=function(){return e.__super__.update.apply(this,arguments)},e.prototype.change=function(t,n,r){return t["trigger.trigger"]||t["play.realtime"]?this.rebuild():e.__super__.change.call(this,t,n,r)},e}(i),e.exports=r},{"./track":96}],92:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("../base/parent"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","present"],e.prototype.init=function(){},e.prototype.make=function(){return this.nodes=[],this.steps=[],this.length=0,this.last=[],this.index=0,this.dirty=[],this._listen("root","root.update",this.update),this._compute("present.length",function(t){return function(){return t.length}}(this))},e.prototype.adopt=function(t){var e;return e=t.node,this.nodes.indexOf(t)<0&&this.nodes.push(e),this.dirty.push(t)},e.prototype.unadopt=function(t){var e;return e=t.node,this.nodes=this.nodes.filter(function(e){return e!==t}),this.dirty.push(t)},e.prototype.update=function(){var t,e,n,r,i;if(this.dirty.length){for(r=this.dirty,e=0,n=r.length;n>e;e++)t=r[e],this.slideReset(t);return i=this.process(this.nodes),this.steps=i[0],this.indices=i[1],this.length=this.steps.length,this.index=null,this.go(this.props.index),this.dirty=[]}},e.prototype.slideLatch=function(t,e,n){return t.slideLatch(e,n)},e.prototype.slideStep=function(t,e,n){return t.slideStep(this.mapIndex(t,e),n)},e.prototype.slideRelease=function(t,e){return t.slideRelease()},e.prototype.slideReset=function(t){return t.slideReset()},e.prototype.mapIndex=function(t,e){return e-this.indices[t.node._id]},e.prototype.process=function(t){var e,n,r,i,o,s,a,u,h,l,c,p;return h=function(t){var e,n,r,i;for(i=[],n=0,r=t.length;r>n;n++)e=t[n],i.push(a(e).filter(o));return i},p=function(t){return function(e){var n,r,i;for(i=[];e&&(r=[t(e),e],e=r[0],n=r[1],r);)i.push(n);return i}},a=p(function(t){return t.parent.traits.hash.present?null:t.parent}),o=function(e){return t.indexOf(e)>=0},i=function(t,e){var n,r,i,o,s,a;if(n=t.length,r=e.length,i=n-r,0!==i)return!1;for(i=Math.min(n,r),o=s=a=i-1;0>=a?0>s:s>0;o=0>=a?++s:--s)if(t[o]!==e[o])return!1;return!0},s=function(t){return t.sort(function(t,e){var n,r,i,o,s,a,u,h,l,c;for(n=t.length,r=e.length,i=Math.min(n,r),a=u=1,c=i;c>=1?c>=u:u>=c;a=c>=1?++u:--u){if(h=t[n-a],l=e[r-a],o=h.props.order,s=l.props.order,null!=o||null!=s){if(null!=o&&null!=s&&0!==(i=o-s))return i;if(null!=o)return-1;if(null!=s)return 1}if(l.order!==h.order)return l.order-h.order}return i=n-r,0!==i?i:0})},l=function(t){var e,n,r,i,o,s;for(o=[],e=[],n=0,r=t.length;r>n;n++)s=t[n],(null!=(i=s[0]).props.steps?o:e).push(s);return[o,e]},n=function(t){var n,i,o,s,a,u,h,l,c,p,f,d;for(c=t[0],n=t[1],l=100,o={},d=[],p=function(t,e){var n,r,i,s,a,u,h,c,p,f,m,v;for(p=(u=t[0]).props,h=t[1],c=null!=h?o[h._id]:0,n=e,r=null!=p.from?c+p.from:n-p.early,v=null!=p.to?c+p.to:n+p.steps+p.late,r=Math.max(0,r),v=Math.min(l,v),null==o[a=u._id]&&(o[a]=r),i=s=f=r,m=v;m>=f?m>s:s>m;i=m>=f?++s:--s)d[i]=(null!=d[i]?d[i]:d[i]=[]).concat(t);return p.steps},i=0,s=0,u=c.length;u>s;s++)f=c[s],i+=p(f,i);for(a=0,h=n.length;h>a;a++)f=n[a],p(f,0);return d=function(){var t,n,i;for(i=[],t=0,n=d.length;n>t;t++)f=d[t],i.push(r(e(f)));return i}(),[d,o]},e=function(t){var e,n,r,i,o;if(t){for(o=[],e=n=0,r=t.length;r>n;e=++n)i=t[e],t.indexOf(i)===e&&o.push(i);return o}return[]},r=function(t){return t.sort(function(t,e){return t.order-e.order})},u=h(t),c=s(u),n(l(c))},e.prototype.go=function(t){var e,n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S,k,A,C,P,L,z,O;for(t=Math.max(0,Math.min(this.length+1,+t||0)),h=this.last,e=null!=(H=this.steps[t-1])?H:[],z=this.props.directed?t-this.index:1,this.index=t,i=function(){var t,n,r;for(r=[],t=0,n=e.length;n>t;t++)b=e[t],this.last.indexOf(b)<0&&r.push(b);return r}.call(this),o=function(){var t,n,r,i;for(r=this.last,i=[],t=0,n=r.length;n>t;t++)b=r[t],e.indexOf(b)<0&&i.push(b);return i}.call(this),L=function(){var t,n,r;for(r=[],t=0,n=e.length;n>t;t++)b=e[t],i.indexOf(b)<0&&o.indexOf(b)<0&&r.push(b);return r}(),n=function(t){return t.sort(function(t,e){return t.order-e.order})},r=function(t){return t.sort(function(t,e){return e.order-t.order})},O=function(t){return t.toString()},M=n(i),s=0,l=M.length;l>s;s++)b=M[s],this.slideLatch(b.controller,!0,z);for(S=n(L),a=0,c=S.length;c>a;a++)b=S[a],this.slideLatch(b.controller,null,z);for(k=n(o),u=0,p=k.length;p>u;u++)b=k[u],this.slideLatch(b.controller,!1,z);for(y=0,f=i.length;f>y;y++)b=i[y],this.slideStep(b.controller,t,z);for(_=0,d=L.length;d>_;_++)b=L[_],this.slideStep(b.controller,t,z);for(T=0,m=o.length;m>T;T++)b=o[T],this.slideStep(b.controller,t,z);for(A=r(i),x=0,v=A.length;v>x;x++)b=A[x],this.slideRelease(b.controller);for(C=r(L),w=0,g=C.length;g>w;w++)b=C[w],this.slideRelease(b.controller);for(P=r(o),R=0,E=P.length;E>R;R++)b=P[R],this.slideRelease(b.controller);this.last=e},e.prototype.change=function(t,e,n){return t["present.index"]||n?this.go(this.props.index):void 0},e}(r),e.exports=i},{"../../../util":175,"../base/parent":47}],93:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("./transition"),o=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","transition","mask","visible","active"],e.prototype.mask=function(t){var e,n,r;return t?(r=this._shaders.shader(),r.pipe(o.GLSL.identity("vec4")),r.fan(),r.pipe(t,this.uniforms),r.next(),r.pipe("reveal.mask",this.uniforms),r.end(),r.pipe("float combine(float a, float b) { return min(a, b); }")):(r=this._shaders.shader(),r.pipe("reveal.mask",this.uniforms)),null!=(e=null!=(n=this._inherit("mask"))?n.mask(r):void 0)?e:r},e}(i),e.exports=r},{"../../../util":175,"./transition":97}],94:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;r=t("../base/parent"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","slide","visible","active"],e.prototype.make=function(){if(this._helpers.visible.make(),this._helpers.active.make(),!this._inherit("present"))throw new Error(this.node.toString()+" must be placed inside ");return this._inherit("present").adopt(this)},e.prototype.unmake=function(){return this._helpers.visible.unmake(),this._helpers.active.unmake(),this._inherit("present")(unadopt(this))},e.prototype.change=function(t,e,n){return t["slide.early"]||t["slide.late"]||t["slide.steps"]||t["slide.from"]||t["slide.to"]?this.rebuild():void 0},e.prototype.slideLatch=function(t,e){return this.trigger({type:"transition.latch",step:e}),null!=t?this._instant(t):void 0},e.prototype.slideStep=function(t,e){return this.trigger({type:"slide.step",index:t,step:e})},e.prototype.slideRelease=function(){return this.trigger({type:"transition.release"})},e.prototype.slideReset=function(){return this._instant(!1),this.trigger({type:"slide.reset"})},e.prototype._instant=function(t){return this.setVisible(t),this.setActive(t)},e}(r),e.exports=i},{"../base/parent":47}],95:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;i=t("./track"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","track","step","trigger","bind"],e.prototype.make=function(){var t,n,r;return e.__super__.make.apply(this,arguments),t=this._inherit("clock"),null==this.actualIndex&&(this.actualIndex=null),this.animateIndex=this._animator.make(this._types.number(0),{clock:t,realtime:this.props.realtime,step:function(t){return function(e){return t.actualIndex=e}}(this)}),null==this.lastIndex&&(this.lastIndex=null),this.animateStep=this._animator.make(this._types.number(0),{clock:t,realtime:this.props.realtime,step:function(t){return function(e){return t.playhead=e,t.update()}}(this)}),this.stops=null!=(n=this.props.stops)?n:function(){r=[];for(var t=0,e=this.script.length;e>=0?e>t:t>e;e>=0?t++:t--)r.push(t);return r}.apply(this),this._listen("slide","slide.reset",function(t){return function(e){return t.lastIndex=null}}(this)),this._listen("slide","slide.step",function(t){return function(e){var n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x;if(f=t.props,n=f.delay,r=f.duration,c=f.pace,y=f.speed,p=f.playback,v=f.rewind,g=f.skip,x=f.trigger,a=Math.max(0,Math.min(t.stops.length-1,e.index-x)),s=t.playhead,T=t.stops[a],null==t.lastIndex&&x)return t.lastIndex=a,t.animateStep.set(T),void t.animateIndex.set(a);for(h=null!=(d=null!=(m=t.actualIndex)?m:t.lastIndex)?d:0,_=a-h,E=t.stops.slice(Math.min(h,a),Math.max(h,a)),o=0,h=E.shift(),u=0,l=E.length;l>u;u++)b=E[u],h===b&&o++,h=b;return t.lastIndex=a,i=y*(e.step>=0?1:v),i*=g?Math.max(1,Math.abs(_)-o):1,r+=Math.abs(T-s)*c/i,s!==T?(t.animateIndex.immediate(a,{delay:n,duration:r,ease:p}),t.animateStep.immediate(T,{delay:n,duration:r,ease:p})):void 0}}(this))},e.prototype.made=function(){return this.update()},e.prototype.unmake=function(){return this.animateIndex.dispose(),this.animateStep.dispose(),this.animateIndex=this.animateStep=null,e.__super__.unmake.apply(this,arguments)},e.prototype.change=function(t,n,r){return t["step.stops"]||t["step.realtime"]?this.rebuild():e.__super__.change.call(this,t,n,r)},e}(i),e.exports=r},{"./track":96}],96:[function(t,e,n){var r,i,o,s,a=function(t,e){function n(){this.constructor=t}for(var r in e)u.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},u={}.hasOwnProperty;i=t("../../primitive"),r=t("../../../util").Ease,s=function(t){var e,n,r;n={};for(e in t)r=t[e],r instanceof Array?n[e]=r.slice():null!=r&&"object"==typeof r?n[e]=s(r):n[e]=r;return n},o=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return a(e,t),e.traits=["node","track","seek","bind"],e.prototype.init=function(){return this.handlers={},this.script=null,this.values=null,this.playhead=0,this.velocity=null,this.section=null,this.expr=null},e.prototype.make=function(){var t,e,n;return this._helpers.bind.make([{to:"track.target",trait:"node",callback:null}]),n=this.props.script,t=this.bind.target.node,this.targetNode=t,e=this._process(t,n),this.script=e[0],this.values=e[1],this.start=e[2],this.end=e[3],e},e.prototype.unmake=function(){return this.unbindExpr(),this._helpers.bind.unmake(),this.script=this.values=this.start=this.end=this.section=this.expr=null,this.playhead=0},e.prototype.bindExpr=function(t){var e;return this.unbindExpr(),this.expr=t,this.targetNode.bind(t,!0),e=this.targetNode.clock,this._attributes.bind(this.measure=function(){var t;return t=null,function(n){return function(){var r;return r=e.getTime().step,null!=t&&(n.velocity=(n.playhead-t)/r),t=n.playhead}}(this)}())},e.prototype.unbindExpr=function(){return null!=this.expr&&this.targetNode.unbind(this.expr,!0),null!=this.measure&&this._attributes.unbind(this.measure),this.expr=this.measure=null},e.prototype._process=function(t,e){var n,r,i,o,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T;if(e instanceof Array){for(E={},r=i=0,l=e.length;l>i;r=++i)_=e[r],E[r]=_;e=E}E=[];for(a in e)_=e[a],null==_&&(_=[]),_ instanceof Array?_={key:+a,props:null!=_[0]?s(_[0]):{},expr:null!=_[1]?s(_[1]):{}}:(_=null!=_.key||_.props||_.expr?s(_):{props:s(_)},_.key=null!=_.key?+_.key:+a,null==_.props&&(_.props={}),null==_.expr&&(_.expr={})),E.push(_);if(e=E,!e.length)return[[],{},0,0];e.sort(function(t,e){return t.key-e.key}),y=e[0].key,n=e[e.length-1].key;for(a in e)_=e[a],"undefined"!=typeof h&&null!==h&&(h.next=_),h=_;h.next=h,e=E,f={},T={};for(a in e){_=e[a],d=_.props;for(o in d)b=d[o],f[o]=!0}for(a in e){_=e[a],m=_.expr;for(o in m)b=m[o],f[o]=!0}for(o in f)f[o]=t.get(o);try{for(o in f)T[o]=[t.attribute(o).T.make(),t.attribute(o).T.make(),t.attribute(o).T.make()]}catch(x){throw console.warn(this.node.toMarkup()),p=this.node.toString()+" - Target "+t+" has no `"+o+"` property",new Error(p)}for(g=[],u=0,c=e.length;c>u;u++){_=e[u];for(o in f)if(b=f[o],b=t.validate(o,null!=(v=_.props[o])?v:b),f[o]=_.props[o]=b,null!=_.expr[o]&&"function"!=typeof _.expr[o])throw console.warn(this.node.toMarkup()),p=this.node.toString()+" - Expression `"+_.expr[o]+"` on property `"+o+"` is not a function",new Error(p);g.push(_)}return[g,T,y,n]},e.prototype.update=function(){var t,e,n,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y;if(f=this.playhead,m=this.script,d=this.props,e=d.ease,g=d.seek,p=this.targetNode,null!=g&&(f=g),m.length){if(s=function(){var t,e,n,r,i;for(n=m[0],t=e=0,r=m.length;r>e&&(i=m[t],!(i.key>f));t=++e)n=i;return n},v=this.section,(!v||fv.next.key)&&(v=s(m,f)),v===this.section)return;this.section=v,a=v,y=v.next,E=a.key,i=y.key,n=function(){switch(e){case"linear":case 0:return r.clamp;case"cosine":case 1:return r.cosine;case"binary":case 2:return r.binary;case"hold":case 3:return r.hold;default:return r.cosine}}(),t=p.clock,h=function(e){return function(n){var r;return null==e.velocity?e.playhead:(r=t.getTime(),e.playhead+e.velocity*(n-r.time))}}(this),u=function(){var t;return t=1/Math.max(1e-4,i-E),function(e){return n((h(e)-E)*t,0,1)}}(),c=function(t){return function(e){var n,r,i,o,s,h,l,c;return i=a.expr[e],h=y.expr[e],o=a.props[e],l=y.props[e],s=function(){throw console.warn(p.toMarkup()),new Error(this.node.toString()+" - Invalid expression result on track `"+e+"`")},r=p.attribute(e),c=t.values[e],n=t._animator,i&&h?function(t,e,o){return function(e,o){var a,l;return t[0]=a=r.T.validate(i(e,o),t[0],s),t[1]=l=r.T.validate(h(e,o),t[1],s),t[2]=n.lerp(r.T,a,l,u(e),t[2])}}(c,a,y):i?function(t,e,o){return function(e,o){var a;return t[0]=a=r.T.validate(i(e,o),t[0],s),t[1]=n.lerp(r.T,a,l,u(e),t[1])}}(c,a,y):h?function(t,e,i){return function(e,i){var a;return t[0]=a=r.T.validate(h(e,i),t[0],s),t[1]=n.lerp(r.T,o,a,u(e),t[1])}}(c,a,y):function(t,e,i){return function(e,i){return t[0]=n.lerp(r.T,o,l,u(e),t[0])}}(c,a,y)}}(this),o={};for(l in a.expr)null==o[l]&&(o[l]=c(l));for(l in y.expr)null==o[l]&&(o[l]=c(l));for(l in a.props)null==o[l]&&(o[l]=c(l));for(l in y.props)null==o[l]&&(o[l]=c(l));return this.bindExpr(o)}},e.prototype.change=function(t,e,n){return t["track.target"]||t["track.script"]||t["track.mode"]?this.rebuild():t["seek.seek"]||n?this.update():void 0},e}(i),e.exports=o},{"../../../util":175,"../../primitive":44}],97:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("../base/parent"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","transition","transform","mask","visible","active"],e.prototype.init=function(){return this.animate=null,this.uniforms=null,this.state={isVisible:!0,isActive:!0,enter:1,exit:1},this.latched=null,this.locked=null},e.prototype.make=function(){var t,e,n;return this.uniforms={transitionFrom:this._attributes.make(this._types.vec4()),transitionTo:this._attributes.make(this._types.vec4()),transitionActive:this._attributes.make(this._types.bool()),transitionScale:this._attributes.make(this._types.vec4()),transitionBias:this._attributes.make(this._types.vec4()),transitionEnter:this._attributes.make(this._types.number()),transitionExit:this._attributes.make(this._types.number()),transitionSkew:this._attributes.make(this._types.number())},e=this._inherit("slide"),n=this._inherit("visible"),t=this._inherit("active"),this._listen(e,"transition.latch",function(t){return function(e){return t.latch(e.step)}}(this)),this._listen(e,"transition.release",function(t){return function(){return t.release()}}(this)),this._listen(n,"visible.change",function(t){return function(){return t.update(t.state.isVisible=n.isVisible)}}(this)),this._listen(t,"active.change",function(e){return function(){return e.update(e.state.isActive=t.isActive)}}(this)),this.animate=this._animator.make(this._types.vec2(1,1),{step:function(t){return function(e){return t.state.enter=e.x,t.state.exit=e.y,t.update()}}(this),complete:function(t){return function(e){return t.complete(e)}}(this)}),this.move=null!=this.props.from||null!=this.props.to},e.prototype.unmake=function(){return this.animate.dispose()},e.prototype.latch=function(t){var e,n,r,i,o,s;return this.locked=null,this.latched=i={isVisible:this.state.isVisible,isActive:this.state.isActive,step:t},s=this.isVisible,s?void 0:(r=i.step>=0,o=r?[0,1]:[1,0],e=o[0],n=o[1],this.animate.set(e,n))},e.prototype.release=function(){var t,e,n,r,i,o,s,a,u,h,l,c,p,f,d;return h=this.latched,f=this.state,this.latched=null,h.isVisible!==f.isVisible&&(u=h.step>=0,d=f.isVisible,l=d?[1,1]:u?[1,0]:[0,1],s=l[0],a=l[1],c=this.props,r=c.duration,i=c.durationEnter,o=c.durationExit,null==i&&(i=r),null==o&&(o=r),r=d*i+!d*o,p=this.props,t=p.delay,e=p.delayEnter,n=p.delayExit,null==e&&(e=t),null==n&&(n=t),t=d*e+!d*n,this.animate.immediate({x:s,y:a},{duration:r,delay:t,ease:"linear"}),this.locked={isVisible:!0,isActive:h.isActive||f.isActive}),this.update()},e.prototype.complete=function(t){return t?(this.locked=null,this.update()):void 0},e.prototype.update=function(){var t,e,n,r,i,o,s,a;if(null==this.latched)return o=this.props,e=o.enter,n=o.exit,null==e&&(e=this.state.enter),null==n&&(n=this.state.exit),r=e*n,a=r>0,i=1>r,this.uniforms.transitionEnter.value=e,this.uniforms.transitionExit.value=n,this.uniforms.transitionActive.value=i,a&&(a=!!this.state.isVisible),null!=this.locked&&(a=this.locked.isVisible),this.isVisible!==a&&(this.isVisible=a,this.trigger({type:"visible.change"})),t=!!(this.state.isActive||(null!=(s=this.locked)?s.isActive:void 0)),this.isActive!==t?(this.isActive=t,this.trigger({type:"active.change"})):void 0},e.prototype.change=function(t,e,n){var r,i,o,s,a,u,h,l,c;return(t["transition.enter"]||t["transition.exit"]||n)&&this.update(),t["transition.stagger"]||n?(a=this.props.stagger,i=a.x<0,o=a.y<0,s=a.z<0,r=a.w<0,h=Math.abs(a.x),l=Math.abs(a.y),c=Math.abs(a.z),u=Math.abs(a.w),this.uniforms.transitionSkew.value=h+l+c+u,this.uniforms.transitionScale.value.set((1-2*i)*h,(1-2*o)*l,(1-2*s)*c,(1-2*r)*u),this.uniforms.transitionBias.value.set(i*h,o*l,s*c,r*u)):void 0},e}(r),e.exports=i},{"../../../util":175,"../base/parent":47}],98:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("../../primitive"),o=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","bind","object","visible","operator","style","compose"],e.defaults={zWrite:!1,zTest:!1,color:"#ffffff"},e.prototype.init=function(){return this.compose=null},e.prototype.resize=function(){var t,e,n,r,i;if(this.compose&&this.bind.source)return e=this.bind.source.getActiveDimensions(),i=e.width,n=e.height,t=e.depth,r=e.items,this.remapUVScale.set(i,n)},e.prototype.make=function(){var t,e,n,r;return this._helpers.bind.make([{to:"operator.source",trait:"source"}]),null!=this.bind.source?(r={remapUVScale:this._attributes.make(this._types.vec2())},this.remapUVScale=r.remapUVScale.value,n=this._shaders.shader(),t=this.props.alpha,this.bind.source.is("image")?(n.pipe("screen.pass.uv",r),n=this.bind.source.imageShader(n)):(n.pipe("screen.map.xy",r),n=this.bind.source.sourceShader(n)),t||n.pipe("color.opaque"),e=this._helpers.style.uniforms(),this.compose=this._renderables.make("screen",{map:n,uniforms:e,linear:!0}),this._helpers.visible.make(),this._helpers.object.make([this.compose])):void 0},e.prototype.made=function(){return this.resize()},e.prototype.unmake=function(){return this._helpers.bind.unmake(),this._helpers.visible.unmake(),this._helpers.object.unmake()},e.prototype.change=function(t,e,n){return t["operator.source"]||t["compose.alpha"]?this.rebuild():void 0},e}(i),e.exports=r},{"../../../util":175,"../../primitive":44}],99:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("../base/parent"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","root","scene","vertex","texture","rtt","source","index","image"],e.defaults={minFilter:"linear",magFilter:"linear",type:"unsignedByte"},e.prototype.init=function(){return this.rtt=this.scene=this.camera=this.width=this.height=this.history=this.rootSize=this.size=null},e.prototype.indexShader=function(t){return t},e.prototype.imageShader=function(t){return this.rtt.shaderRelative(t)},e.prototype.sourceShader=function(t){return this.rtt.shaderAbsolute(t,this.history)},e.prototype.getDimensions=function(){return{items:1,width:this.width,height:this.height,depth:this.history}},e.prototype.getActiveDimensions=function(){return this.getDimensions()},e.prototype.make=function(){var t,e,n,r,i,o,s,a,u,h,l,c,p,f,d;return this.parentRoot=this._inherit("root"),this.rootSize=this.parentRoot.getSize(),this._listen(this.parentRoot,"root.pre",this.pre),this._listen(this.parentRoot,"root.update",this.update),this._listen(this.parentRoot,"root.render",this.render),this._listen(this.parentRoot,"root.post",this.post),this._listen(this.parentRoot,"root.camera",this.setCamera),this._listen(this.parentRoot,"root.resize",function(t){return this.resize(t.size)}),null!=this.rootSize?(s=this.props,o=s.minFilter,i=s.magFilter,l=s.type,a=this.props,f=a.width,e=a.height,r=a.history,h=a.size,u=h===this.node.attributes["rtt.size"]["enum"].relative,d=u?this.rootSize.renderWidth:1,n=u?this.rootSize.renderHeight:1,this.width=Math.round(null!=f?f*d:this.rootSize.renderWidth),this.height=Math.round(null!=e?e*n:this.rootSize.renderHeight),this.history=r,this.aspect=t=this.width/this.height,null==this.scene&&(this.scene=this._renderables.make("scene")),this.rtt=this._renderables.make("renderToTexture",{scene:this.scene,camera:this._context.defaultCamera,width:this.width,height:this.height,frames:this.history,minFilter:o,magFilter:i,type:l}),t=f||e?t:this.rootSize.aspect,p=null!=f?f:this.rootSize.viewWidth,c=null!=e?e:this.rootSize.viewHeight,this.size={renderWidth:this.width,renderHeight:this.height,aspect:t,viewWidth:p,viewHeight:c,pixelRatio:this.height/c}):void 0},e.prototype.made=function(){return this.trigger({type:"source.rebuild"}),this.size?this.trigger({type:"root.resize",size:this.size}):void 0},e.prototype.unmake=function(t){return null!=this.rtt?(this.rtt.dispose(),t||this.scene.dispose(),this.rtt=this.width=this.height=this.history=null):void 0},e.prototype.change=function(t,e,n){return e.texture||t["rtt.width"]||t["rtt.height"]?this.rebuild():t["root.camera"]||n?(this._unattach(),this._attach(this.props.camera,"camera",this.setCamera,this,this,!0),this.setCamera()):void 0},e.prototype.adopt=function(t){var e,n,r,i,o;for(i=t.renders,o=[],e=0,n=i.length;n>e;e++)r=i[e],o.push(this.scene.add(r));return o},e.prototype.unadopt=function(t){var e,n,r,i,o;for(i=t.renders,o=[],e=0,n=i.length;n>e;e++)r=i[e],o.push(this.scene.remove(r));return o},e.prototype.resize=function(t){var e,n,r,i;return this.rootSize=t,n=this.props,i=n.width,e=n.height,t=n.size,r=t===this.node.attributes["rtt.size"]["enum"].relative,!this.rtt||null==i||null==e||r?this.rebuild():void 0},e.prototype.select=function(t){return this._root.node.model.select(t,[this.node])},e.prototype.watch=function(t,e){return this._root.node.model.watch(t,e)},e.prototype.unwatch=function(t){return this._root.node.model.unwatch(t)},e.prototype.pre=function(t){return this.trigger(t)},e.prototype.update=function(t){var e;return null!=(e=this.getOwnCamera())&&(e.aspect=this.aspect||1,e.updateProjectionMatrix()),this.trigger(t)},e.prototype.render=function(t){var e;return this.trigger(t),null!=(e=this.rtt)?e.render(this.getCamera()):void 0},e.prototype.post=function(t){return this.trigger(t)},e.prototype.setCamera=function(){var t,e;return t=null!=(e=this.select(this.props.camera)[0])?e.controller:void 0,this.camera!==t?(this.camera=t,this.rtt.camera=this.getCamera(),this.trigger({type:"root.camera"})):this.camera?void 0:this.trigger({type:"root.camera"})},e.prototype.getOwnCamera=function(){var t;return null!=(t=this.camera)?t.getCamera():void 0},e.prototype.getCamera=function(){var t;return null!=(t=this.getOwnCamera())?t:this._inherit("root").getCamera()},e.prototype.vertex=function(t,e){return 2===e?t.pipe("view.position"):3===e?t.pipe("root.position"):t},e}(r),e.exports=i},{"../../../util":175,"../base/parent":47}],100:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("../../primitive"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","bind","shader"],e.freeform=!0,e.prototype.init=function(){return this.shader=null},e.prototype.make=function(){var t,e,n,r,i,o,s,a,u,h,l,c;if(s=this.props,r=s.language,t=s.code,"glsl"!==r)throw new Error("GLSL required");for(this._helpers.bind.make([{to:"shader.sources",trait:"source",multiple:!0}]),u=this._shaders.fetch(t),l=this._types,c={},o=function(t){return function(t){var e;switch(t){case"i":return l["int"]();case"f":return l.number();case"v2":return l.vec2();case"v3":return l.vec3();case"v4":return l.vec4();case"m3":return l.mat3();case"m4":return l.mat4();case"t":return l.object();default:return e=t.split(""),"v"===e.pop()?l.array(o(e.join(""))):null}}}(this),a=u._signatures.uniform,n=0,i=a.length;i>n;n++)e=a[n],(h=o(e.type))&&(c[e.name]=h);return this.reconfigure({props:{uniform:c}})},e.prototype.made=function(){return this.trigger({type:"source.rebuild"})},e.prototype.unmake=function(){return this.shader=null},e.prototype.change=function(t,e,n){return t["shader.uniforms"]||t["shader.code"]||t["shader.language"]?this.rebuild():void 0},e.prototype.shaderBind=function(t){var e,n,r,i,o,s,a,u,h,l,c,p,f;null==t&&(t={}),a=this.props,i=a.language,e=a.code,u=this.node.attributes;for(r in u)f=u[r],null!=f.type&&null!=f["short"]&&"uniform"===f.ns&&null==t[s=f["short"]]&&(t[s]=f);if(null!=(p=this.props.uniforms))for(r in p)f=p[r],t[r]=f;if(l=this._shaders.shader(),null!=this.bind.sources)for(h=this.bind.sources,n=0,o=h.length;o>n;n++)c=h[n],l.require(c.sourceShader(this._shaders.shader()));return l.pipe(e,t)},e}(r),e.exports=i},{"../../../util":175,"../../primitive":44}],101:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("../operator/operator"),o=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","bind","operator","texture","text","format","font"],e.defaults={minFilter:"linear",magFilter:"linear"},e.prototype.init=function(){return e.__super__.init.apply(this,arguments),this.atlas=this.buffer=this.used=this.time=null,this.filled=!1},e.prototype.sourceShader=function(t){return this.buffer.shader(t)},e.prototype.textShader=function(t){return this.atlas.shader(t)},e.prototype.textIsSDF=function(){return this.props.sdf>0},e.prototype.textHeight=function(){return this.props.detail},e.prototype.make=function(){var t,e,n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g;return this._helpers.bind.make([{to:"operator.source",trait:"raw"}]),l=this.props,h=l.minFilter,u=l.magFilter,d=l.type,c=this.props,o=c.font,f=c.style,m=c.variant,v=c.weight,n=c.detail,p=c.sdf,this.atlas=this._renderables.make("textAtlas",{font:o,size:n,style:f,variant:m,weight:v,outline:p,minFilter:h,magFilter:u,type:d}),h=THREE.NearestFilter,u=THREE.NearestFilter,d=THREE.FloatType,r=this.bind.source.getDimensions(),a=r.items,g=r.width,s=r.height,e=r.depth,this.buffer=this._renderables.make("voxelBuffer",{width:g,height:s,depth:e,channels:4,items:a,minFilter:h,magFilter:u,type:d}),t=this.atlas,i=this.buffer.streamer.emit,this.buffer.streamer.emit=function(e){return t.map(e,i)},this.clockParent=this._inherit("clock"),this._listen("root","root.update",this.update)},e.prototype.made=function(){return e.__super__.made.apply(this,arguments),this.resize()},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),this.buffer&&(this.buffer.dispose(),this.buffer=null),this.atlas?(this.atlas.dispose(),this.atlas=null):void 0},e.prototype.update=function(){var t,e,n;return e=this.bind.source.rawBuffer(),t=this.buffer,this.filled&&!this.props.live||!this.through?void 0:(this.time=this.clockParent.getTime(),n=this.used,this.atlas.begin(),this.used=this.through(),this.buffer.write(this.used),this.atlas.end(),this.filled=!0,n!==this.used?this.trigger({type:"source.resize"}):void 0)},e.prototype.change=function(t,e,n){var r,i,o,s,a,u;return e.font?this.rebuild():t["format.expr"]||t["format.digits"]||t["format.data"]||n?(u=this.props,i=u.digits,o=u.expr,r=u.data,null==o&&(o=null!=r?function(t,e,n,i,o){return r[o]}:function(t){return t}),s=o.length,null!=i&&(o=function(t){return function(e,n,r,o,s,a,u,h,l,c){return+t(e,n,r,o,s,a,u,h,l,c).toPrecision(i)}}(o)),a=s>8?function(t){return function(e,n,r,i,s,a,u,h,l,c,p){return e(o(n,r,i,s,a,u,h,l,t.time.clock,t.time.step))}}(this):function(t){return function(t,e,n,r,i,s,a,u,h){return t(o(e,n,r,i,s,a,u,h))}}(this),this.through=this.bind.source.rawBuffer().through(a,this.buffer)):void 0},e}(i),e.exports=r},{"../../../util":175,"../operator/operator":78}],102:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("../../primitive"),o=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","bind","object","visible","style","label","attach","geometry","position"],e.prototype.make=function(){var t,n,r,i,s,a,u,h,l,c,p,f,d,m,v,g,E,y;return e.__super__.make.apply(this,arguments),this._helpers.bind.make([{to:"label.text",trait:"text"},{to:"geometry.points",trait:"source"},{to:"geometry.colors",trait:"source"}]),null!=this.bind.points&&null!=this.bind.text?(l=this.bind.points.getDimensions(),m=this.bind.text.getDimensions(),v=this.bind.text.textIsSDF(),s=Math.min(l.items,m.items),y=Math.min(l.width,m.width),i=Math.min(l.height,m.height),r=Math.min(l.depth,m.depth),c=this.bind.points.sourceShader(this._shaders.shader()),c=this._helpers.position.pipeline(c),f=this.bind.text.sourceShader(this._shaders.shader()),u=this._shaders.shader().pipe("label.map"),u.pipe(this.bind.text.textShader(this._shaders.shader())), -a={spriteDepth:this.node.attributes["attach.depth"],spriteOffset:this.node.attributes["attach.offset"],spriteSnap:this.node.attributes["attach.snap"],spriteScale:this._attributes.make(this._types.number()),outlineStep:this._attributes.make(this._types.number()),outlineExpand:this._attributes.make(this._types.number()),outlineColor:this.node.attributes["label.background"]},this.spriteScale=a.spriteScale,this.outlineStep=a.outlineStep,this.outlineExpand=a.outlineExpand,p=v?"label.outline":"label.alpha",n=this._shaders.shader().pipe(p,a),this.bind.colors&&(t=this._shaders.shader(),this.bind.colors.sourceShader(t)),h=this._helpers.object.mask(),d=this._helpers.style.uniforms(),E=this._inherit("unit").getUnitUniforms(),g=o.JS.merge(E,d,a),this.sprite=this._renderables.make("sprite",{uniforms:g,width:y,height:i,depth:r,items:s,position:c,sprite:f,map:u,combine:n,color:t,mask:h,linear:!0}),this._helpers.visible.make(),this._helpers.object.make([this.sprite])):void 0},e.prototype.unmake=function(){return this._helpers.bind.unmake(),this._helpers.visible.unmake(),this._helpers.object.unmake(),this.sprite=null},e.prototype.resize=function(){var t,e,n,r,i,o;return r=this.bind.points.getActiveDimensions(),i=this.bind.text.getActiveDimensions(),n=Math.min(r.items,i.items),o=Math.min(r.width,i.width),e=Math.min(r.height,i.height),t=Math.min(r.depth,i.depth),this.sprite.geometry.clip(o,e,t,n)},e.prototype.change=function(t,e,n){var r,i,o,s,a;if(e.geometry||t["label.text"])return this.rebuild();if(null!=this.bind.points)return a=this.props.size,o=this.props.outline,r=this.props.expand,i=this.bind.text.textHeight(),s=a/i,this.outlineExpand.value=r/s*16/255,this.outlineStep.value=o/s*16/255,this.spriteScale.value=s},e}(i),e.exports=r},{"../../../util":175,"../../primitive":44}],103:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("../operator/resample"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","bind","operator","resample","sampler:x","sampler:y","sampler:z","sampler:w","include","text"],e.prototype.init=function(){return this.sourceSpec=[{to:"operator.source",trait:"text"}]},e.prototype.textShader=function(t){return this.bind.source.textShader(t)},e.prototype.textIsSDF=function(){var t;return(null!=(t=this.bind.source)?t.props.sdf:void 0)>0},e.prototype.textHeight=function(){var t;return null!=(t=this.bind.source)?t.props.detail:void 0},e}(r),e.exports=i},{"../../../util":175,"../operator/resample":81}],104:[function(t,e,n){var r,i,o,s,a=function(t,e){function n(){this.constructor=t}for(var r in e)u.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},u={}.hasOwnProperty;r=t("../data/buffer"),s=t("../data/voxel"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return a(e,t),e.traits=["node","buffer","active","data","texture","voxel","text","font"],e.defaults={minFilter:"linear",magFilter:"linear"},e.finals={channels:1},e.prototype.init=function(){return e.__super__.init.apply(this,arguments),this.atlas=null},e.prototype.textShader=function(t){return this.atlas.shader(t)},e.prototype.textIsSDF=function(){return this.props.sdf>0},e.prototype.textHeight=function(){return this.props.detail},e.prototype.make=function(){var t,e,n,i,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S;return d=this.props,f=d.minFilter,p=d.magFilter,R=d.type,m=this.props,h=m.font,w=m.style,H=m.variant,M=m.weight,s=m.detail,T=m.sdf,this.atlas=this._renderables.make("textAtlas",{font:h,size:s,style:w,variant:H,weight:M,outline:T,minFilter:f,magFilter:p,type:R}),this.minFilter=THREE.NearestFilter,this.magFilter=THREE.NearestFilter,this.type=THREE.FloatType,r.prototype.make.call(this),f=null!=(v=this.minFilter)?v:this.props.minFilter,p=null!=(g=this.magFilter)?g:this.props.magFilter,R=null!=(E=this.type)?E:this.props.type,S=this.props.width,l=this.props.height,i=this.props.depth,y=this.props.bufferWidth,_=this.props.bufferHeight,b=this.props.bufferDepth,e=this.props.channels,c=this.props.items,a=this.spec={channels:e,items:c,width:S,height:l,depth:i},this.items=a.items,this.channels=a.channels,n=this.props.data,a=o.Data.getDimensions(n,a),x=this.space,x.width=Math.max(y,a.width||1),x.height=Math.max(_,a.height||1),x.depth=Math.max(b,a.depth||1),this.buffer=this._renderables.make(this.storage,{width:x.width,height:x.height,depth:x.depth,channels:4,items:c,minFilter:f,magFilter:p,type:R}),t=this.atlas,u=this.buffer.streamer.emit,this.buffer.streamer.emit=function(e){return t.map(e,u)}},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),this.atlas?(this.atlas.dispose(),this.atlas=null):void 0},e.prototype.update=function(){return this.atlas.begin(),e.__super__.update.apply(this,arguments),this.atlas.end()},e.prototype.change=function(t,n,r){return n.font?this.rebuild():e.__super__.change.call(this,t,n,r)},e}(s),e.exports=i},{"../../../util":175,"../data/buffer":55,"../data/voxel":61}],105:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;i=t("../base/parent"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","clock","seek","play"],e.prototype.init=function(){return this.skew=0,this.last=0,this.time={now:+new Date/1e3,time:0,delta:0,clock:0,step:0}},e.prototype.make=function(){return this._listen("clock","clock.tick",this.tick)},e.prototype.reset=function(){return this.skew=0},e.prototype.tick=function(t){var e,n,r,i,o,s,a,u,h,l,c,p,f;return h=this.props,i=h.from,f=h.to,c=h.speed,l=h.seek,o=h.pace,n=h.delay,u=h.realtime,s=this._inherit("clock").getTime(),p=u?s.time:s.clock,r=u?s.delta:s.step,a=c/o,this.skew+=r*(a-1),this.last>p&&(this.skew=0),this.time.now=s.now+this.skew,this.time.time=s.time,this.time.delta=s.delta,e=null!=l?l:s.clock+this.skew,this.time.clock=Math.min(f,i+Math.max(0,e-n*a)),this.time.step=r*a,this.last=p,this.trigger(t)},e.prototype.getTime=function(){return this.time},e}(i),e.exports=r},{"../base/parent":47}],106:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;i=t("../base/parent"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","clock","now"],e.prototype.init=function(){var t;return this.now=t=+new Date/1e3,this.skew=0,this.time={now:t,time:0,delta:0,clock:0,step:0}},e.prototype.make=function(){return this.clockParent=this._inherit("clock"),this._listen("clock","clock.tick",this.tick)},e.prototype.unmake=function(){return this.clockParent=null},e.prototype.change=function(t,e,n){return t["date.now"]?this.skew=0:void 0},e.prototype.tick=function(t){var e,n,r,i,o,s,a;return i=this.props,e=i.now,s=i.seek,n=i.pace,a=i.speed,r=this.clockParent.getTime(),this.skew+=r.step*n/a,null!=s&&(this.skew=s),this.time.now=this.time.time=this.time.clock=(null!=(o=this.props.now)?o:this.now)+this.skew,this.time.delta=this.time.step=r.delta,this.trigger(t)},e.prototype.getTime=function(){return this.time},e}(i),e.exports=r},{"../base/parent":47}],107:[function(t,e,n){var r,i;i=t("./types"),r={node:{id:i.nullable(i.string()),classes:i.classes()},entity:{active:i.bool(!0)},object:{visible:i.bool(!0)},unit:{scale:i.nullable(i.number()),fov:i.nullable(i.number()),focus:i.nullable(i.number(1),!0)},span:{range:i.nullable(i.vec2(-1,1))},view:{range:i.array(i.vec2(-1,1),4)},view3:{position:i.vec3(),quaternion:i.quat(),rotation:i.vec3(),scale:i.vec3(1,1,1),eulerOrder:i.swizzle("xyz")},view4:{position:i.vec4(),scale:i.vec4(1,1,1,1)},layer:{depth:i.number(1),fit:i.fit("y")},vertex:{pass:i.vertexPass()},fragment:{pass:i.fragmentPass(),gamma:i.bool(!1)},transform3:{position:i.vec3(),quaternion:i.quat(),rotation:i.vec3(),eulerOrder:i.swizzle("xyz"),scale:i.vec3(1,1,1),matrix:i.mat4(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1)},transform4:{position:i.vec4(),scale:i.vec4(1,1,1,1),matrix:i.mat4(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1)},camera:{proxy:i.bool(!1),position:i.nullable(i.vec3()),quaternion:i.nullable(i.quat()),rotation:i.nullable(i.vec3()),lookAt:i.nullable(i.vec3()),up:i.nullable(i.vec3()),eulerOrder:i.swizzle("xyz"),fov:i.nullable(i.number(1))},polar:{bend:i.number(1),helix:i.number(0)},spherical:{bend:i.number(1)},stereographic:{bend:i.number(1)},interval:{axis:i.axis()},area:{axes:i.swizzle([1,2],2)},volume:{axes:i.swizzle([1,2,3],3)},origin:{origin:i.vec4()},scale:{divide:i.number(10),unit:i.number(1),base:i.number(10),mode:i.scale(),start:i.bool(!0),end:i.bool(!0),zero:i.bool(!0),factor:i.positive(i.number(1)),nice:i.bool(!0)},grid:{lineX:i.bool(!0),lineY:i.bool(!0),crossed:i.bool(!1),closedX:i.bool(!1),closedY:i.bool(!1)},axis:{detail:i["int"](1),crossed:i.bool(!1)},data:{data:i.nullable(i.data()),expr:i.nullable(i.emitter()),bind:i.nullable(i.func()),live:i.bool(!0)},buffer:{channels:i["enum"](4,[1,2,3,4]),items:i["int"](1),fps:i.nullable(i["int"](60)),hurry:i["int"](5),limit:i["int"](60),realtime:i.bool(!1),observe:i.bool(!1),aligned:i.bool(!1)},sampler:{centered:i.bool(!1),padding:i.number(0)},array:{width:i.nullable(i.positive(i["int"](1),!0)),bufferWidth:i["int"](1),history:i["int"](1)},matrix:{width:i.nullable(i.positive(i["int"](1),!0)),height:i.nullable(i.positive(i["int"](1),!0)),history:i["int"](1),bufferWidth:i["int"](1),bufferHeight:i["int"](1)},voxel:{width:i.nullable(i.positive(i["int"](1),!0)),height:i.nullable(i.positive(i["int"](1),!0)),depth:i.nullable(i.positive(i["int"](1),!0)),bufferWidth:i["int"](1),bufferHeight:i["int"](1),bufferDepth:i["int"](1)},resolve:{expr:i.nullable(i.func()),items:i["int"](1)},style:{opacity:i.positive(i.number(1)),color:i.color(),blending:i.blending(),zWrite:i.bool(!0),zTest:i.bool(!0),zIndex:i.positive(i.round()),zBias:i.number(0),zOrder:i.nullable(i["int"]())},geometry:{points:i.select(),colors:i.nullable(i.select())},point:{size:i.positive(i.number(4)),sizes:i.nullable(i.select()),shape:i.shape(),optical:i.bool(!0),fill:i.bool(!0),depth:i.number(1)},line:{width:i.positive(i.number(2)),depth:i.positive(i.number(1)),join:i.join(),stroke:i.stroke(),proximity:i.nullable(i.number(1/0)),closed:i.bool(!1)},mesh:{fill:i.bool(!0),shaded:i.bool(!1),map:i.nullable(i.select()),lineBias:i.number(5)},strip:{line:i.bool(!1)},face:{line:i.bool(!1)},arrow:{size:i.number(3),start:i.bool(!1),end:i.bool(!1)},ticks:{normal:i.vec3(0,0,1),size:i.positive(i.number(10)),epsilon:i.positive(i.number(.001))},attach:{offset:i.vec2(0,-20),snap:i.bool(!1),depth:i.number(0)},format:{digits:i.nullable(i.positive(i.number(3))),data:i.nullable(i.data()),expr:i.nullable(i.func()),live:i.bool(!0)},font:{font:i.font("sans-serif"),style:i.string(),variant:i.string(),weight:i.string(),detail:i.number(24),sdf:i.number(5)},label:{text:i.select(),size:i.number(16),outline:i.number(2),expand:i.number(0),background:i.color(1,1,1)},overlay:{opacity:i.number(1),zIndex:i.positive(i.round(0))},dom:{points:i.select(),html:i.select(),size:i.number(16),outline:i.number(2),zoom:i.number(1),color:i.nullable(i.color()),attributes:i.nullable(i.object()),pointerEvents:i.bool(!1)},texture:{minFilter:i.filter("nearest"),magFilter:i.filter("nearest"),type:i.type("float")},shader:{sources:i.nullable(i.select()),language:i.string("glsl"),code:i.string(),uniforms:i.nullable(i.object())},include:{shader:i.select()},operator:{source:i.select()},spread:{unit:i.mapping(),items:i.nullable(i.vec4()),width:i.nullable(i.vec4()),height:i.nullable(i.vec4()),depth:i.nullable(i.vec4()),alignItems:i.anchor(),alignWidth:i.anchor(),alignHeight:i.anchor(),alignDepth:i.anchor()},grow:{scale:i.number(1),items:i.nullable(i.anchor()),width:i.nullable(i.anchor()),height:i.nullable(i.anchor()),depth:i.nullable(i.anchor())},split:{order:i.transpose("wxyz"),axis:i.nullable(i.axis()),length:i["int"](1),overlap:i["int"](0)},join:{order:i.transpose("wxyz"),axis:i.nullable(i.axis()),overlap:i["int"](0)},swizzle:{order:i.swizzle()},transpose:{order:i.transpose()},repeat:{items:i.number(1),width:i.number(1),height:i.number(1),depth:i.number(1)},slice:{items:i.nullable(i.vec2()),width:i.nullable(i.vec2()),height:i.nullable(i.vec2()),depth:i.nullable(i.vec2())},lerp:{size:i.mapping("absolute"),items:i.nullable(i.number()),width:i.nullable(i.number()),height:i.nullable(i.number()),depth:i.nullable(i.number())},subdivide:{items:i.nullable(i.positive(i["int"](),!0)),width:i.nullable(i.positive(i["int"](),!0)),height:i.nullable(i.positive(i["int"](),!0)),depth:i.nullable(i.positive(i["int"](),!0)),bevel:i.number(1),lerp:i.bool(!0)},resample:{indices:i.number(4),channels:i.number(4),sample:i.mapping(),size:i.mapping("absolute"),items:i.nullable(i.number()),width:i.nullable(i.number()),height:i.nullable(i.number()),depth:i.nullable(i.number())},readback:{type:i.type("float"),expr:i.nullable(i.func()),data:i.data(),channels:i["enum"](4,[1,2,3,4]),items:i.nullable(i["int"]()),width:i.nullable(i["int"]()),height:i.nullable(i["int"]()),depth:i.nullable(i["int"]())},root:{speed:i.number(1),camera:i.select("[camera]")},inherit:{source:i.select(),traits:i.array(i.string())},rtt:{size:i.mapping("absolute"),width:i.nullable(i.number()),height:i.nullable(i.number()),history:i["int"](1)},compose:{alpha:i.bool(!1)},present:{index:i["int"](1),directed:i.bool(!0),length:i.number(0)},slide:{order:i.nullable(i["int"](0)),steps:i.number(1),early:i["int"](0),late:i["int"](0),from:i.nullable(i["int"](0)),to:i.nullable(i["int"](1))},transition:{stagger:i.vec4(),enter:i.nullable(i.number(1)),exit:i.nullable(i.number(1)),delay:i.number(0),delayEnter:i.nullable(i.number(0)),delayExit:i.nullable(i.number(0)),duration:i.number(.3),durationEnter:i.nullable(i.number(0)),durationExit:i.nullable(i.number(0))},move:{from:i.vec4(),to:i.vec4()},seek:{seek:i.nullable(i.number(0))},track:{target:i.select(),script:i.object({}),ease:i.ease("cosine")},trigger:{trigger:i.nullable(i["int"](1),!0)},step:{playback:i.ease("linear"),stops:i.nullable(i.array(i.number())),delay:i.number(0),duration:i.number(.3),pace:i.number(0),speed:i.number(1),rewind:i.number(2),skip:i.bool(!0),realtime:i.bool(!1)},play:{delay:i.number(0),pace:i.number(1),speed:i.number(1),from:i.number(0),to:i.number(1/0),realtime:i.bool(!1),loop:i.bool(!1)},now:{now:i.nullable(i.timestamp()),seek:i.nullable(i.number(0)),pace:i.number(1),speed:i.number(1)}},e.exports=r},{"./types":115}],108:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;i=t("./transform"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","include","fragment","bind"],e.prototype.make=function(){return this._helpers.bind.make([{to:"include.shader",trait:"shader",optional:!0}])},e.prototype.unmake=function(){return this._helpers.bind.unmake()},e.prototype.change=function(t,e,n){return e.include||t["fragment.gamma"]?this.rebuild():void 0},e.prototype.fragment=function(t,n){return null!=this.bind.shader&&n===this.props.pass&&(this.props.gamma&&t.pipe("mesh.gamma.out"),t.pipe(this.bind.shader.shaderBind()),t.split(),this.props.gamma&&t.pipe("mesh.gamma.in"),t.pass()),e.__super__.fragment.call(this,t,n)},e}(i),e.exports=r},{"./transform":111}],109:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("./transform"),o=Math.PI,r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","vertex","layer"],e.prototype.make=function(){return this._listen("root","root.resize",this.update),this.uniforms={layerScale:this._attributes.make(this._types.vec4()),layerBias:this._attributes.make(this._types.vec4())}},e.prototype.update=function(){var t,e,n,r,i,s,a,u,h,l,c,p;switch(n=this._inherit("root").getCamera(),p=this._inherit("root").getSize(),e=null!=(u=n.aspect)?u:1,s=null!=(h=n.fov)?h:1,a=Math.tan(s*o/360),t=this.node.attributes["layer.fit"]["enum"],l=this.props,i=l.fit,r=l.depth,c=l.scale,i){case t.contain:i=e>1?t.y:t.x;break;case t.cover:i=e>1?t.x:t.y}switch(i){case t.x:this.uniforms.layerScale.value.set(a*e,a*e);break;case t.y:this.uniforms.layerScale.value.set(a,a)}return this.uniforms.layerBias.value.set(0,0,-r,0)},e.prototype.change=function(t,e,n){return t["layer.fit"]||t["layer.depth"]||n?this.update():void 0},e.prototype.vertex=function(t,e){return 2===e?t.pipe("layer.position",this.uniforms):3===e?t.pipe("root.position"):t},e}(i),e.exports=r},{"./transform":111}],110:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;i=t("../base/parent"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","include","mask","bind"],e.prototype.make=function(){return this._helpers.bind.make([{to:"include.shader",trait:"shader",optional:!0}])},e.prototype.unmake=function(){return this._helpers.bind.unmake()},e.prototype.change=function(t,e,n){return e.include?this.rebuild():void 0},e.prototype.mask=function(t){var e,n,r;return null!=this.bind.shader?t?(r=this._shaders.shader(),r.pipe(Util.GLSL.identity("vec4")),r.fan(),r.pipe(t),r.next(),r.pipe(this.bind.shader.shaderBind()),r.end(),r.pipe("float combine(float a, float b) { return min(a, b); }")):(r=this._shaders.shader(),r.pipe(this.bind.shader.shaderBind())):r=t,null!=(e=null!=(n=this._inherit("mask"))?n.mask(r):void 0)?e:r},e}(i),e.exports=r},{"../base/parent":47}],111:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;r=t("../base/parent"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","vertex","fragment"],e.prototype.vertex=function(t,e){var n,r;return null!=(n=null!=(r=this._inherit("vertex"))?r.vertex(t,e):void 0)?n:t},e.prototype.fragment=function(t,e){var n,r;return null!=(n=null!=(r=this._inherit("fragment"))?r.fragment(t,e):void 0)?n:t},e}(r),e.exports=i},{"../base/parent":47}],112:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./transform"),o=t("../../../util"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","vertex","transform3"],e.prototype.make=function(){return this.uniforms={transformMatrix:this._attributes.make(this._types.mat4())},this.composer=o.Three.transformComposer()},e.prototype.unmake=function(){return delete this.uniforms},e.prototype.change=function(t,e,n){var r,i,o,s,a,u;if(t["transform3.pass"])return this.rebuild();if(e.transform3||n)return o=this.props.position,s=this.props.quaternion,a=this.props.rotation,u=this.props.scale,i=this.props.matrix,r=this.props.eulerOrder,this.uniforms.transformMatrix.value=this.composer(o,a,s,u,i,r)},e.prototype.vertex=function(t,n){return n===this.props.pass&&t.pipe("transform3.position",this.uniforms),e.__super__.vertex.call(this,t,n)},e}(r),e.exports=i},{"../../../util":175,"./transform":111}],113:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;r=t("./transform"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","vertex","transform4"],e.prototype.make=function(){return this.uniforms={transformMatrix:this._attributes.make(this._types.mat4()),transformOffset:this.node.attributes["transform4.position"]},this.transformMatrix=this.uniforms.transformMatrix.value},e.prototype.unmake=function(){return delete this.uniforms},e.prototype.change=function(t,e,n){var r,i,o;if(t["transform4.pass"])return this.rebuild();if(e.transform4||n)return i=this.props.scale,r=this.props.matrix,o=this.transformMatrix,o.copy(r),o.scale(i)},e.prototype.vertex=function(t,n){return n===this.props.pass&&t.pipe("transform4.position",this.uniforms),e.__super__.vertex.call(this,t,n)},e}(r),e.exports=i},{"./transform":111}],114:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;r=t("./transform"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","include","vertex","bind"],e.prototype.make=function(){return this._helpers.bind.make([{to:"include.shader",trait:"shader",optional:!0}])},e.prototype.unmake=function(){return this._helpers.bind.unmake()},e.prototype.change=function(t,e,n){return e.include?this.rebuild():void 0},e.prototype.vertex=function(t,n){return null!=this.bind.shader&&n===this.props.pass&&t.pipe(this.bind.shader.shaderBind()),e.__super__.vertex.call(this,t,n)},e}(r),e.exports=i},{"./transform":111}],115:[function(t,e,n){var r,i,o,s=[].indexOf||function(t){for(var e=0,n=this.length;n>e;e++)if(e in this&&this[e]===t)return e;return-1};i=t("../../util"),r={array:function(t,e,n){var r,i;return null==n&&(n=null),r=t.lerp?function(e,n,r,i){var o,s,a,u;for(a=Math.min(e.length,n.length),o=s=0,u=a;u>=0?u>s:s>u;o=u>=0?++s:--s)r[o]=t.lerp(e[o],n[o],r[o],i);return r}:void 0,i=t.op?function(e,n,r,i){var o,s,a,u;for(a=Math.min(e.length,n.length),o=s=0,u=a;u>=0?u>s:s>u;o=u>=0?++s:--s)r[o]=t.op(e[o],n[o],r[o],i);return r}:void 0,null==n||n instanceof Array||(n=[n]),{uniform:function(){return t.uniform?t.uniform()+"v":void 0},make:function(){var r,i,o,s;if(null!=n)return n.slice();if(!e)return[];for(s=[],r=i=0,o=e;o>=0?o>i:i>o;r=o>=0?++i:--i)s.push(t.make());return s},validate:function(n,r,i){var o,s,a,u,h,l;for(n instanceof Array||(n=[n]),u=r.length=e?e:n.length,o=a=0,h=u;h>=0?h>a:a>h;o=h>=0?++a:--a)s=null!=(l=n[o])?l:t.make(),r[o]=t.validate(s,r[o],i);return r},equals:function(e,n){var r,i,o,s,a,u;if(r=e.length,i=n.length,r!==i)return!1;for(a=Math.min(r,i),o=s=0,u=a;u>=0?u>s:s>u;o=u>=0?++s:--s)if(!("function"==typeof t.equals?t.equals(e[o],n[o]):void 0))return!1;return!0},lerp:r,op:i,clone:function(e){var n,r,i,o;for(i=[],n=0,r=e.length;r>n;n++)o=e[n],i.push(t.clone(o));return i}}},letters:function(t,e,n){var i,o,s,a,u;if(null==n&&(n=null),null!=n)for(n===""+n&&(n=n.split("")),o=s=0,a=n.length;a>s;o=++s)u=n[o],n[o]=t.validate(u,u);return i=r.array(t,e,n),{uniform:function(){return i.uniform()},make:function(){return i.make()},validate:function(t,e,n){return t===""+t&&(t=t.split("")),i.validate(t,e,n)},equals:function(t,e){return i.equals(t,e)},clone:i.clone}},nullable:function(t,e){var n,r,i,o;return null==e&&(e=!1),o=e?t.make():null,n=t.emitter?function(e,n){return null==n?e:null==e?n:t.emitter(e,n)}:void 0,r=t.lerp?function(e,n,r,i){return null===e||null===n?.5>i?e:n:(null==r&&(r=t.make()),o=t.lerp(e,n,r,i),r)}:void 0,i=t.op?function(e,n,r,i){return null===e||null===n?null:(null==r&&(r=t.make()),o=t.op(e,n,r,i))}:void 0,{make:function(){return o},validate:function(e,n,r){return null===e?e:(null===n&&(n=t.make()),t.validate(e,n,r))},uniform:function(){return"function"==typeof t.uniform?t.uniform():void 0},equals:function(e,n){var r,i,o;return r=null===e,i=null===n,r&&i?!0:r^i?!1:null!=(o="function"==typeof t.equals?t.equals(e,n):void 0)?o:e===n},lerp:r,op:i,emitter:n}},"enum":function(t,e,n){var r,i,o,s,a,u,h;for(null==e&&(e=[]),null==n&&(n={}),r=0,h={},i=0,s=e.length;s>i;i++)o=e[i],o!==+o&&null==n[o]&&(n[o]=r++);for(u=0,a=e.length;a>u;u++)o=e[u],o===+o&&(h[o]=o);for(o in n)r=n[o],h[r]=!0;return null==h[t]&&(t=n[t]),{"enum":function(){return n},make:function(){return t},validate:function(t,e,r){var i;return i=h[t]?t:n[t],null!=i?i:r()}}},enumber:function(t,e,n){var i;return null==n&&(n={}),i=r["enum"](t,e,n),{"enum":i["enum"],uniform:function(){return"f"},make:function(){var e;return null!=(e=i.make())?e:+t},validate:function(t,e,n){return t===+t?t:i.validate(t,e,n)},op:function(t,e,n,r){return r(t,e)}}},select:function(t){return null==t&&(t="<"),{make:function(){return t},validate:function(t,e,n){return"string"==typeof t?t:"object"==typeof t?t:n()}}},bool:function(t){return t=!!t,{uniform:function(){return"f"},make:function(){return t},validate:function(t,e,n){return!!t}}},"int":function(t){return null==t&&(t=0),t=+Math.round(t),{uniform:function(){return"i"},make:function(){return t},validate:function(t,e,n){var r;return t!==(r=+t)?n():Math.round(r)||0},op:function(t,e,n,r){return r(t,e)}}},round:function(t){return null==t&&(t=0),t=+Math.round(t),{uniform:function(){return"f"},make:function(){return t},validate:function(t,e,n){var r;return t!==(r=+t)?n():Math.round(r)||0},op:function(t,e,n,r){return r(t,e)}}},number:function(t){return null==t&&(t=0),{uniform:function(){return"f"},make:function(){return+t},validate:function(t,e,n){var r;return t!==(r=+t)?n():r||0},op:function(t,e,n,r){return r(t,e)}}},positive:function(t,e){return null==e&&(e=!1),{uniform:t.uniform,make:t.make,validate:function(n,r,i){return n=t.validate(n,r,i),0>n||e&&0>=n?i():n},op:function(t,e,n,r){return r(t,e)}}},string:function(t){return null==t&&(t=""),{make:function(){return""+t},validate:function(t,e,n){var r;return t!==(r=""+t)?n():r}}},func:function(){return{make:function(){return function(){}},validate:function(t,e,n){return"function"==typeof t?t:n()}}},emitter:function(){return{make:function(){return function(t){return t(1,1,1,1)}},validate:function(t,e,n){return"function"==typeof t?t:n()},emitter:function(t,e){return i.Data.getLerpEmitter(t,e)}}},object:function(t){return{make:function(){return null!=t?t:{}},validate:function(t,e,n){return"object"==typeof t?t:n()},clone:function(t){return JSON.parse(JSON.stringify(t))}}},timestamp:function(t){return null==t&&(t=null),"string"==typeof t&&(t=Date.parse(t)),{uniform:function(){return"f"},make:function(){return null!=t?t:+new Date},validate:function(t,e,n){var r;return t=Date.parse(t),t!==(r=+t)?n():t},op:function(t,e,n,r){return r(t,e)}}},vec2:function(t,e){var n;return null==t&&(t=0),null==e&&(e=0),n=[t,e],{uniform:function(){return"v2"},make:function(){return new THREE.Vector2(t,e)},validate:function(r,i,o){var s,a,u,h;if(r===+r&&(r=[r]),r instanceof THREE.Vector2)i.copy(r);else if(r instanceof Array)r=r.concat(n.slice(r.length)),i.set.apply(i,r);else{if(null==r)return o();u=null!=(s=r.x)?s:t,h=null!=(a=r.y)?a:e,i.set(u,h)}return i},equals:function(t,e){return t.x===e.x&&t.y===e.y},op:function(t,e,n,r){return n.x=r(t.x,e.x),n.y=r(t.y,e.y),n}}},ivec2:function(t,e){var n,i;return null==t&&(t=0),null==e&&(e=0),i=r.vec2(t,e),n=i.validate,i.validate=function(t,e,r){return n(t,e,r),e.x=Math.round(e.x),e.y=Math.round(e.y),e},i},vec3:function(t,e,n){var r;return null==t&&(t=0),null==e&&(e=0),null==n&&(n=0),r=[t,e,n],{uniform:function(){return"v3"},make:function(){return new THREE.Vector3(t,e,n)},validate:function(i,o,s){var a,u,h,l,c,p;if(i===+i&&(i=[i]),i instanceof THREE.Vector3)o.copy(i);else if(i instanceof Array)i=i.concat(r.slice(i.length)),o.set.apply(o,i);else{if(null==i)return s();l=null!=(a=i.x)?a:t,c=null!=(u=i.y)?u:e,p=null!=(h=i.z)?h:n,o.set(l,c,p)}return o},equals:function(t,e){return t.x===e.x&&t.y===e.y&&t.z===e.z},op:function(t,e,n,r){return n.x=r(t.x,e.x),n.y=r(t.y,e.y),n.z=r(t.z,e.z),n}}},ivec3:function(t,e,n){var i,o;return null==t&&(t=0),null==e&&(e=0),null==n&&(n=0),o=r.vec3(t,e,n),i=o.validate,o.validate=function(t,e){return i(t,e,invalid),e.x=Math.round(e.x),e.y=Math.round(e.y),e.z=Math.round(e.z),e},o},vec4:function(t,e,n,r){var i;return null==t&&(t=0),null==e&&(e=0),null==n&&(n=0),null==r&&(r=0),i=[t,e,n,r],{uniform:function(){return"v4"},make:function(){return new THREE.Vector4(t,e,n,r)},validate:function(o,s,a){var u,h,l,c,p,f,d,m;if(o===+o&&(o=[o]),o instanceof THREE.Vector4)s.copy(o);else if(o instanceof Array)o=o.concat(i.slice(o.length)),s.set.apply(s,o);else{if(null==o)return a();f=null!=(u=o.x)?u:t,d=null!=(h=o.y)?h:e,m=null!=(l=o.z)?l:n,p=null!=(c=o.w)?c:r,s.set(f,d,m,p)}return s},equals:function(t,e){return t.x===e.x&&t.y===e.y&&t.z===e.z&&t.w===e.w},op:function(t,e,n,r){return n.x=r(t.x,e.x),n.y=r(t.y,e.y),n.z=r(t.z,e.z),n.w=r(t.w,e.w),n}}},ivec4:function(t,e,n,i){var o,s;return null==t&&(t=0),null==e&&(e=0),null==n&&(n=0),null==i&&(i=0),s=r.vec4(t,e,n,i),o=s.validate,s.validate=function(t,e){return o(t,e,invalid),e.x=Math.round(e.x),e.y=Math.round(e.y),e.z=Math.round(e.z),e.w=Math.round(e.w),e},s},mat3:function(t,e,n,r,i,o,s,a,u){var h;return null==t&&(t=1),null==e&&(e=0),null==n&&(n=0),null==r&&(r=0),null==i&&(i=1),null==o&&(o=0),null==s&&(s=0),null==a&&(a=0),null==u&&(u=1),h=[t,e,n,r,i,o,s,a,u],{uniform:function(){return"m4"},make:function(){var h;return h=new THREE.Matrix3,h.set(t,e,n,r,i,o,s,a,u),h},validate:function(t,e,n){if(t instanceof THREE.Matrix3)e.copy(t);else{if(!(t instanceof Array))return n();t=t.concat(h.slice(t.length)),e.set.apply(e,t)}return e}}},mat4:function(t,e,n,r,i,o,s,a,u,h,l,c,p,f,d,m){var v;return null==t&&(t=1),null==e&&(e=0),null==n&&(n=0),null==r&&(r=0),null==i&&(i=0),null==o&&(o=1),null==s&&(s=0),null==a&&(a=0),null==u&&(u=0),null==h&&(h=0),null==l&&(l=1),null==c&&(c=0),null==p&&(p=0),null==f&&(f=0),null==d&&(d=0),null==m&&(m=1),v=[t,e,n,r,i,o,s,a,u,h,l,c,p,f,d,m],{uniform:function(){return"m4"},make:function(){var v;return v=new THREE.Matrix4,v.set(t,e,n,r,i,o,s,a,u,h,l,c,p,f,d,m),v},validate:function(t,e,n){if(t instanceof THREE.Matrix4)e.copy(t);else{if(!(t instanceof Array))return n();t=t.concat(v.slice(t.length)),e.set.apply(e,t)}return e}}},quat:function(t,e,n,i){var o;return null==t&&(t=0),null==e&&(e=0),null==n&&(n=0),null==i&&(i=1),o=r.vec4(t,e,n,i),{uniform:function(){return"v4"},make:function(){return new THREE.Quaternion},validate:function(t,e,n){return t instanceof THREE.Quaternion?e.copy(t):e=o.validate(t,e,n),e.normalize(),e},equals:function(t,e){return t.x===e.x&&t.y===e.y&&t.z===e.z&&t.w===e.w},op:function(t,e,n,r){return n.x=r(t.x,e.x),n.y=r(t.y,e.y),n.z=r(t.z,e.z),n.w=r(t.w,e.w),n.normalize(),n},lerp:function(t,e,n,r){return THREE.Quaternion.slerp(t,e,n,r),n}}},color:function(t,e,n){var r;return null==t&&(t=.5),null==e&&(e=.5),null==n&&(n=.5),r=[t,e,n],{uniform:function(){return"c"},make:function(){return new THREE.Color(t,e,n)},validate:function(i,o,s){var a,u,h,l,c,p;if(i===""+i?i=(new THREE.Color).setStyle(i):i===+i&&(i=new THREE.Color(i)),i instanceof THREE.Color)o.copy(i);else if(i instanceof Array)i=i.concat(r.slice(i.length)),o.setRGB.apply(o,i);else{if(null==i)return s();p=null!=(h=i.r)?h:t,u=null!=(l=i.g)?l:e,a=null!=(c=i.b)?c:n,o.set(p,u,a)}return o},equals:function(t,e){return t.r===e.r&&t.g===e.g&&t.b===e.b},op:function(t,e,n,r){return n.r=r(t.r,e.r),n.g=r(t.g,e.g),n.b=r(t.b,e.b),n}}},axis:function(t,e){var n,r,i;return null==t&&(t=1),null==e&&(e=!1),n={x:1,y:2,z:3,w:4,W:1,H:2,D:3,I:4,zero:0,"null":0,width:1,height:2,depth:3,items:4},r=e?[0,1,2,3,4]:[1,2,3,4],null!=(i=n[t])&&(t=i),{make:function(){return t},validate:function(t,e,o){var a;return null!=(i=n[t])&&(t=i),t=null!=(a=Math.round(t))?a:0,s.call(r,t)>=0?t:o()}}},transpose:function(t){var e,n;return null==t&&(t=[1,2,3,4]),n=r.letters(r.axis(null,!1),0,t), -e=r.letters(r.axis(null,!1),4,t),{make:function(){return e.make()},validate:function(t,r,i){var o,s,a,u,h;return u=[1,2,3,4],n.validate(t,u,i),u.length<4&&(a=[1,2,3,4].filter(function(t){return-1===u.indexOf(t)}),u=u.concat(a)),h=function(){var t,e,n;for(n=[],o=t=0,e=u.length;e>t;o=++t)s=u[o],n.push(u.indexOf(s)===o);return n}(),h.indexOf(!1)<0?e.validate(u,r,i):i()},equals:e.equals,clone:e.clone}},swizzle:function(t,e){var n,i;return null==t&&(t=[1,2,3,4]),null==e&&(e=null),null==e&&(e=t.length),t=t.slice(0,e),i=r.letters(r.axis(null,!1),0,t),n=r.letters(r.axis(null,!0),e,t),{make:function(){return n.make()},validate:function(r,o,s){var a;return a=t.slice(),i.validate(r,a,s),a.length0?1/s-1:0,v=this.props.position,_=this.props.scale,g=this.props.quaternion,E=this.props.rotation,f=this.props.range,l=this.props.eulerOrder,M=f[0].x,S=f[1].x,k=f[2].x,a=f[0].y-M||1,u=f[1].y-S||1,h=f[2].y-k||1,x=_.x,w=_.y,R=_.z,m=a>0?1:-1,y=i.Axis.recenterAxis(S,u,s),S=y[0],u=y[1],r=Math.abs(u),c=a+(r*m-a)*s,b=c/x,T=u/w,this.aspect=o=Math.abs(b/T),this.uniforms.polarFocus.value=p,this.uniforms.polarAspect.value=o,this.viewMatrix.set(2/c,0,0,-(2*M+a)/a,0,2/u,0,-(2*S+u)/u,0,0,2/h,-(2*k+h)/h,0,0,0,1),H=this.composer(v,E,g,_,null,l),this.viewMatrix.multiplyMatrices(H,this.viewMatrix),t["view.range"]||e.polar?this.trigger({type:"view.range"}):void 0},e.prototype.vertex=function(t,n){return 1===n&&t.pipe("polar.position",this.uniforms),e.__super__.vertex.call(this,t,n)},e.prototype.axis=function(t){var e,n,r;return r=this.props.range[t-1],n=r.x,e=r.y,2===t&&this.bend>0&&(e=Math.max(Math.abs(e),Math.abs(n)),n=Math.max(-this.focus/this.aspect,n)),new THREE.Vector2(n,e)},e}(o),e.exports=r},{"../../../util":175,"./view":122}],119:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;o=t("./view"),i=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","object","visible","view","view3","spherical","vertex"],e.prototype.make=function(){var t;return e.__super__.make.apply(this,arguments),t=this._attributes.types,this.uniforms={sphericalBend:this.node.attributes["spherical.bend"],sphericalFocus:this._attributes.make(this._types.number()),sphericalAspectX:this._attributes.make(this._types.number()),sphericalAspectY:this._attributes.make(this._types.number()),sphericalScaleY:this._attributes.make(this._types.number()),viewMatrix:this._attributes.make(this._types.mat4())},this.viewMatrix=this.uniforms.viewMatrix.value,this.composer=i.Three.transformComposer(),this.aspectX=1,this.aspectY=1},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),delete this.viewMatrix,delete this.objectMatrix,delete this.aspectX,delete this.aspectY,delete this.uniforms},e.prototype.change=function(t,e,n){var r,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S,k,A,C,P,L,z,O;if(e.view||e.view3||e.spherical||n)return this.bend=u=this.props.bend,this.focus=m=u>0?1/u-1:0,y=this.props.position,w=this.props.scale,_=this.props.quaternion,b=this.props.rotation,v=this.props.range,p=this.props.eulerOrder,L=v[0].x,z=v[1].x,O=v[2].x,h=v[0].y-L||1,l=v[1].y-z||1,c=v[2].y-O||1,k=w.x,A=w.y,C=w.z,T=i.Axis.recenterAxis(z,l,u),z=T[0],l=T[1],x=i.Axis.recenterAxis(O,c,u),O=x[0],c=x[1],g=h>0?1:-1,E=l>0?1:-1,r=Math.abs(c),f=h+(r*g-h)*u,d=l+(r*E-l)*u,H=f/k,M=d/A,S=c/C,this.aspectX=o=Math.abs(H/S),this.aspectY=s=Math.abs(M/S/o),a=l/h*k/A*2,this.scaleY=R=Math.min(s/u,1+(a-1)*u),this.uniforms.sphericalBend.value=u,this.uniforms.sphericalFocus.value=m,this.uniforms.sphericalAspectX.value=o,this.uniforms.sphericalAspectY.value=s,this.uniforms.sphericalScaleY.value=R,this.viewMatrix.set(2/f,0,0,-(2*L+h)/h,0,2/d,0,-(2*z+l)/l,0,0,2/c,-(2*O+c)/c,0,0,0,1),P=this.composer(y,b,_,w,null,p),this.viewMatrix.multiplyMatrices(P,this.viewMatrix),t["view.range"]||e.spherical?this.trigger({type:"view.range"}):void 0},e.prototype.vertex=function(t,n){return 1===n&&t.pipe("spherical.position",this.uniforms),e.__super__.vertex.call(this,t,n)},e.prototype.axis=function(t){var e,n,r;return r=this.props.range[t-1],n=r.x,e=r.y,3===t&&this.bend>0&&(e=Math.max(Math.abs(e),Math.abs(n)),n=Math.max(-this.focus/this.aspectX+.001,n)),new THREE.Vector2(n,e)},e}(o),e.exports=r},{"../../../util":175,"./view":122}],120:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;o=t("./view"),i=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","object","visible","view","view3","stereographic","vertex"],e.prototype.make=function(){var t;return e.__super__.make.apply(this,arguments),t=this._attributes.types,this.uniforms={stereoBend:this.node.attributes["stereographic.bend"],viewMatrix:this._attributes.make(this._types.mat4())},this.viewMatrix=this.uniforms.viewMatrix.value,this.composer=i.Three.transformComposer()},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),delete this.viewMatrix,delete this.rotationMatrix,delete this.uniforms},e.prototype.change=function(t,e,n){var r,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b;if(e.view||e.view3||e.stereographic||n)return this.bend=r=this.props.bend,l=this.props.position,d=this.props.scale,c=this.props.quaternion,p=this.props.rotation,h=this.props.range,u=this.props.eulerOrder,y=h[0].x,_=h[1].x,b=h[2].x,o=h[0].y-y||1,s=h[1].y-_||1,a=h[2].y-b||1,m=d.x,v=d.y,g=d.z,f=i.Axis.recenterAxis(b,a,r,1),b=f[0],a=f[1],this.uniforms.stereoBend.value=r,this.viewMatrix.set(2/o,0,0,-(2*y+o)/o,0,2/s,0,-(2*_+s)/s,0,0,2/a,-(2*b+a)/a,0,0,0,1),E=this.composer(l,p,c,d,null,u),this.viewMatrix.multiplyMatrices(E,this.viewMatrix),t["view.range"]||e.stereographic?this.trigger({type:"view.range"}):void 0},e.prototype.vertex=function(t,n){return 1===n&&t.pipe("stereographic.position",this.uniforms),e.__super__.vertex.call(this,t,n)},e}(o),e.exports=r},{"../../../util":175,"./view":122}],121:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;o=t("./view"),i=t("../../../util"),r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.traits=["node","object","visible","view","view4","stereographic","vertex"],e.prototype.make=function(){return e.__super__.make.apply(this,arguments),this.uniforms={basisOffset:this._attributes.make(this._types.vec4()),basisScale:this._attributes.make(this._types.vec4()),stereoBend:this.node.attributes["stereographic.bend"]},this.basisScale=this.uniforms.basisScale.value,this.basisOffset=this.uniforms.basisOffset.value},e.prototype.unmake=function(){return e.__super__.unmake.apply(this,arguments),delete this.basisScale,delete this.basisOffset,delete this.uniforms},e.prototype.change=function(t,e,n){var r,o,s,a,u,h,l,c,p,f,d,m,v,g;if(e.view||e.view4||e.stereographic||n)return this.bend=r=this.props.bend,c=this.props.position,f=this.props.scale,h=this.props.range,m=h[0].x,v=h[1].x,g=h[2].x,d=h[3].x,s=h[0].y-m||1,a=h[1].y-v||1,u=h[2].y-g||1,o=h[3].y-d||1,l=function(t,e){return t.x*=e.x,t.y*=e.y,t.z*=e.z,t.w*=e.w},p=i.Axis.recenterAxis(d,o,r,1),d=p[0],o=p[1],this.basisScale.set(2/s,2/a,2/u,2/o),this.basisOffset.set(-(2*m+s)/s,-(2*v+a)/a,-(2*g+u)/u,-(2*d+o)/o),l(this.basisScale,f),l(this.basisOffset,f),this.basisOffset.add(c),t["view.range"]||e.stereographic?this.trigger({type:"view.range"}):void 0},e.prototype.vertex=function(t,n){return 1===n&&t.pipe("stereographic4.position",this.uniforms),e.__super__.vertex.call(this,t,n)},e}(o),e.exports=r},{"../../../util":175,"./view":122}],122:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;r=t("../transform/transform"),i=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return o(e,t),e.traits=["node","object","visible","view","vertex"],e.prototype.make=function(){return this._helpers.visible.make()},e.prototype.unmake=function(){return this._helpers.visible.unmake()},e.prototype.axis=function(t){return this.props.range[t-1]},e}(r),e.exports=i},{"../transform/transform":111}],123:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("./databuffer"),o=t("../../util"),r=function(t){function e(t,n,r){this.width=r.width||1,this.history=r.history||1,this.samples=this.width,this.wrap=this.history>1,r.width=this.width,r.height=this.history,r.depth=1,e.__super__.constructor.call(this,t,n,r)}return s(e,t),e.prototype.build=function(t){return e.__super__.build.apply(this,arguments),this.index=0,this.pad=0,this.streamer=this.generate(this.data)},e.prototype.setActive=function(t){return this.pad=Math.max(0,this.width-t)},e.prototype.fill=function(){var t,e,n,r,i,o,s,a,u;for(t=this.callback,"function"==typeof t.reset&&t.reset(),s=this.streamer,r=s.emit,u=s.skip,e=s.count,n=s.done,a=s.reset,a(),o=this.samples-this.pad,i=0;!n()&&o>i&&t(r,i++)!==!1;);return Math.floor(e()/this.items)},e.prototype.write=function(t){return null==t&&(t=this.samples),t*=this.items,this.texture.write(this.data,0,this.index,t,1),this.dataPointer.set(.5,this.index+.5),this.index=(this.index+this.history-1)%this.history,this.filled=Math.min(this.history,this.filled+1)},e.prototype.through=function(t,e){var n,r,i,s,a,u,h,l;return h=l=this.streamer,n=h.consume,r=h.done,s=(i=e.streamer).emit,a=0,u=function(){return n(function(e,n,r,i){return t(s,e,n,r,i,a)})},u=o.Data.repeatCall(u,this.items),function(t){return function(){var e;for(l.reset(),i.reset(),e=t.samples-t.pad,a=0;!r()&&e>a;)u(),a++;return l.count()}}(this)},e}(i),e.exports=r},{"../../util":175,"./databuffer":126}],124:[function(t,e,n){var r,i,o,s,a,u,h=function(t,e){function n(){this.constructor=t}for(var r in e)l.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},l={}.hasOwnProperty;s=t("../renderable"),u=t("../../util"),o=t("./texture/datatexture"),i=t("./texture/backedtexture"),r=function(t){function e(t,n,r){null==this.width&&(this.width=r.width||512),null==this.height&&(this.height=r.height||512),null==this.channels&&(this.channels=r.channels||4),null==this.backed&&(this.backed=r.backed||!1),this.samples=this.width*this.height,e.__super__.constructor.call(this,t,n),this.build(r)}return h(e,t),e.prototype.shader=function(t){return t.pipe("map.2d.data",this.uniforms),t.pipe("sample.2d",this.uniforms),this.channels<4&&t.pipe(u.GLSL.swizzleVec4(["0000","x000","xw00","xyz0"][this.channels])),t},e.prototype.build=function(t){var e;return this.klass=e=this.backed?i:o,this.texture=new e(this.gl,this.width,this.height,this.channels,t),this.uniforms={dataPointer:{type:"v2",value:new THREE.Vector2(0,0)}},this._adopt(this.texture.uniforms),this.reset()},e.prototype.reset=function(){return this.rows=[],this.bottom=0},e.prototype.resize=function(t,e){if(!this.backed)throw new Error("Cannot resize unbacked texture atlas");return t>2048&&e>2048?console.warn("Giant text atlas "+t+"x"+e+"."):console.info("Resizing text atlas "+t+"x"+e+"."),this.texture.resize(t,e),this.width=t,this.height=e,this.samples=t*e},e.prototype.collapse=function(t){var e,n,r;return r=this.rows,r.splice(r.indexOf(t),1),this.bottom=null!=(e=null!=(n=r[r.length-1])?n.bottom:void 0)?e:0,this.last===t?this.last=null:void 0},e.prototype.allocate=function(t,e,n,r){var i,o,s,u,h,l,c,p,f,d,m,v;if(v=this.width,s=this.height,p=2*n,e>v)return this.resize(2*v,2*s),this.last=null,this.allocate(t,e,n,r);if(d=this.last,null!=d&&d.height>=n&&d.heightl;u=++l)if(d=f[u],o=d.top-i,o>=n&&0>h&&(h=u,m=i),i=d.bottom,d.height>=n&&d.height=0)d=new a(m,n),this.rows.splice(h,0,d);else{if(m=i,i+=n,i>=s)return this.resize(2*v,2*s),this.last=null,this.allocate(t,e,n,r);d=new a(m,n),this.rows.push(d),this.bottom=i}d.append(t,e,n,r),this.last=d},e.prototype.read=function(){return this.texture.textureObject},e.prototype.write=function(t,e,n,r,i){return this.texture.write(t,e,n,r,i)},e.prototype.dispose=function(){return this.texture.dispose(),this.data=null,e.__super__.dispose.apply(this,arguments)},e}(s),a=function(){function t(t,e){this.top=t,this.bottom=t+e,this.width=0,this.height=e,this.alive=0,this.keys=[]}return t.prototype.append=function(t,e,n,r){var i,o;return i=this.width,o=this.top,this.alive++,this.width+=e,this.keys.push(t),r(this,i,o)},t}(),e.exports=r},{"../../util":175,"../renderable":161,"./texture/backedtexture":133,"./texture/datatexture":134}],125:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("../renderable"),o=t("../../util"),r=function(t){function e(t,n,r){null==this.items&&(this.items=r.items||1),null==this.samples&&(this.samples=r.samples||1),null==this.channels&&(this.channels=r.channels||4),null==this.callback&&(this.callback=r.callback||function(){}),e.__super__.constructor.call(this,t,n)}return s(e,t),e.prototype.dispose=function(){return e.__super__.dispose.apply(this,arguments)},e.prototype.update=function(){var t;return t=this.fill(),this.write(t),t},e.prototype.setActive=function(t,e,n,r){},e.prototype.setCallback=function(t){this.callback=t},e.prototype.write=function(){},e.prototype.fill=function(){},e.prototype.generate=function(t){return o.Data.getStreamer(t,this.samples,this.channels,this.items)},e}(i),e.exports=r},{"../../util":175,"../renderable":161}],126:[function(t,e,n){var r,i,o,s,a=function(t,e){function n(){this.constructor=t}for(var r in e)u.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},u={}.hasOwnProperty;r=t("./buffer"),o=t("./texture/datatexture"),s=t("../../util"),i=function(t){function e(t,n,r){this.width=r.width||1,this.height=r.height||1,this.depth=r.depth||1,null==this.samples&&(this.samples=this.width*this.height*this.depth),e.__super__.constructor.call(this,t,n,r),this.build(r)}return a(e,t),e.prototype.shader=function(t,e){var n;return null==e&&(e=4),this.items>1||this.depth>1?(4!==e&&t.pipe(s.GLSL.extendVec(e,4)),t.pipe("map.xyzw.texture",this.uniforms)):2!==e&&t.pipe(s.GLSL.truncateVec(e,2)),n=this.wrap?".wrap":"",t.pipe("map.2d.data"+n,this.uniforms),t.pipe("sample.2d",this.uniforms),this.channels<4&&t.pipe(s.GLSL.swizzleVec4(["0000","x000","xw00","xyz0"][this.channels])),t},e.prototype.build=function(t){return this.data=new Float32Array(this.samples*this.channels*this.items),this.texture=new o(this.gl,this.items*this.width,this.height*this.depth,this.channels,t),this.filled=0,this.used=0,this._adopt(this.texture.uniforms),this._adopt({dataPointer:{type:"v2",value:new THREE.Vector2},textureItems:{type:"f",value:this.items},textureHeight:{type:"f",value:this.height}}),this.dataPointer=this.uniforms.dataPointer.value,this.streamer=this.generate(this.data)},e.prototype.dispose=function(){return this.data=null,this.texture.dispose(),e.__super__.dispose.apply(this,arguments)},e.prototype.getFilled=function(){return this.filled},e.prototype.setCallback=function(t){return this.callback=t,this.filled=0},e.prototype.copy=function(t){var e,n,r,i,o;for(i=Math.min(t.length,this.samples*this.channels*this.items),e=this.data,n=r=0,o=i;o>=0?o>r:r>o;n=o>=0?++r:--r)e[n]=t[n];return this.write(Math.ceil(i/this.channels/this.items))},e.prototype.write=function(t){var e,n;return null==t&&(t=this.samples),e=t/this.width,t*=this.items,n=1>e?t:this.items*this.width,e=Math.ceil(e),this.texture.write(this.data,0,0,n,e),this.dataPointer.set(.5,.5),this.filled=1,this.used=t},e.prototype.through=function(t,e){var n,r,i,o,a,u,h,l;return h=l=this.streamer,n=h.consume,r=h.done,o=(i=e.streamer).emit,a=0,u=function(){return n(function(e,n,r,i){return t(o,e,n,r,i,a)})},u=s.Data.repeatCall(u,this.items),function(t){return function(){var e;for(l.reset(),i.reset(),e=t.used,a=0;!r()&&e>a;)u(),a++;return l.count()}}(this)},e}(r),e.exports=i},{"../../util":175,"./buffer":125,"./texture/datatexture":134}],127:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./databuffer"),o=t("../../util"),i=function(t){function e(t,n,r){this.width=r.width||1,this.height=r.height||1,this.history=r.history||1,this.samples=this.width*this.height,this.wrap=this.history>1,r.depth=this.history,e.__super__.constructor.call(this,t,n,r)}return s(e,t),e.prototype.build=function(t){return e.__super__.build.apply(this,arguments),this.index=0,this.pad={x:0,y:0},this.streamer=this.generate(this.data)},e.prototype.getFilled=function(){return this.filled},e.prototype.setActive=function(t,e){var n;return n=[Math.max(0,this.width-t),Math.max(0,this.height-e)],this.pad.x=n[0],this.pad.y=n[1],n},e.prototype.fill=function(){var t,e,n,r,i,o,s,a,u,h,l,c,p,f;if(t=this.callback,"function"==typeof t.reset&&t.reset(),l=this.streamer,r=l.emit,f=l.skip,e=l.count,n=l.done,p=l.reset,p(),u=this.width,h=this.pad.x,a=this.samples-this.pad.y*u,i=o=s=0,h)for(;!n()&&a>s&&(s++,c=t(r,i,o),++i===u-h&&(f(h),i=0,o++),c!==!1););else for(;!n()&&a>s&&(s++,c=t(r,i,o),++i===u&&(i=0,o++),c!==!1););return Math.floor(e()/this.items)},e.prototype.write=function(t){var e,n;return null==t&&(t=this.samples),t*=this.items,n=this.width*this.items,e=Math.ceil(t/n),this.texture.write(this.data,0,this.index*this.height,n,e),this.dataPointer.set(.5,this.index*this.height+.5),this.index=(this.index+this.history-1)%this.history,this.filled=Math.min(this.history,this.filled+1)},e.prototype.through=function(t,e){var n,r,i,s,a,u,h,l,c;return l=c=this.streamer,n=l.consume,r=l.done,s=(i=e.streamer).emit,a=u=0,h=function(){return n(function(e,n,r,i){return t(s,e,n,r,i,a,u)})},h=o.Data.repeatCall(h,this.items),function(t){return function(){var e,n,o,s;if(c.reset(),i.reset(),o=t.width,s=t.pad.x,n=t.samples-t.pad.y*o,a=u=e=0,s)for(;!r()&&n>e;)e++,h(),++a===o-s&&(skip(s),a=0,u++);else for(;!r()&&n>e;)e++,h(),++a===o&&(a=0,u++);return c.count()}}(this)},e}(r),e.exports=i},{"../../util":175,"./databuffer":126}],128:[function(t,e,n){var r,i,o,s,a=function(t,e){function n(){this.constructor=t}for(var r in e)u.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},u={}.hasOwnProperty;o=t("../renderable"),i=t("./rendertotexture"),s=t("../../util"),r=function(t){function e(t,n,r){null==this.items&&(this.items=r.items||1),null==this.channels&&(this.channels=r.channels||4),null==this.width&&(this.width=r.width||1),null==this.height&&(this.height=r.height||1),null==this.depth&&(this.depth=r.depth||1),r.format=THREE.RGBAFormat,r.width=this._width=this.items*this.width,r.height=this._height=this.height*this.depth,r.frames=1,delete r.items,delete r.depth,delete r.channels,e.__super__.constructor.call(this,t,n,r),this._adopt({textureItems:{type:"f",value:this.items},textureHeight:{type:"f",value:this.height}})}return a(e,t),e.prototype.shaderAbsolute=function(t){return null==t&&(t=this.shaders.shader()),t.pipe("map.xyzw.texture",this.uniforms),e.__super__.shaderAbsolute.call(this,t,1,2)},e}(i),e.exports=r},{"../../util":175,"../renderable":161,"./rendertotexture":131}],129:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./buffer"),o=t("../../util"),i=function(t){function e(t,n,r){this.width=r.width||1,this.height=r.height||1,this.depth=r.depth||1,null==this.samples&&(this.samples=this.width*this.height*this.depth),e.__super__.constructor.call(this,t,n,r),this.build(r)}return s(e,t),e.prototype.build=function(t){return this.data=[],this.data.length=this.samples,this.filled=0,this.pad={x:0,y:0,z:0},this.streamer=this.generate(this.data)},e.prototype.dispose=function(){return this.data=null,e.__super__.dispose.apply(this,arguments)},e.prototype.getFilled=function(){return this.filled},e.prototype.setActive=function(t,e,n){var r;return r=[this.width-t,this.height-e,this.depth-n],this.pad.x=r[0],this.pad.y=r[1],this.pad.z=r[2],r},e.prototype.read=function(){return this.data},e.prototype.copy=function(t){var e,n,r,i,o,s;for(r=Math.min(t.length,this.samples),e=this.data,s=[],n=i=0,o=r;o>=0?o>i:i>o;n=o>=0?++i:--i)s.push(e[n]=t[n]);return s},e.prototype.fill=function(){var t,e,n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g;if(t=this.callback,"function"==typeof t.reset&&t.reset(),d=this.streamer,r=d.emit,g=d.skip,e=d.count,n=d.done,v=d.reset,v(),l=this.width,h=this.height,c=this.depth,p=this.pad.x,f=this.pad.y,u=this.samples-this.pad.z*l*h,i=o=s=a=0,p>0||f>0)for(;!n()&&u>a&&(a++,m=t(r,i,o,s),++i===l-p&&(g(p),i=0,++o===h-f&&(g(l*f),o=0,s++)),m!==!1););else for(;!n()&&u>a&&(a++,m=t(r,i,o,s),++i===l&&(i=0,++o===h&&(o=0,s++)),m!==!1););return this.filled=1,e()},e}(r),e.exports=i},{"../../util":175,"./buffer":125}],130:[function(t,e,n){var r,i,o,s,a,u,h=function(t,e){function n(){this.constructor=t}for(var r in e)l.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},l={}.hasOwnProperty;a=t("../renderable"),r=t("./buffer"),i=t("./memo"),o=t("../meshes/memoscreen"),u=t("../../util"),s=function(t){function e(t,n,r){null==this.items&&(this.items=r.items||1),null==this.channels&&(this.channels=r.channels||4),null==this.width&&(this.width=r.width||1),null==this.height&&(this.height=r.height||1),null==this.depth&&(this.depth=r.depth||1),null==this.type&&(this.type=r.type||THREE.FloatType),null==this.stpq&&(this.stpq=r.stpq||!1),this.isFloat=this.type===THREE.FloatType,this.active=this.sampled=this.rect=this.pad=null,e.__super__.constructor.call(this,t,n),this.build(r)}return h(e,t),e.prototype.build=function(t){var e,n,r,s,a,h,l,c,p,f,d,m,v,g;return p=t.map,h=t.indexer,l=null!=h&&!h.empty(),c=this.items,g=this.width,a=this.height,n=this.depth,d=this.stpq,f=p,l&&(this._adopt({indexModulus:{type:"v4",value:new THREE.Vector4(c,c*g,c*g*a,1)}}),f=this.shaders.shader(),f.require(p),f.require(h),f.pipe("float.index.pack",this.uniforms)),this.isFloat&&this.channels>1&&(this.floatMemo=new i(this.renderer,this.shaders,{items:c,channels:4,width:g,height:a,depth:n,history:0,type:THREE.FloatType}),this.floatCompose=new o(this.renderer,this.shaders,{map:f,items:c,width:g,height:a,depth:n,stpq:d}),this.floatMemo.adopt(this.floatCompose),d=!1,f=this.shaders.shader(),this.floatMemo.shaderAbsolute(f)),this.isFloat?(m=this.channels,e=4):(m=1,e=this.channels),m>1?(r=this.shaders.shader(),r.pipe(u.GLSL.mapByte2FloatOffset(m)),r.require(f),r.pipe("float.stretch"),r.pipe("float.encode"),f=r):this.isFloat&&(r=this.shaders.shader(),r.pipe(f),r.pipe(u.GLSL.truncateVec4(4,1)),r.pipe("float.encode"),f=r),this.byteMemo=new i(this.renderer,this.shaders,{items:c*m,channels:4,width:g,height:a,depth:n,history:0,type:THREE.UnsignedByteType}),this.byteCompose=new o(this.renderer,this.shaders,{map:f,items:c*m,width:g,height:a,depth:n,stpq:d}),this.byteMemo.adopt(this.byteCompose),v=c*g*m,s=a*n,this.samples=this.width*this.height*this.depth,this.bytes=new Uint8Array(v*s*4),this.isFloat&&(this.floats=new Float32Array(this.bytes.buffer)),this.data=this.isFloat?this.floats:this.bytes,this.streamer=this.generate(this.data),this.active={items:0,width:0,height:0,depth:0},this.sampled={items:0,width:0,height:0,depth:0},this.rect={w:0,h:0},this.pad={x:0,y:0,z:0,w:0},this.stretch=m,this.isIndexed=l,this.setActive(c,g,a,n)},e.prototype.generate=function(t){return u.Data.getStreamer(t,this.samples,4,this.items)},e.prototype.setActive=function(t,e,n,r){var i,o,s,a,u,h,l,c;if(t!==this.active.items||e!==this.active.width||n!==this.active.height||r!==this.active.depth)return o=[t,e,n,r],this.active.items=o[0], -this.active.width=o[1],this.active.height=o[2],this.active.depth=o[3],null!=(s=this.floatCompose)&&s.cover(e,n,r),null!=(a=this.byteCompose)&&a.cover(e*this.stretch,n,r),t=this.items,e=this.active.width,n=1===this.depth?this.active.height:this.height,r=this.active.depth,c=t*e*this.stretch,i=n*r,u=[t,e,n,r],this.sampled.items=u[0],this.sampled.width=u[1],this.sampled.height=u[2],this.sampled.depth=u[3],h=[c,i],this.rect.w=h[0],this.rect.h=h[1],l=[this.sampled.width-this.active.width,this.sampled.height-this.active.height,this.sampled.depth-this.active.depth,this.sampled.items-this.active.items],this.pad.x=l[0],this.pad.y=l[1],this.pad.z=l[2],this.pad.w=l[3],l},e.prototype.update=function(t){var e,n;return null!=(e=this.floatMemo)&&e.render(t),null!=(n=this.byteMemo)?n.render(t):void 0},e.prototype.post=function(){return this.renderer.setRenderTarget(this.byteMemo.target.write),this.gl.readPixels(0,0,this.rect.w,this.rect.h,gl.RGBA,gl.UNSIGNED_BYTE,this.bytes)},e.prototype.readFloat=function(t){var e;return null!=(e=this.floatMemo)?e.read(t):void 0},e.prototype.readByte=function(t){var e;return null!=(e=this.byteMemo)?e.read(t):void 0},e.prototype.setCallback=function(t){return this.emitter=this.callback(t)},e.prototype.callback=function(t){var e,n,r,i,o;return this.isIndexed?(r=this.width,n=this.height,i=this.depth,o=this.items,e=function(e,i,s,a){var u,h,l,c,p;return u=a,p=u%o,u=(u-p)/o,h=u%r,u=(u-h)/r,l=u%n,u=(u-l)/n,c=u,t(e,i,s,a,h,l,c,p)},e.reset=function(){return"function"==typeof t.reset?t.reset():void 0},e):t},e.prototype.iterate=function(){var t,e,n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b;for(i=this.emitter,"function"==typeof i.reset&&i.reset(),E=this.streamer,e=E.consume,b=E.skip,n=E.count,r=E.done,_=E.reset,_(),c=0|this.sampled.width,l=0|this.sampled.height,p=0|this.sampled.depth,f=0|this.sampled.items,m=0|this.pad.x,v=0|this.pad.y,g=0|this.pad.z,d=0|this.pad.w,h=c*l*f*(p-g),this.isIndexed||(t=i,i=function(e,n,r,i){return t(e,n,r,i,o,s,a,u)}),o=s=a=u=l=0;!r()&&h>l&&(l++,y=e(i),++u===f-d&&(b(m),u=0,++o===c-m&&(b(f*m),o=0,++s===l-v&&(b(f*c*v),s=0,a++))),y!==!1););return Math.floor(n()/f)},e.prototype.dispose=function(){var t,e,n,r,i,o;return null!=(t=this.floatMemo)&&t.unadopt(this.floatCompose),null!=(e=this.floatMemo)&&e.dispose(),null!=(n=this.floatCompose)&&n.dispose(),null!=(r=this.byteMemo)&&r.unadopt(this.byteCompose),null!=(i=this.byteMemo)&&i.dispose(),null!=(o=this.byteCompose)&&o.dispose(),this.floatMemo=this.byteMemo=this.floatCompose=this.byteCompose=null},e}(a),e.exports=s},{"../../util":175,"../meshes/memoscreen":155,"../renderable":161,"./buffer":125,"./memo":128}],131:[function(t,e,n){var r,i,o,s,a=function(t,e){function n(){this.constructor=t}for(var r in e)u.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},u={}.hasOwnProperty;o=t("../renderable"),r=t("./texture/rendertarget"),s=t("../../util"),i=function(t){function e(t,n,r){var i;this.scene=null!=(i=r.scene)?i:new THREE.Scene,this.camera=r.camera,e.__super__.constructor.call(this,t,n),this.build(r)}return a(e,t),e.prototype.shaderRelative=function(t){return null==t&&(t=this.shaders.shader()),t.pipe("sample.2d",this.uniforms)},e.prototype.shaderAbsolute=function(t,e,n){var r;return null==e&&(e=1),null==n&&(n=4),null==t&&(t=this.shaders.shader()),1>=e?(n>2&&t.pipe(s.GLSL.truncateVec(n,2)),t.pipe("map.2d.data",this.uniforms),t.pipe("sample.2d",this.uniforms)):(r=s.GLSL.sample2DArray(Math.min(e,this.target.frames)),4>n&&t.pipe(s.GLSL.extendVec(n,4)),t.pipe("map.xyzw.2dv"),t.split(),t.pipe("map.2d.data",this.uniforms),t.pass(),t.pipe(r,this.uniforms))},e.prototype.build=function(t){var e;return this.camera||(this.camera=new THREE.PerspectiveCamera,this.camera.position.set(0,0,3),this.camera.lookAt(new THREE.Vector3)),"function"==typeof(e=this.scene).inject&&e.inject(),this.target=new r(this.gl,t.width,t.height,t.frames,t),this.target.warmup(function(t){return function(e){return t.renderer.setRenderTarget(e)}}(this)),this.renderer.setRenderTarget(null),this._adopt(this.target.uniforms),this._adopt({dataPointer:{type:"v2",value:new THREE.Vector2(.5,.5)}}),this.filled=0},e.prototype.adopt=function(t){var e,n,r,i,o;for(i=t.renders,o=[],e=0,n=i.length;n>e;e++)r=i[e],o.push(this.scene.add(r));return o},e.prototype.unadopt=function(t){var e,n,r,i,o;for(i=t.renders,o=[],e=0,n=i.length;n>e;e++)r=i[e],o.push(this.scene.remove(r));return o},e.prototype.render=function(t){var e;return null==t&&(t=this.camera),this.renderer.render(null!=(e=this.scene.scene)?e:this.scene,t,this.target.write),this.target.cycle(),this.filled=0?d>l:l>d;h=d>=0?++l:--l)u=("00"+Math.max(0,8*-h+128-8*!h).toString(16)).slice(-2),r.push("#"+u+u+u);return m=new Uint8Array(p*c*2),this.canvas=n,this.context=o,this.lineHeight=c,this.maxWidth=p,this.colors=r,this.scratch=m,this._allocate=this.allocate.bind(this),this._write=this.write.bind(this)},e.prototype.reset=function(){return e.__super__.reset.apply(this,arguments),this.mapped={}},e.prototype.begin=function(){var t,e,n,r,i;for(n=this.rows,r=[],t=0,e=n.length;e>t;t++)i=n[t],r.push(i.alive=0);return r},e.prototype.end=function(){var t,e,n,r,i,o,s,a,u;for(o=this.mapped,s=this.rows.slice(),t=0,r=s.length;r>t;t++)if(u=s[t],0===u.alive){for(a=u.keys,n=0,i=a.length;i>n;n++)e=a[n],delete o[e];this.collapse(u)}},e.prototype.map=function(t,e){var n,r,i,o,s,a,u;return s=this.mapped,r=s[t],null!=r?(r.row.alive++,e(r.x,r.y,r.w,r.h)):(this.draw(t),i=this.scratch,a=this.scratchW,o=this.scratchH,n=this._allocate,u=this._write,n(t,a,o,function(n,r,h){return s[t]={x:r,y:h,w:a,h:o,row:n},u(i,r,h,a,o),e(r,h,a,o)}))},e.prototype.draw=function(t){var e,n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R;if(x=this.width,h=this.lineHeight,y=this.outline,o=this.context,a=this.scratch,g=this.maxWidth,i=this.colors,w=y+1,R=Math.round(1.05*h-1),m=o.measureText(t),x=Math.min(g,Math.ceil(m.width+2*w+1)),o.clearRect(0,0,x,h),0===this.outline){for(o.fillText(t,w,R),s=(c=o.getImageData(0,0,x,h)).data,p=3,l=f=0,_=s.length/4;_>=0?_>f:f>_;l=_>=0?++f:--f)a[l]=s[p],p+=4;return this.scratchW=x,this.scratchH=h}for(o.globalCompositeOperation="source-over",l=d=b=y+1;1>=b?1>=d:d>=1;l=1>=b?++d:--d)p=l>1?2*l-2:l,o.strokeStyle=i[p-1],o.lineWidth=p,o.strokeText(t,w,R);for(o.globalCompositeOperation="multiply",o.fillText(t,w,R),s=(c=o.getImageData(0,0,x,h)).data,p=0,u=this.gamma,l=E=0,T=s.length/4;T>=0?T>E:E>T;l=T>=0?++E:--E)e=s[p],v=e?s[p+1]/e:1,.5===u&&(v=Math.sqrt(v)),v=Math.min(1,Math.max(0,v)),n=256-e,r=n+(e-n)*v,a[l]=Math.max(0,Math.min(255,r+2)),p+=4;return this.scratchW=x,this.scratchH=h},e}(r),e.exports=o},{"./atlas":124}],133:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;o=t("../../../Util"),i=t("./datatexture"),r=function(t){function e(t,n,r,i,o){e.__super__.constructor.call(this,t,n,r,i,o),this.data=new this.ctor(this.n)}return s(e,t),e.prototype.resize=function(t,e){var n,r,i,o;return r=this.data,o=this.width,i=this.height,this.width=t,this.height=e,this.n=t*e*this.channels,this.data=new this.ctor(this.n),n=this.gl,n.bindTexture(n.TEXTURE_2D,this.texture),n.pixelStorei(n.UNPACK_ALIGNMENT,1),n.texImage2D(n.TEXTURE_2D,0,this.format,t,e,0,this.format,this.type,this.data),this.uniforms.dataResolution.value.set(1/t,1/e),this.write(r,0,0,o,i)},e.prototype.write=function(t,n,r,i,o){var s,a,u,h,l,c,p,f,d,m,v,g;if(f=this.width,a=this.data,s=this.channels,u=0,f===i&&0===n)for(h=r*i*s,c=i*o*s;c>u;)a[h++]=t[u++];else for(p=f*s,d=i*s,m=n*s,g=r,v=r+o;v>g;){for(l=0,h=m+g*p;l++=0?r>n:n>r;t=r>=0?++n:--n)i.push(e());return i}.call(this),this.reads=function(){var n,r,i;for(i=[],t=n=0,r=this.buffers;r>=0?r>n:n>r;t=r>=0?++n:--n)i.push(e());return i}.call(this),this.write=e(),this.index=0,this.uniforms={dataResolution:{type:"v2",value:new THREE.Vector2(1/this.width,1/this.height)},dataTexture:{type:"t",value:this.reads[0]},dataTextures:{type:"tv",value:this.reads}}},t.prototype.cycle=function(){var t,e,n,r,i,o,s,a,u;for(o=["__webglTexture","__webglFramebuffer","__webglRenderbuffer"],e=this.buffers,n=function(t,e){var n,r,i;for(n=0,i=o.length;i>n;n++)r=o[n],e[r]=t[r];return null},t=function(t,n){return(t+n+2*e)%e},n(this.write,this.targets[this.index]),u=this.reads,r=i=0,s=u.length;s>i;r=++i)a=u[r],n(this.targets[t(this.index,-r)],a);return this.index=t(this.index,1),n(this.targets[this.index],this.write)},t.prototype.warmup=function(t){var e,n,r,i;for(i=[],e=n=0,r=this.buffers;r>=0?r>n:n>r;e=r>=0?++n:--n)t(this.write),i.push(this.cycle());return i},t.prototype.dispose=function(){var t,e,n,r;for(n=this.targets,t=0,e=n.length;e>t;t++)r=n[t],r.dispose();return this.targets=this.reads=this.write=null},t}(),e.exports=r},{}],136:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./databuffer"),i=t("../../util"),o=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return s(e,t),e.prototype.build=function(t){return e.__super__.build.apply(this,arguments),this.pad={x:0,y:0,z:0},this.streamer=this.generate(this.data)},e.prototype.setActive=function(t,e,n){var r;return r=[Math.max(0,this.width-t),Math.max(0,this.height-e),Math.max(0,this.depth-n)],this.pad.x=r[0],this.pad.y=r[1],this.pad.z=r[2],r},e.prototype.fill=function(){var t,e,n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g;if(t=this.callback,"function"==typeof t.reset&&t.reset(),d=this.streamer,r=d.emit,g=d.skip,e=d.count,n=d.done,v=d.reset,v(),l=this.width,h=this.height,c=this.depth,p=this.pad.x,f=this.pad.y,u=this.samples-this.pad.z*l*h,i=o=s=a=0,p>0||f>0)for(;!n()&&u>a&&(a++,m=t(r,i,o,s),++i===l-p&&(g(p),i=0,++o===h-f&&(g(l*f),o=0,s++)),m!==!1););else for(;!n()&&u>a&&(a++,m=t(r,i,o,s),++i===l&&(i=0,++o===h&&(o=0,s++)),m!==!1););return Math.floor(e()/this.items)},e.prototype.through=function(t,e){var n,r,o,s,a,u,h,l,c,p;return c=p=this.streamer,n=c.consume,r=c.done,s=(o=e.streamer).emit,a=u=h=0,l=function(){return n(function(e,n,r,i){return t(s,e,n,r,i,a,u,h)})},l=i.Data.repeatCall(l,this.items),function(t){return function(){var e,n,i,s,c,f,d;if(p.reset(),o.reset(),s=t.width,i=t.height,c=t.depth,f=t.pad.x,d=t.pad.y,n=t.samples-t.pad.z*s*i,a=u=h=e=0,f>0||d>0)for(;!r()&&n>e;)e++,l(),++a===s-f&&(skip(f),a=0,++u===i-d&&(skip(s*d),u=0,h++));else for(;!r()&&n>e;)e++,l(),++a===s&&(a=0,++u===i&&(u=0,h++));return p.count()}}(this)},e}(r),e.exports=o},{"../../util":175,"./databuffer":126}],137:[function(t,e,n){var r;r={sprite:t("./meshes/sprite"),point:t("./meshes/point"),line:t("./meshes/line"),surface:t("./meshes/surface"),face:t("./meshes/face"),strip:t("./meshes/strip"),arrow:t("./meshes/arrow"),screen:t("./meshes/screen"),memoScreen:t("./meshes/memoscreen"),debug:t("./meshes/debug"),dataBuffer:t("./buffer/databuffer"),arrayBuffer:t("./buffer/arraybuffer"),matrixBuffer:t("./buffer/matrixbuffer"),voxelBuffer:t("./buffer/voxelbuffer"),pushBuffer:t("./buffer/pushbuffer"),renderToTexture:t("./buffer/rendertotexture"),memo:t("./buffer/memo"),readback:t("./buffer/readback"),atlas:t("./buffer/atlas"),textAtlas:t("./buffer/textatlas"),scene:t("./scene")},e.exports=r},{"./buffer/arraybuffer":123,"./buffer/atlas":124,"./buffer/databuffer":126,"./buffer/matrixbuffer":127,"./buffer/memo":128,"./buffer/pushbuffer":129,"./buffer/readback":130,"./buffer/rendertotexture":131,"./buffer/textatlas":132,"./buffer/voxelbuffer":136,"./meshes/arrow":150,"./meshes/debug":152,"./meshes/face":153,"./meshes/line":154,"./meshes/memoscreen":155,"./meshes/point":156,"./meshes/screen":157,"./meshes/sprite":158,"./meshes/strip":159,"./meshes/surface":160,"./scene":162}],138:[function(t,e,n){var r;r=function(){function t(t,e,n){this.classes=t,this.renderer=e,this.shaders=n}return t.prototype.getTypes=function(){return Object.keys(this.classes)},t.prototype.make=function(t,e){return new this.classes[t](this.renderer,this.shaders,e)},t}(),e.exports=r},{}],139:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;i=t("./clipgeometry"),r=function(t){function e(t){var n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S,k,A,C,P,L,z,O,D,F,U,B,N,V,I,j,G,W,q,X,Y;for(e.__super__.constructor.call(this,t),this._clipUniforms(),this.sides=V=+t.sides||12,this.samples=N=+t.samples||2,this.strips=j=+t.strips||1,this.ribbons=B=+t.ribbons||1,this.layers=_=+t.layers||1,this.flip=d=null!=(A=t.flip)?A:!1,this.anchor=r=null!=(C=t.anchor)?C:d?0:N-1,s=j*B*_,H=(V+2)*s,W=2*V*s,this.addAttribute("index",new THREE.BufferAttribute(new Uint16Array(3*W),1)),this.addAttribute("position4",new THREE.BufferAttribute(new Float32Array(4*H),4)),this.addAttribute("arrow",new THREE.BufferAttribute(new Float32Array(3*H),3)),this.addAttribute("attach",new THREE.BufferAttribute(new Float32Array(2*H),2)),this._autochunk(),v=this._emitter("index"),M=this._emitter("position4"),o=this._emitter("arrow"),a=this._emitter("attach"),p=[],E=g=0,P=V;P>=0?P>g:g>P;E=P>=0?++g:--g)i=E/V*τ,p.push([Math.cos(i),Math.sin(i),1]);for(l=0,m=b=0,L=s;L>=0?L>b:b>L;m=L>=0?++b:--b){for(G=l++,h=G+V+1,E=T=0,z=V;z>=0?z>T:T>z;E=z>=0?++T:--T)n=l+E%V,u=l+(E+1)%V,v(G),v(n),v(u),v(u),v(n),v(h);l+=V+1}for(I=d?1:-1,f=d?N-1:0,x=r+I,q=r,y=w=0,O=_;O>=0?O>w:w>O;y=O>=0?++w:--w)for(Y=R=0,D=B;D>=0?D>R:R>D;Y=D>=0?++R:--R)for(X=S=0,F=j;F>=0?F>S:S>F;X=F>=0?++S:--S){for(M(q,X,Y,y),o(0,0,0),a(x,f),E=k=0,U=V;U>=0?U>k:k>U;E=U>=0?++k:--k)M(q,X,Y,y),c=p[E],o(c[0],c[1],c[2]),a(x,f);M(q,X,Y,y),o(0,0,1),a(x,f)}this._finalize(),this.clip()}return o(e,t),e.prototype.clip=function(t,e,n,r){var i,o,s,a;return null==t&&(t=this.samples),null==e&&(e=this.strips),null==n&&(n=this.ribbons),null==r&&(r=this.layers),a=Math.max(0,t-1),this._clipGeometry(t,e,n,r),t>this.anchor?(i=[r,n,e],o=[this.layers,this.ribbons,this.strips],s=this.sides*this._reduce(i,o)):s=0,this._offsets([{start:0,count:6*s}])},e}(i),e.exports=r},{"./clipgeometry":140}],140:[function(t,e,n){var r,i,o,s,a=function(t,e){function n(){this.constructor=t}for(var r in e)u.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},u={}.hasOwnProperty;i=t("./geometry"),o=!1,s=function(){var t;return t=+new Date,function(e){var n;return n=+new Date-t,console.log(e,n+" ms"),n}},r=function(t){function e(){return e.__super__.constructor.apply(this,arguments)}return a(e,t),e.prototype._clipUniforms=function(){return this.geometryClip=new THREE.Vector4(1e10,1e10,1e10,1e10),this.geometryResolution=new THREE.Vector4,this.mapSize=new THREE.Vector4,null==this.uniforms&&(this.uniforms={}),this.uniforms.geometryClip={type:"v4",value:this.geometryClip},this.uniforms.geometryResolution={type:"v4",value:this.geometryResolution},this.uniforms.mapSize={type:"v4",value:this.mapSize}},e.prototype._clipGeometry=function(t,e,n,r){var i,o;return i=function(t){return Math.max(0,t-1)},o=function(t){return 1/Math.max(1,t-1)},this.geometryClip.set(i(t),i(e),i(n),i(r)),this.geometryResolution.set(o(t),o(e),o(n),o(r))},e.prototype._clipMap=function(t,e,n,r){return this.mapSize.set(t,e,n,r)},e.prototype._clipOffsets=function(t,e,n,r,i,o,s,a,u){var h,l,c;return h=[r,n,e,i],c=[a,s,o,u],l=this._reduce(h,c),this._offsets([{start:0,count:l*t}])},e}(i),e.exports=r},{"./geometry":142}],141:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;r=t("./clipgeometry"),i=function(t){function e(t){var n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S,k,A;for(e.__super__.constructor.call(this,t),this._clipUniforms(),this.items=a=+t.items||2,this.width=M=+t.width||1,this.height=i=+t.height||1,this.depth=r=+t.depth||1,this.sides=R=Math.max(0,a-2),w=M*i*r,m=a*w,H=R*w,this.addAttribute("index",new THREE.BufferAttribute(new Uint16Array(3*H),1)),this.addAttribute("position4",new THREE.BufferAttribute(new Float32Array(4*m),4)),this._autochunk(),s=this._emitter("index"),v=this._emitter("position4"),n=0,o=h=0,E=w;E>=0?E>h:h>E;o=E>=0?++h:--h){for(u=c=0,y=R;y>=0?y>c:c>y;u=y>=0?++c:--c)s(n),s(n+u+1),s(n+u+2);n+=a}for(A=p=0,_=r;_>=0?_>p:p>_;A=_>=0?++p:--p)for(k=f=0,b=i;b>=0?b>f:f>b;k=b>=0?++f:--f)for(S=d=0,T=M;T>=0?T>d:d>T;S=T>=0?++d:--d)for(l=g=0,x=a;x>=0?x>g:g>x;l=x>=0?++g:--g)v(S,k,A,l);this._finalize(),this.clip()}return o(e,t),e.prototype.clip=function(t,e,n,r){var i;return null==t&&(t=this.width),null==e&&(e=this.height),null==n&&(n=this.depth),null==r&&(r=this.items),i=Math.max(0,r-2),this._clipGeometry(t,e,n,r),this._clipOffsets(3,t,e,n,i,this.width,this.height,this.depth,this.sides)},e}(r),e.exports=i},{"./clipgeometry":140}],142:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=!1,o=function(){var t;return t=+new Date,function(e){var n;return n=+new Date-t,console.log(e,n+" ms"),n}},r=function(t){function e(){THREE.BufferGeometry.call(this),null==this.uniforms&&(this.uniforms={}),null==this.offsets&&(this.offsets=[]),i&&(this.tock=o()),this.chunked=!1,this.limit=65535}return s(e,t),e.prototype._reduce=function(t,e){var n,r,i,o,s,a,u;for(a=!1,r=i=0,o=t.length;o>i;r=++i)n=t[r],s=e[r],a&&(t[r]=s),n>1&&(a=!0);return u=t.reduce(function(t,e){return t*e})},e.prototype._emitter=function(t){var e,n,r,i,o,s,a,u;return n=this.attributes[t],r=n.itemSize,e=n.array,o=0,s=function(t){return e[o++]=t},u=function(t,n){return e[o++]=t,e[o++]=n},a=function(t,n,r){return e[o++]=t,e[o++]=n,e[o++]=r},i=function(t,n,r,i){return e[o++]=t,e[o++]=n,e[o++]=r,e[o++]=i},[null,s,u,a,i][r]},e.prototype._autochunk=function(){var t,e,n,r,i,o;n=this.attributes.index,o=this.attributes;for(r in o)if(e=o[r],"index"!==r&&n){i=e.array.length/e.itemSize,i>this.limit&&(this.chunked=!0);break}return this.chunked&&!n.u16?(n.u16=t=n.array,n.array=new Uint32Array(t.length)):void 0},e.prototype._finalize=function(){var t;if(this.chunked)return t=this.attributes.index,this.chunks=this._chunks(t.array,this.limit),this._chunkify(t,this.chunks),i?this.tock(this.constructor.name):void 0},e.prototype._chunks=function(t,e){var n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E;for(i=[],f=0,E=t[0],o=t[0],v=function(t){var e,n,r;return r=3*f,n=3*t,e=n-r,i.push({index:E,start:r,count:e,end:n})},d=Math.floor(t.length/3),m=0,s=a=0,g=d;g>=0?g>a:a>g;s=g>=0?++a:--a)u=t[m++],h=t[m++],l=t[m++],p=Math.min(u,h,l),c=Math.max(u,h,l),n=Math.min(E,p),r=Math.max(o,c),r-n>e&&(v(s),n=p,r=c,f=s),E=n,o=r;return v(d),i},e.prototype._chunkify=function(t,e){var n,r,i,o,s,a,u,h,l,c;if(t.u16){for(r=t.array,c=t.u16,o=0,a=e.length;a>o;o++)for(n=e[o],u=n.index,i=s=h=n.start,l=n.end;l>=h?l>s:s>l;i=l>=h?++s:--s)c[i]=r[i]-u;return t.array=t.u16,delete t.u16}},e.prototype._offsets=function(t){var e,n,r,i,o,s,a,u,h,l,c,p;if(this.chunked)for(i=this.chunks,c=this.offsets,c.length=null,s=0,u=t.length;u>s;s++)for(l=t[s],p=l.start,o=l.count-p,a=0,h=i.length;h>a;a++)r=i[a],n=r.start,e=r.end,(n>=p&&o>n||e>p&&o>=e||p>n&&e>o)&&(n=Math.max(p,n),e=Math.min(o,e),c.push({index:r.index,start:n,count:e-n}));else this.offsets=t;return null},e}(THREE.BufferGeometry),e.exports=r},{}],143:[function(t,e,n){n.Geometry=t("./geometry"),n.ArrowGeometry=t("./arrowgeometry"),n.FaceGeometry=t("./facegeometry"),n.LineGeometry=t("./linegeometry"),n.ScreenGeometry=t("./screengeometry"),n.SpriteGeometry=t("./spritegeometry"),n.StripGeometry=t("./stripgeometry"),n.SurfaceGeometry=t("./surfacegeometry")},{"./arrowgeometry":139,"./facegeometry":141,"./geometry":142,"./linegeometry":144,"./screengeometry":145,"./spritegeometry":146,"./stripgeometry":147,"./surfacegeometry":148}],144:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;r=t("./clipgeometry"),i=function(t){function e(t){var n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S,k,A,C,P,L,z,O,D,F,U,B,N,V,I,j,G,W,q,X,Y,Z,K,Q,J,$,tt,et;for(e.__super__.constructor.call(this,t),this._clipUniforms(),this.closed=o=t.closed||!1,this.samples=I=(+t.samples||2)+(o?1:0),this.strips=W=+t.strips||1,this.ribbons=N=+t.ribbons||1,this.layers=v=+t.layers||1,this.detail=s=+t.detail||1,E=I-1,this.joints=f=s-1,this.vertices=K=(E-1)*f+I,this.segments=j=(E-1)*f+E,J=I-(o?1:0),x=K*W*N*v*2,H=j*W*N*v,X=2*H,this.addAttribute("index",new THREE.BufferAttribute(new Uint16Array(3*X),1)),this.addAttribute("position4",new THREE.BufferAttribute(new Float32Array(4*x),4)),this.addAttribute("line",new THREE.BufferAttribute(new Float32Array(2*x),2)),this.addAttribute("strip",new THREE.BufferAttribute(new Float32Array(2*x),2)),s>1&&this.addAttribute("joint",new THREE.BufferAttribute(new Float32Array(x),1)),this._autochunk(),l=this._emitter("index"),w=this._emitter("position4"),g=this._emitter("line"),G=this._emitter("strip"),s>1&&(p=this._emitter("joint")),i=0,h=_=0,S=N*v;S>=0?S>_:_>S;h=S>=0?++_:--_)for(c=b=0,k=W;k>=0?k>b:b>k;c=k>=0?++b:--b){for(d=T=0,P=j;P>=0?P>T:T>P;d=P>=0?++T:--T)l(i),l(i+1),l(i+2),l(i+2),l(i+1),l(i+3),i+=2;i+=2}if(u=o?function(){return 0}:function(t){return 0===t?-1:t===I-1?1:0},s>1)for(m=R=0,L=v;L>=0?L>R:R>L;m=L>=0?++R:--R)for(et=M=0,z=N;z>=0?z>M:M>z;et=z>=0?++M:--M)for(tt=V=0,O=W;O>=0?O>V:V>O;tt=O>=0?++V:--V)for($=q=0,D=I;D>=0?D>q:q>D;$=D>=0?++q:--q)if(o&&($%=J),a=u($),0!==a)w($,tt,et,m),w($,tt,et,m),g(a,1),g(a,-1),G(0,j),G(0,j),p(.5),p(.5);else for(y=Y=0,F=s;F>=0?F>Y:Y>F;y=F>=0?++Y:--Y)w($,tt,et,m),w($,tt,et,m),g(a,1),g(a,-1),G(0,j),G(0,j),p(y/f),p(y/f);else for(m=Z=0,U=v;U>=0?U>Z:Z>U;m=U>=0?++Z:--Z)for(et=Q=0,B=N;B>=0?B>Q:Q>B;et=B>=0?++Q:--Q)for(tt=n=0,A=W;A>=0?A>n:n>A;tt=A>=0?++n:--n)for($=r=0,C=I;C>=0?C>r:r>C;$=C>=0?++r:--r)o&&($%=J),a=u($),w($,tt,et,m),w($,tt,et,m),g(a,1),g(a,-1),G(0,j),G(0,j);this._finalize(),this.clip()}return o(e,t),e.prototype.clip=function(t,e,n,r){var i,o;return null==t&&(t=this.samples-this.closed),null==e&&(e=this.strips),null==n&&(n=this.ribbons),null==r&&(r=this.layers),i=Math.max(0,t-(this.closed?0:1)),o=t+(t-2)*this.joints,i=o-1,this._clipGeometry(o,e,n,r),this._clipOffsets(6,i,e,n,r,this.segments,this.strips,this.ribbons,this.layers)},e}(r),e.exports=i},{"./clipgeometry":140}],145:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;i=t("./surfacegeometry"),r=function(t){function e(t){var n,r;null==this.uniforms&&(this.uniforms={}),this.uniforms.geometryScale={type:"v4",value:new THREE.Vector4},t.width=Math.max(2,null!=(n=+t.width)?n:2),t.height=Math.max(2,null!=(r=+t.height)?r:2),this.cover(),e.__super__.constructor.call(this,t)}return o(e,t),e.prototype.cover=function(t,e,n,r){this.scaleX=null!=t?t:1,this.scaleY=null!=e?e:1,this.scaleZ=null!=n?n:1,this.scaleW=null!=r?r:1},e.prototype.clip=function(t,n,r,i){var o;return null==t&&(t=this.width),null==n&&(n=this.height),null==r&&(r=this.surfaces),null==i&&(i=this.layers),e.__super__.clip.call(this,t,n,r,i),o=function(t){return 1/Math.max(1,t-1)},this.uniforms.geometryScale.value.set(o(t)*this.scaleX,o(n)*this.scaleY,o(r)*this.scaleZ,o(i)*this.scaleW)},e}(i),e.exports=r},{"./surfacegeometry":148}],146:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;r=t("./clipgeometry"),i=function(t){function e(t){var n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S,k,A,C;for(e.__super__.constructor.call(this,t),this._clipUniforms(),this.items=a=+t.items||2,this.width=S=+t.width||1,this.height=i=+t.height||1,this.depth=r=+t.depth||1,w=a*S*i*r,v=4*w,H=2*w,this.addAttribute("index",new THREE.BufferAttribute(new Uint16Array(3*H),1)),this.addAttribute("position4",new THREE.BufferAttribute(new Float32Array(4*v),4)),this.addAttribute("sprite",new THREE.BufferAttribute(new Float32Array(2*v),2)),this._autochunk(),s=this._emitter("index"),g=this._emitter("position4"),R=this._emitter("sprite"),E=[[-1,-1],[-1,1],[1,-1],[1,1]],n=0,o=u=0,y=w;y>=0?y>u:u>y;o=y>=0?++u:--u)s(n),s(n+1),s(n+2),s(n+1),s(n+2),s(n+3),n+=4;for(C=h=0,_=r;_>=0?_>h:h>_;C=_>=0?++h:--h)for(A=p=0,b=i;b>=0?b>p:p>b;A=b>=0?++p:--p)for(k=f=0,T=S;T>=0?T>f:f>T;k=T>=0?++f:--f)for(l=d=0,x=a;x>=0?x>d:d>x;l=x>=0?++d:--d)for(m=0,c=E.length;c>m;m++)M=E[m],g(k,A,C,l),R(M[0],M[1]);this._finalize(),this.clip()}return o(e,t),e.prototype.clip=function(t,e,n,r){return null==t&&(t=this.width),null==e&&(e=this.height),null==n&&(n=this.depth),null==r&&(r=this.items),this._clipGeometry(t,e,n,r),this._clipOffsets(6,t,e,n,r,this.width,this.height,this.depth,this.items)},e}(r),e.exports=i},{"./clipgeometry":140}],147:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;r=t("./clipgeometry"),i=function(t){function e(t){var n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S,k,A,C,P,L,z;for(e.__super__.constructor.call(this,t),this._clipUniforms(),this.items=u=+t.items||2,this.width=C=+t.width||1,this.height=o=+t.height||1,this.depth=r=+t.depth||1,this.sides=S=Math.max(0,u-2),M=C*o*r,g=u*M,A=S*M,this.addAttribute("index",new THREE.BufferAttribute(new Uint16Array(3*A),1)),this.addAttribute("position4",new THREE.BufferAttribute(new Float32Array(4*g),4)),this.addAttribute("strip",new THREE.BufferAttribute(new Float32Array(3*g),3)),this._autochunk(),a=this._emitter("index"),E=this._emitter("position4"),k=this._emitter("strip"),n=0,s=l=0,b=M;b>=0?b>l:l>b;s=b>=0?++l:--l){for(m=n,h=f=0,T=S;T>=0?T>f:f>T;h=T>=0?++f:--f)1&h?(a(m+1),a(m),a(m+2)):(a(m),a(m+1),a(m+2)),m++;n+=u}for(p=u-1,z=d=0,x=r;x>=0?x>d:d>x;z=x>=0?++d:--d)for(L=v=0,w=o;w>=0?w>v:v>w;L=w>=0?++v:--v)for(P=y=0,R=C;R>=0?R>y:y>R;P=R>=0?++y:--y){for(i=1,E(P,L,z,0),k(1,2,i),c=_=1,H=p;H>=1?H>_:_>H;c=H>=1?++_:--_)E(P,L,z,c),k(c-1,c+1,i=-i);E(P,L,z,p),k(p-2,p-1,-i)}this._finalize(),this.clip()}return o(e,t),e.prototype.clip=function(t,e,n,r){var i;return null==t&&(t=this.width),null==e&&(e=this.height),null==n&&(n=this.depth),null==r&&(r=this.items),i=Math.max(0,r-2),this._clipGeometry(t,e,n,r),this._clipOffsets(3,t,e,n,i,this.width,this.height,this.depth,this.sides)},e}(r),e.exports=i},{"./clipgeometry":140}],148:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;r=t("./clipgeometry"),i=function(t){function e(t){var n,r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S,k,A,C,P,L,z,O,D,F,U,B,N,V,I,j; -for(e.__super__.constructor.call(this,t),this._clipUniforms(),this.closedX=r=t.closedX||!1,this.closedY=i=t.closedY||!1,this.width=U=(+t.width||2)+(r?1:0),this.height=h=(+t.height||2)+(i?1:0),this.surfaces=D=+t.surfaces||1,this.layers=m=+t.layers||1,B=U-(r?1:0),N=h-(i?1:0),this.segmentsX=L=Math.max(0,U-1),this.segmentsY=z=Math.max(0,h-1),_=U*h*D*m,x=L*z*D*m,F=2*x,this.addAttribute("index",new THREE.BufferAttribute(new Uint16Array(3*F),1)),this.addAttribute("position4",new THREE.BufferAttribute(new Float32Array(4*_),4)),this.addAttribute("surface",new THREE.BufferAttribute(new Float32Array(2*_),2)),this._autochunk(),c=this._emitter("index"),b=this._emitter("position4"),O=this._emitter("surface"),n=0,l=v=0,R=D*m;R>=0?R>v:v>R;l=R>=0?++v:--v){for(p=g=0,H=z;H>=0?H>g:g>H;p=H>=0?++g:--g){for(f=E=0,M=L;M>=0?M>E:E>M;f=M>=0?++E:--E)c(n),c(n+1),c(n+U),c(n+U),c(n+1),c(n+U+1),n++;n++}n+=U}for(a=r?function(){return 0}:function(t){return 0===t?-1:t===L?1:0},u=i?function(){return 0}:function(t){return 0===t?-1:t===z?1:0},d=y=0,S=m;S>=0?S>y:y>S;d=S>=0?++y:--y)for(j=T=0,k=D;k>=0?k>T:T>k;j=k>=0?++T:--T)for(I=w=0,A=h;A>=0?A>w:w>A;I=A>=0?++w:--w)for(i&&(I%=N),s=u(I),V=P=0,C=U;C>=0?C>P:P>C;V=C>=0?++P:--P)r&&(V%=B),o=a(V),b(V,I,j,d),O(o,s);this._finalize(),this.clip()}return o(e,t),e.prototype.clip=function(t,e,n,r){var i,o;return null==t&&(t=this.width),null==e&&(e=this.height),null==n&&(n=this.surfaces),null==r&&(r=this.layers),i=Math.max(0,t-1),o=Math.max(0,e-1),this._clipGeometry(t,e,n,r),this._clipOffsets(6,i,o,n,r,this.segmentsX,this.segmentsY,this.surfaces,this.layers)},e.prototype.map=function(t,e,n,r){return null==t&&(t=this.width),null==e&&(e=this.height),null==n&&(n=this.surfaces),null==r&&(r=this.layers),this._clipMap(t,e,n,r)},e}(r),e.exports=i},{"./clipgeometry":140}],149:[function(t,e,n){n.Scene=t("./scene"),n.Factory=t("./factory"),n.Renderable=t("./scene"),n.Classes=t("./classes")},{"./classes":137,"./factory":138,"./scene":162}],150:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;o=t("./base"),i=t("../geometry").ArrowGeometry,r=function(t){function e(t,n,r){var o,s,a,u,h,l,c,p,f,d,m,v,g,E;e.__super__.constructor.call(this,t,n,r),g=r.uniforms,f=r.material,m=r.position,o=r.color,p=r.mask,c=r.map,s=r.combine,v=r.stpq,l=r.linear,null==g&&(g={}),h=null!=g.styleColor,this.geometry=new i({sides:r.sides,samples:r.samples,strips:r.strips,ribbons:r.ribbons,layers:r.layers,anchor:r.anchor,flip:r.flip}),this._adopt(g),this._adopt(this.geometry.uniforms),u=n.material(),E=u.vertex,E.pipe(this._vertexColor(o,p)),E.require(this._vertexPosition(m,f,c,1,v)),E.pipe("arrow.position",this.uniforms),E.pipe("project.position",this.uniforms),u.fragment=a=this._fragmentColor(h,f,o,p,c,1,v,s,l),a.pipe("fragment.color",this.uniforms),this.material=this._material(u.link({})),d=new THREE.Mesh(this.geometry,this.material),d.frustumCulled=!1,d.matrixAutoUpdate=!1,this._raw(d),this.renders=[d]}return s(e,t),e.prototype.dispose=function(){return this.geometry.dispose(),this.material.dispose(),this.renders=this.geometry=this.material=null,e.__super__.dispose.apply(this,arguments)},e}(o),e.exports=r},{"../geometry":143,"./base":151}],151:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("../renderable"),o=t("../../util"),r=function(t){function e(t,n,r){var i;e.__super__.constructor.call(this,t,n,r),this.zUnits=null!=(i=r.zUnits)?i:0}return s(e,t),e.prototype.raw=function(){var t,e,n,r;for(r=this.renders,t=0,e=r.length;e>t;t++)n=r[t],this._raw(n);return null},e.prototype.depth=function(t,e){var n,r,i,o;for(o=this.renders,n=0,r=o.length;r>n;n++)i=o[n],this._depth(i,t,e);return null},e.prototype.polygonOffset=function(t,e){var n,r,i,o;for(o=this.renders,n=0,r=o.length;r>n;n++)i=o[n],this._polygonOffset(i,t,e);return null},e.prototype.show=function(t,e,n){var r,i,o,s,a;for(s=this.renders,a=[],r=0,i=s.length;i>r;r++)o=s[r],a.push(this._show(o,t,e,n));return a},e.prototype.hide=function(){var t,e,n,r;for(r=this.renders,t=0,e=r.length;e>t;t++)n=r[t],this._hide(n);return null},e.prototype._material=function(t){var e,n,r,i,o,s,a,u;for(s=this.renderer.getPrecision(),u=" precision "+s+" float;\n precision "+s+" int;\nuniform mat4 modelMatrix;\nuniform mat4 modelViewMatrix;\nuniform mat4 projectionMatrix;\nuniform mat4 viewMatrix;\nuniform mat3 normalMatrix;\nuniform vec3 cameraPosition;",e=" precision "+s+" float;\n precision "+s+" int;\nuniform mat4 viewMatrix;\nuniform vec3 cameraPosition;",o=new THREE.RawShaderMaterial(t),a=["vertexGraph","fragmentGraph"],n=0,i=a.length;i>n;n++)r=a[n],o[r]=t[r];return o.vertexShader=[u,o.vertexShader].join("\n"),o.fragmentShader=[e,o.fragmentShader].join("\n"),o},e.prototype._raw=function(t){return t.rotationAutoUpdate=!1,t.frustumCulled=!1,t.matrixAutoUpdate=!1,t.material.defaultAttributeValues=void 0},e.prototype._depth=function(t,e,n){var r;return r=t.material,r.depthWrite=e,r.depthTest=n},e.prototype._polygonOffset=function(t,e,n){var r,i;return n-=this.zUnits,r=0!==n,i=t.material,i.polygonOffset=r,r?(i.polygonOffsetFactor=e,i.polygonOffsetUnits=n):void 0},e.prototype._show=function(t,e,n,r){var i;return e=!0,i=t.material,t.renderOrder=-r,t.visible=!0,i.transparent=e,i.blending=n,null},e.prototype._hide=function(t){return t.visible=!1},e.prototype._vertexColor=function(t,e){var n;if(t||e)return n=this.shaders.shader(),t&&(n.require(t),n.pipe("mesh.vertex.color",this.uniforms)),e&&(n.require(e),n.pipe("mesh.vertex.mask",this.uniforms)),n},e.prototype._vertexPosition=function(t,e,n,r,i){var o,s;return s=this.shaders.shader(),(n||e&&e!==!0)&&(o={},(r>0||i)&&(o.POSITION_MAP=""),r>0&&(o[["POSITION_U","POSITION_UV","POSITION_UVW","POSITION_UVWO"][r-1]]=""),i&&(o.POSITION_STPQ="")),s.require(t),s.pipe("mesh.vertex.position",this.uniforms,o)},e.prototype._fragmentColor=function(t,e,n,r,i,s,a,u,h){var l,c,p,f;return c=this.shaders.shader(),f=!1,p=!1,l={},s>0&&(l[["POSITION_U","POSITION_UV","POSITION_UVW","POSITION_UVWO"][s-1]]=""),a&&(l.POSITION_STPQ=""),t&&(c.pipe("style.color",this.uniforms),f=!0,(n||i||e)&&((!h||n)&&c.pipe("mesh.gamma.in"),p=!0)),n&&(c.isolate(),c.pipe("mesh.fragment.color",this.uniforms),(!h||f)&&c.pipe("mesh.gamma.in"),c.end(),f&&c.pipe(o.GLSL.binaryOperator("vec4","*")),h&&f&&c.pipe("mesh.gamma.out"),f=!0,p=!0),i&&(!f&&u&&c.pipe(o.GLSL.constant("vec4","vec4(1.0)")),c.isolate(),c.require(i),c.pipe("mesh.fragment.map",this.uniforms,l),h||c.pipe("mesh.gamma.in"),c.end(),u?c.pipe(u):f&&c.pipe(o.GLSL.binaryOperator("vec4","*")),f=!0,p=!0),e&&(f||c.pipe(o.GLSL.constant("vec4","vec4(1.0)")),e===!0?c.pipe("mesh.fragment.shaded",this.uniforms):(c.require(e),c.pipe("mesh.fragment.material",this.uniforms,l)),p=!0),p&&!h&&c.pipe("mesh.gamma.out"),r&&(c.pipe("mesh.fragment.mask",this.uniforms),f&&c.pipe(o.GLSL.binaryOperator("vec4","*"))),c},e}(i),e.exports=r},{"../../util":175,"../renderable":161}],152:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;r=t("./base"),i=function(t){function e(t,n,r){var i;e.__super__.constructor.call(this,t,n,r),this.geometry=new THREE.PlaneGeometry(1,1),this.material=new THREE.MeshBasicMaterial({map:r.map}),this.material.side=THREE.DoubleSide,i=new THREE.Mesh(this.geometry,this.material),i.position.x+=r.x||0,i.position.y+=r.y||0,i.frustumCulled=!1,i.scale.set(2,2,2),i.__debug=!0,this.objects=[i]}return o(e,t),e.prototype.dispose=function(){return this.geometry.dispose(),this.material.dispose(),this.objects=this.geometry=this.material=null,e.__super__.dispose.apply(this,arguments)},e}(r),e.exports=i},{"./base":151}],153:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./base"),o=t("../geometry").FaceGeometry,i=function(t){function e(t,n,r){var i,s,a,u,h,l,c,p,f,d,m,v,g,E;e.__super__.constructor.call(this,t,n,r),g=r.uniforms,f=r.material,m=r.position,i=r.color,p=r.mask,c=r.map,s=r.combine,v=r.stpq,l=r.linear,null==g&&(g={}),null==f&&(f=!0),h=null!=g.styleColor,this.geometry=new o({items:r.items,width:r.width,height:r.height,depth:r.depth}),this._adopt(g),this._adopt(this.geometry.uniforms),u=n.material(),E=u.vertex,E.pipe(this._vertexColor(i,p)),E.require(this._vertexPosition(m,f,c,2,v)),f||E.pipe("face.position",this.uniforms),f&&E.pipe("face.position.normal",this.uniforms),E.pipe("project.position",this.uniforms),u.fragment=a=this._fragmentColor(h,f,i,p,c,2,v,s,l),a.pipe("fragment.color",this.uniforms),this.material=this._material(u.link({side:THREE.DoubleSide})),d=new THREE.Mesh(this.geometry,this.material),this._raw(d),this.renders=[d]}return s(e,t),e.prototype.dispose=function(){return this.geometry.dispose(),this.material.dispose(),this.renders=this.geometry=this.material=null,e.__super__.dispose.apply(this,arguments)},e}(r),e.exports=i},{"../geometry":143,"./base":151}],154:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./base"),o=t("../geometry").LineGeometry,i=function(t){function e(t,n,r){var i,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R;e.__super__.constructor.call(this,t,n,r),w=r.uniforms,g=r.material,y=r.position,s=r.color,v=r.mask,m=r.map,a=r.combine,T=r.stpq,d=r.linear,i=r.clip,x=r.stroke,f=r.join,_=r.proximity,null==w&&(w={}),x=[null,"dotted","dashed"][x],p=null!=w.styleColor,f=null!=(b=["miter","round","bevel"][f])?b:"miter",h={miter:1,round:4,bevel:2}[f],this.geometry=new o({samples:r.samples,strips:r.strips,ribbons:r.ribbons,layers:r.layers,anchor:r.anchor,closed:r.closed,detail:h}),this._adopt(w),this._adopt(this.geometry.uniforms),c=n.material(),u={},x&&(u.LINE_STROKE=""),i&&(u.LINE_CLIP=""),null!=_&&(u.LINE_PROXIMITY=""),u["LINE_JOIN_"+f.toUpperCase()]="",h>1&&(u.LINE_JOIN_DETAIL=h),R=c.vertex,R.pipe(this._vertexColor(s,v)),R.require(this._vertexPosition(y,g,m,2,T)),R.pipe("line.position",this.uniforms,u),R.pipe("project.position",this.uniforms),l=c.fragment,x&&l.pipe("fragment.clip."+x,this.uniforms),i&&l.pipe("fragment.clip.ends",this.uniforms),null!=_&&l.pipe("fragment.clip.proximity",this.uniforms),l.pipe(this._fragmentColor(p,g,s,v,m,2,T,a,d)),l.pipe("fragment.color",this.uniforms),this.material=this._material(c.link({side:THREE.DoubleSide})),E=new THREE.Mesh(this.geometry,this.material),this._raw(E),this.renders=[E]}return s(e,t),e.prototype.dispose=function(){return this.geometry.dispose(),this.material.dispose(),this.renders=this.geometry=this.material=null,e.__super__.dispose.apply(this,arguments)},e}(r),e.exports=i},{"../geometry":143,"./base":151}],155:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("./screen"),o=t("../../util"),r=function(t){function e(t,n,r){var i,o,s,a,u,h,l,c,p,f,d,m;for(this.memo=(h=r.items,m=r.width,o=r.height,i=r.depth,d=r.stpq,r),a=function(t){return 1/Math.max(1,t)},u=function(t){return 1/Math.max(1,t-1)},this.uniforms={remapUVScale:{type:"v2",value:new THREE.Vector2(h*m,o*i)},remapModulus:{type:"v2",value:new THREE.Vector2(h,o)},remapModulusInv:{type:"v2",value:new THREE.Vector2(a(h),a(o))},remapSTPQScale:{type:"v4",value:new THREE.Vector4(u(m),u(o),u(i),u(h))}},c=n.shader(),c.pipe("screen.map.xyzw",this.uniforms),null!=r.map&&(d&&c.pipe("screen.map.stpq",this.uniforms),c.pipe(r.map)),e.__super__.constructor.call(this,t,n,{map:c,linear:!0}),f=this.renders,s=0,l=f.length;l>s;s++)p=f[s],p.transparent=!1}return s(e,t),e.prototype.cover=function(t,e,n,r){var i,o,s;return null==t&&(t=this.memo.width),null==e&&(e=this.memo.height),null==n&&(n=this.memo.depth),null==r&&(r=this.memo.items),i=function(t){return 1/Math.max(1,t-1)},this.uniforms.remapSTPQScale.value.set(i(t),i(e),i(n),i(r)),o=t/this.memo.width,s=n/this.memo.depth,1===this.memo.depth&&(s=e/this.memo.height),this.geometry.cover(o,s)},e}(i),e.exports=r},{"../../util":175,"./screen":157}],156:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./base"),o=t("../geometry").SpriteGeometry,i=function(t){function e(t,n,r){var i,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S,k,A,C,P,L,z,O;e.__super__.constructor.call(this,t,n,r),z=r.uniforms,_=r.material,w=r.position,u=r.color,P=r.size,y=r.mask,E=r.map,h=r.combine,g=r.linear,A=r.shape,b=r.optical,d=r.fill,L=r.stpq,null==z&&(z={}),A=null!=(R=+A)?R:0,null==d&&(d=!0),v=null!=z.styleColor,C=["circle","square","diamond","up","down","left","right"],x=["circle","generic","generic","generic","generic","generic","generic"],k=[1.2,1,1.414,1.16,1.16,1.16,1.16],T=null!=(H=x[A])?H:x[0],s=null!=(M=C[A])?M:C[0],i=null!=(S=b&&k[A])?S:1,a=d?T:T+".hollow",this.geometry=new o({items:r.items,width:r.width,height:r.height,depth:r.depth}),this._adopt(z),this._adopt(this.geometry.uniforms),l={POINT_SHAPE_SCALE:+(i+1e-5)},f=n.material(),O=f.vertex,O.pipe(this._vertexColor(u,y)),P?(O.isolate(),O.require(P),O.require("point.size.varying",this.uniforms),O.end()):O.require("point.size.uniform",this.uniforms),O.require(this._vertexPosition(w,_,E,2,L)),O.pipe("point.position",this.uniforms,l),O.pipe("project.position",this.uniforms),f.fragment=p=this._fragmentColor(v,_,u,y,E,2,L,h,g),c=n.material(),c.vertex.pipe(O),p=c.fragment.pipe(f.fragment),p.require("point.mask."+s,this.uniforms),p.require("point.alpha."+a,this.uniforms),p.pipe("point.edge",this.uniforms),m=n.material(),m.vertex.pipe(O),p=m.fragment.pipe(f.fragment),p.require("point.mask."+s,this.uniforms),p.require("point.alpha."+a,this.uniforms),p.pipe("point.fill",this.uniforms),this.fillMaterial=this._material(m.link({side:THREE.DoubleSide})),this.edgeMaterial=this._material(c.link({side:THREE.DoubleSide})),this.fillObject=new THREE.Mesh(this.geometry,this.fillMaterial),this.edgeObject=new THREE.Mesh(this.geometry,this.edgeMaterial),this._raw(this.fillObject),this._raw(this.edgeObject),this.renders=[this.fillObject,this.edgeObject]}return s(e,t),e.prototype.show=function(t,e,n,r){return this._show(this.edgeObject,!0,e,n,r),this._show(this.fillObject,t,e,n,r)},e.prototype.dispose=function(){return this.geometry.dispose(),this.edgeMaterial.dispose(),this.fillMaterial.dispose(),this.renders=this.edgeObject=this.fillObject=this.geometry=this.edgeMaterial=this.fillMaterial=null,e.__super__.dispose.apply(this,arguments)},e}(r),e.exports=i},{"../geometry":143,"./base":151}],157:[function(t,e,n){var r,i,o,s,a=function(t,e){function n(){this.constructor=t}for(var r in e)u.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},u={}.hasOwnProperty;r=t("./base"),o=t("../geometry").ScreenGeometry,s=t("../../util"),i=function(t){function e(t,n,r){var i,s,a,u,h,l,c,p,f,d;e.__super__.constructor.call(this,t,n,r),f=r.uniforms,l=r.map,i=r.combine,p=r.stpq,h=r.linear,null==f&&(f={}),u=null!=f.styleColor,this.geometry=new o({width:r.width,height:r.height}),this._adopt(f),this._adopt(this.geometry.uniforms),a=n.material(),d=a.vertex,d.pipe("raw.position.scale",this.uniforms),d.fan(),d.pipe("stpq.xyzw.2d",this.uniforms),d.next(),d.pipe("screen.position",this.uniforms),d.join(),a.fragment=s=this._fragmentColor(u,!1,null,null,l,2,p,i,h),s.pipe("fragment.color",this.uniforms),this.material=this._material(a.link({side:THREE.DoubleSide})),c=new THREE.Mesh(this.geometry,this.material),c.frustumCulled=!1,this._raw(c),this.renders=[c]}return a(e,t),e.prototype.dispose=function(){return this.geometry.dispose(),this.material.dispose(),this.renders=this.geometry=this.material=null,e.__super__.dispose.apply(this,arguments)},e}(r),e.exports=i},{"../../util":175,"../geometry":143,"./base":151}],158:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./base"),o=t("../geometry").SpriteGeometry,i=function(t){function e(t,n,r){var i,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_;e.__super__.constructor.call(this,t,n,r),y=r.uniforms,m=r.material,v=r.position,g=r.sprite,f=r.map,s=r.combine,p=r.linear,i=r.color,d=r.mask,E=r.stpq,null==y&&(y={}),c=null!=y.styleColor,this.geometry=new o({items:r.items,width:r.width,height:r.height,depth:r.depth}),this._adopt(y),this._adopt(this.geometry.uniforms),h=n.material(),_=h.vertex,_.pipe(this._vertexColor(i,d)),_.require(this._vertexPosition(v,m,f,2,E)),_.require(g),_.pipe("sprite.position",this.uniforms),_.pipe("project.position",this.uniforms),h.fragment=u=this._fragmentColor(c,m,i,d,f,2,E,s,p),a=n.material(),a.vertex.pipe(_),a.fragment.pipe(u),a.fragment.pipe("fragment.transparent",this.uniforms),l=n.material(),l.vertex.pipe(_),l.fragment.pipe(u),l.fragment.pipe("fragment.solid",this.uniforms),this.fillMaterial=this._material(l.link({side:THREE.DoubleSide})),this.edgeMaterial=this._material(a.link({side:THREE.DoubleSide})),this.fillObject=new THREE.Mesh(this.geometry,this.fillMaterial),this.edgeObject=new THREE.Mesh(this.geometry,this.edgeMaterial),this._raw(this.fillObject),this._raw(this.edgeObject),this.renders=[this.fillObject,this.edgeObject]}return s(e,t),e.prototype.show=function(t,e,n,r){return this._show(this.edgeObject,!0,e,n,r),this._show(this.fillObject,t,e,n,r)},e.prototype.dispose=function(){return this.geometry.dispose(),this.edgeMaterial.dispose(),this.fillMaterial.dispose(),this.nreders=this.geometry=this.edgeMaterial=this.fillMaterial=this.edgeObject=this.fillObject=null,e.__super__.dispose.apply(this,arguments)},e}(r),e.exports=i},{"../geometry":143,"./base":151}],159:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;r=t("./base"),o=t("../geometry").StripGeometry,i=function(t){function e(t,n,r){var i,s,a,u,h,l,c,p,f,d,m,v,g,E;e.__super__.constructor.call(this,t,n,r),g=r.uniforms,f=r.material,m=r.position,i=r.color,p=r.mask,c=r.map,s=r.combine,l=r.linear,v=r.stpq,null==g&&(g={}),null==f&&(f=!0),h=null!=g.styleColor,this.geometry=new o({items:r.items,width:r.width,height:r.height,depth:r.depth}),this._adopt(g),this._adopt(this.geometry.uniforms),u=n.material(),E=u.vertex,E.pipe(this._vertexColor(i,p)),E.require(this._vertexPosition(m,f,c,2,v)),f||E.pipe("mesh.position",this.uniforms),f&&E.pipe("strip.position.normal",this.uniforms),E.pipe("project.position",this.uniforms),u.fragment=a=this._fragmentColor(h,f,i,p,c,2,v,s,l),a.pipe("fragment.color",this.uniforms),this.material=this._material(u.link({side:THREE.DoubleSide})),d=new THREE.Mesh(this.geometry,this.material),this._raw(d),this.renders=[d]}return s(e,t),e.prototype.dispose=function(){return this.geometry.dispose(),this.material.dispose(),this.renders=this.geometry=this.material=null,e.__super__.dispose.apply(this,arguments)},e}(r),e.exports=i},{"../geometry":143,"./base":151}],160:[function(t,e,n){var r,i,o,s,a=function(t,e){function n(){this.constructor=t}for(var r in e)u.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},u={}.hasOwnProperty;r=t("./base"),o=t("../geometry").SurfaceGeometry,s=t("../../util"),i=function(t){function e(t,n,r){var i,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b;e.__super__.constructor.call(this,t,n,r),_=r.uniforms,v=r.material,E=r.position,i=r.color,m=r.mask,d=r.map,s=r.combine,f=r.linear,y=r.stpq,p=r.intUV,null==_&&(_={}),null==v&&(v=!0),c=null!=_.styleColor,l=null!=_.surfaceHollow,this.geometry=new o({width:r.width,height:r.height,surfaces:r.surfaces,layers:r.layers,closedX:r.closedX,closedY:r.closedY}),this._adopt(_),this._adopt(this.geometry.uniforms),h=n.material(),b=h.vertex,p&&(a={POSITION_UV_INT:""}),b.pipe(this._vertexColor(i,m)),b.require(this._vertexPosition(E,v,d,2,y)),v||b.pipe("surface.position",this.uniforms,a),v&&b.pipe("surface.position.normal",this.uniforms,a),b.pipe("project.position",this.uniforms),h.fragment=u=this._fragmentColor(c,v,i,m,d,2,y,s,f),u.pipe("fragment.color",this.uniforms),this.material=this._material(h.link({side:THREE.DoubleSide})),g=new THREE.Mesh(this.geometry,this.material),this._raw(g),this.renders=[g]}return a(e,t),e.prototype.dispose=function(){return this.geometry.dispose(),this.material.dispose(),this.renders=this.geometry=this.material=null,e.__super__.dispose.apply(this,arguments)},e}(r),e.exports=i},{"../../util":175,"../geometry":143,"./base":151}],161:[function(t,e,n){var r;r=function(){function t(t,e){this.renderer=t,this.shaders=e,this.gl=this.renderer.context,null==this.uniforms&&(this.uniforms={})}return t.prototype.dispose=function(){return this.uniforms=null},t.prototype._adopt=function(t){var e,n;for(e in t)n=t[e],this.uniforms[e]=n},t.prototype._set=function(t){var e,n;for(e in t)n=t[e],null!=this.uniforms[e]&&(this.uniforms[e].value=n)},t}(),e.exports=r},{}],162:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty,u=[].indexOf||function(t){for(var e=0,n=this.length;n>e;e++)if(e in this&&this[e]===t)return e;return-1};i=t("./renderable"),r=function(t){function e(){e.__super__.constructor.apply(this,arguments),this.rotationAutoUpdate=!1,this.frustumCulled=!1,this.matrixAutoUpdate=!1}return s(e,t),e}(THREE.Object3D),o=function(t){function e(t,n,i){e.__super__.constructor.call(this,t,n,i),this.root=new r,null!=(null!=i?i.scene:void 0)&&(this.scene=i.scene),null==this.scene&&(this.scene=new THREE.Scene),this.pending=[],this.async=0,this.scratch=new THREE.WebGLRenderTarget(1,1),this.camera=new THREE.PerspectiveCamera}return s(e,t),e.prototype.inject=function(t){return null!=t&&(this.scene=t),this.scene.add(this.root)},e.prototype.unject=function(){var t;return null!=(t=this.scene)?t.remove(this.root):void 0},e.prototype.add=function(t){return this.async?this.pending.push(t):this._add(t)},e.prototype.remove=function(t){return this.pending=this.pending.filter(function(e){return e!==t}),null!=t.parent?this._remove(t):void 0},e.prototype._add=function(t){return this.root.add(t)},e.prototype._remove=function(t){return this.root.remove(t)},e.prototype.dispose=function(){return null!=this.root.parent?this.unject():void 0},e.prototype.warmup=function(t){return this.async=+t||0},e.prototype.render=function(){var t,e,n,r,i,o,s;if(this.pending.length){for(e=this.root.children,t=[],n=r=0,o=this.async;(o>=0?o>r:r>o)&&(i=this.pending.shift(),i);n=o>=0?++r:--r)this._add(i),t.push(t);return s=e.map(function(t){var e;return e=t.visible}),e.map(function(e){return e.visible=u.call(t,e)<0}),this.renderer.render(this.scene,this.camera,this.scratch),e.map(function(t,e){return t.visible=s[e]})}},e.prototype.toJSON=function(){return this.root.toJSON()},e}(i),e.exports=o},{"./renderable":161}],163:[function(t,e,n){var r,i;i=t("../../vendor/shadergraph/src"),r=function(t){var e;return e=function(e){var n,r,i,o,s;if(o=t[e],null!=o)return o;if(r="#"===(i=e[0])||"."===i||":"===i||"["===i,s=r?e:"#"+e,n=document.querySelector(s),null!=n&&"SCRIPT"===n.tagName)return n.textContent||n.innerText;throw new Error("Unknown shader `"+e+"`")},new i(e,{autoInspect:!0})},e.exports=r},{"../../vendor/shadergraph/src":208}],164:[function(t,e,n){n.Factory=t("./factory"),n.Snippets=t("../../build/shaders")},{"../../build/shaders":1,"./factory":163}],165:[function(t,e,n){THREE.Bootstrap.registerPlugin("splash",{defaults:{color:"mono",fancy:!0},listen:["ready","mathbox/init:init","mathbox/progress:progress","mathbox/destroy:destroy"],uninstall:function(){return this.destroy()},ready:function(t,e){return e.MathBox&&!this.div?init(t,e):void 0},init:function(t,e){var n,r,i,o,s,a,u;return this.destroy(),n=this.options.color,i='
    \n \n
    \n
    ',this.div=r=document.createElement("div"),r.innerHTML=i,e.element.appendChild(r),s=2*Math.random()-1,a=2*Math.random()-1,u=2*Math.random()-1,o=1/Math.sqrt(s*s+a*a+u*u),this.loader=r.querySelector(".mathbox-loader"),this.bar=r.querySelector(".mathbox-progress > div"),this.gyro=r.querySelectorAll(".mathbox-logo > div"),this.transforms=["rotateZ(22deg) rotateX(24deg) rotateY(30deg)","rotateZ(11deg) rotateX(12deg) rotateY(15deg) scale3d(.6, .6, .6)"],this.random=[s*o,a*o,u*o],this.start=e.Time.now,this.timer=null},progress:function(t,e){var n,r,i,o,s,a,u,h,l,c,p,f,d,m;if(this.div&&(n=t.current,p=t.total,f=p>n,clearTimeout(this.timer),f?(this.loader.classList.remove("mathbox-exit"),this.loader.style.display="block"):(this.loader.classList.add("mathbox-exit"),this.timer=setTimeout(function(t){return function(){return t.loader.style.display="none"}}(this),150)),m=p>n?.1*Math.round(1e3*n/p)+"%":"100%",this.bar.style.width=m,this.options.fancy)){for(d=this.random,i=Math.max(0,Math.min(1,e.Time.now-this.start)),s=function(t,n){return null==n&&(n=0),t.replace(/(-?[0-9.e]+)deg/g,function(t,r){return+r+d[n++]*i*e.Time.step*60+"deg"})},h=this.gyro,l=[],o=a=0,u=h.length;u>a;o=++a)r=h[o],this.transforms[o]=c=s(this.transforms[o]),l.push(r.style.transform=r.style.WebkitTransform=c);return l}},destroy:function(){var t;return null!=(t=this.div)&&t.remove(),this.div=null}})},{}],166:[function(t,e,n){var r,i,o;o=t("../util").Ease,i=function(){function t(t){this.context=t,this.anims=[]}return t.prototype.make=function(t,e){var n;return n=new r(this,this.context.time,t,e),this.anims.push(n),n},t.prototype.unmake=function(t){var e;return this.anims=function(){var n,r,i,o;for(i=this.anims,o=[],n=0,r=i.length;r>n;n++)e=i[n],e!==t&&o.push(e);return o}.call(this)},t.prototype.update=function(){var t,e;return e=this.context.time,this.anims=function(){var n,r,i,o;for(i=this.anims,o=[],n=0,r=i.length;r>n;n++)t=i[n],t.update(e)!==!1&&o.push(t);return o}.call(this)},t.prototype.lerp=function(t,e,n,r,i){var o,s,a,u;if(null==i&&(i=t.make()),t.lerp)i=t.lerp(e,n,i,r);else if(t.emitter){if(s=e.emitterFrom,u=n.emitterTo,null!=s&&null!=u&&s===u)return s.lerp(r),s;o=t.emitter(e,n),e.emitterFrom=o,n.emitterTo=o}else t.op?(a=function(t,e){return t===+t&&e===+e?t+(e-t)*r:r>.5?e:t},i=t.op(e,n,i,a)):i=r>.5?n:e;return i},t}(),r=function(){function t(t,e,n,r){this.animator=t,this.time=e,this.type=n,this.options=r,this.value=this.type.make(),this.target=this.type.make(),this.queue=[]}return t.prototype.dispose=function(){return this.animator.unmake(this)},t.prototype.set=function(){var t,e,n;return e=this.target,n=arguments.length>1?[].slice.call(arguments):arguments[0],t=!1,n=this.type.validate(n,e,function(){return t=!0}),t||(e=n),this.cancel(),this.target=this.value,this.value=e,this.notify()},t.prototype.getTime=function(){var t,e;return t=this.options.clock,e=t?t.getTime():this.time,this.options.realtime?e.time:e.clock},t.prototype.cancel=function(t){var e,n,r,i,o,s;for(null==t&&(t=this.getTime()),o=this.queue,n=function(){var e,n,r;for(r=[],e=0,n=o.length;n>e;e++)s=o[e],s.end>=t&&r.push(s);return r}(),this.queue=function(){var e,n,r;for(r=[],e=0,n=o.length;n>e;e++)s=o[e],s.endr;r++)s=n[r],"function"==typeof s.complete&&s.complete(!1);"function"==typeof(e=this.options).complete&&e.complete(!1)},t.prototype.notify=function(){var t;return"function"==typeof(t=this.options).step?t.step(this.value):void 0},t.prototype.immediate=function(t,e){var n,r,i,o,s,a,u,h,l,c;return i=e.duration,r=e.delay,o=e.ease,h=e.step,n=e.complete,c=this.getTime(),u=c+r,s=u+i,a=!1,l=this.type.make(),t=this.type.validate(t,l,function(){return a=!0,null}),void 0!==t&&(l=t),this.cancel(u),this.queue.push({from:null,to:l,start:u,end:s,ease:o,step:h,complete:n})},t.prototype.update=function(t){var e,n,r,i,s,a,u,h,l,c,p,f,d,m,v,g;if(this.time=t,0===this.queue.length)return!0;for(r=this.getTime(),g=this.value,c=this.queue,e=!1;!e;){if(p=f=c[0],h=p.from,v=p.to,d=p.start,a=p.end,m=p.step,i=p.complete,s=p.ease,null==h&&(h=f.from=this.type.clone(this.value)),u=o.clamp((r-d)/Math.max(1e-5,a-d)||0,0,1),0===u)return;if(l=function(){switch(s){case"linear":case 0:return null;case"cosine":case 1:return o.cosine;case"binary":case 2:return o.binary;case"hold":case 3:return o.hold;default:return o.cosine}}(),null!=l&&(u=l(u)),e=1>u,g=e?this.animator.lerp(this.type,h,v,u,g):v,"function"==typeof m&&m(g),!e&&("function"==typeof i&&i(!0),"function"==typeof(n=this.options).complete&&n.complete(!0),c.shift(),0===c.length))break}return this.value=g,this.notify()},t}(),e.exports=i},{"../util":175}],167:[function(t,e,n){var r,i;i=t("../util"),r=function(){function t(t,e,n){var r,i,o,s,a,u,h,l,c,p;for(this._context=t,this._up=e,this._targets=n,l=this._context.controller.getRoot(),null==this._targets&&(this._targets=[l]),this.isRoot=1===this._targets.length&&this._targets[0]===l,this.isLeaf=1===this._targets.length&&null==this._targets[0].children,u=this._targets,r=i=0,s=u.length;s>i;r=++i)c=u[r],this[r]=c;for(this.length=this._targets.length,h=this._context.controller.getTypes(),o=0,a=h.length;a>o;o++)p=h[o],"root"!==p&&!function(t){return function(e){return t[e]=function(n,r){return t.add(e,n,r)}}}(this)(p)}return t.prototype.v2=function(){return this},t.prototype.select=function(t){var e;return e=this._context.model.select(t,this.isRoot?null:this._targets),this._push(e)},t.prototype.eq=function(t){return this._targets.length>t?this._push([this._targets[t]]):this._push([])},t.prototype.filter=function(t){var e;return"string"==typeof t&&(e=this._context.model._matcher(t),t=function(t){return e(t)}),this._push(this._targets.filter(t))},t.prototype.map=function(t){var e,n,r,i;for(i=[],e=n=0,r=this.length;r>=0?r>n:n>r;e=r>=0?++n:--n)i.push(t(this[e],e,this));return i},t.prototype.each=function(t){var e,n,r;for(e=n=0,r=this.length;r>=0?r>n:n>r;e=r>=0?++n:--n)t(this[e],e,this);return this},t.prototype.add=function(t,e,n){var r,i,o,s,a,u,h;if(r=this._context.controller,this.isLeaf)return this._pop().add(t,e,n);for(a=[],u=this._targets,i=0,o=u.length;o>i;i++)h=u[i],s=r.make(t,e,n),r.add(s,h),a.push(s);return this._push(a)},t.prototype.remove=function(t){var e,n,r,i;if(t)return this.select(t).remove();for(r=this._targets.slice().reverse(),e=0,n=r.length;n>e;e++)i=r[e],this._context.controller.remove(i);return this._pop()},t.prototype.set=function(t,e){var n,r,i,o;for(i=this._targets,n=0,r=i.length;r>n;n++)o=i[n],this._context.controller.set(o,t,e);return this},t.prototype.getAll=function(t){var e,n,r,i,o;for(r=this._targets,i=[],e=0,n=r.length;n>e;e++)o=r[e],i.push(this._context.controller.get(o,t));return i},t.prototype.get=function(t){var e;return null!=(e=this._targets[0])?e.get(t):void 0},t.prototype.evaluate=function(t,e){var n;return null!=(n=this._targets[0])?n.evaluate(t,e):void 0},t.prototype.bind=function(t,e){var n,r,i,o;for(i=this._targets,n=0,r=i.length;r>n;n++)o=i[n],this._context.controller.bind(o,t,e);return this},t.prototype.unbind=function(t){var e,n,r,i;for(r=this._targets,e=0,n=r.length;n>e;e++)i=r[e],this._context.controller.unbind(i,t);return this},t.prototype.end=function(){return(this.isLeaf?this._pop():this)._pop()},t.prototype._push=function(e){return new t(this._context,this,e); -},t.prototype._pop=function(){var t;return null!=(t=this._up)?t:this},t.prototype._reset=function(){var t,e;return null!=(t=null!=(e=this._up)?e.reset():void 0)?t:this},t.prototype.map=function(t){return this._targets.map(t)},t.prototype.on=function(){var t;return t=arguments,this._targets.map(function(e){return e.on.apply(e,t)}),this},t.prototype.off=function(){var t;return t=arguments,this._targets.map(function(e){return e.on.apply(e,t)}),this},t.prototype.toString=function(){var t;return t=this._targets.map(function(t){return t.toString()}),this._targets.length>1?"["+t.join(", ")+"]":t[0]},t.prototype.toMarkup=function(){var t;return t=this._targets.map(function(t){return t.toMarkup()}),t.join("\n\n")},t.prototype.print=function(){return i.Pretty.print(this._targets.map(function(t){return t.toMarkup()}).join("\n\n")),this},t.prototype.debug=function(){var t,e,n,r,i,o,s,a;for(e=this.inspect(),console.log("Renderables: ",e.renderables),console.log("Renders: ",e.renders),console.log("Shaders: ",e.shaders),t=function(t){return t.constructor.toString().match("function +([^(]*)")[1]},a=[],o=e.shaders,n=0,r=o.length;r>n;n++)s=o[n],i=t(s.owner),a.push(i+" - Vertex"),a.push(s.vertex),a.push(i+" - Fragment"),a.push(s.fragment);return ShaderGraph.inspect(a)},t.prototype.inspect=function(t,e,n){var r,i,o,s,a,u,h,l,c,p,f,d,m;for("boolean"==typeof e&&(n=e,e=null),null==n&&(n=!0),l=function(t){var e,n;return null!=(e=null!=(n=t.controller)?n.objects:void 0)?e:[]},c=d=function(t,n){var r,i,o,s;if(null==n&&(n=[]),(!e||t.traits.hash[e])&&n.push(l(t)),null!=t.children)for(s=t.children,i=0,o=s.length;o>i;i++)r=s[i],d(r,n);return n},i=function(t){return t=t.reduce(function(t,e){return t.concat(e)},[]),t=t.filter(function(e,n){return null!=e&&t.indexOf(e)===n})},h=function(t,e){var n;return n={},n.owner=t,n.geometry=e.geometry,n.material=e.material,n.vertex=e.material.vertexGraph,n.fragment=e.material.fragmentGraph,n},o={nodes:this._targets.slice(),renderables:[],renders:[],shaders:[]},p=this._targets,s=0,u=p.length;u>s;s++){m=p[s],n&&m.print(t,"info"),r={renderables:f=i(c(m)),renders:i(f.map(function(t){return t.renders})),shaders:i(f.map(function(t){var e;return null!=(e=t.renders)?e.map(function(e){return h(t,e)}):void 0}))};for(a in r)o[a]=o[a].concat(r[a])}return o},t}(),e.exports=r},{"../util":175}],168:[function(t,e,n){var r,i;i=t("../util"),r=function(){function t(t,e){this.model=t,this.primitives=e}return t.prototype.getRoot=function(){return this.model.getRoot()},t.prototype.getTypes=function(){return this.primitives.getTypes()},t.prototype.make=function(t,e,n){return this.primitives.make(t,e,n)},t.prototype.get=function(t,e){return t.get(e)},t.prototype.set=function(t,e,n){var r,i;try{return t.set(e,n)}catch(i){return r=i,t.print(null,"warn"),console.error(r)}},t.prototype.bind=function(t,e,n){var r,i;try{return t.bind(e,n)}catch(i){return r=i,t.print(null,"warn"),console.error(r)}},t.prototype.unbind=function(t,e){var n,r;try{return t.unbind(e)}catch(r){return n=r,t.print(null,"warn"),console.error(n)}},t.prototype.add=function(t,e){return null==e&&(e=this.model.getRoot()),e.add(t)},t.prototype.remove=function(t){var e;return e=t.parent,e?e.remove(t):void 0},t}(),e.exports=r},{"../util":175}],169:[function(t,e,n){n.Animator=t("./animator"),n.API=t("./api"),n.Controller=t("./controller")},{"./animator":166,"./api":167,"./controller":168}],170:[function(t,e,n){var r=[].indexOf||function(t){for(var e=0,n=this.length;n>e;e++)if(e in this&&this[e]===t)return e;return-1};n.setOrigin=function(t,e,n){var i,o,s,a;return+e===e&&(e=[e]),o=r.call(e,1)>=0?0:n.x,s=r.call(e,2)>=0?0:n.y,a=r.call(e,3)>=0?0:n.z,i=r.call(e,4)>=0?0:n.w,t.set(o,s,a,i)},n.addOrigin=function(){var t;return t=new THREE.Vector4,function(e,r,i){return n.setOrigin(t,r,i),e.add(t)}}(),n.setDimension=function(t,e){var n,r,i,o;return r=1===e?1:0,i=2===e?1:0,o=3===e?1:0,n=4===e?1:0,t.set(r,i,o,n)},n.setDimensionNormal=function(t,e){var n,r,i,o;return r=1===e?1:0,i=2===e?1:0,o=3===e?1:0,n=4===e?1:0,t.set(i,o+r,n,0)},n.recenterAxis=function(){var t;return t=[0,0],function(e,n,r,i){var o,s,a,u,h,l;return null==i&&(i=0),r>0&&(h=e,l=e+n,o=Math.max(Math.abs(h),Math.abs(l)),s=o*i,u=Math.min(h,l),a=Math.max(h,l),e=u+(-o+s-u)*r,n=a+(o+s-a)*r-e),t[0]=e,t[1]=n,t}}()},{}],171:[function(t,e,n){e.exports=self={bind:function(t,e){return function(n,r){r.__binds||(r.__binds=[]);var i=t;_.isArray(n)&&(i=n[0],n=n[1]);for(var o=/^([^.:]*(?:\.[^.:]+)*)?(?:\:(.*))?$/.exec(n),s=o[1].split(/\./g),a=s.pop(),u=o[2]||a,h=s.shift(),l={"this":r}[h]||e[h]||t[h]||i;l&&(n=s.shift());)l=l[n];if(l&&(l.on||l.addEventListener)){var c=function(e){r[u]&&r[u](e,t)};self._polyfill(l,["addEventListener","on"],function(t){l[t](a,c)});var p={target:l,name:a,callback:c};return r.__binds.push(p),c}throw"Cannot bind '"+n+"' in "+this.__name}},unbind:function(){return function(t){t.__binds&&(t.__binds.forEach(function(t){self._polyfill(t.target,["removeEventListener","off"],function(e){t.target[e](t.name,t.callback)})}.bind(this)),t.__binds=[])}},apply:function(t){THREE.EventDispatcher.prototype.apply(t),t.trigger=self._trigger,t.triggerOnce=self._triggerOnce,t.on=t.addEventListener,t.off=t.removeEventListener,t.dispatchEvent=t.trigger},_triggerOnce:function(t){this.trigger(t),this._listeners&&delete this._listeners[t.type]},_trigger:function(t){if(void 0!==this._listeners){var e=t.type,n=this._listeners[e];if(void 0!==n){n=n.slice();var r=n.length;t.target=this;for(var i=0;r>i;i++)n[i].call(this,t,this)}}},_polyfill:function(t,e,n){e.map(function(e){return t.method}),e.length&&n(e[0])}}},{}],172:[function(t,e,n){var r;n.getSizes=r=function(t){var e,n;for(n=[],e=t;"string"!=typeof e&&null!=(null!=e?e.length:void 0);)n.push(e.length),e=e[0];return n},n.getDimensions=function(t,e){var n,i,o,s,a,u,h,l,c,p,f,d,m,v,g;return null==e&&(e={}),a=e.items,n=e.channels,g=e.width,s=e.height,i=e.depth,o={},t&&t.length?(v=r(t),l=v.length,o.channels=1!==n&&v.length>1?v.pop():n,o.items=1!==a&&v.length>1?v.pop():a,o.width=1!==g&&v.length>1?v.pop():g,o.height=1!==s&&v.length>1?v.pop():s,o.depth=1!==i&&v.length>1?v.pop():i,u=l,1===n&&u++,1===a&&u>1&&u++,1===g&&u>2&&u++,1===s&&u>3&&u++,h=null!=(c=v.pop())?c:1,1>=u&&(h/=null!=(p=o.channels)?p:1),2>=u&&(h/=null!=(f=o.items)?f:1),3>=u&&(h/=null!=(d=o.width)?d:1),4>=u&&(h/=null!=(m=o.height)?m:1),h=Math.floor(h),null==o.width&&(o.width=h,h=1),null==o.height&&(o.height=h,h=1),null==o.depth&&(o.depth=h,h=1),o):{items:a,channels:n,width:null!=g?g:0,height:null!=s?s:0,depth:null!=i?i:0}},n.repeatCall=function(t,e){switch(e){case 0:return function(){return!0};case 1:return function(){return t()};case 2:return function(){return t(),t()};case 3:return function(){return t(),t(),t(),t()};case 4:return function(){return t(),t(),t(),t()};case 6:return function(){return t(),t(),t(),t(),t(),t()};case 8:return function(){return t(),t(),t(),t(),t(),t()}}},n.makeEmitter=function(t,e,n){var r,i,o,s;for(r=function(){switch(n){case 0:return function(){return!0};case 1:return function(e){return e(t())};case 2:return function(e){return e(t(),t())};case 3:return function(e){return e(t(),t(),t())};case 4:return function(e){return e(t(),t(),t(),t())};case 6:return function(e){return e(t(),t(),t(),t(),t(),t())};case 8:return function(e){return e(t(),t(),t(),t(),t(),t(),t(),t())}}}(),o=null;e>0;)i=Math.min(e,8),s=function(){switch(i){case 1:return function(t){return r(t)};case 2:return function(t){return r(t),r(t)};case 3:return function(t){return r(t),r(t),r(t)};case 4:return function(t){return r(t),r(t),r(t),r(t)};case 5:return function(t){return r(t),r(t),r(t),r(t),r(t)};case 6:return function(t){return r(t),r(t),r(t),r(t),r(t),r(t)};case 7:return function(t){return r(t),r(t),r(t),r(t),r(t),r(t),r(t)};case 8:return function(t){return r(t),r(t),r(t),r(t),r(t),r(t),r(t),r(t)}}}(),o=null!=o?function(t,e){return function(n){return t(n),e(n)}}(s,o):s,e-=i;return s=null!=o?o:function(){return!0},s.reset=t.reset,s.rebind=t.rebind,s},n.getThunk=function(t){var e,n,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S;switch(H=r(t),d=H.length,e=H.pop(),n=H.pop(),i=H.pop(),o=H.pop(),s=!1,d){case 0:S=function(){return 0},S.reset=function(){};break;case 1:h=0,S=function(){return t[h++]},S.reset=function(){return h=0};break;case 2:h=l=0,a=null!=(m=t[l])?m:[],S=function(){var n,r;return r=a[h++],h===e&&(h=0,l++,a=null!=(n=t[l])?n:[]),r},S.reset=function(){var e;h=l=0,a=null!=(e=t[l])?e:[]};break;case 3:h=l=c=0,R=null!=(v=t[c])?v:[],a=null!=(g=R[l])?g:[],S=function(){var r,i,o;return o=a[h++],h===e&&(h=0,l++,l===n&&(l=0,c++,R=null!=(r=t[c])?r:[]),a=null!=(i=R[l])?i:[]),o},S.reset=function(){var e,n;h=l=c=0,R=null!=(e=t[c])?e:[],a=null!=(n=R[l])?n:[]};break;case 4:h=l=c=p=0,M=null!=(E=t[p])?E:[],R=null!=(y=M[c])?y:[],a=null!=(_=R[l])?_:[],S=function(){var r,o,s,u;return u=a[h++],h===e&&(h=0,l++,l===n&&(l=0,c++,c===i&&(c=0,p++,M=null!=(r=t[p])?r:[]),R=null!=(o=M[c])?o:[]),a=null!=(s=R[l])?s:[]),u},S.reset=function(){var e,n,r;h=l=c=p=0,M=null!=(e=t[p])?e:[],R=null!=(n=M[c])?n:[],a=null!=(r=R[l])?r:[]};break;case 5:h=l=c=p=f=0,u=null!=(b=t[f])?b:[],M=null!=(T=u[p])?T:[],R=null!=(x=M[c])?x:[],a=null!=(w=R[l])?w:[],S=function(){var r,s,d,m,v;return v=a[h++],h===e&&(h=0,l++,l===n&&(l=0,c++,c===i&&(c=0,p++,p===o&&(p=0,f++,u=null!=(r=t[f])?r:[]),M=null!=(s=u[p])?s:[]),R=null!=(d=M[c])?d:[]),a=null!=(m=R[l])?m:[]),v},S.reset=function(){var e,n,r,i;h=l=c=p=f=0,u=null!=(e=t[f])?e:[],M=null!=(n=u[p])?n:[],R=null!=(r=M[c])?r:[],a=null!=(i=R[l])?i:[]}}return S.rebind=function(o){return t=o,H=r(t),H.length&&(e=H.pop()),H.length&&(n=H.pop()),H.length&&(i=H.pop()),H.length?o=H.pop():void 0},S},n.getStreamer=function(t,e,n,r){var i,o,s,a,u,h,l,c,p;return l=u=h=0,c=function(){return l=e*n*r,u=h=0},o=function(){return h},s=function(){return 0>=l-u},p=function(){switch(n){case 1:return function(t){u+=t,h+=t};case 2:return function(t){u+=2*t,h+=t};case 3:return function(t){u+=3*t,h+=t};case 4:return function(t){u+=4*t,h+=t}}}(),i=function(){switch(n){case 1:return function(e){e(t[u++]),++h};case 2:return function(e){e(t[u++],t[u++]),++h};case 3:return function(e){e(t[u++],t[u++],t[u++]),++h};case 4:return function(e){e(t[u++],t[u++],t[u++],t[u++]),++h}}}(),a=function(){switch(n){case 1:return function(e){t[u++]=e,++h};case 2:return function(e,n){t[u++]=e,t[u++]=n,++h};case 3:return function(e,n,r){t[u++]=e,t[u++]=n,t[u++]=r,++h};case 4:return function(e,n,r,i){t[u++]=e,t[u++]=n,t[u++]=r,t[u++]=i,++h}}}(),i.reset=c,a.reset=c,c(),{emit:a,consume:i,skip:p,count:o,done:s,reset:c}},n.getLerpEmitter=function(t,e){var n,r,i,o,s,a,u,h,l,c,p;return p=new Float32Array(4096),s=a=.5,u=h=l=c=0,r=function(t,e,n,r){return l++,p[u++]=t*s,p[u++]=e*s,p[u++]=n*s,p[u++]=r*s},i=function(t,e,n,r){return c++,p[h++]+=t*a,p[h++]+=e*a,p[h++]+=n*a,p[h++]+=r*a},n=Math.max(t.length,e.length),o=3>=n?function(n,o,s){var a,f,d,m,v,g;for(u=h=l=c=0,t(r,o,s),e(i,o,s),d=Math.min(l,c),f=0,g=[],a=m=0,v=d;v>=0?v>m:m>v;a=v>=0?++m:--m)g.push(n(p[f++],p[f++],p[f++],p[f++]));return g}:5>=n?function(n,o,s,a,f){var d,m,v,g,E,y;for(u=h=l=c=0,t(r,o,s,a,f),e(i,o,s,a,f),v=Math.min(l,c),m=0,y=[],d=g=0,E=v;E>=0?E>g:g>E;d=E>=0?++g:--g)y.push(n(p[m++],p[m++],p[m++],p[m++]));return y}:7>=n?function(n,o,s,a,f,d,m){var v,g,E,y,_;for(u=h=l=c=0,t(r,o,s,a,f,d,m),e(i,o,s,a,f,d,m),g=Math.min(l,c),v=0,_=[],m=E=0,y=g;y>=0?y>E:E>y;m=y>=0?++E:--E)_.push(n(p[v++],p[v++],p[v++],p[v++]));return _}:9>=n?function(n,o,s,a,f,d,m,v,g){var E,y,_,b;for(u=h=l=c=0,t(r,o,s,a,f,d,m,v,g),e(i,o,s,a,f,d,m,v,g),E=Math.min(l,c),g=0,b=[],v=y=0,_=E;_>=0?_>y:y>_;v=_>=0?++y:--y)b.push(n(p[g++],p[g++],p[g++],p[g++]));return b}:function(n,o,s,a,f,d,m,v,g,E,y){var _,b,T,x;for(u=h=0,t(r,o,s,a,f,d,m,v,g,E,y),e(i,o,s,a,f,d,m,v,g,E,y),_=Math.min(l,c),g=0,x=[],v=b=0,T=_;T>=0?T>b:b>T;v=T>=0?++b:--b)x.push(n(p[g++],p[g++],p[g++],p[g++]));return x},o.lerp=function(t){var e;return e=[1-t,t],s=e[0],a=e[1],e},o},n.getLerpThunk=function(t,e){var r,i,o,s,a,u;return i=n.getSizes(t).reduce(function(t,e){return t*e}),o=n.getSizes(e).reduce(function(t,e){return t*e}),r=Math.min(i,o),a=n.getThunk(t),u=n.getThunk(e),s=new Float32Array(r),s.lerp=function(t){var e,n,i,o;for(a.reset(),u.reset(),i=0,o=[];r>i;)e=a(),n=u(),o.push(s[i++]=e+(n-e)*t);return o},s}},{}],173:[function(t,e,n){var r,i;i=Math.PI,r={clamp:function(t,e,n){return Math.max(e,Math.min(n,t))},cosine:function(t){return.5-.5*Math.cos(r.clamp(t,0,1)*i)},binary:function(t){return+(t>=.5)},hold:function(t){return+(t>=1)}},e.exports=r},{}],174:[function(t,e,n){var r,i,o,s,a,u=[].indexOf||function(t){for(var e=0,n=this.length;n>e;e++)if(e in this&&this[e]===t)return e;return-1};i="xyzw".split(""),r={0:-1,x:0,y:1,z:2,w:3},o=function(t){return t===""+t&&(t=t.split("")),t===+t&&(t=[t]),t},a=function(t){return t===+t&&(t="vec"+t),"vec1"===t&&(t="float"),t},s=function(t){return t=""+t,t.indexOf(".")<0?t+=".0":void 0},n.mapByte2FloatOffset=function(t){var e;return null==t&&(t=4),e=s(t),"vec4 float2ByteIndex(vec4 xyzw, out float channelIndex) {\n float relative = xyzw.w / "+e+";\n float w = floor(relative);\n channelIndex = (relative - w) * "+e+";\n return vec4(xyzw.xyz, w);\n}"},n.sample2DArray=function(t){var e,n;return n=function(t,e){var r,i;return t===e?i="return texture2D(dataTextures["+t+"], uv);":(r=Math.ceil(t+(e-t)/2),i="if (z < "+(r-.5)+") {\n "+n(t,r-1)+"\n}\nelse {\n "+n(r,e)+"\n}"),i=i.replace(/\n/g,"\n ")},e=n(0,t-1),"uniform sampler2D dataTextures["+t+"];\n\nvec4 sample2DArray(vec2 uv, float z) {\n "+e+"\n}"},n.binaryOperator=function(t,e,n){return t=a(t),null!=n?t+" binaryOperator("+t+" a) {\n return a "+e+" "+n+";\n}":t+" binaryOperator("+t+" a, "+t+" b) {\n return a "+e+" b;\n}"},n.extendVec=function(t,e,r){var i,o,u,h;return null==r&&(r=0),t>e?n.truncateVec(t,e):(o=e-t,t=a(t),e=a(e),r=s(r),u=function(){h=[];for(var t=0;o>=0?o>=t:t>=o;o>=0?t++:t--)h.push(t);return h}.apply(this).map(function(t){return t?r:"v"}),i=u.join(","),e+" extendVec("+t+" v) { return "+e+"("+i+"); }")},n.truncateVec=function(t,e){var r;return e>t?n.extendVec(t,e):(r="."+"xyzw".substr(0,e),t=a(t),e=a(e),e+" truncateVec("+t+" v) { return v"+r+"; }")},n.injectVec4=function(t){var e,n,i,s,a,u,h;for(h=["0.0","0.0","0.0","0.0"],t=o(t),t=t.map(function(t){return t===""+t?r[t]:t}),i=s=0,a=t.length;a>s;i=++s)n=t[i],h[n]=["a","b","c","d"][i];return u=h.slice(0,4).join(", "),e=["float a","float b","float c","float d"].slice(0,t.length),"vec4 inject("+e+") {\n return vec4("+u+");\n}"},n.swizzleVec4=function(t,e){var n,i;for(null==e&&(e=null),n=["0.0","xyzw.x","xyzw.y","xyzw.z","xyzw.w"],null==e&&(e=t.length),t=o(t),t=t.map(function(t){var e;return e=+t,u.call([0,1,2,3,4],e)>=0&&(t=+t),t===""+t&&(t=r[t]+1),n[t]});t.lengths;e=++s)u=t[e],l=i[e],n=r[u],c[n]="xyzw."+l;return h=c.join(", "),"vec4 invertSwizzle(vec4 xyzw) {\n return vec4("+h+");\n}"},n.identity=function(t){var e;return e=[].slice.call(arguments),e.length>1?(e=e.map(function(t,e){return["inout",t,String.fromCharCode(97+e)].join(" ")}),e=e.join(", "),"void identity("+e+") { }"):t+" identity("+t+" x) {\n return x;\n}"},n.constant=function(t,e){return t+" constant() {\n return "+e+";\n}"},n.toType=a},{}],175:[function(t,e,n){n.Axis=t("./axis"),n.Data=t("./data"),n.Ease=t("./ease"),n.GLSL=t("./glsl"),n.JS=t("./js"),n.Pretty=t("./pretty"),n.Three=t("./three"),n.Ticks=t("./ticks"),n.VDOM=t("./vdom")},{"./axis":170,"./data":172,"./ease":173,"./glsl":174,"./js":176,"./pretty":177,"./three":178,"./ticks":179,"./vdom":180}],176:[function(t,e,n){n.merge=function(){var t,e,n,r,i,o;for(o={},t=0,n=arguments.length;n>t;t++){r=arguments[t];for(e in r)i=r[e],o[e]=i}return o},n.clone=function(t){return JSON.parse(JSON.serialize(t))},n.parseQuoted=function(t){var e,n,r,i,o,s,a,u,h,l;for(e="",l=function(t){return t=t.replace(/\\/g,"")},a=function(t){return e.length&&s.push(l(e)),e=null!=t?t:""},t=t.split(/(?=(?:\\.|["' ,]))/g),u=!1,s=[],i=0,o=t.length;o>i;i++)switch(r=t[i],n=r[0],h=r.slice(1),n){case'"':case"'":if(u)u===n?(u=!1,a(h)):e+=r;else{if(""!==e)throw new Error("ParseError: String `"+t+"` does not contain comma-separated quoted tokens.");u=n,e+=h}break;case" ":case",":u?e+=r:a(h);break;default:e+=r}return a(),s}},{}],177:[function(t,e,n){var r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E;r=5,i=1e-4,o=function(t,e){return Math.abs(t/e-Math.round(t/e))i?"-":"",i+=n):"1"!==n&&(i+=r?""+n:"*"+n),i+"/"+e},u=[{1:1},{1:1,"τ":2*Math.PI},{1:1,"π":Math.PI},{1:1,"τ":2*Math.PI,"π":Math.PI},{1:1,e:Math.E},{1:1,"τ":2*Math.PI,e:Math.E},{1:1,"π":Math.PI,e:Math.E},{1:1,"τ":2*Math.PI,"π":Math.PI,e:Math.E}],c=[[420,[2,3,5,7]],[88200,[2,3,5,7]],[60060,[2,3,5,7,11,13]],[861764,[2,17,19,23,29]],[65536,[2]],[1e6,[2,5]]],g=function(t){var e,n,o,a,p,f,d,m,v,g;return t&&(e=t.cache,o=t.compact,v=t.tau,d=t.pi,a=t.e,g=t.threshold,m=t.precision),o=+!!(null!=o?o:!0),v=+!!(null!=v?v:!0),d=+!!(null!=d?d:!0),a=+!!(null!=a?a:!0),e=+!!(null!=e?e:!0),g=+(null!=g?g:i),m=+(null!=m?m:r),p=v+2*d+4*a,n=p+g+m,f=e?{}:null,function(t){var e,n,i,a,d,m,v,g,E,y,_,b,T,x,w,R,H,M;if(null!=f){if(null!=(n=f[t]))return n;if(t===Math.round(t))return f[t]=""+t}w=""+t,e=w.length+w.indexOf(".")+2,b=function(t){var n;return n=t.length,e>=n?(w=""+t,e=n):void 0},H=u[p];for(g in H)if(d=H[g],s(t/d))b(""+l(t/d,1,g,o));else for(m=0,E=c.length;E>m;m++)if(M=c[m],a=M[0],_=M[1],x=t/d*a,s(x)){for(v=0,y=_.length;y>v;v++)for(R=_[v];s(T=x/R)&&s(i=a/R);)x=T,a=i;b(""+h(t/d,a,g,o));break}return(""+t).length>r&&b(""+t.toPrecision(r)),null!=f&&(f[t]=w),w}},E=function(t,e){return null==e&&(e="info"),t=v(t),console[e].apply(console,t)},v=function(t){var e,n,r,i,o,s,a,u;return a="color:rgb(128,0,128)",n="color:rgb(144,64,0)",s="color:rgb(0,0,192)",i="color:rgb(0,70,156)",u="color:inherit",o=!1,r=0,e=[],t=t.replace(/(\\[<={}> "'])|(=>|[<={}> "'])/g,function(t,h,l){var c;return(null!=h?h.length:void 0)?h:o&&'"'!==l&&"'"!==l?l:r&&'"'!==l&&"'"!==l&&"{"!==l&&"}"!==l?l:c=function(){switch(l){case"<":return e.push(a),"%c<";case">":return e.push(a),e.push(u),"%c>%c";case" ":return e.push(n)," %c";case"=":case"=>":return e.push(a),"%c"+l;case'"':case"'":return o=!o,o?(e.push(r?n:s),l+"%c"):(e.push(r?i:a),"%c"+l);case"{":return 0===r++?(e.push(i),"%c"+l):l;case"}":return 0===--r?(e.push(a),l+"%c"):l;default:return l}}()}),[t].concat(e)},m=function(t,e){return d(t,e,"=")},f=function(t,e){return d(t,e,"=>")},d=function(){var t;return t=g({compact:!1}),function(e,n,r){var i,o,s;return i=function(t){return t===""+ +t||t.match(/^[A-Za-z_][A-Za-z0-9]*$/)?t:JSON.stringify(t)},s=function(t){return t.match('\n*"')?t:"{"+t+"}"},o=function(e){var n,r;if(e instanceof Array)return"["+e.map(o).join(", ")+"]";switch(typeof e){case"string":return e.match("\n")?'"\n'+e+'"\n':'"'+e+'"';case"function":return e=""+e,e.match("\n"),e=e.replace(/^function (\([^)]+\))/,"$1 =>"),e=e.replace(/^(\([^)]+\)) =>\s*{\s*return\s*([^}]+)\s*;\s*}/,"$1 => $2");case"number":return t(e);default:return null!=e&&e!==!!e?null!=e._up?o(e.map(function(t){return t})):e.toMarkup?e.toString():"{"+function(){var t;t=[];for(n in e)r=e[n],e.hasOwnProperty(n)&&t.push(i(n)+": "+o(r));return t}().join(", ")+"}":""+JSON.stringify(e)}},[e,r,s(o(n))].join("")}}(),a=function(t){return t=t.replace(/&/g,"&"),t=t.replace(/",t=a(t),r=0,i=n.length;i>r;r++)e=n[r],t=t.replace(/%([a-z])/,function(t,e){var r;switch(r=n.shift(),e){case"c":return'';default:return a(r)}});return o+=t,o+=""},e.exports={markup:v,number:g,print:E,format:p,JSX:{prop:m,bind:f}}},{}],178:[function(t,e,n){n.paramToGL=function(t,e){return e===THREE.RepeatWrapping?t.REPEAT:e===THREE.ClampToEdgeWrapping?t.CLAMP_TO_EDGE:e===THREE.MirroredRepeatWrapping?t.MIRRORED_REPEAT:e===THREE.NearestFilter?t.NEAREST:e===THREE.NearestMipMapNearestFilter?t.NEAREST_MIPMAP_NEAREST:e===THREE.NearestMipMapLinearFilter?t.NEAREST_MIPMAP_LINEAR:e===THREE.LinearFilter?t.LINEAR:e===THREE.LinearMipMapNearestFilter?t.LINEAR_MIPMAP_NEAREST:e===THREE.LinearMipMapLinearFilter?t.LINEAR_MIPMAP_LINEAR:e===THREE.UnsignedByteType?t.UNSIGNED_BYTE:e===THREE.UnsignedShort4444Type?t.UNSIGNED_SHORT_4_4_4_4:e===THREE.UnsignedShort5551Type?t.UNSIGNED_SHORT_5_5_5_1:e===THREE.UnsignedShort565Type?t.UNSIGNED_SHORT_5_6_5:e===THREE.ByteType?t.BYTE:e===THREE.ShortType?t.SHORT:e===THREE.UnsignedShortType?t.UNSIGNED_SHORT:e===THREE.IntType?t.INT:e===THREE.UnsignedIntType?t.UNSIGNED_INT:e===THREE.FloatType?t.FLOAT:e===THREE.AlphaFormat?t.ALPHA:e===THREE.RGBFormat?t.RGB:e===THREE.RGBAFormat?t.RGBA:e===THREE.LuminanceFormat?t.LUMINANCE:e===THREE.LuminanceAlphaFormat?t.LUMINANCE_ALPHA:e===THREE.AddEquation?t.FUNC_ADD:e===THREE.SubtractEquation?t.FUNC_SUBTRACT:e===THREE.ReverseSubtractEquation?t.FUNC_REVERSE_SUBTRACT:e===THREE.ZeroFactor?t.ZERO:e===THREE.OneFactor?t.ONE:e===THREE.SrcColorFactor?t.SRC_COLOR:e===THREE.OneMinusSrcColorFactor?t.ONE_MINUS_SRC_COLOR:e===THREE.SrcAlphaFactor?t.SRC_ALPHA:e===THREE.OneMinusSrcAlphaFactor?t.ONE_MINUS_SRC_ALPHA:e===THREE.DstAlphaFactor?t.DST_ALPHA:e===THREE.OneMinusDstAlphaFactor?t.ONE_MINUS_DST_ALPHA:e===THREE.DstColorFactor?t.DST_COLOR:e===THREE.OneMinusDstColorFactor?t.ONE_MINUS_DST_COLOR:e===THREE.SrcAlphaSaturateFactor?t.SRC_ALPHA_SATURATE:0},n.paramToArrayStorage=function(t){switch(t){case THREE.UnsignedByteType:return Uint8Array;case THREE.ByteType:return Int8Array;case THREE.ShortType:return Int16Array;case THREE.UnsignedShortType:return Uint16Array;case THREE.IntType:return Int32Array;case THREE.UnsignedIntType:return Uint32Array;case THREE.FloatType:return Float32Array}},n.swizzleToEulerOrder=function(t){return t.map(function(t){return["","X","Y","Z"][t]}).join("")},n.transformComposer=function(){var t,e,r,i,o;return t=new THREE.Euler,r=new THREE.Quaternion,e=new THREE.Vector3,i=new THREE.Vector3,o=new THREE.Matrix4,function(s,a,u,h,l,c){return null==c&&(c="XYZ"),null!=a?(c instanceof Array&&(c=n.swizzleToEulerOrder(c)),t.setFromVector3(a,c),r.setFromEuler(t)):r.set(0,0,0,1),null!=u&&r.multiply(u),null!=s?e.copy(s):e.set(0,0,0),null!=h?i.copy(h):i.set(1,1,1),o.compose(e,r,i),null!=l&&o.multiplyMatrices(o,l),o}}},{}],179:[function(t,e,n){var r,i,o,s,a;o=function(t,e,n,r,i,o,s,a,u,h){var l,c,p,f,d,m,v,g,E,y;return null==h&&(h=!0),n||(n=10),r||(r=1),i||(i=10),o||(o=1),v=e-t,d=v/n,h?(r||(r=1),i||(i=10),m=r*Math.pow(i,Math.floor(Math.log(d/r)/Math.log(i))),p=i%2===0?[i/2,1,.5]:i%3===0?[i/3,1,1/3]:[1],E=function(){var t,e,n;for(n=[],t=0,e=p.length;e>t;t++)c=p[t],n.push(m*c);return n}(),l=1/0,g=E.reduce(function(t,e){var n;return c=e/d,n=Math.max(c,1/c),l>n?(l=n,e):t},m),g*=o,t=Math.ceil(t/g+ +!s)*g,e=(Math.floor(e/g)-+!a)*g,n=Math.ceil((e-t)/g),y=function(){var e,r,i;for(i=[],f=e=0,r=n;r>=0?r>=e:e>=r;f=r>=0?++e:--e)i.push(t+f*g);return i}(),u||(y=y.filter(function(t){return 0!==t})),y):(y=function(){var e,r,i;for(i=[],f=e=0,r=n;r>=0?r>=e:e>=r;f=r>=0?++e:--e)i.push(t+f*d);return i}(),s||y.shift(),a||y.pop(),u||(y=y.filter(function(t){return 0!==t})),y)},s=function(t,e,n,r,i,o,s,a,u,h){throw new Error("Log ticks not yet implemented.")},r=0,i=1,a=function(t,e,n,a,u,h,l,c,p,f,d){switch(t){case r:return o(e,n,a,u,h,l,c,p,f,d);case i:return s(e,n,a,u,h,l,c,p,f,d)}},n.make=a,n.linear=o,n.log=s},{}],180:[function(t,e,n){var r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b;for(r=[],l=0,i={},a=function(){return{id:l++,type:null,props:null,children:null,rendered:null,instance:null}},h=function(t){var e,n,i,o;for(t*=2,t=Math.max(0,r.length-t),o=[],e=n=0,i=t;i>=0?i>n:n>i;e=i>=0?++n:--n)o.push(r.push(a()));return o},u=function(t,e,n){var i;return i=r.length?r.pop():a(),i.type=null!=t?t:"div",i.props=null!=e?e:null,i.children=null!=n?n:null,i},g=function(t){var e,n,i,o;if(t.type&&(n=t.children,t.type=t.props=t.children=t.instance=null,r.push(t),null!=n))for(i=0,o=n.length;o>i;i++)e=n[i],g(e)},o=function(t,e,n,r,s){var a,h,l,c,p,f,d,v,g,E,T,x,w,R,H,M,S,k,A,C,P,L,z,O,D,F,U,B,N,V;if(null!=t){if(null==e)return m(t,r,s);if(t instanceof Node){if(F=t===e)return}else F=typeof t==typeof e&&null!==e&&null!==t&&t.type===e.type;if(F){if(t.instance=e.instance,B=(null!=(P=t.type)?P.isComponentClass:void 0)?t.type:i[t.type],A=null!=e?e.props:void 0,H=t.props,c=null!=(L=null!=e?e.children:void 0)?L:null,R=t.children,null!=H&&(H.children=R),null!=B){if(f=n._COMPONENT_DIRTY,null!=A!=(null!=H)&&(f=!0),c!==R&&(f=!0),null!=A&&null!=H){if(!f)for(E in A)H.hasOwnProperty(E)||(f=!0);if(!f)for(E in H)V=H[E],(C=A[E])!==V&&(f=!0)}if(f){p=e.instance,null==t.props&&(t.props={}),z=p.defaultProps;for(g in z)N=z[g],null==(a=t.props)[g]&&(a[g]=N);t.props.children=t.children,"function"==typeof p.willReceiveProps&&p.willReceiveProps(t.props),U=n._COMPONENT_FORCE||(null!=(O="function"==typeof p.shouldUpdate?p.shouldUpdate(t.props):void 0)?O:!0),U&&(M=p.getNextState(),"function"==typeof p.willUpdate&&p.willUpdate(t.props,M)),S=p.props,k=p.applyNextState(),p.props=t.props,p.children=t.children,U&&(t=t.rendered="function"==typeof p.render?p.render(u,t.props,t.children):void 0,o(t,e.rendered,n,r,s),"function"==typeof p.didUpdate&&p.didUpdate(S,k))}return}if(null!=A)for(E in A)H.hasOwnProperty(E)||b(n,E,A[E]);if(null!=H)for(E in H)V=H[E],(C=A[E])!==V&&"children"!==E&&y(n,E,V,C);if(null!=R)if("string"==(D=typeof R)||"number"===D)R!==c&&(n.textContent=R);else if(null!=R.type)o(R,c,n.childNodes[0],n,0);else if(l=n.childNodes,null!=c)for(d=v=0,x=R.length;x>v;d=++v)h=R[d],o(h,c[d],l[d],n,d);else for(d=T=0,w=R.length;w>T;d=++T)h=R[d],o(h,null,l[d],n,d);else null!=c&&(_(null,n),n.innerHTML="");return}return _(e.instance,n),n.remove(),m(t,r,s)}return null!=e?(_(e.instance,n),e.node.remove()):void 0},m=function(t,e,n){var r,o,s,a,h,l,c,p,f,d,v,g,E,_,b,T,x,w,R,H;if(null==n&&(n=0),w=(null!=(g=t.type)?g.isComponentClass:void 0)?t.type:i[t.type],t instanceof Node)v=t;else{if(null!=w){if(h=(null!=(E=t.type)?E.isComponentClass:void 0)?t.type:i[t.type],!h)return t=t.rendered=u("noscript"),v=m(t,e,n);t.instance=a=new h(e),null==t.props&&(t.props={}),_=a.defaultProps;for(p in _)R=_[p],null==(r=t.props)[p]&&(r[p]=R);return t.props.children=t.children,a.props=t.props,a.children=t.children,a.setState("function"==typeof a.getInitialState?a.getInitialState():void 0),"function"==typeof a.willMount&&a.willMount(),t=t.rendered="function"==typeof a.render?a.render(u,t.props,t.children):void 0,v=m(t,e,n),"function"==typeof a.didMount&&a.didMount(t),v._COMPONENT=a,v}if("string"==(b=typeof t)||"number"===b)v=document.createTextNode(t);else{v=document.createElement(t.type),T=t.props;for(f in T)H=T[f],y(v,f,H)}if(s=t.children,null!=s)if("string"==(x=typeof s)||"number"===x)v.textContent=s;else if(null!=s.type)m(s,v,0);else for(l=c=0,d=s.length;d>c;l=++c)o=s[l],m(o,v,l)}return e.insertBefore(v,e.childNodes[n]),v},_=function(t,e){var n,r,i,o,s,a;if(t){"function"==typeof t.willUnmount&&t.willUnmount();for(i in t)delete t[i]}for(s=e.childNodes,a=[],r=0,o=s.length;o>r;r++)n=s[r],_(n._COMPONENT,n),a.push(delete n._COMPONENT);return a},v=function(t){var e,n,r,i;if("undefined"==typeof document)return!0;if(null!=document.documentElement.style[t])return t;for(t=t[0].toUpperCase()+t.slice(1),i=["webkit","moz","ms","o"],e=0,n=i.length;n>e;e++)if(r=i[e],null!=document.documentElement.style[r+t])return r+t},d={},E=["transform"],c=0,f=E.length;f>c;c++)p=E[c],d[p]=v(p);y=function(t,e,n,r){var i,o,s;{if("style"!==e)return null!=t[e]?void(t[e]=n):void(t instanceof Node&&t.setAttribute(e,n));for(i in n)s=n[i],(null!=r?r[i]:void 0)!==s&&(t.style[null!=(o=d[i])?o:i]=s)}},b=function(t,e,n){var r,i,o;if("style"!==e)null!=t[e]&&(t[e]=void 0),t instanceof Node&&t.removeAttribute(e);else for(r in n)o=n[r],t.style[null!=(i=d[r])?i:r]=""},s=function(t){var e,n,r,i,o;r={willMount:"componentWillMount",didMount:"componentDidMount",willReceiveProps:"componentWillReceiveProps",shouldUpdate:"shouldComponentUpdate",willUpdate:"componentWillUpdate",didUpdate:"componentDidUpdate",willUnmount:"componentWillUnmount"};for(n in r)i=r[n],null==t[n]&&(t[n]=t[i]);return e=function(){function e(e,n,r,i){var o,s,a,u;this.props=null!=n?n:{},this.state=null!=r?r:null,this.children=null!=i?i:null,o=function(t,e){return"function"==typeof t?t.bind(e):t};for(s in t)u=t[s],this[s]=o(u,this);a=null,this.setState=function(t){null==a&&(a=t?null!=a?a:{}:null);for(s in t)u=t[s],a[s]=u;e._COMPONENT_DIRTY=!0},this.forceUpdate=function(){var t,n;for(e._COMPONENT_FORCE=e._COMPONENT_DIRTY=!0,t=e,n=[];t=t.parentNode;)t._COMPONENT?n.push(t._COMPONENT_FORCE=!0):n.push(void 0);return n},this.getNextState=function(){return a},this.applyNextState=function(){var t,n;return e._COMPONENT_FORCE=e._COMPONENT_DIRTY=!1,t=this.state,n=[null,a],a=n[0],this.state=n[1],t}}return e}(),e.isComponentClass=!0,e.prototype.defaultProps=null!=(o="function"==typeof t.getDefaultProps?t.getDefaultProps():void 0)?o:{},e},e.exports={element:u,recycle:g,apply:o,hint:h,Types:i,createClass:s}},{}],181:[function(t,e,n){var r;r=function(){function t(e,n){this.parent=null!=n?n:null,this.id=t.id(),this.nodes=[],e&&this.add(e)}return t.index=0,t.id=function(e){return++t.index},t.IN=0,t.OUT=1,t.prototype.inputs=function(){var t,e,n,r,i,o,s,a,u;for(e=[],a=this.nodes,t=0,r=a.length;r>t;t++)for(o=a[t],u=o.inputs,n=0,i=u.length;i>n;n++)s=u[n],null===s.input&&e.push(s);return e},t.prototype.outputs=function(){var t,e,n,r,i,o,s,a,u;for(s=[],a=this.nodes,t=0,n=a.length;n>t;t++)for(i=a[t],u=i.outputs,e=0,r=u.length;r>e;e++)o=u[e],0===o.output.length&&s.push(o);return s},t.prototype.getIn=function(t){var e;return function(){var n,r,i,o;for(i=this.inputs(),o=[],n=0,r=i.length;r>n;n++)e=i[n],e.name===t&&o.push(e);return o}.call(this)[0]},t.prototype.getOut=function(t){var e;return function(){var n,r,i,o;for(i=this.outputs(),o=[],n=0,r=i.length;r>n;n++)e=i[n],e.name===t&&o.push(e);return o}.call(this)[0]},t.prototype.add=function(t,e){var n,r,i;{if(!t.length){if(t.graph&&!e)throw new Error("Adding node to two graphs at once");return t.graph=this,this.nodes.push(t)}for(r=0,i=t.length;i>r;r++)n=t[r],this.add(n)}},t.prototype.remove=function(t,e){var n,r,i;{if(!t.length){if(t.graph!==this)throw new Error("Removing node from wrong graph.");return e||t.disconnect(),this.nodes.splice(this.nodes.indexOf(t),1),t.graph=null}for(r=0,i=t.length;i>r;r++)n=t[r],this.remove(n)}},t.prototype.adopt=function(t){var e,n,r;{if(!t.length)return t.graph.remove(t,!0),this.add(t,!0);for(n=0,r=t.length;r>n;n++)e=t[n],this.adopt(e)}},t}(),e.exports=r},{}],182:[function(t,e,n){n.Graph=t("./graph"),n.Node=t("./node"),n.Outlet=t("./outlet"),n.IN=n.Graph.IN,n.OUT=n.Graph.OUT},{"./graph":181,"./node":183,"./outlet":184}],183:[function(t,e,n){var r,i,o;r=t("./graph"),o=t("./outlet"),i=function(){function t(e,n){this.owner=e,this.graph=null,this.inputs=[],this.outputs=[],this.all=[],this.outlets=null,this.id=t.id(),this.setOutlets(n)}return t.index=0,t.id=function(e){return++t.index},t.prototype.getIn=function(t){var e;return function(){var n,r,i,o;for(i=this.inputs,o=[],n=0,r=i.length;r>n;n++)e=i[n],e.name===t&&o.push(e);return o}.call(this)[0]},t.prototype.getOut=function(t){var e;return function(){var n,r,i,o;for(i=this.outputs,o=[],n=0,r=i.length;r>n;n++)e=i[n],e.name===t&&o.push(e);return o}.call(this)[0]},t.prototype.get=function(t){return this.getIn(t)||this.getOut(t)},t.prototype.setOutlets=function(t){var e,n,r,i,s,a,u,h,l,c,p,f;if(null!=t){if(null==this.outlets){for(this.outlets={},r=0,u=t.length;u>r;r++)p=t[r],p instanceof o||(p=o.make(p)),this._add(p);return}for(n=function(t){return[t.name,t.inout,t.type].join("-")},c={},i=0,h=t.length;h>i;i++)p=t[i], -c[n(p)]=!0;f=this.outlets;for(a in f)p=f[a],a=n(p),c[a]?c[a]=p:this._remove(p);for(s=0,l=t.length;l>s;s++)p=t[s],e=c[n(p)],e instanceof o?this._morph(e,p):(p instanceof o||(p=o.make(p)),this._add(p))}return this.outlets},t.prototype.connect=function(t,e,n){var r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b;for(d={},s={},b=function(t){return _+"/"+t.hint},m=t.inputs,a=0,l=m.length;l>a;a++)r=m[a],(n||!r.input)&&(_=r.type,o=b(r),s[o]||(s[o]=r),d[_]=f=d[_]||[],f.push(r));for(y=this.outputs,y=y.filter(function(t){return!(e&&t.output.length)}),v=y.slice(),u=0,c=v.length;c>u;u++)E=v[u],_=E.type,o=b(E),i=d[_],(r=s[o])&&(E.connect(r),delete s[o],i.splice(i.indexOf(r),1),y.splice(y.indexOf(E),1));if(!y.length)return this;for(g=y.slice(),h=0,p=g.length;p>h;h++)E=g[h],_=E.type,i=d[_],i&&i.length&&E.connect(i.shift());return this},t.prototype.disconnect=function(t){var e,n,r,i,o,s,a;for(s=this.inputs,e=0,r=s.length;r>e;e++)o=s[e],o.disconnect();for(a=this.outputs,n=0,i=a.length;i>n;n++)o=a[n],o.disconnect();return this},t.prototype._key=function(t){return[t.name,t.inout].join("-")},t.prototype._add=function(t){var e;if(e=this._key(t),t.node)throw new Error("Adding outlet to two nodes at once.");if(this.outlets[e])throw new Error("Adding two identical outlets to same node. ("+e+")");return t.node=this,t.inout===r.IN&&this.inputs.push(t),t.inout===r.OUT&&this.outputs.push(t),this.all.push(t),this.outlets[e]=t},t.prototype._morph=function(t,e){var n;return n=this._key(e),delete this.outlets[n],t.morph(e),n=this._key(e),this.outlets[n]=e},t.prototype._remove=function(t){var e,n;if(n=this._key(t),e=t.inout,t.node!==this)throw new Error("Removing outlet from wrong node.");return t.disconnect(),t.node=null,delete this.outlets[n],t.inout===r.IN&&this.inputs.splice(this.inputs.indexOf(t),1),t.inout===r.OUT&&this.outputs.splice(this.outputs.indexOf(t),1),this.all.splice(this.all.indexOf(t),1),this},t}(),e.exports=i},{"./graph":181,"./outlet":184}],184:[function(t,e,n){var r,i;r=t("./graph"),i=function(){function t(e,n,r,i,o,s){this.inout=e,this.name=n,this.hint=r,this.type=i,this.meta=null!=o?o:{},this.id=s,null==this.hint&&(this.hint=t.hint(this.name)),this.node=null,this.input=null,this.output=[],null==this.id&&(this.id=t.id(this.hint))}return t.make=function(e,n){var r,i,o,s;if(null==n&&(n={}),i=n,null!=e.meta){o=e.meta;for(r in o)s=o[r],i[r]=s}return new t(e.inout,e.name,e.hint,e.type,i)},t.index=0,t.id=function(e){return"_io_"+ ++t.index+"_"+e},t.hint=function(t){return t=t.replace(/^_io_[0-9]+_/,""),t=t.replace(/_i_o$/,""),t=t.replace(/(In|Out|Inout|InOut)$/,"")},t.prototype.morph=function(t){return this.inout=t.inout,this.name=t.name,this.hint=t.hint,this.type=t.type,this.meta=t.meta},t.prototype.dupe=function(e){var n;return null==e&&(e=this.id),n=t.make(this),n.name=e,n},t.prototype.connect=function(t){if(this.inout===r.IN&&t.inout===r.OUT)return t.connect(this);if(this.inout!==r.OUT||t.inout!==r.IN)throw new Error("Can only connect out to in.");if(t.input!==this)return t.disconnect(),t.input=this,this.output.push(t)},t.prototype.disconnect=function(t){var e,n,r,i;if(this.input&&this.input.disconnect(this),this.output.length){if(!t){for(i=this.output,e=0,r=i.length;r>e;e++)t=i[e],t.input=null;return this.output=[]}if(n=this.output.indexOf(t),n>=0)return this.output.splice(n,1),t.input=null}},t}(),e.exports=i},{"./graph":181}],185:[function(t,e,n){var r,i,o,s,a,u;i=t("../graph"),a=t("../linker").Program,o=t("../linker").Layout,u=!1,r=function(){function t(){var t;null==this.namespace&&(this.namespace=a.entry()),this.node=new i.Node(this,null!=(t="function"==typeof this.makeOutlets?this.makeOutlets():void 0)?t:{})}return t.previous=function(t){var e;return null!=(e=t.input)?e.node.owner:void 0},t.prototype.refresh=function(){var t;return this.node.setOutlets(null!=(t="function"==typeof this.makeOutlets?this.makeOutlets():void 0)?t:{})},t.prototype.clone=function(){return new t},t.prototype.compile=function(t,e){var n;return n=new a(t,null!=e?e:a.entry(),this.node.graph),this.call(n,0),n.assemble()},t.prototype.link=function(t,e){var n,r;return r=this.compile(t,e),n=new o(t,this.node.graph),this._include(r,n,0),this["export"](n,0),n.link(r)},t.prototype.call=function(t,e){},t.prototype.callback=function(t,e,n,r,i){},t.prototype["export"]=function(t,e){},t.prototype._info=function(t){var e,n,r;return r=null!=(e=null!=(n=this.node.owner.snippet)?n._name:void 0)?e:this.node.owner.namespace,null!=t?r+="."+t:void 0},t.prototype._outlet=function(t,e){var n;return n=i.Outlet.make(t,e),n.meta.def=t,n},t.prototype._call=function(t,e,n){return e.call(this.node,t,n)},t.prototype._require=function(t,e){return e.require(this.node,t)},t.prototype._inputs=function(e,n,r){var i,o,s,a,u,h,l;for(u=e.main.signature,l=[],o=0,s=u.length;s>o;o++)i=u[o],a=this.node.get(i.name),l.push(null!=(h=t.previous(a))?h.call(n,r+1):void 0);return l},t.prototype._callback=function(t,e,n,r,i,o){return e.callback(this.node,t,n,r,i,o)},t.prototype._include=function(t,e,n){return e.include(this.node,t,n)},t.prototype._link=function(e,n,r){var i,o,a,h,l,c,p,f,d,m,v,g;for(u&&console.log("block::_link",this.toString(),e.namespace),d=e.symbols,g=[],a=0,l=d.length;l>a;a++){if(h=d[a],o=e.externals[h],p=this.node.get(o.name),!p)throw new s("External not found on "+this._info(o.name));if(null==p.meta.child){for(m=[p,p,null],c=m[0],f=m[1],i=m[2];!i&&f;)v=[p.meta.parent,f],f=v[0],p=v[1];if(i=t.previous(p),!i)throw new s("Missing connection on "+this._info(o.name));u&&console.log("callback -> ",this.toString(),o.name,p),i.callback(n,r+1,h,o,p.input),g.push(null!=i?i["export"](n,r+1):void 0)}}return g},t.prototype._trace=function(e,n,r){var i,o,s,a,h,l,c;for(u&&console.log("block::_trace",this.toString(),e.namespace),h=e.main.signature,c=[],o=0,s=h.length;s>o;o++)i=h[o],a=this.node.get(i.name),c.push(null!=(l=t.previous(a))?l["export"](n,r+1):void 0);return c},t}(),s=function(t){var e;return e=new Error(t),e.name="OutletError",e},s.prototype=new Error,e.exports=r},{"../graph":205,"../linker":210}],186:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;r=t("./block"),i=function(t){function e(t){this.snippet=t,this.namespace=this.snippet.namespace,e.__super__.constructor.apply(this,arguments)}return o(e,t),e.prototype.clone=function(){return new e(this.snippet)},e.prototype.makeOutlets=function(){var t,e,n,r,i,o,s;return r=this.snippet.main.signature,e=this.snippet.externals,s=this.snippet.symbols,o=function(){var t,e,n;for(n=[],t=0,e=r.length;e>t;t++)i=r[t],n.push(this._outlet(i,{callback:!1}));return n}.call(this),t=function(){var t,r,i;for(i=[],t=0,r=s.length;r>t;t++)n=s[t],i.push(this._outlet(e[n],{callback:!0}));return i}.call(this),o.concat(t)},e.prototype.call=function(t,e){return this._call(this.snippet,t,e),this._inputs(this.snippet,t,e)},e.prototype["export"]=function(t,e){return t.visit(this.namespace,e)?(this._link(this.snippet,t,e),this._trace(this.snippet,t,e)):void 0},e}(r),e.exports=i},{"./block":185}],187:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;o=t("../graph"),r=t("./block"),i=function(t){function e(t){this.graph=t,e.__super__.constructor.apply(this,arguments)}return s(e,t),e.prototype.refresh=function(){return e.__super__.refresh.apply(this,arguments),delete this.subroutine},e.prototype.clone=function(){return new e(this.graph)},e.prototype.makeOutlets=function(){var t,e,n,r,i,s,a,u,h,l,c,p;for(this.make(),u=[],n=[],h=[],t=function(t){return function(t,e){var n,r;return t.meta.callback?t.inout===o.IN?(r=t.dupe(),null==(n=r.meta).child&&(n.child=t),t.meta.parent=r,u.push(r)):void 0:e.push(t.type)}}(this),l=this.graph.inputs(),e=0,i=l.length;i>e;e++)a=l[e],t(a,n);for(c=this.graph.outputs(),r=0,s=c.length;s>r;r++)a=c[r],t(a,h);return n=n.join(","),h=h.join(","),p="("+n+")("+h+")",u.push({name:"callback",type:p,inout:o.OUT,meta:{callback:!0,def:this.subroutine.main}}),u},e.prototype.make=function(){return this.subroutine=this.graph.compile(this.namespace)},e.prototype["export"]=function(t,e){return t.visit(this.namespace,e)?(this._link(this.subroutine,t,e),this.graph["export"](t,e)):void 0},e.prototype.call=function(t,e){return this._require(this.subroutine,t,e)},e.prototype.callback=function(t,e,n,r,i){return this._include(this.subroutine,t,e),this._callback(this.subroutine,t,e,n,r,i)},e}(r),e.exports=i},{"../graph":205,"./block":185}],188:[function(t,e,n){n.Block=t("./block"),n.Call=t("./call"),n.Callback=t("./callback"),n.Isolate=t("./isolate"),n.Join=t("./join")},{"./block":185,"./call":186,"./callback":187,"./isolate":189,"./join":190}],189:[function(t,e,n){var r,i,o,s=function(t,e){function n(){this.constructor=t}for(var r in e)a.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},a={}.hasOwnProperty;i=t("../graph"),r=t("./block"),o=function(t){function e(t){this.graph=t,e.__super__.constructor.apply(this,arguments)}return s(e,t),e.prototype.refresh=function(){return e.__super__.refresh.apply(this,arguments),delete this.subroutine},e.prototype.clone=function(){return new e(this.graph)},e.prototype.makeOutlets=function(){var t,e,n,r,o,s,a,u,h,l,c,p,f,d,m;for(this.make(),l=[],d={},e={},c=["inputs","outputs"],r=0,s=c.length;s>r;r++)for(m=c[r],p=this.graph[m](),o=0,a=p.length;a>o;o++)h=p[o],u=void 0,"return"!==(f=h.hint)&&"callback"!==f||h.inout!==i.OUT||(u=h.hint),null!=d[u]&&(u=void 0),n=h.dupe(u),null==(t=n.meta).child&&(t.child=h),h.meta.parent=n,null!=u&&(d[u]=!0),e[h.name]=n,l.push(n);return l},e.prototype.make=function(){return this.subroutine=this.graph.compile(this.namespace)},e.prototype.call=function(t,e){return this._call(this.subroutine,t,e),this._inputs(this.subroutine,t,e)},e.prototype["export"]=function(t,e){return t.visit(this.namespace,e)?(this._link(this.subroutine,t,e),this._trace(this.subroutine,t,e),this.graph["export"](t,e)):void 0},e.prototype.callback=function(t,e,n,r,i){return i=i.meta.child,i.node.owner.callback(t,e,n,r,i)},e}(r),e.exports=o},{"../graph":205,"./block":185}],190:[function(t,e,n){var r,i,o=function(t,e){function n(){this.constructor=t}for(var r in e)s.call(e,r)&&(t[r]=e[r]);return n.prototype=e.prototype,t.prototype=new n,t.__super__=e.prototype,t},s={}.hasOwnProperty;r=t("./block"),i=function(t){function e(t){this.nodes=t,e.__super__.constructor.apply(this,arguments)}return o(e,t),e.prototype.clone=function(){return new e(this.nodes)},e.prototype.makeOutlets=function(){return[]},e.prototype.call=function(t,e){var n,r,i,o,s,a;for(s=this.nodes,a=[],r=0,i=s.length;i>r;r++)o=s[r],n=o.owner,a.push(n.call(t,e));return a},e.prototype["export"]=function(t,e){var n,r,i,o,s,a;for(s=this.nodes,a=[],r=0,i=s.length;i>r;r++)o=s[r],n=o.owner,a.push(n["export"](t,e));return a},e}(r),e.exports=i},{"./block":185}],191:[function(t,e,n){var r,i,o;o=t("./queue"),i=t("./hash"),r=function(t){var e,n;return e={},n=o(100),function(r){var o,s;return s=r.length>32?"##"+i(r).toString(16):r,o=n(s),null!=o&&delete e[o],null==e[s]&&(e[s]=t(r)),e[s].clone()}},e.exports=r},{"./hash":193,"./queue":197}],192:[function(t,e,n){var r,i,o,s,a;o=t("../graph").Graph,r=t("../block"),a=t("../visualize"),i=function(){function t(t,e,n){this.language=t,this.fetch=e,this.config=n,this.graph()}return t.prototype.pipe=function(e,n,r,i){return e instanceof t?this._concat(e):null!=e&&this._call(e,n,r,i),this},t.prototype.call=function(t,e,n,r){return this.pipe(t,e,n,r)},t.prototype.require=function(e,n,r,i){return e instanceof t?this._import(e):null!=e&&(this.callback(),this._call(e,n,r,i),this.end()),this},t.prototype["import"]=function(t,e,n,r){return this.require(t,e,n,r)},t.prototype.split=function(){return this._group("_combine",!0),this},t.prototype.fan=function(){return this._group("_combine",!1),this},t.prototype.isolate=function(){return this._group("_isolate"),this},t.prototype.callback=function(){return this._group("_callback"),this},t.prototype.next=function(){return this._next(),this},t.prototype.pass=function(){var t;return t=this._stack[2].end,this.end(),this._state.end=this._state.end.concat(t),this},t.prototype.end=function(){var t,e,n,r;return n=this._exit(),r=n[0],t=n[1],e=r.op,this[e]&&this[e](r,t),this},t.prototype.join=function(){return this.end()},t.prototype.graph=function(){for(var t,e;(null!=(e=this._stack)?e.length:void 0)>1;)this.end();return this._graph&&this._tail(this._state,this._graph),t=this._graph,this._graph=new o,this._state=new s,this._stack=[this._state],t},t.prototype.compile=function(t){return null==t&&(t="main"),this.graph().compile(t)},t.prototype.link=function(t){return null==t&&(t="main"),this.graph().link(t)},t.prototype.serialize=function(){return a.serialize(this._graph)},t.prototype.empty=function(){return 0===this._graph.nodes.length},t.prototype._concat=function(t){var e,n,i;if(0===t._state.nodes.length)return this;this._tail(t._state,t._graph);try{e=new r.Isolate(t._graph)}catch(i){throw n=i,this.config.autoInspect&&a.inspect(n,this._graph,t),n}return this._auto(e),this},t.prototype._import=function(t){var e,n,i;if(0===t._state.nodes.length)throw"Can't import empty callback";this._tail(t._state,t._graph);try{e=new r.Callback(t._graph)}catch(i){throw n=i,this.config.autoInspect&&a.inspect(n,this._graph,t),n}return this._auto(e),this},t.prototype._combine=function(t,e){var n,r,i,o,s,a,u,h;for(a=t.start,r=0,o=a.length;o>r;r++)for(h=a[r],u=e.end,i=0,s=u.length;s>i;i++)n=u[i],n.connect(h,t.multi);return e.end=t.end,e.nodes=e.nodes.concat(t.nodes)},t.prototype._isolate=function(t,e){var n,i,o,s;if(t.nodes.length){s=this._subgraph(t),this._tail(t,s);try{n=new r.Isolate(s)}catch(o){throw i=o,this.config.autoInspect&&a.inspect(i,this._graph,s),i}return this._auto(n)}},t.prototype._callback=function(t,e){var n,i,o,s;if(t.nodes.length){s=this._subgraph(t),this._tail(t,s);try{n=new r.Callback(s)}catch(o){throw i=o,this.config.autoInspect&&a.inspect(i,this._graph,s),i}return this._auto(n)}},t.prototype._call=function(t,e,n,i){var o,s;return s=this.fetch(t),s.bind(this.config,e,n,i),o=new r.Call(s),this._auto(o)},t.prototype._subgraph=function(t){var e;return e=new o(null,this._graph),e.adopt(t.nodes),e},t.prototype._tail=function(t,e){var n;if(n=t.end.concat(t.tail),n=n.filter(function(t,e){return n.indexOf(t)===e}),n.length>1&&(n=new r.Join(n),n=[n.node],this._graph.add(n)),e.tail=n[0],t.end=n,t.tail=[],!e.tail)throw new Error("Cannot finalize empty graph");return e.compile=function(t){return function(n){var r,i;null==n&&(n="main");try{return e.tail.owner.compile(t.language,n)}catch(i){throw r=i,t.config.autoInspect&&e.inspect(r),r}}}(this),e.link=function(t){return function(n){var r,i;null==n&&(n="main");try{return e.tail.owner.link(t.language,n)}catch(i){throw r=i,t.config.autoInspect&&e.inspect(r),r}}}(this),e["export"]=function(t){return function(t,n){return e.tail.owner["export"](t,n)}}(this),e.inspect=function(t){return null==t&&(t=null),a.inspect(t,e)}},t.prototype._group=function(t,e){return this._push(t,e),this._push(),this},t.prototype._next=function(){var t;return t=this._pop(),this._state.start=this._state.start.concat(t.start),this._state.end=this._state.end.concat(t.end),this._state.nodes=this._state.nodes.concat(t.nodes),this._state.tail=this._state.tail.concat(t.tail),this._push()},t.prototype._exit=function(){return this._next(),this._pop(),[this._pop(),this._state]},t.prototype._push=function(t,e){return this._stack.unshift(new s(t,e)),this._state=this._stack[0]},t.prototype._pop=function(){var t;return this._state=this._stack[1],null==this._state&&(this._state=new s),null!=(t=this._stack.shift())?t:new s},t.prototype._auto=function(t){return t.node.inputs.length?this._append(t):this._insert(t)},t.prototype._append=function(t){var e,n,r,i,o;for(i=t.node,this._graph.add(i),o=this._state.end,n=0,r=o.length;r>n;n++)e=o[n],e.connect(i);return this._state.start.length||(this._state.start=[i]),this._state.end=[i],this._state.nodes.push(i),i.outputs.length?void 0:this._state.tail.push(i)},t.prototype._prepend=function(t){var e,n,r,i,o;for(r=t.node,this._graph.add(r),i=this._state.start,e=0,n=i.length;n>e;e++)o=i[e],r.connect(o);return this._state.end.length||(this._state.end=[r]),this._state.start=[r],this._state.nodes.push(r),r.outputs.length?void 0:this._state.tail.push(r)},t.prototype._insert=function(t){var e;return e=t.node,this._graph.add(e),this._state.start.push(e),this._state.end.push(e),this._state.nodes.push(e),e.outputs.length?void 0:this._state.tail.push(e)},t}(),s=function(){function t(t,e,n,r,i,o){this.op=null!=t?t:null,this.multi=null!=e?e:!1,this.start=null!=n?n:[],this.end=null!=r?r:[],this.nodes=null!=i?i:[],this.tail=null!=o?o:[]}return t}(),e.exports=i},{"../block":188,"../graph":205,"../visualize":216}],193:[function(t,e,n){var r,i,o,s,a,u,h,l;r=3432918353,i=461845907,o=3864292196,s=2246822507,a=3266489909,h=function(t,e){var n,r,i,o;return n=t>>>16&65535,r=65535&t,i=e>>>16&65535,o=65535&e,r*o+(n*o+r*i<<16>>>0)|0},null!=Math.imul&&(l=Math.imul(4294967295,5),-5===l&&(h=Math.imul)),u=function(t){var e,n,u,l,c,p;for(c=t.length,l=Math.floor(c/2),u=e=0,p=function(){return t.charCodeAt(u++)},n=function(t,n){var s;return s=t|n<<16,s^=s<<9,s=h(s,r),s=s<<15|s>>>17,s=h(s,i),e^=s,e=e<<13|e>>>19,e=h(e,5),e=e+o|0};l--;)n(p(),p());return 1&c&&n(p(),0),e^=c,e^=e>>>16,e=h(e,s),e^=e>>>13,e=h(e,a),e^=e>>>16},e.exports=u},{}],194:[function(t,e,n){n.Factory=t("./factory"),n.Material=t("./material"),n.library=t("./library"),n.cache=t("./cache"),n.queue=t("./queue"),n.hash=t("./hash")},{"./cache":191,"./factory":192,"./hash":193,"./library":195,"./material":196,"./queue":197}],195:[function(t,e,n){var r;r=function(t,e,n){var r,i,o,s;return r=null,s={},null!=e&&("function"==typeof e?r=function(r){return n(t,r,e(r))}:"object"==typeof e&&(r=function(r){if(null==e[r])throw new Error("Unknown snippet `"+r+"`");return n(t,r,e[r])})),o=function(e){return n(t,"",e)},null==r?o:(i=function(t){return t.match(/[{;]/)?o(t):(s[t]=!0,r(t))},i.used=function(t){return null==t&&(t=s),s=t},i)},e.exports=r},{}],196:[function(t,e,n){var r,i,o,s;o=!1,i=t("../visualize"),s=function(){var t;return t=+new Date,function(e){var n;return n=+new Date-t,console.log(e,n+" ms"),n}},r=function(){function t(t,e){this.vertex=t,this.fragment=e,o&&(this.tock=s())}return t.prototype.build=function(t){return this.link(t)},t.prototype.link=function(t){var e,n,r,s,a,u,h,l,c,p,f,d,m,v;for(null==t&&(t={}),f={},m={},e={},v=this.vertex.link("main"),n=this.fragment.link("main"),u=[v,n],r=0,a=u.length;a>r;r++){p=u[r],h=p.uniforms;for(s in h)d=h[s],f[s]=d;l=p.varyings;for(s in l)d=l[s],m[s]=d;c=p.attributes;for(s in c)d=c[s],e[s]=d}return t.vertexShader=v.code,t.vertexGraph=v.graph,t.fragmentShader=n.code,t.fragmentGraph=n.graph,t.attributes=e,t.uniforms=f,t.varyings=m,t.inspect=function(){return i.inspect("Vertex Shader",v,"Fragment Shader",n.graph)},o&&this.tock("Material build"),t},t.prototype.inspect=function(){return i.inspect("Vertex Shader",this.vertex,"Fragment Shader",this.fragment.graph)},t}(),e.exports=r},{"../visualize":216}],197:[function(t,e,n){var r;r=function(t){var e,n,r,i,o,s;return null==t&&(t=100),i={},r=null,s=null,n=0,e=function(t){return t.prev=null,t.next=r,null!=r&&(r.prev=t),r=t,null==s?s=t:void 0},o=function(t){var e,n;return n=t.prev,e=t.next,null!=n&&(n.next=e),null!=e&&(e.prev=n),r===t&&(r=e),s===t?s=n:void 0},function(a){var u,h;return(h=i[a]&&h!==r)?(o(h),e(h)):(n===t?(u=s.key,o(s),delete i[u]):n++,h={next:r,prev:null,key:a},e(h),i[a]=h),u}},e.exports=r},{}],198:[function(t,e,n){var r,i,o,s;r=function(t){var e,n,r,s,a;return n=t.ast,r=t.code,a=t.signatures,s=i(a),e=o(r,s),[a,e]},s=function(){var t;return t=+new Date,function(e){var n;return n=+new Date-t,console.log(e,n+" ms"),n}},i=function(t){var e,n,r,i,o,s,a,u,h,l;for(s={},h=function(t){return s[t.name]=!0},h(t.main),a=["external","internal","varying","uniform","attribute"],e=0,i=a.length;i>e;e++)for(r=a[e],u=t[r],n=0,o=u.length;o>n;n++)l=u[n],h(l);return s},o=function(t,e){var n,r;return r=new RegExp("\\b("+function(){var t;t=[];for(n in e)t.push(n);return t}().join("|")+")\\b","g"),t=t.replace(/\/\/[^\n]*/g,""),t=t.replace(/\/\*([^*]|\*[^\/])*\*\//g,""),function(i,o,s){var a,u,h,l;null==i&&(i=""),null==o&&(o={}),null==s&&(s={}),h={};for(n in e)h[n]=null!=o[n]?n:i+n;return a=t.replace(r,function(t){return h[t]}),u=function(){var t;t=[];for(n in s)l=s[n],t.push("#define "+n+" "+l);return t}(),u.length&&u.push(""),u.join("\n")+a}},e.exports=r},{}],199:[function(t,e,n){e.exports={SHADOW_ARG:"_i_o",RETURN_ARG:"return"}},{}],200:[function(t,e,n){var r,i,o,s,a,u,h;e.exports=i={},i["in"]=0,i.out=1,i.inout=2,s=function(t){return t.token.data},i.node=function(t){var e,n;return"function"===(null!=(e=t.children[5])?e.type:void 0)?i["function"](t):"keyword"===(null!=(n=t.token)?n.type:void 0)?i.external(t):void 0},i.external=function(t){var e,n,r,i,o,a,u,h,l,c,p,f,d;for(e=t.children,p=s(e[1]),f=s(e[3]),d=s(e[4]),a=e[5],"attribute"!==p&&"uniform"!==p&&"varying"!==p&&(p="global"),h=[],c=a.children,n=i=0,o=c.length;o>i;n=++i)e=c[n],"ident"===e.type&&(r=s(e),u=a.children[n+1],l="quantifier"===(null!=u?u.type:void 0),h.push({decl:"external",storage:p,type:d,ident:r,quant:!!l,count:l}));return h},i["function"]=function(t){var e,n,r,o,a,u,h,l,c,p;return r=t.children,l=s(r[1]),c=s(r[3]),p=s(r[4]),u=r[5],h=s(u.children[0]),e=u.children[1],n=u.children[2],a=function(){var t,n,r,s;for(r=e.children,s=[],t=0,n=r.length;n>t;t++)o=r[t],s.push(i.argument(o));return s}(),[{decl:"function",storage:l,type:p,ident:h,body:!!n,args:a}]},i.argument=function(t){var e,n,r,i,o,a,u,h;return e=t.children,u=s(e[1]),i=s(e[2]),h=s(e[4]),o=e[5],r=s(o.children[0]),a=o.children[1],n=a?a.children[0].token.data:void 0,{decl:"argument",storage:u,inout:i,type:h,ident:r,quant:!!a,count:n}},i.param=function(t,e,n,r,o){var s,a,u;return a=[],null!=e&&a.push(e),null!=n&&a.push(n),a.push(""),a=a.join(" "),u=r?"["+o+"]":"",""!==t&&(t+=" "),s=function(e,n){return(n?t:"")+(""+a+e+u)},s.split=function(t){return i.param(t,e,n,r,o)},s},h="undefined"!=typeof window,u=h&&!!window.THREE,o={"int":0,"float":0,vec2:u?THREE.Vector2:null,vec3:u?THREE.Vector3:null,vec4:u?THREE.Vector4:null,mat2:null,mat3:u?THREE.Matrix3:null,mat4:u?THREE.Matrix4:null,sampler2D:0,samplerCube:0},a={"int":"i","float":"f",vec2:"v2",vec3:"v3",vec4:"v4",mat2:"m2",mat3:"m3",mat4:"m4",sampler2D:"t",samplerCube:"t"},i.type=function(t,e,n,s,u,h){var l,c,p,f,d,m,v;return l={"in":i["in"],out:i.out,inout:i.inout},d={"const":"const"},m=a[e],n&&(m+="v"),v=o[e],(null!=v?v.call:void 0)&&(v=new v),n&&(v=[v]),c=null!=(f=l[u])?f:l["in"],h=d[h],p=i.param(u,h,e,n,s),new r(t,m,e,p,v,c)},r=function(){function t(t,e,n,r,i,o,s){this.name=t,this.type=e,this.spec=n,this.param=r,this.value=i,this.inout=o,this.meta=s}return t.prototype.split=function(){var e,n,r,o;return r=null!=this.meta.shadowed,e=r?"in":"out",n=r?i["in"]:i.out,o=this.param.split(e),new t(this.name,this.type,this.spec,o,this.value,n)},t.prototype.copy=function(e,n){var r;return r=new t(null!=e?e:this.name,this.type,this.spec,this.param,this.value,this.inout,n)},t}()},{}],201:[function(t,e,n){var r,i,o;i=t("../graph"),r=t("./constants"),e.exports=o={unshadow:function(t){var e;return e=t.replace(r.SHADOW_ARG,""),e!==t?e:null},lines:function(t){return t.join("\n")},list:function(t){return t.join(", ")},statements:function(t){return t.join(";\n")},body:function(t){return{entry:t,type:"void",params:[],signature:[],"return":"",vars:{},calls:[],post:[],chain:{}}},define:function(t,e){return"#define "+t+" "+e},"function":function(t,e,n,r,i){return t+" "+e+"("+n+") {\n"+r+i+"}"},invoke:function(t,e,n){return t=t?t+" = ":"",n=o.list(n)," "+t+e+"("+n+")"},same:function(t,e){var n,i,o,s,a;for(o=s=0,a=t.length;a>s;o=++s){if(n=t[o],i=e[o],!i)return!1;if(n.type!==i.type)return!1;if(n.name===r.RETURN_ARG!=(i.name===r.RETURN_ARG))return!1}return!0},call:function(t,e,n,i,s){var a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w;for(u=[],T="",x=1,f=0,d=i.length;d>f;f++){if(a=i[f],v=a.name,h=l=t(v),y=null,m=null,g=!1,c=a.inout,p=v===r.RETURN_ARG,(w=null!=(_=a.meta)?_.shadowed:void 0)&&(y=t(w),y&&(s.vars[y]=" "+a.param(y),s.calls.push(" "+y+" = "+l),e(w)?m={shadowed:y}:a=a.split())),(w=null!=(b=a.meta)?b.shadow:void 0)&&(y=t(w))){if(e(w)){m={shadow:y};continue}a=a.split(),g=!0}p?T=l:g||u.push(null!=y?y:l),e(v)?(E="push",p?""===s["return"]?(E="unshift",h=v,s.type=a.spec,s["return"]=" return "+l,s.vars[l]=" "+a.param(l)):(s.vars[l]=" "+a.param(l),s.params.push(a.param(l,!0))):s.params.push(a.param(l,!0)),a=a.copy(h,m),s.signature[E](a)):s.vars[l]=" "+a.param(l)}return s.calls.push(o.invoke(T,n,u))},build:function(t,e){var n,r,i,s,a,u,h,l,c,p,f;return a=t.entry,i=null,e&&1===e.length&&"main"!==a&&(n=t,r=e[0].module,o.same(t.signature,r.main.signature)&&(i=o.define(a,r.entry))),null==i&&(f=function(){var e,n;e=t.vars,n=[];for(p in e)s=e[p],n.push(s);return n}(),e=t.calls,h=t.post,u=t.params,c=t.type,l=t["return"],e=e.concat(h),""!==l&&e.push(l),e.push(""),f.length?(f.push(""),f=o.statements(f)+"\n"):f="",e=o.statements(e),u=o.list(u),i=o["function"](c,a,u,f,e)),{signature:t.signature,code:i,name:a}},links:function(t){var e,n,r,i;for(i={defs:[],bodies:[]},e=0,r=t.length;r>e;e++)n=t[e],o.link(n,i);return i.defs=o.lines(i.defs),i.bodies=o.statements(i.bodies),""===i.defs&&delete i.defs,""===i.bodies&&delete i.bodies,i},link:function(t){return function(t,e){var n,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S;if(y=t.module,b=t.name,l=t.external,g=y.main,h=y.entry,o.same(g.signature,l.signature))return e.defs.push(o.define(b,h));for(p=[],w=[],E={},M=[y.namespace,r.RETURN_ARG].join(""),R=l.signature,f=0,d=R.length;d>f;f++)u=R[f],v=u.inout===i.IN?p:w,v.push(u);for(H=g.signature,_=0,m=H.length;m>_;_++)u=H[_],v=u.inout===i.IN?p:w,T=v.shift(),a=T.name,a===r.RETURN_ARG&&(a=M),E[u.name]=a;return s=function(t){return E[t]},n=function(){return!0},c=o.body(),o.call(s,n,h,g.signature,c),c.entry=h,E={"return":M},s=function(t){var e;return null!=(e=E[t])?e:t},x=o.body(),S=o.call(s,n,h,l.signature,x),x.calls=c.calls,x.entry=b,e.bodies.push(o.build(c).code.split(" {")[0]),e.bodies.push(o.build(x).code)}}(this),defuse:function(t){var e,n,r,i,o,s,a,u,h,l,c,p,f,d,m;for(f=/([A-Za-z0-9_]+\s+)?[A-Za-z0-9_]+\s+[A-Za-z0-9_]+\s*\([^)]*\)\s*;\s*/gm,m=function(t){return t.replace(f,function(t){return""})},n=t.split(/(?=[{}])/g),l=0,o=a=0,u=n.length;u>a;o=++a){switch(e=n[o],e[0]){case"{":l++;break;case"}":l--}if(0===l){for(r=e.split(/^[ \t]*#/m),s=p=0,h=r.length;h>p;s=++p)c=r[s],s>0?(c=c.split(/\n/),i=c.shift(),d=c.join("\n"),r[s]=[i,m(d)].join("\n")):r[s]=m(c);n[o]=r.join("#")}}return t=n.join("")},dedupe:function(t){var e,n;return e={},n=/((attribute|uniform|varying)\s+)[A-Za-z0-9_]+\s+([A-Za-z0-9_]+)\s*(\[[^\]]*\]\s*)?;\s*/gm,t.replace(n,function(t,n,r,i,o){return e[i]?"":(e[i]=!0,t)})},hoist:function(t){var e,n,r,i,o,s,a,u;for(u=/^#define ([^ ]+ _pg_[0-9]+_|_pg_[0-9]+_ [^ ]+)$/,o=t.split(/\n/g),e=[],a=[],n=0,r=o.length;r>n;n++)i=o[n],s=i.match(u)?e:a,s.push(i);return e.concat(a).join("\n")}}},{"../graph":205,"./constants":199}],202:[function(t,e,n){var r,i,o,s,a;for(n.compile=t("./compile"),n.parse=t("./parse"),n.generate=t("./generate"),s=t("./constants"),a=r=0,o=s.length;o>r;a=++r)i=s[a],n[i]=a},{"./compile":198,"./constants":199,"./generate":201,"./parse":203}],203:[function(t,e,n){var r,i,o,s,a,u,h,l,c,p,f,d,m,v;m=t("../../vendor/glsl-tokenizer"),c=t("../../vendor/glsl-parser"),s=t("./decl"),r=t("./constants"),o=!1,h=function(t,e){var n,r;return n=l(t,e),r=p(n,e)},l=function(t,e){var n,r,i,s,a,u,h,l,p,f,v;o&&(v=d());try{p=m().process(c(),e),f=p[0],n=f[0],a=p[1]}catch(s){r=s,a=[{message:r}]}if(o&&v("GLSL Tokenize & Parse"),u=function(t){var e,n;return t=t.split("\n"),e=(""+t.length).length,n=function(t){return(t=""+t).lengthh;h++)i=a[h],console.error(t+" -",i.message);throw new Error("GLSL parse error")}return n},p=function(t,e){var n,r,s,h,l,c,p;return o&&(p=d()),c=[],v(u,i(c),t,""),h=f(c),s=h[0],r=h[1],n=h[2],l=a(s,r,n),o&&p("GLSL AST"),{ast:t,code:e,signatures:l}},u=function(t,e){switch(t.type){case"decl":return e(s.node(t)),!1}return!0},i=function(t){return function(e){var n,r,i,o;if(null!=e){for(o=[],n=0,r=e.length;r>n;n++)i=e[n],o.push(t.push(i));return o}}},f=function(t){var e,n,r,i,o,s,a,u,h;for(a=null,i=[],n=[],u={},r=!1,o=0,s=t.length;s>o;o++)h=t[o],h.body?(u[h.ident]&&(n=function(){var t,r,i;for(i=[],t=0,r=n.length;r>t;t++)e=n[t],e.ident!==h.ident&&i.push(e);return i}(),delete u[h.ident]),i.push(h),"main"===h.ident?(a=h,r=!0):r||(a=h)):"global"===h.storage?i.push(h):(n.push(h),u[h.ident]=!0);return[a,i,n]},a=function(t,e,n){var i,o,a,u,h,l,c,p,f;for(p={uniform:[],attribute:[],varying:[],external:[],internal:[],global:[],main:null},o=function(t){return s.type(t.ident,t.type,t.quant,t.count,t.inout,t.storage)},a=function(t,e){var n,i,a,u,h,l,c,p,f,d,m;for(d=function(){var e,n,r,s;for(r=t.args,s=[],e=0,n=r.length;n>e;e++)i=r[e],s.push(o(i));return s}(),c=0,p=d.length;p>c;c++)u=d[c],u.inout===s.inout&&(n=u,a=u.copy(),n.inout=s["in"],a.inout=s.out,a.meta={shadow:n.name},a.name+=r.SHADOW_ARG,n.meta={shadowed:a.name},d.push(a));return"void"!==t.type&&d.unshift(s.type(r.RETURN_ARG,t.type,!1,"","out")),l=function(){var t,e,n;for(n=[],t=0,e=d.length;e>t;t++)u=d[t],u.inout===s["in"]&&n.push(u.type);return n}().join(","),f=function(){var t,e,n;for(n=[],t=0,e=d.length;e>t;t++)u=d[t],u.inout===s.out&&n.push(u.type);return n}().join(","),m="("+l+")("+f+")",h={name:t.ident,type:m,signature:d,inout:e,spec:t.type}},p.main=a(t,s.out),u=0,l=e.length;l>u;u++)f=e[u],p.internal.push({name:f.ident});for(h=0,c=n.length;c>h;h++)switch(f=n[h],f.decl){case"external":i=o(f),p[f.storage].push(i);break;case"function":i=a(f,s["in"]),p.external.push(i)}return p},o=!1,v=function(t,e,n,r){var i,s,a,u,h,l,c,p;if(o&&console.log(r,n.type,null!=(l=n.token)?l.data:void 0,null!=(c=n.token)?c.type:void 0),h=t(n,e))for(p=n.children,s=a=0,u=p.length;u>a;s=++a)i=p[s],v(t,e,i,r+" ",o);return null},d=function(){var t;return t=+new Date,function(e){var n;return n=+new Date-t,console.log(e,n+" ms"),n}},e.exports=v,e.exports=h},{"../../vendor/glsl-parser":219,"../../vendor/glsl-tokenizer":223,"./constants":199,"./decl":200}],204:[function(t,e,n){var r;r=function(){function t(e,n){this.parent=null!=n?n:null,this.id=t.id(),this.nodes=[],e&&this.add(e)}return t.index=0,t.id=function(e){return++t.index},t.IN=0,t.OUT=1,t.prototype.inputs=function(){var t,e,n,r,i,o,s,a,u;for(e=[],a=this.nodes,t=0,r=a.length;r>t;t++)for(o=a[t],u=o.inputs,n=0,i=u.length;i>n;n++)s=u[n],null===s.input&&e.push(s);return e},t.prototype.outputs=function(){var t,e,n,r,i,o,s,a,u;for(s=[],a=this.nodes,t=0,n=a.length;n>t;t++)for(i=a[t],u=i.outputs,e=0,r=u.length;r>e;e++)o=u[e],0===o.output.length&&s.push(o);return s},t.prototype.getIn=function(t){var e;return function(){var n,r,i,o;for(i=this.inputs(),o=[],n=0,r=i.length;r>n;n++)e=i[n],e.name===t&&o.push(e);return o}.call(this)[0]},t.prototype.getOut=function(t){var e;return function(){var n,r,i,o;for(i=this.outputs(),o=[],n=0,r=i.length;r>n;n++)e=i[n],e.name===t&&o.push(e);return o}.call(this)[0]},t.prototype.add=function(t,e){var n,r,i;{if(!t.length){if(t.graph&&!e)throw new Error("Adding node to two graphs at once");return t.graph=this,this.nodes.push(t)}for(r=0,i=t.length;i>r;r++)n=t[r],this.add(n)}},t.prototype.remove=function(t,e){var n,r,i;{if(!t.length){if(t.graph!==this)throw new Error("Removing node from wrong graph.");return e||t.disconnect(),this.nodes.splice(this.nodes.indexOf(t),1),t.graph=null}for(r=0,i=t.length;i>r;r++)n=t[r],this.remove(n)} -},t.prototype.adopt=function(t){var e,n,r;{if(!t.length)return t.graph.remove(t,!0),this.add(t,!0);for(n=0,r=t.length;r>n;n++)e=t[n],this.adopt(e)}},t}(),e.exports=r},{}],205:[function(t,e,n){n.Graph=t("./graph"),n.Node=t("./node"),n.Outlet=t("./outlet"),n.IN=n.Graph.IN,n.OUT=n.Graph.OUT},{"./graph":204,"./node":206,"./outlet":207}],206:[function(t,e,n){var r,i,o;r=t("./graph"),o=t("./outlet"),i=function(){function t(e,n){this.owner=e,this.graph=null,this.inputs=[],this.outputs=[],this.all=[],this.outlets=null,this.id=t.id(),this.setOutlets(n)}return t.index=0,t.id=function(e){return++t.index},t.prototype.getIn=function(t){var e;return function(){var n,r,i,o;for(i=this.inputs,o=[],n=0,r=i.length;r>n;n++)e=i[n],e.name===t&&o.push(e);return o}.call(this)[0]},t.prototype.getOut=function(t){var e;return function(){var n,r,i,o;for(i=this.outputs,o=[],n=0,r=i.length;r>n;n++)e=i[n],e.name===t&&o.push(e);return o}.call(this)[0]},t.prototype.get=function(t){return this.getIn(t)||this.getOut(t)},t.prototype.setOutlets=function(t){var e,n,r,i,s,a,u,h,l,c,p,f;if(null!=t){if(null==this.outlets){for(this.outlets={},r=0,u=t.length;u>r;r++)p=t[r],p instanceof o||(p=o.make(p)),this._add(p);return}for(n=function(t){return[t.name,t.inout,t.type].join("-")},c={},i=0,h=t.length;h>i;i++)p=t[i],c[n(p)]=!0;f=this.outlets;for(a in f)p=f[a],a=n(p),c[a]?c[a]=p:this._remove(p);for(s=0,l=t.length;l>s;s++)p=t[s],e=c[n(p)],e instanceof o?this._morph(e,p):(p instanceof o||(p=o.make(p)),this._add(p))}return this.outlets},t.prototype.connect=function(t,e,n){var r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b;for(d={},s={},b=function(t){return _+"/"+t.hint},m=t.inputs,a=0,l=m.length;l>a;a++)r=m[a],(n||!r.input)&&(_=r.type,o=b(r),s[o]||(s[o]=r),d[_]=f=d[_]||[],f.push(r));for(y=this.outputs,y=y.filter(function(t){return!(e&&t.output.length)}),v=y.slice(),u=0,c=v.length;c>u;u++)E=v[u],_=E.type,o=b(E),i=d[_],(r=s[o])&&(E.connect(r),delete s[o],i.splice(i.indexOf(r),1),y.splice(y.indexOf(E),1));if(!y.length)return this;for(g=y.slice(),h=0,p=g.length;p>h;h++)E=g[h],_=E.type,i=d[_],i&&i.length&&E.connect(i.shift());return this},t.prototype.disconnect=function(t){var e,n,r,i,o,s,a;for(s=this.inputs,e=0,r=s.length;r>e;e++)o=s[e],o.disconnect();for(a=this.outputs,n=0,i=a.length;i>n;n++)o=a[n],o.disconnect();return this},t.prototype._key=function(t){return[t.name,t.inout].join("-")},t.prototype._add=function(t){var e;if(e=this._key(t),t.node)throw new Error("Adding outlet to two nodes at once.");if(this.outlets[e])throw new Error("Adding two identical outlets to same node. ("+e+")");return t.node=this,t.inout===r.IN&&this.inputs.push(t),t.inout===r.OUT&&this.outputs.push(t),this.all.push(t),this.outlets[e]=t},t.prototype._morph=function(t,e){var n;return n=this._key(e),delete this.outlets[n],t.morph(e),n=this._key(e),this.outlets[n]=e},t.prototype._remove=function(t){var e,n;if(n=this._key(t),e=t.inout,t.node!==this)throw new Error("Removing outlet from wrong node.");return t.disconnect(),t.node=null,delete this.outlets[n],t.inout===r.IN&&this.inputs.splice(this.inputs.indexOf(t),1),t.inout===r.OUT&&this.outputs.splice(this.outputs.indexOf(t),1),this.all.splice(this.all.indexOf(t),1),this},t}(),e.exports=i},{"./graph":204,"./outlet":207}],207:[function(t,e,n){var r,i;r=t("./graph"),i=function(){function t(e,n,r,i,o,s){this.inout=e,this.name=n,this.hint=r,this.type=i,this.meta=null!=o?o:{},this.id=s,null==this.hint&&(this.hint=t.hint(this.name)),this.node=null,this.input=null,this.output=[],null==this.id&&(this.id=t.id(this.hint))}return t.make=function(e,n){var r,i,o,s;if(null==n&&(n={}),i=n,null!=e.meta){o=e.meta;for(r in o)s=o[r],i[r]=s}return new t(e.inout,e.name,e.hint,e.type,i)},t.index=0,t.id=function(e){return"_io_"+ ++t.index+"_"+e},t.hint=function(t){return t=t.replace(/^_io_[0-9]+_/,""),t=t.replace(/_i_o$/,""),t=t.replace(/(In|Out|Inout|InOut)$/,"")},t.prototype.morph=function(t){return this.inout=t.inout,this.name=t.name,this.hint=t.hint,this.type=t.type,this.meta=t.meta},t.prototype.dupe=function(e){var n;return null==e&&(e=this.id),n=t.make(this),n.name=e,n},t.prototype.connect=function(t){if(this.inout===r.IN&&t.inout===r.OUT)return t.connect(this);if(this.inout!==r.OUT||t.inout!==r.IN)throw new Error("Can only connect out to in.");if(t.input!==this)return t.disconnect(),t.input=this,this.output.push(t)},t.prototype.disconnect=function(t){var e,n,r,i;if(this.input&&this.input.disconnect(this),this.output.length){if(!t){for(i=this.output,e=0,r=i.length;r>e;e++)t=i[e],t.input=null;return this.output=[]}if(n=this.output.indexOf(t),n>=0)return this.output.splice(n,1),t.input=null}},t}(),e.exports=i},{"./graph":204}],208:[function(t,e,n){var r,i,o,s,a,u,h,l,c,p,f,d,m;r=t("./block"),i=t("./factory"),o=t("./glsl"),s=t("./graph"),a=t("./linker"),l=t("./visualize"),f=i.library,c=i.cache,m=l.visualize,p=l.inspect,h=a.Snippet,d=function(t,e){var n,r,i,o;null==e&&(e={}),r={};for(n in t)o=t[n],r[n]=null!=(i=e[n])?i:t[n];return r},u=function(){function t(e,n){var r;return this instanceof t?(r={globalUniforms:!1,globalVaryings:!0,globalAttributes:!0,globals:[],autoInspect:!1},this.config=d(r,n),void(this.fetch=c(f(o,e,h.load)))):new t(e,n)}return t.prototype.shader=function(t){var e;return null==t&&(t={}),e=d(this.config,t),new i.Factory(o,this.fetch,e)},t.prototype.material=function(t){return new i.Material(this.shader(t),this.shader(t))},t.prototype.inspect=function(e){return t.inspect(e)},t.prototype.visualize=function(e){return t.visualize(e)},t.Block=r,t.Factory=i,t.GLSL=o,t.Graph=s,t.Linker=a,t.Visualize=l,t.inspect=function(t){return p(t)},t.visualize=function(t){return m(t)},t}(),e.exports=u,"undefined"!=typeof window&&(window.ShaderGraph=u)},{"./block":188,"./factory":194,"./glsl":202,"./graph":205,"./linker":210,"./visualize":216}],209:[function(t,e,n){var r,i,o;r=t("../graph"),i=t("./priority"),o=function(t,e,n,o){var s,a,u,h,l,c,p,f,d,m,v,g,E,y;return h=t.generate,u={},g=[],E={},y={},a={},f={},m=function(){var t,r,s,c,p,d,m,_,b;for(d in o)m=o[d],v(m.node,m.module);return _=l(n),t=_[0],n=_[1],null!=e&&(t.entry=e),p=h.build(t,n),b=function(){var t;t=[];for(d in f)c=f[d],t.push(c);return t}().sort(function(t,e){return i.compare(t.priority,e.priority)}),s=b.map(function(t){return t.code}),s.push(p.code),r=h.lines(s),{namespace:p.name,library:f,body:p.code,code:r,main:p,entry:p.name,symbols:g,externals:u,uniforms:E,varyings:y,attributes:a}},l=function(t){return function(t){var e,n,r,i,o,s;for(t=function(){var e;e=[];for(s in t)n=t[s],e.push(n);return e}(),t.sort(function(t,e){return e.priority-t.priority}),r=function(t,n,r){var i,o,s,a;return c(t,n,r),a=n.main,s=n.entry,o=function(e){return d(t,e)},i=function(e){return p(t,e)},h.call(o,i,s,a.signature,e)},e=h.body(),i=0,o=t.length;o>i;i++)n=t[i],r(n.node,n.module,n.priority);return[e,t]}}(this),s=function(t,e,n){var r;return r=f[t],null!=r?r.priority=i.max(r.priority,n):f[t]={code:e,priority:n}},c=function(t,e,n){var r,o,u,h,l,c,p,f;n=i.make(n),l=e.library;for(h in l)u=l[h],s(h,u.code,i.nest(n,u.priority));s(e.namespace,e.body,n),c=e.uniforms;for(o in c)r=c[o],E[o]=r;p=e.varyings;for(o in p)r=p[o],y[o]=r;f=e.attributes;for(o in f)r=f[o],a[o]=r;return v(t,e)},v=function(t,e){var n,r,i,o,s,a,h,l,c;for(h=e.symbols,l=[],i=0,a=h.length;a>i;i++)if(s=h[i],r=e.externals[s],p(t,r.name)){n={};for(o in r)c=r[o],n[o]=c;n.name=d(t,r.name),u[s]=n,l.push(g.push(s))}else l.push(void 0);return l},p=function(t,e){var n;return n=t.get(e),n.inout===r.IN?null===n.input:n.inout===r.OUT?0===n.output.length:void 0},d=function(t,e){var n;return(n=t.get(e))?(n.input&&(n=n.input),e=n.name,n.id):null},m()},e.exports=o},{"../graph":205,"./priority":213}],210:[function(t,e,n){n.Snippet=t("./snippet"),n.Program=t("./program"),n.Layout=t("./layout"),n.assemble=t("./assemble"),n.link=t("./link"),n.priority=t("./priority"),n.load=n.Snippet.load},{"./assemble":209,"./layout":211,"./link":212,"./priority":213,"./program":214,"./snippet":215}],211:[function(t,e,n){var r,i,o,s;i=t("./snippet"),s=t("./link"),o=!1,r=function(){function t(t,e){this.language=t,this.graph=e,this.links=[],this.includes=[],this.modules={},this.visits={}}return t.prototype.callback=function(t,e,n,r,i){return this.links.push({node:t,module:e,priority:n,name:r,external:i})},t.prototype.include=function(t,e,n){var r;return null!=(r=this.modules[e.namespace])?r.priority=Math.max(n,r.priority):(this.modules[e.namespace]=!0,this.includes.push({node:t,module:e,priority:n}))},t.prototype.visit=function(t){return o&&console.log("Visit",t,!this.visits[t]),this.visits[t]?!1:this.visits[t]=!0},t.prototype.link=function(t){var e,n,r;e=s(this.language,this.links,this.includes,t),r=new i;for(n in e)r[n]=e[n];return r.graph=this.graph,r},t}(),e.exports=r},{"./link":212,"./snippet":215}],212:[function(t,e,n){var r,i,o;r=t("../graph"),i=t("./priority"),o=function(t,e,n,o){var s,a,u,h,l,c,p,f,d,m,v,g;return h=t.generate,c=[],m=[],u={},v={},a={},g={},f={},d=function(){var t,r,s,p,d,m,E,y,_,b;for(s=h.links(e),p=[],null!=s.defs&&p.push(s.defs),null!=s.bodies&&p.push(s.bodies),d=0,m=n.length;m>d;d++)y=n[d],l(y.node,y.module,y.priority);return b=function(){var t;t=[];for(_ in f)E=f[_],t.push(E);return t}().sort(function(t,e){return i.compare(t.priority,e.priority)}),c=b.map(function(t){return t.code}),t=h.lines(c),t=h.defuse(t),p.length&&(t=[h.lines(p),t].join("\n")),t=h.hoist(t),t=h.dedupe(t),r=o,{namespace:r.main.name,code:t,main:r.main,entry:r.main.name,externals:u,uniforms:v,attributes:a,varyings:g}},s=function(t,e,n){var r;return r=f[t],null!=r?r.priority=i.max(r.priority,n):f[t]={code:e,priority:n}},l=function(t,e,n){var r,o,h,l,c,f,d,E,y,_,b,T,x;n=i.make(n),E=e.library;for(d in E)f=E[d],s(d,f.code,i.nest(n,f.priority));s(e.namespace,e.body,n),y=e.uniforms;for(l in y)r=y[l],v[l]=r;_=e.varyings;for(l in _)r=_[l],g[l]=r;b=e.attributes;for(l in b)r=b[l],a[l]=r;for(T=e.symbols,x=[],h=0,c=T.length;c>h;h++)l=T[h],o=e.externals[l],p(t,o.name)?(u[l]=o,x.push(m.push(l))):x.push(void 0);return x},p=function(t,e){var n,i,o,s;if(i=t.get(e),!i)throw n=null!=(o=null!=(s=t.owner.snippet)?s._name:void 0)?o:t.owner.namespace,new Error("Unable to link program. Unlinked callback `"+e+"` on `"+n+"`");return i.inout===r.IN?null===i.input:i.inout===r.OUT?0===i.output.length:void 0},d()},e.exports=o},{"../graph":205,"./priority":213}],213:[function(t,e,n){n.make=function(t){var e;return null==t&&(t=[]),t instanceof Array||(t=[null!=(e=+t)?e:0]),t},n.nest=function(t,e){return t.concat(e)},n.compare=function(t,e){var n,r,i,o,s,a;for(i=Math.min(t.length,e.length),n=r=0,a=i;a>=0?a>r:r>a;n=a>=0?++r:--r){if(o=t[n],s=e[n],o>s)return-1;if(s>o)return 1}return t=t.length,e=e.length,t>e?-1:e>t?1:0},n.max=function(t,e){return n.compare(t,e)>0?e:t}},{}],214:[function(t,e,n){var r,i,o;i=t("./snippet"),o=t("./assemble"),r=function(){function t(t,e,n){this.language=t,this.namespace=e,this.graph=n,this.calls={},this.requires={}}return t.index=0,t.entry=function(){return"_pg_"+ ++t.index+"_"},t.prototype.call=function(t,e,n){var r,i;return i=e.namespace,(r=this.calls[i])?r.priority=Math.max(r.priority,n):this.calls[i]={node:t,module:e,priority:n},this},t.prototype.require=function(t,e){var n;return n=e.namespace,this.requires[n]={node:t,module:e}},t.prototype.assemble=function(){var e,n,r,s;e=o(this.language,null!=(r=this.namespace)?r:t.entry,this.calls,this.requires),s=new i;for(n in e)s[n]=e[n];return s.graph=this.graph,s},t}(),e.exports=r},{"./assemble":209,"./snippet":215}],215:[function(t,e,n){var r;r=function(){function t(t,e,n,r,i){var o;this.language=t,this._signatures=e,this._compiler=n,this._name=r,this._original=i,this.namespace=null,this.code=null,this.main=null,this.entry=null,this.uniforms=null,this.externals=null,this.symbols=null,this.attributes=null,this.varyings=null,this.language||delete this.language,this._signatures||delete this._signatures,this._compiler||delete this._compiler,this._original||delete this._original,this._name||(this._name=null!=(o=this._signatures)?o.main.name:void 0)}return t.index=0,t.namespace=function(){return"_sn_"+ ++t.index+"_"},t.load=function(e,n,r){var i,o,s,a;return o=e.parse(n,r),s=e.compile(o),a=s[0],i=s[1],new t(e,a,i,n,r)},t.prototype.clone=function(){return new t(this.language,this._signatures,this._compiler,this._name,this._original)},t.prototype.bind=function(e,n,r,i){var o,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R,H,M,S,k,A,C,P,L,z,O,D,F,U,B,N,V,I,j,G;if(n===""+n?(L=[n,null!=r?r:{},null!=i?i:{}],r=L[0],n=L[1],i=L[2]):r!==""+r&&(z=[null!=r?r:{},void 0],i=z[0],r=z[1]),this.main=this._signatures.main,this.namespace=null!=(O=null!=r?r:this.namespace)?O:t.namespace(),this.entry=this.namespace+this.main.name,this.uniforms={},this.varyings={},this.attributes={},this.externals={},this.symbols=[],d={},f={},m=function(t){return f[t]=!0,t},M=function(t){return function(e){return t.namespace+e}}(this),e.globals)for(D=e.globals,v=0,b=D.length;b>v;v++)y=D[v],m(y);for(a=e.globalUniforms?m:M,u=e.globalVaryings?m:M,o=e.globalAttributes?m:M,s=M,G=function(t){return function(t){return d[t.name]=!0}}(this),I=function(t){return function(e,n){return t.uniforms[a(null!=n?n:e.name)]=e}}(this),j=function(t){return function(e){return t.varyings[u(e.name)]=e}}(this),h=function(t){return function(e){return t.attributes[o(e.name)]=e}}(this),p=function(t){return function(e){var n;return n=s(e.name),t.externals[n]=e,t.symbols.push(n)}}(this),P=function(t){return{type:t.type,name:t.name,value:t.value}},F=this._signatures.uniform,g=0,T=F.length;T>g;g++)l=F[g],G(l);for(U=this._signatures.uniform,_=0,x=U.length;x>_;_++)l=U[_],I(P(l));for(B=this._signatures.varying,S=0,w=B.length;w>S;S++)l=B[S],j(P(l));for(N=this._signatures.external,k=0,R=N.length;R>k;k++)l=N[k],p(l);for(V=this._signatures.attribute,C=0,H=V.length;H>C;C++)l=V[C],h(P(l));for(A in n)l=n[A],d[A]&&I(l,A);return this.body=this.code=this._compiler(this.namespace,f,i),i&&(c=function(){var t;t=[];for(E in i)j=i[E],t.push("#define "+E+" "+j);return t}().join("\n"),c.length&&(this._original=[c,"//----------------------------------------",this._original].join("\n"))),null},t}(),e.exports=r},{}],216:[function(t,e,n){var r,i,o,s,a,u;r=t("../Graph").Graph,n.serialize=a=t("./serialize"),n.markup=i=t("./markup"),u=function(t){var e;if(t)return t.nodes?(e=a(t),i.process(e)):t},s=function(t){return null==t?t:t instanceof Array?t.map(s):null!=t.vertex&&null!=t.fragment?[s(t.vertex,s(t.fragment))]:null!=t._graph?t._graph:null!=t.graph?t.graph:t},o=function(t){var e,n,r,i;for(i=[],n=0,r=t.length;r>n;n++)e=t[n],e instanceof Array?i=i.concat(o(e)):null!=e&&i.push(e);return i},n.visualize=function(){var t,e;return e=o(s([].slice.call(arguments))),i.merge(function(){var n,r,i;for(i=[],n=0,r=e.length;r>n;n++)t=e[n],t&&i.push(u(t));return i}())},n.inspect=function(){var t,e,r,o,s,a;for(t=n.visualize.apply(null,arguments),r=i.overlay(t),a=document.querySelectorAll(".shadergraph-overlay"),o=0,s=a.length;s>o;o++)e=a[o],e.remove();return document.body.appendChild(r),t.update(),r}},{"../Graph":182,"./markup":217,"./serialize":218}],217:[function(t,e,n){var r,i,o,s,a,u,h,l,c,p,f,d,m,v,g,E;h=t("../factory/hash"),g=function(t){return(""+t).replace(/^\s+|\s+$/g,"")},a=function(t,e,n,r){return"rgba("+[t,e,n,r].join(", ")+")"},l=function(t,e){var n,r,i,o,s,u,l;return null==e&&(e=1),r=1193046^h(t),l=255&r,i=r>>>8&255,n=r>>>16&255,o=Math.max(l,i,n),u=140/o,s=Math.round(o/3),l=Math.min(255,Math.round(u*Math.max(l,s))),i=Math.min(255,Math.round(u*Math.max(i,s))),n=Math.min(255,Math.round(u*Math.max(n,s))),a(l,i,n,e)},u=function(t){return t=null!=t?t:"",t.replace(/&/g,"&").replace(//g,">").replace(/'/g,"'").replace(/"/g,""")},m=function(t){var e,n;return n=[],e=i(t,n),e.update=function(){return s(e,n)},r(e),e},r=function(t){var e,n,r,i,o;for(n=t.querySelectorAll(".shadergraph-code"),o=[],r=0,i=n.length;i>r;r++)e=n[r],o.push(function(){var t;return t=e,t.parentNode.classList.add("shadergraph-has-code"),t.parentNode.addEventListener("click",function(e){return t.style.display={block:"none",none:"block"}[t.style.display||"none"]})}());return o},o=function(t){var e,n,r,i,o,s,a,u,h,l,c,p,f,d,m;for(c={},u={},f=t.nodes,e=0,i=f.length;i>e;e++)l=f[e],c[l.id]=l;for(d=t.links,n=0,o=d.length;o>n;n++)a=d[n],null==u[h=a.from]&&(u[h]=[]),u[a.from].push(a);for(p=function(t,e){var n,r,i,o;if(null==e&&(e=0),t.depth=Math.max(null!=(o=t.depth)?o:0,e),i=u[t.id])for(n=0,r=i.length;r>n;n++)a=i[n],p(c[a.to],e+1);return null},m=t.nodes,r=0,s=m.length;s>r;r++)l=m[r],null==l.depth&&p(l);return null},i=function(t,e){var n,r,s,a,h,c,p,f,d,m,v,E,y,_,b,T,x,w,R,H,M,S,k,A,C,P;for(o(t),P=document.createElement("div"),P.classList.add("shadergraph-graph"),c=[],M={},S=t.nodes,f=0,E=S.length;E>f;f++){for(R=S[f],r=document.createElement("div"),r.classList.add("shadergraph-node"),r.classList.add("shadergraph-node-"+R.type),r.innerHTML='
    '+u(R.name)+"
    ",n=function(t,e){var n,i;return n=l(t.type),i=document.createElement("div"),i.classList.add("shadergraph-outlet"),i.classList.add("shadergraph-outlet-"+e),i.innerHTML='
    \n
    '+u(t.type)+'
    \n
    '+u(t.name)+"
    ",r.appendChild(i),M[t.id]=i.querySelector(".shadergraph-point")},k=R.inputs,d=0,y=k.length;y>d;d++)H=k[d],n(H,"in");for(A=R.outputs,m=0,_=A.length;_>m;m++)H=A[m],n(H,"out");null!=R.graph?r.appendChild(i(R.graph,e)):(s=document.createElement("div"),s.classList.add("shadergraph-clear"),r.appendChild(s)),null!=R.code&&(p=document.createElement("div"),p.classList.add("shadergraph-code"),p.innerHTML=u(g(R.code)),r.appendChild(p)),h=c[R.depth],null==h&&(h=document.createElement("div"),h.classList.add("shadergraph-column"),c[R.depth]=h),h.appendChild(r)}for(v=0,b=c.length;b>v;v++)h=c[v],null!=h&&P.appendChild(h);for(C=t.links,w=0,T=C.length;T>w;w++)x=C[w],a=l(x.type),e.push({color:a,out:M[x.out],"in":M[x["in"]]});return P},v=function(t){return t*t},d=function(t,e,n,r){var i,o,s,a,u,h,l,c;return o=n-t,s=r-e,i=Math.sqrt(v(o)+v(s)),c=Math.abs(s)>Math.abs(o),c?(h=(t+n)/2,l=(e+r)/2,a=s>0?.3:-.3,u=Math.min(Math.abs(o)/2,20+i/8),["M",t,e,"C",t+u,e+",",h,l-i*a,h,l,"C",h,l+i*a,n-u,r+",",n,r].join(" ")):(u=Math.min(Math.abs(o)/2.5,20+i/4),["M",t,e,"C",t+u,e+",",n-u,r+",",n,r].join(" "))},c=function(t){return null==t&&(t="svg"),document.createElementNS("http://www.w3.org/2000/svg",t)},s=function(t,e){var n,r,i,o,s,a,u,h,l,p,f,m;if(null!=t.parentNode){for(f=t.getBoundingClientRect(),s=0,u=e.length;u>s;s++)p=e[s],n=p.out.getBoundingClientRect(),r=p["in"].getBoundingClientRect(),p.coords={x1:(n.left+n.right)/2-f.left,y1:(n.top+n.bottom)/2-f.top,x2:(r.left+r.right)/2-f.left,y2:(r.top+r.bottom)/2-f.top};for(m=t.querySelector("svg"),null!=m&&t.removeChild(m),i=t;i.parentNode&&0===i.offsetHeight;)i=i.parentNode;for(m=c(),m.setAttribute("width",i.offsetWidth),m.setAttribute("height",i.offsetHeight),a=0,h=e.length;h>a;a++)p=e[a],o=p.coords,l=c("path"),l.setAttribute("d",d(o.x1,o.y1,o.x2,o.y2)),l.setAttribute("stroke",p.color),l.setAttribute("stroke-width",3),l.setAttribute("fill","transparent"),m.appendChild(l);return t.appendChild(m)}},f=function(t){var e,n,r,i;return n=document.createElement("div"),n.setAttribute("class","shadergraph-overlay"),e=document.createElement("div"),e.setAttribute("class","shadergraph-close"),e.innerHTML="×",i=document.createElement("div"),i.setAttribute("class","shadergraph-view"),r=document.createElement("div"),r.setAttribute("class","shadergraph-inside"),r.appendChild(t),i.appendChild(r),n.appendChild(i),n.appendChild(e),e.addEventListener("click",function(){return n.parentNode.removeChild(n)}),n},E=function(t){var e;return t instanceof Node?t:(e=document.createElement("span"),e.innerText=null!=t?t:"",e)},p=function(t){var e,n,r,i;if(1!==t.length){for(e=document.createElement("div"),r=0,i=t.length;i>r;r++)n=t[r],e.appendChild(E(n));return e.update=function(){var e,r,i;for(i=[],e=0,r=t.length;r>e;e++)n=t[e],i.push("function"==typeof n.update?n.update():void 0);return i},e}return E(t[0])},e.exports={process:m,merge:p,overlay:f}},{"../factory/hash":193}],218:[function(t,e,n){var r,i,o;r=t("../block"),i=function(t){return"("===t.type[0]},o=function(t){var e,n,i,s,a,u,h,l,c,p,f,d,m,v,g,E,y,_,b,T,x,w,R;for(v=[],d=[],b=t.nodes,i=0,l=b.length;l>i;i++){for(m=b[i],_={id:m.id,name:null,type:null,depth:null,graph:null,inputs:[],outputs:[]},v.push(_),s=_.inputs,y=_.outputs,e=m.owner,e instanceof r.Call?(_.name=e.snippet._name,_.type="call",_.code=e.snippet._original):e instanceof r.Callback?(_.name="Callback",_.type="callback",_.graph=o(e.graph)):e instanceof r.Isolate?(_.name="Isolate",_.type="isolate",_.graph=o(e.graph)):e instanceof r.Join?(_.name="Join",_.type="join"):null!=e&&(null==_.name&&(_.name=null!=(T=e.name)?T:e.type),null==_.type&&(_.type=e.type),null==_.code&&(_.code=e.code),null!=e.graph&&(_.graph=o(e.graph))),n=function(t){return t=t.replace(")(",")→("),t=t.replace("()","")},x=m.inputs,a=0,c=x.length;c>a;a++)E=x[a],s.push({id:E.id,name:E.name,type:n(E.type),open:null==E.input});for(w=m.outputs,u=0,p=w.length;p>u;u++)for(E=w[u],y.push({id:E.id,name:E.name,type:n(E.type),open:!E.output.length}),R=E.output,h=0,f=R.length;f>h;h++)g=R[h],d.push({from:m.id,out:E.id,to:g.node.id,"in":g.id,type:n(E.type)})}return{nodes:v,links:d}},e.exports=o},{"../block":188}],219:[function(t,e,n){e.exports=t("./lib/index")},{"./lib/index":221}],220:[function(t,e,n){function r(){return this}function i(t,e){var n=E[t];return e=e||0,n?e>n.lbp&&(n.lbp=e):(n=Object.create(g),n.id=t,n.lbp=e,E[t]=n),n}function o(t){var e,n=d;for(c(),e=n.nud();t=m.length)return void(d=E["(end)"]);if(e=m[v++],n=e.data,i=e.type,"ident"===i)o=f.scope.find(n)||f.create_node(),i=o.type;else if("builtin"===i)o=E["(builtin)"];else if("keyword"===i)o=E["(keyword)"];else if("operator"===i){if(o=E[n],!o)return f.unexpected("unknown operator `"+n+"`")}else{if("float"!==i&&"integer"!==i)return f.unexpected("unexpected token.");i="literal",o=E["(literal)"]}return o&&(o.nud||(o.nud=r),o.children||(o.children=[])),o=Object.create(o),o.token=e,o.type=i,o.data||(o.data=n),d=o}function p(t){return function(){return f.unexpected(t)}}var f,d,m,v,g={nud:function(){return this.children&&this.children.length?this:p("unexpected")()},led:p("missing operator")},E={};i("(ident)").nud=r,i("(keyword)").nud=r,i("(builtin)").nud=r,i("(literal)").nud=r,i("(end)"),i(":"),i(";"),i(","),i(")"),i("]"),i("}"),a("&&",30),a("||",30),s("|",43),s("^",44),s("&",45),s("==",46),s("!=",46),s("<",47),s("<=",47),s(">",47),s(">=",47),s(">>",48),s("<<",48),s("+",50),s("-",50),s("*",60),s("/",60),s("%",60),s("?",20,function(t){return this.children=[t,o(0),(c(":"),o(0))],this.type="ternary",this}),s(".",80,function(t){return d.type="literal",f.fake(d),this.children=[t,d],c(),this}),s("[",80,function(t){return this.children=[t,o(0)],this.type="binary",c("]"),this}),s("(",80,function(t){if(this.children=[t],this.type="call",")"!==d.data)for(;;){if(this.children.push(o(0)),","!==d.data)break;c(",")}return c(")"),this}),u("-"),u("+"),u("!"),u("~"),u("defined"),u("(",function(){return this.type="group",this.children=[o(0)],c(")"),this}),u("++"),u("--"),h("++"),h("--"),l("="),l("+="),l("-="),l("*="),l("/="),l("%="),l("&="),l("|="),l("^="),l(">>="),l("<<="),e.exports=function(t,e){function n(t){f.unshift(t,!1);for(var e=0,r=t.children.length;r>e;++e)n(t.children[e]);f.shift()}f=t,m=e,v=0;var r;if(m.length){if(c(),r=o(0),r.parent=f[0],n(r),v1?void ht("unexpected EOF"):void qt.emit("end")}function n(){return Qt||!Yt.length?!1:(Ct=Zt[0])&&!qt.paused}function r(t){Yt.unshift(t),Yt.shift()}function f(t,e){t.parent=Yt[0];var n=[].unshift.call(this,t);if(e=void 0===e?!0:e,p){for(var r="",i=0,o=this.length-1;o>i;++i)r+=" |";console.log(r,"\\"+t.type,t.token.data)}return e&&Pt!==t&&Pt.children.push(t),Pt=t,n}function M(){var t=[].shift.call(this),e=Xt[this.length],n=!1;if(p){for(var r="",i=0,o=this.length;o>i;++i)r+=" |";console.log(r,"/"+t.type)}return Xt.length?"function"==typeof Xt[0]?n=Xt[0](t):void 0!==e&&(n=e.test?e.test(t.type):e===t.type):n=!0,n&&qt.emit("data",t),Pt=t.parent,t}function S(){function t(){if(Ct.data===Yt[0].expecting)return Yt.scope.exit(),Yt.shift();switch(Ct.type){case"preprocessor":return Yt.fake(it()),void Zt.shift();default:return void Yt.unshift(zt())}}return pt(function(){return Yt.scope.enter(),c},t)()}function N(){if(Yt[0].brace)return"}"!==Ct.data?ht("expected `}`, got "+Ct.data):(Yt[0].brace=!1,Zt.shift(),Yt.shift());switch(Ct.type){case"eof":return Yt.shift();case"keyword":switch(Ct.data){case"for":return Yt.unshift(Nt());case"if":return Yt.unshift(Vt());case"while":return Yt.unshift(It());case"do":return Yt.unshift(Gt());case"break":return Yt.fake(i(P,Ct)),Zt.shift();case"continue":return Yt.fake(i(L,Ct)),Zt.shift();case"discard":return Yt.fake(i(z,Ct)),Zt.shift();case"return":return Yt.unshift(jt());case"precision":return Yt.unshift(Dt())}return Yt.unshift(ot(W));case"ident":var t;if(t=Yt.scope.find(Ct.data))return"struct"===t.parent.type?Yt.unshift(ot(W)):Yt.unshift(at(";"));case"operator":if("{"===Ct.data){Yt[0].brace=!0;var e=Lt();return e.expecting="}",Zt.shift(),Yt.unshift(e)}if(";"===Ct.data)return Zt.shift(),Yt.shift();default:return Yt.unshift(at(";"))}}function Q(){function t(){return"invariant"===Ct.data?d.flags&V?(Yt.unshift(ut()),c):ht("`invariant` is not allowed here"):(Yt.fake(i(D,{data:"",position:Ct.position})),c)}function e(){return o(Ct)?d.flags&I?(Yt.unshift(ut()),c):ht("storage is not allowed here"):(Yt.fake(i(D,{data:"",position:Ct.position})),c)}function n(){return s(Ct)?d.flags&j?ht("parameter is not allowed here"):(Yt.unshift(ut()),c):(Yt.fake(i(D,{data:"",position:Ct.position})),c)}function r(){return a(Ct)?(Yt.unshift(ut()),c):(Yt.fake(i(D,{data:"",position:Ct.position})),c)}function u(){if("struct"===Ct.data)return d.flags&G?(Yt.unshift(st()),c):ht("cannot nest structs");if("keyword"===Ct.type)return Yt.unshift(ut()),c;var t=Yt.scope.find(Ct.data);return t?(Yt.fake(Object.create(t)),Zt.shift(),c):ht("expected user defined type, struct or keyword, got "+Ct.data)}function h(){return","!==Ct.data||d.flags&B?"["===Ct.data?void Yt.unshift(Wt()):")"===Ct.data?Yt.shift():";"===Ct.data?d.stage+3:"ident"!==Ct.type?(console.log(Ct),ht("expected identifier, got "+Ct.data)):(d.collected_name=Zt.shift(),c):Yt.shift()}function l(){return"("===Ct.data?(Zt.unshift(d.collected_name),delete d.collected_name,Yt.unshift(Ut()),d.stage+2):c}function p(){return Zt.unshift(d.collected_name),delete d.collected_name,Yt.unshift(Ot()),c}function f(){return Yt.shift()}var d=Yt[0];return pt(t,e,n,r,u,h,l,p,f)()}function J(){if("ident"===Ct.type){var t=Ct.data;return Yt.unshift(Ft()),void Yt.scope.define(t)}if("operator"===Ct.type){if(","===Ct.data)return Yt[1].flags&B?Zt.shift():Yt.shift();if("="===Ct.data)return Yt[1].flags&U?(Zt.shift(),void Yt.unshift(at(",",";"))):ht("`=` is not allowed here.");if("["===Ct.data)return void Yt.unshift(Wt())}return Yt.shift()}function $(){return"keyword"===Ct.type?(Yt[0].type="keyword",void(Yt[0].mode=k)):"ident"===Ct.type?(Yt[0].type="ident",void(Yt[0].mode=d)):ht("expected keyword or user-defined name, got "+Ct.data)}function tt(){return"keyword"!==Ct.type?ht("expected keyword, got "+Ct.data):(Yt.shift(),Zt.shift())}function et(){return"ident"!==Ct.type?ht("expected user-defined name, got "+Ct.data):(Yt[0].data=Ct.data,Yt.shift(),Zt.shift())}function nt(){function t(t){return h(Yt,t),Yt.shift()}var e=Yt[0].expecting;if(Yt[0].tokens=Yt[0].tokens||[],void 0===Yt[0].parenlevel&&(Yt[0].parenlevel=0,Yt[0].bracelevel=0),Yt[0].parenlevel<1&&e.indexOf(Ct.data)>-1)return t(Yt[0].tokens);switch("("===Ct.data?++Yt[0].parenlevel:")"===Ct.data&&--Yt[0].parenlevel,Ct.data){case"{":++Yt[0].bracelevel;break;case"}":--Yt[0].bracelevel;break;case"(":++Yt[0].parenlevel;break;case")":--Yt[0].parenlevel}return Yt[0].parenlevel<0?ht("unexpected `)`"):Yt[0].bracelevel<0?ht("unexpected `}`"):void Yt[0].tokens.push(Zt.shift())}function rt(t){return function(){return i(t,Ct)}}function it(){return i(K[Ct.type],Ct,Pt)}function ot(t){var e=i(_,Ct,Pt);return e.flags=t,e}function st(t,e){var n=i(g,Ct,Pt);return n.allow_assign=void 0===t?!0:t,n.allow_comma=void 0===e?!0:e,n}function at(){var t=i(R,Ct,Pt);return t.expecting=[].slice.call(arguments),t}function ut(t){var e=Ct;return t&&(e={type:"(implied)",data:"(default)",position:e.position}),i(k,e,Pt)}function ht(t){Qt=!0,qt.emit("error",new Error((t||"unexpected "+Yt)+" at line "+Yt[0].token.line))}function lt(t,e){return ct(t,Ct.type)&&ct(e,Ct.data)}function ct(t,e){switch(typeof t){case"string":return e!==t&&ht("expected `"+t+"`, got "+e+"\n"+Ct.data),!Qt;case"object":return t&&-1===t.indexOf(e)&&ht("expected one of `"+t.join("`, `")+"`, got "+e),!Qt}return!0}function pt(){var t,e,n=[].slice.call(arguments);return function(){var r=Yt[0];return r.stage||(r.stage=0),(t=n[r.stage])?(e=t(),e===c?++r.stage:void(void 0!==e&&(r.stage=e))):ht("parser in undefined state!")}}function ft(t,e){return e=e||"operator",function(){if(lt(e,t)){var n=Zt.shift(),r=Yt[0].children,i=r[r.length-1];return i&&i.token&&n.preceding&&(i.token.succeeding=i.token.succeeding||[],i.token.succeeding=i.token.succeeding.concat(n.preceding)),c}}}function dt(t){return function(){return Yt.unshift(at(t)),c}}function mt(t){return t?function(){var t=Ct.data;return lt("ident")&&(Yt.unshift(Ft()),Yt.scope.define(t),c)}:function(){if(lt("ident")){var t=Object.create(Yt.scope.find(Ct.data));return t.token=Ct,Zt.shift(),c}}}function vt(){return function(){var t=Lt();return t.expecting="}",Yt.unshift(t),c}}function gt(t){return function(){var e=Yt[0].stage;return"{"!==Ct.data?(Yt.unshift(zt()),e+t):(Zt.shift(),c)}}function Et(){return function(){return Yt.shift(),Yt.shift()}}function yt(){_t=pt(ft("struct","keyword"),function(){return"{"===Ct.data?(Yt.fake(i(d,{data:"",position:Ct.position,type:"ident"})),c):mt(!0)()},function(){return Yt.scope.enter(),c},ft("{"),function(){return"}"===Ct.data?(Yt.scope.exit(),Zt.shift(),Yt.shift()):";"===Ct.data?void Zt.shift():void Yt.unshift(ot(X))}),bt=pt(function(){return Zt.shift(),c},function(){return lt("keyword",["lowp","mediump","highp"])&&(Yt.unshift(ut()),c)},function(){return Yt.unshift(ut()),c},function(){return Yt.shift()}),Tt=pt(ft("["),dt("]"),ft("]"),function(){return Yt.shift()}),xt=pt(ft("for","keyword"),ft("("),function(){var t;if("ident"===Ct.type){if((t=Yt.scope.find(Ct.data))||(t=Yt.create_node()),"struct"===t.parent.type)return Yt.unshift(ot(W)), -c}else if("builtin"===Ct.type||"keyword"===Ct.type)return Yt.unshift(ot(W)),c;return dt(";")()},ft(";"),dt(";"),ft(";"),dt(")"),ft(")"),gt(3),vt(),ft("}"),Et()),wt=pt(ft("if","keyword"),ft("("),dt(")"),ft(")"),gt(3),vt(),ft("}"),function(){return"else"===Ct.data?(Zt.shift(),Yt.unshift(zt()),c):Et()()},Et()),Rt=pt(ft("return","keyword"),function(){return";"===Ct.data?c:(Yt.unshift(at(";")),c)},function(){Zt.shift(),Et()()}),Ht=pt(ft("while","keyword"),ft("("),dt(")"),ft(")"),gt(3),vt(),ft("}"),Et()),Mt=pt(ft("do","keyword"),gt(3),vt(),ft("}"),ft("while","keyword"),ft("("),dt(")"),ft(")"),Et()),St=pt(function(){for(var t=1,e=Yt.length;e>t;++t)if(Yt[t].mode===E)return ht("function definition is not allowed within another function");return c},function(){if(lt("ident")){var t=Ct.data,e=Yt.scope.find(t);return Yt.unshift(Ft()),Yt.scope.define(t),Yt.scope.enter(e?e.scope:null),c}},ft("("),function(){return Yt.unshift(Bt()),c},ft(")"),function(){return";"===Ct.data?(Yt.scope.exit(),Yt.shift(),Yt.shift()):c},ft("{"),vt(),ft("}"),function(){return Yt.scope.exit(),c},function(){return Yt.shift(),Yt.shift(),Yt.shift()}),kt=pt(function(){return"void"===Ct.data?(Yt.fake(ut()),Zt.shift(),c):")"===Ct.data?void Yt.shift():"struct"===Ct.data?(Yt.unshift(st(Y,Z)),c):(Yt.unshift(ot(q)),c)},function(){return","===Ct.data?(Zt.shift(),0):")"===Ct.data?void Yt.shift():void ht("expected one of `,` or `)`, got "+Ct.data)})}var _t,bt,Tt,xt,wt,Rt,Ht,Mt,St,kt,At,Ct,Pt,Lt=rt(v),zt=rt(m),Ot=rt(b),Dt=rt(H),Ft=rt(d),Ut=(rt(A),rt(E)),Bt=rt(y),Nt=rt(T),Vt=rt(w),It=rt(x),jt=rt(C),Gt=rt(O),Wt=rt(F),qt=u(t,e),Xt=arguments.length?[].slice.call(arguments):[],Yt=[],Zt=[],Kt=[],Qt=!1;return Yt.shift=M,Yt.unshift=f,Yt.fake=r,Yt.unexpected=ht,Yt.scope=new l(Yt),Yt.create_node=function(){var t=i(d,Ct);return t.parent=qt.program,t},yt(),Pt=Lt(),Pt.expecting="(eof)",Pt.mode=v,Pt.token={type:"(program)",data:"(program)"},At=Pt,qt.program=At,qt.scope=function(t){return 1===arguments.length&&(Yt.scope=t),Yt.scope},Yt.unshift(Pt),qt}function i(t,e){return{mode:t,token:e,children:[],type:Q[t]}}function o(t){return"const"===t.data||"attribute"===t.data||"uniform"===t.data||"varying"===t.data}function s(t){return"in"===t.data||"inout"===t.data||"out"===t.data}function a(t){return"highp"===t.data||"mediump"===t.data||"lowp"===t.data}e.exports=r;var u=t("../../through"),h=t("./expr"),l=t("./scope"),c=new Object,p=!1,f=0,d=f++,m=f++,v=f++,g=f++,E=f++,y=f++,_=f++,b=f++,T=f++,x=f++,w=f++,R=f++,H=f++,M=f++,S=f++,k=f++,A=f++,C=f++,P=f++,L=f++,z=f++,O=f++,D=f++,F=f++,U=1,B=2,N=4,V=8,I=16,j=32,G=64,W=255,q=W&~(U|B|j|V|N),X=W&~(U|V|I|G),Y=!1,Z=!1,K={"block-comment":M,"line-comment":M,preprocessor:S},Q=f=["ident","stmt","stmtlist","struct","function","functionargs","decl","decllist","forloop","whileloop","if","expr","precision","comment","preprocessor","keyword","keyword_or_ident","return","break","continue","discard","do-while","placeholder","quantifier"]},{"../../through":227,"./expr":220,"./scope":222}],222:[function(t,e,n){function r(t){return this.constructor!==r?new r(t):(this.state=t,this.scopes=[],void(this.current=null))}e.exports=r;var i=r,o=i.prototype;o.enter=function(t){this.scopes.push(this.current=this.state[0].scope=t||{})},o.exit=function(){this.scopes.pop(),this.current=this.scopes[this.scopes.length-1]},o.define=function(t){this.current[t]=this.state[0]},o.find=function(t,e){for(var n=this.scopes.length-1;n>-1;--n)if(this.scopes[n].hasOwnProperty(t))return this.scopes[n][t];return null}},{}],223:[function(t,e,n){function r(){function t(t){t.length&&D.queue({type:T[B],data:t,position:I,line:V})}function e(t){for(F=0,W+=t.toString(),O=W.length;L=W[F],O>F;)switch(B){case l:F=H();break;case c:F=R();break;case p:F=w();break;case f:F=M();break;case d:F=A();break;case b:F=k();break;case m:F=C();break;case h:F=P();break;case y:F=x();break;case u:F=r()}U+=F,W=W.slice(F)}function n(e){N.length&&t(N.join("")),B=_,t("(eof)"),D.queue(null)}function r(){return N=N.length?[]:N,"/"===z&&"*"===L?(I=U+F-1,B=l,z=L,F+1):"/"===z&&"/"===L?(I=U+F-1,B=c,z=L,F+1):"#"===L?(B=p,I=U+F,F):/\s/.test(L)?(B=y,I=U+F,F):(j=/\d/.test(L),G=/[^\w_]/.test(L),I=U+F,B=j?d:G?f:h,F)}function x(){return"\n"===L&&++V,/[^\s]/g.test(L)?(t(N.join("")),B=u,F):(N.push(L),z=L,F+1)}function w(){return"\n"===L&&++V,"\n"===L&&"\\"!==z?(t(N.join("")),B=u,F):(N.push(L),z=L,F+1)}function R(){return w()}function H(){return"/"===L&&"*"===z?(N.push(L),t(N.join("")),B=u,F+1):("\n"===L&&++V,N.push(L),z=L,F+1)}function M(){if("."===z&&/\d/.test(L))return B=m,F;if("/"===z&&"*"===L)return B=l,F;if("/"===z&&"/"===L)return B=c,F;if("."===L&&N.length){for(;S(N););return B=m,F}if(";"===L){if(N.length)for(;S(N););return t(L),B=u,F+1}var e=2===N.length&&"="!==L;if(/[\w_\d\s]/.test(L)||e){for(;S(N););return B=u,F}return N.push(L),z=L,F+1}function S(e){for(var n,r=0,i=e.length;;){n=s.indexOf(e.slice(0,e.length+r).join(""));{if(-1!==n)return t(s[n]),I+=s[n].length,N=N.slice(s[n].length),N.length;if(r-=1,i-=1,0>i)return 0}}}function k(){return/[^a-fA-F0-9]/.test(L)?(t(N.join("")),B=u,F):(N.push(L),z=L,F+1)}function A(){return"."===L?(N.push(L),B=m,z=L,F+1):/[eE]/.test(L)?(N.push(L),B=m,z=L,F+1):"x"===L&&1===N.length&&"0"===N[0]?(B=b,N.push(L),z=L,F+1):/[^\d]/.test(L)?(t(N.join("")),B=u,F):(N.push(L),z=L,F+1)}function C(){return"f"===L&&(N.push(L),z=L,F+=1),/[eE]/.test(L)?(N.push(L),z=L,F+1):/[^\d]/.test(L)?(t(N.join("")),B=u,F):(N.push(L),z=L,F+1)}function P(){if(/[^\d\w_]/.test(L)){var e=N.join("");return B=o.indexOf(e)>-1?E:a.indexOf(e)>-1?g:v,t(N.join("")),B=u,F}return N.push(L),z=L,F+1}var L,z,O,D=i(e,n),F=0,U=0,B=u,N=[],V=1,I=0,j=!1,G=!1,W="";return D}e.exports=r;var i=t("../through"),o=t("./lib/literals"),s=t("./lib/operators"),a=t("./lib/builtins"),u=999,h=9999,l=0,c=1,p=2,f=3,d=4,m=5,v=6,g=7,E=8,y=9,_=10,b=11,T=["block-comment","line-comment","preprocessor","operator","integer","float","ident","builtin","keyword","whitespace","eof","integer"]},{"../through":227,"./lib/builtins":224,"./lib/literals":225,"./lib/operators":226}],224:[function(t,e,n){e.exports=["gl_Position","gl_PointSize","gl_ClipVertex","gl_FragCoord","gl_FrontFacing","gl_FragColor","gl_FragData","gl_FragDepth","gl_Color","gl_SecondaryColor","gl_Normal","gl_Vertex","gl_MultiTexCoord0","gl_MultiTexCoord1","gl_MultiTexCoord2","gl_MultiTexCoord3","gl_MultiTexCoord4","gl_MultiTexCoord5","gl_MultiTexCoord6","gl_MultiTexCoord7","gl_FogCoord","gl_MaxLights","gl_MaxClipPlanes","gl_MaxTextureUnits","gl_MaxTextureCoords","gl_MaxVertexAttribs","gl_MaxVertexUniformComponents","gl_MaxVaryingFloats","gl_MaxVertexTextureImageUnits","gl_MaxCombinedTextureImageUnits","gl_MaxTextureImageUnits","gl_MaxFragmentUniformComponents","gl_MaxDrawBuffers","gl_ModelViewMatrix","gl_ProjectionMatrix","gl_ModelViewProjectionMatrix","gl_TextureMatrix","gl_NormalMatrix","gl_ModelViewMatrixInverse","gl_ProjectionMatrixInverse","gl_ModelViewProjectionMatrixInverse","gl_TextureMatrixInverse","gl_ModelViewMatrixTranspose","gl_ProjectionMatrixTranspose","gl_ModelViewProjectionMatrixTranspose","gl_TextureMatrixTranspose","gl_ModelViewMatrixInverseTranspose","gl_ProjectionMatrixInverseTranspose","gl_ModelViewProjectionMatrixInverseTranspose","gl_TextureMatrixInverseTranspose","gl_NormalScale","gl_DepthRangeParameters","gl_DepthRange","gl_ClipPlane","gl_PointParameters","gl_Point","gl_MaterialParameters","gl_FrontMaterial","gl_BackMaterial","gl_LightSourceParameters","gl_LightSource","gl_LightModelParameters","gl_LightModel","gl_LightModelProducts","gl_FrontLightModelProduct","gl_BackLightModelProduct","gl_LightProducts","gl_FrontLightProduct","gl_BackLightProduct","gl_FogParameters","gl_Fog","gl_TextureEnvColor","gl_EyePlaneS","gl_EyePlaneT","gl_EyePlaneR","gl_EyePlaneQ","gl_ObjectPlaneS","gl_ObjectPlaneT","gl_ObjectPlaneR","gl_ObjectPlaneQ","gl_FrontColor","gl_BackColor","gl_FrontSecondaryColor","gl_BackSecondaryColor","gl_TexCoord","gl_FogFragCoord","gl_Color","gl_SecondaryColor","gl_TexCoord","gl_FogFragCoord","gl_PointCoord","radians","degrees","sin","cos","tan","asin","acos","atan","pow","exp","log","exp2","log2","sqrt","inversesqrt","abs","sign","floor","ceil","fract","mod","min","max","clamp","mix","step","smoothstep","length","distance","dot","cross","normalize","faceforward","reflect","refract","matrixCompMult","lessThan","lessThanEqual","greaterThan","greaterThanEqual","equal","notEqual","any","all","not","texture2D","texture2DProj","texture2DLod","texture2DProjLod","textureCube","textureCubeLod"]},{}],225:[function(t,e,n){e.exports=["precision","highp","mediump","lowp","attribute","const","uniform","varying","break","continue","do","for","while","if","else","in","out","inout","float","int","void","bool","true","false","discard","return","mat2","mat3","mat4","vec2","vec3","vec4","ivec2","ivec3","ivec4","bvec2","bvec3","bvec4","sampler1D","sampler2D","sampler3D","samplerCube","sampler1DShadow","sampler2DShadow","struct","asm","class","union","enum","typedef","template","this","packed","goto","switch","default","inline","noinline","volatile","public","static","extern","external","interface","long","short","double","half","fixed","unsigned","input","output","hvec2","hvec3","hvec4","dvec2","dvec3","dvec4","fvec2","fvec3","fvec4","sampler2DRect","sampler3DRect","sampler2DRectShadow","sizeof","cast","namespace","using"]},{}],226:[function(t,e,n){e.exports=["<<=",">>=","++","--","<<",">>","<=",">=","==","!=","&&","||","+=","-=","*=","/=","%=","&=","^=","|=","(",")","[","]",".","!","~","*","/","%","+","-","<",">","&","^","|","?",":","=",",",";","{","}"]},{}],227:[function(t,e,n){var r;r=function(t,e){var n,r;return r=[],n=[],{output:r,parser:null,write:t,end:e,process:function(e,n){return this.parser=e,t(n),this.flush(),this.parser.flush()},flush:function(){return e(),[r,n]},queue:function(t){var e;return null!=t&&null!=(e=this.parser)?e.write(t):void 0},emit:function(t,e){return"data"===t&&null==e.parent&&r.push(e),"error"===t?n.push(e):void 0}}},e.exports=r},{}]},{},[30]); \ No newline at end of file diff --git a/htdocs/mathbox.css b/htdocs/mathbox.css deleted file mode 100644 index dd93e59ee..000000000 --- a/htdocs/mathbox.css +++ /dev/null @@ -1,461 +0,0 @@ -.shadergraph-graph { - font: 12px sans-serif; - line-height: 25px; - position: relative; -} -.shadergraph-graph:after { - content: ' '; - display: block; - height: 0; - font-size: 0; - clear: both; -} -.shadergraph-graph svg { - pointer-events: none; -} -.shadergraph-clear { - clear: both; -} -.shadergraph-graph svg { - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - width: auto; - height: auto; -} -.shadergraph-column { - float: left; -} -.shadergraph-node .shadergraph-graph { - float: left; - clear: both; - overflow: visible; -} -.shadergraph-node .shadergraph-graph .shadergraph-node { - margin: 5px 15px 15px; -} -.shadergraph-node { - margin: 5px 15px 25px; - background: rgba(0, 0, 0, .1); - border-radius: 5px; - box-shadow: 0 1px 2px rgba(0, 0, 0, .2), - 0 1px 10px rgba(0, 0, 0, .2); - min-height: 35px; - float: left; - clear: left; - position: relative; -} -.shadergraph-type { - font-weight: bold; -} -.shadergraph-header { - font-weight: bold; - text-align: center; - height: 25px; - background: rgba(0, 0, 0, .3); - text-shadow: 0 1px 2px rgba(0, 0, 0, .25); - color: #fff; - border-top-left-radius: 5px; - border-top-right-radius: 5px; - margin-bottom: 5px; - padding: 0 10px; -} -.shadergraph-outlet div { -} -.shadergraph-outlet-in .shadergraph-name { - margin-right: 7px; -} -.shadergraph-outlet-out .shadergraph-name { - margin-left: 7px; -} - -.shadergraph-name { - margin: 0 4px; -} -.shadergraph-point { - margin: 6px; - width: 11px; - height: 11px; - border-radius: 7.5px; - background: rgba(255, 255, 255, 1); -} -.shadergraph-outlet-in { - float: left; - clear: left; -} -.shadergraph-outlet-in div { - float: left; -} -.shadergraph-outlet-out { - float: right; - clear: right; -} -.shadergraph-outlet-out div { - float: right; -} - -.shadergraph-node-callback { - background: rgba(205, 209, 221, .5); - box-shadow: 0 1px 2px rgba(0, 10, 40, .2), - 0 1px 10px rgba(0, 10, 40, .2); -} -.shadergraph-node-callback > .shadergraph-header { - background: rgba(0, 20, 80, .3); -} -.shadergraph-graph .shadergraph-graph .shadergraph-node-callback { - background: rgba(0, 20, 80, .1); -} - -.shadergraph-node-call { - background: rgba(209, 221, 205, .5); - box-shadow: 0 1px 2px rgba(10, 40, 0, .2), - 0 1px 10px rgba(10, 40, 0, .2); -} -.shadergraph-node-call > .shadergraph-header { - background: rgba(20, 80, 0, .3); -} -.shadergraph-graph .shadergraph-graph .shadergraph-node-call { - background: rgba(20, 80, 0, .1); -} - -.shadergraph-node-isolate { - background: rgba(221, 205, 209, .5); - box-shadow: 0 1px 2px rgba(40, 0, 10, .2), - 0 1px 10px rgba(40, 0, 10, .2); -} -.shadergraph-node-isolate > .shadergraph-header { - background: rgba(80, 0, 20, .3); -} -.shadergraph-graph .shadergraph-graph .shadergraph-node-isolate { - background: rgba(80, 0, 20, .1); -} - -.shadergraph-node.shadergraph-has-code { - cursor: pointer; -} -.shadergraph-node.shadergraph-has-code::before { - position: absolute; - content: ' '; - top: 0; - left: 0; - right: 0; - bottom: 0; - display: none; - border: 2px solid rgba(0, 0, 0, .25); - border-radius: 5px; -} -.shadergraph-node.shadergraph-has-code:hover::before { - display: block; -} -.shadergraph-code { - z-index: 10000; - display: none; - position: absolute; - background: #fff; - color: #000; - white-space: pre; - padding: 10px; - border-radius: 5px; - box-shadow: 0 1px 2px rgba(0, 0, 0, .2), - 0 1px 10px rgba(0, 0, 0, .2); - font-family: monospace; - font-size: 10px; - line-height: 12px; -} - -.shadergraph-overlay { - position: fixed; - top: 50%; - left: 0; - right: 0; - bottom: 0; - background: #fff; - border-top: 1px solid #CCC; -} -.shadergraph-overlay .shadergraph-view { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - overflow: auto; -} -.shadergraph-overlay .shadergraph-inside { - width: 4000px; - min-height: 100%; - box-sizing: border-box; -} -.shadergraph-overlay .shadergraph-close { - position: absolute; - top: 5px; - right: 5px; - padding: 4px; - border-radius: 16px; - background: rgba(255,255,255,.3); - color: rgba(0, 0, 0, .3); - cursor: pointer; - font-size: 24px; - line-height: 24px; - width: 24px; - text-align: center; - vertical-align: middle; -} -.shadergraph-overlay .shadergraph-close:hover { - background: rgba(255,255,255,1); - color: rgba(0, 0, 0, 1); -} -.shadergraph-overlay .shadergraph-graph { - padding-top: 10px; - overflow: visible; - min-height: 100%; -} -.shadergraph-overlay span { - display: block; - padding: 5px 15px; - margin: 0; - background: rgba(0, 0, 0, .1); - font-weight: bold; - font-family: sans-serif; -} -.mathbox-loader { - position: absolute; - top: 50%; - left: 50%; - -webkit-transform: translate(-50%, -50%); - transform: translate(-50%, -50%); - padding: 10px; - border-radius: 50%; - background: #fff; -} - -.mathbox-loader.mathbox-exit { - opacity: 0; - -webkit-transition: - opacity .15s ease-in-out; - transition: - opacity .15s ease-in-out; -} - -.mathbox-progress { - height: 10px; - border-radius: 5px; - width: 80px; - margin: 0 auto 20px; - box-shadow: - 1px 1px 1px rgba(255, 255, 255, .2), - 1px -1px 1px rgba(255, 255, 255, .2), - -1px 1px 1px rgba(255, 255, 255, .2), - -1px -1px 1px rgba(255, 255, 255, .2); - background: #ccc; - overflow: hidden; -} - -.mathbox-progress > div { - display: block; - width: 0px; - height: 10px; - background: #888; -} - -.mathbox-logo { - position: relative; - width: 140px; - height: 100px; - margin: 0 auto 10px; - -webkit-perspective: 200px; - perspective: 200px; -} - -.mathbox-logo > div { - position: absolute; - left: 0; - top: 0; - bottom: 0; - right: 0; - -webkit-transform-style: preserve-3d; - transform-style: preserve-3d; -} - -.mathbox-logo > :nth-child(1) { - -webkit-transform: rotateZ(22deg) rotateX(24deg) rotateY(30deg); - transform: rotateZ(22deg) rotateX(24deg) rotateY(30deg); -} - -.mathbox-logo > :nth-child(2) { - -webkit-transform: rotateZ(11deg) rotateX(12deg) rotateY(15deg) scale3d(.6, .6, .6); - transform: rotateZ(11deg) rotateX(12deg) rotateY(15deg) scale3d(.6, .6, .6); -} - -.mathbox-logo > div > div { - position: absolute; - top: 50%; - left: 50%; - margin-left: -100px; - margin-top: -100px; - width: 200px; - height: 200px; - box-sizing: border-box; - border-radius: 50%; -} - -.mathbox-logo > div > :nth-child(1) { - -webkit-transform: scale(0.5, 0.5); - transform: rotateX(30deg) scale(0.5, 0.5); -} - -.mathbox-logo > div > :nth-child(2) { - -webkit-transform: rotateX(90deg) scale(0.42, 0.42); - transform: rotateX(90deg) scale(0.42, 0.42); -} - -.mathbox-logo > div > :nth-child(3) { - -webkit-transform: rotateY(90deg) scale(0.35, 0.35); - transform: rotateY(90deg) scale(0.35, 0.35); -} - -.mathbox-logo > :nth-child(1) > :nth-child(1) { - border: 16px solid #808080; -} -.mathbox-logo > :nth-child(1) > :nth-child(2) { - border: 19px solid #A0A0A0; -} -.mathbox-logo > :nth-child(1) > :nth-child(3) { - border: 23px solid #C0C0C0; -} -.mathbox-logo > :nth-child(2) > :nth-child(1) { - border: 27px solid #808080; -} -.mathbox-logo > :nth-child(2) > :nth-child(2) { - border: 32px solid #A0A0A0; -} -.mathbox-logo > :nth-child(2) > :nth-child(3) { - border: 38px solid #C0C0C0; -} - -.mathbox-splash-blue .mathbox-progress { - background: #def; -} -.mathbox-splash-blue .mathbox-progress > div { - background: #1979e7; -} -.mathbox-splash-blue .mathbox-logo > :nth-child(1) > :nth-child(1) { - border-color: #1979e7; -} -.mathbox-splash-blue .mathbox-logo > :nth-child(1) > :nth-child(2) { - border-color: #33b0ff; -} -.mathbox-splash-blue .mathbox-logo > :nth-child(1) > :nth-child(3) { - border-color: #75eaff; -} -.mathbox-splash-blue .mathbox-logo > :nth-child(2) > :nth-child(1) { - border-color: #18487F; -} -.mathbox-splash-blue .mathbox-logo > :nth-child(2) > :nth-child(2) { - border-color: #33b0ff; -} -.mathbox-splash-blue .mathbox-logo > :nth-child(2) > :nth-child(3) { - border-color: #75eaff; -} - - - - -.mathbox-overlays { - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - pointer-events: none; - transform-style: preserve-3d; - overflow: hidden; -} -.mathbox-overlays > div { - transform-style: preserve-3d; -} -.mathbox-overlay > div { - position: absolute; - will-change: transform, opacity; -} -.mathbox-label { - font-family: sans-serif; -} -.mathbox-outline-1 { - text-shadow: - -1px -1px 0px rgb(255, 255, 255), - 1px 1px 0px rgb(255, 255, 255), - -1px 1px 0px rgb(255, 255, 255), - 1px -1px 0px rgb(255, 255, 255), - 1px 0px 1px rgb(255, 255, 255), - -1px 0px 1px rgb(255, 255, 255), - 0px -1px 1px rgb(255, 255, 255), - 0px 1px 1px rgb(255, 255, 255); -} -.mathbox-outline-2 { - text-shadow: - 0px -2px 0px rgb(255, 255, 255), - 0px 2px 0px rgb(255, 255, 255), - -2px 0px 0px rgb(255, 255, 255), - 2px 0px 0px rgb(255, 255, 255), - -1px -2px 0px rgb(255, 255, 255), - -2px -1px 0px rgb(255, 255, 255), - -1px 2px 0px rgb(255, 255, 255), - -2px 1px 0px rgb(255, 255, 255), - 1px 2px 0px rgb(255, 255, 255), - 2px 1px 0px rgb(255, 255, 255), - 1px -2px 0px rgb(255, 255, 255), - 2px -1px 0px rgb(255, 255, 255); -} -.mathbox-outline-3 { - text-shadow: - 3px 0px 0px rgb(255, 255, 255), - -3px 0px 0px rgb(255, 255, 255), - 0px 3px 0px rgb(255, 255, 255), - 0px -3px 0px rgb(255, 255, 255), - - -2px -2px 0px rgb(255, 255, 255), - -2px 2px 0px rgb(255, 255, 255), - 2px 2px 0px rgb(255, 255, 255), - 2px -2px 0px rgb(255, 255, 255), - - -1px -2px 1px rgb(255, 255, 255), - -2px -1px 1px rgb(255, 255, 255), - -1px 2px 1px rgb(255, 255, 255), - -2px 1px 1px rgb(255, 255, 255), - 1px 2px 1px rgb(255, 255, 255), - 2px 1px 1px rgb(255, 255, 255), - 1px -2px 1px rgb(255, 255, 255), - 2px -1px 1px rgb(255, 255, 255); -} -.mathbox-outline-4 { - text-shadow: - 4px 0px 0px rgb(255, 255, 255), - -4px 0px 0px rgb(255, 255, 255), - 0px 4px 0px rgb(255, 255, 255), - 0px -4px 0px rgb(255, 255, 255), - - -3px -2px 0px rgb(255, 255, 255), - -3px 2px 0px rgb(255, 255, 255), - 3px 2px 0px rgb(255, 255, 255), - 3px -2px 0px rgb(255, 255, 255), - - -2px -3px 0px rgb(255, 255, 255), - -2px 3px 0px rgb(255, 255, 255), - 2px 3px 0px rgb(255, 255, 255), - 2px -3px 0px rgb(255, 255, 255), - - -1px -2px 1px rgb(255, 255, 255), - -2px -1px 1px rgb(255, 255, 255), - -1px 2px 1px rgb(255, 255, 255), - -2px 1px 1px rgb(255, 255, 255), - 1px 2px 1px rgb(255, 255, 255), - 2px 1px 1px rgb(255, 255, 255), - 1px -2px 1px rgb(255, 255, 255), - 2px -1px 1px rgb(255, 255, 255); - -} -.mathbox-outline-fill, .mathbox-outline-fill * { - color: #fff !important; -} diff --git a/htdocs/mstile-144x144.png b/htdocs/mstile-144x144.png new file mode 100644 index 000000000..224e4ab80 Binary files /dev/null and b/htdocs/mstile-144x144.png differ diff --git a/htdocs/openwebrx.css b/htdocs/openwebrx.css deleted file mode 100644 index 9e75d8441..000000000 --- a/htdocs/openwebrx.css +++ /dev/null @@ -1,973 +0,0 @@ -/* - - This file is part of OpenWebRX, - an open-source SDR receiver software with a web UI. - Copyright (c) 2013-2015 by Andras Retzler - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation, either version 3 of the - License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -*/ - -html, body -{ - margin: 0; - padding: 0; - height: 100%; - font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; - overflow: hidden; -} - -select -{ - font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; -} - -input -{ - vertical-align:middle; -} - -input[type=range] -{ - -webkit-appearance: none; - margin: 0 0; -} -input[type=range]:focus -{ - outline: none; -} - -input[type=range]::-webkit-slider-runnable-track -{ - height: 5px; - cursor: pointer; - animate: 0.2s; - box-shadow: 0px 0px 0px #000000; - background: #B6B6B6; - /*border-radius: 11px;*/ - border: 1px solid #8A8A8A; -} - -input[type=range]::-webkit-slider-thumb -{ - box-shadow: 1px 1px 1px #828282; - border: 1px solid #8A8A8A; - height: 15px; - width: 15px; - border-radius: 10px; - background: #FFFFFF; - cursor: pointer; - -webkit-appearance: none; - margin-top: -7px; -} - -input[type=range]:focus::-webkit-slider-runnable-track -{ - background: #B6B6B6; -} - -input[type=range]::-moz-range-track -{ - height: 3px; - cursor: pointer; - animate: 0.2s; - box-shadow: 0px 0px 0px #000000; - background: #B6B6B6; - border-radius: 11px; - border: 1px solid #8A8A8A; -} - -input[type=range]::-moz-range-thumb -{ - box-shadow: 1px 1px 1px #828282; - border: 1px solid #8A8A8A; - height: 12px; - width: 12px; - border-radius: 10px; - background: #FFFFFF; - cursor: pointer; -} - -input[type=range]::-ms-track -{ - width: 100%; - height: 7px; - cursor: pointer; - animate: 0.2s; - background: transparent; - border-color: transparent; - color: transparent; -} - -input[type=range]::-ms-fill-lower - { - background: #B6B6B6; - border: 1px solid #8A8A8A; - border-radius: 22px; - box-shadow: 0px 0px 0px #000000; -} - -input[type=range]::-ms-fill-upper -{ - background: #B6B6B6; - border: 1px solid #8A8A8A; - border-radius: 22px; - box-shadow: 0px 0px 0px #000000; -} - -input[type=range]::-ms-thumb -{ - box-shadow: 1px 1px 1px #828282; - border: 1px solid #8A8A8A; - height: 24px; - width: 7px; - border-radius: 0px; - background: #FFFFFF; - cursor: pointer; -} - -input[type=range]:focus::-ms-fill-lower -{ - background: #B6B6B6; -} - -input[type=range]:focus::-ms-fill-upper -{ - background: #B6B6B6; -} - -#webrx-top-container -{ - position: relative; - z-index:1000; -} - -.webrx-top-bar-parts -{ - position: absolute; - top: 0px; - left: 0px; - width:100%; - height:67px; -} - -#webrx-top-bar-background -{ - background-color: #808080; - opacity: 0.15; - filter:alpha(opacity=15); -} - -#webrx-top-bar -{ - margin:0; - padding:0; - user-select: none; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; -} - -#webrx-top-logo -{ - position: absolute; - top: 12px; - left: 15px; -} - -#webrx-ha5kfu-top-logo -{ - position: absolute; - top: 15px; - right: 15px; -} - -#webrx-top-photo -{ - width: 100%; - display: block; -} - -#webrx-rx-avatar-background -{ - cursor:pointer; - position: absolute; - left: 285px; - top: 6px; -} - -#webrx-rx-avatar -{ - cursor:pointer; - position: absolute; - left: 289px; - top: 10px; - width: 46px; - height: 46px; -} - -#webrx-top-photo-clip -{ - max-height: 350px; - overflow: hidden; - position: relative; -} - -/*#webrx-bottom-bar -{ - position: absolute; - bottom: 0px; - width: 100%; - height: 117px; - background-image:url(gfx/webrx-bottom-bar.png); -}*/ - -#webrx-page-container -{ - min-height:100%; - position:relative; -} - -/*#webrx-photo-gradient-left -{ - position: absolute; - bottom: 0px; - left: 0px; - background-image:url(gfx/webrx-photo-gradient-corner.png); - width: 59px; - height: 92px; - -} - -#webrx-photo-gradient-middle -{ - position: absolute; - bottom: 0px; - left: 59px; - right: 59px; - height: 92px; - background-image:url(gfx/webrx-photo-gradient-middle.png); -} - -#webrx-photo-gradient-right -{ - position: absolute; - bottom: 0px; - right: 0px; - background-image:url(gfx/webrx-photo-gradient-corner.png); - width: 59px; - height: 92px; - -webkit-transform:scaleX(-1); - -moz-transform:scaleX(-1); - -ms-transform:scaleX(-1); - -o-transform:scaleX(-1); - transform:scaleX(-1); -}*/ - -#webrx-rx-photo-title -{ - position: absolute; - left: 15px; - top: 78px; - color: White; - font-size: 16pt; - text-shadow: 1px 1px 4px #444; - opacity: 1; -} - -#webrx-rx-photo-desc -{ - position: absolute; - left: 15px; - top: 109px; - color: White; - font-size: 10pt; - font-weight: bold; - text-shadow: 0px 0px 6px #444; - opacity: 1; - line-height: 1.5em; -} - -#webrx-rx-photo-desc a -{ - /*color: #007df1;*/ - color: #5ca8ff; - text-shadow: none; - /*text-shadow: 0px 0px 7px #fff;*/ -} - -#webrx-rx-title -{ - white-space:nowrap; - overflow: hidden; - cursor:pointer; - position: absolute; - left: 350px; - top: 13px; - font-family: "DejaVu Sans", Verdana, Geneva, sans-serif; - color: #909090; - font-size: 11pt; - font-weight: bold; -} - -#webrx-rx-desc -{ - white-space:nowrap; - overflow: hidden; - cursor:pointer; - font-size: 10pt; - color: #909090; - position: absolute; - left: 350px; - top: 34px; -} - -#webrx-rx-desc a -{ - color: #909090; - /*text-decoration: none;*/ -} - -#openwebrx-rx-details-arrow -{ - cursor:pointer; - position: absolute; - left: 470px; - top: 51px; -} - -#openwebrx-rx-details-arrow a -{ - margin: 0; - padding: 0; -} - -#openwebrx-rx-details-arrow-down -{ - display:none; -} - -/*canvas#waterfall-canvas -{ - border-style: none; - border-width: 1px; - height: 150px; - width: 100%; -}*/ - -#openwebrx-scale-container -{ - height: 47px; - background-image: url("gfx/openwebrx-scale-background.png"); - background-repeat: repeat-x; - overflow: hidden; - z-index:1000; - position: relative; -} - -#webrx-canvas-container -{ - /*background-image:url('gfx/openwebrx-blank-background-1.jpg');*/ - position: relative; - height: 2000px; - overflow-y: scroll; - overflow-x: hidden; - /*background-color: #646464;*/ - /*background-image: -webkit-linear-gradient(top, rgba(247,247,247,1) 0%, rgba(0,0,0,1) 100%);*/ - background-image: url('gfx/openwebrx-background-cool-blue.png'); - background-repeat: no-repeat; - background-color: #1e5f7f; - cursor: crosshair; -} - -#webrx-canvas-container canvas -{ - position: absolute; - border-style: none; - image-rendering: crisp-edges; - image-rendering: -webkit-optimize-contrast; - /*transition: left 200ms, width 200ms;*/ -} - -#openwebrx-mathbox-container -{ - overflow: none; - display: none; -} - -#openwebrx-phantom-canvas -{ - position: absolute; - width: 0px; - height: 0px; -} - -/*#openwebrx-canvas-gradient-background -{ - overflow: hidden; - width: 100%; - height: 396px; -}*/ - -#openwebrx-log-scroll -{ - /*overflow-y:auto;*/ - height: 125px; - width: 619px -} - -.nano .nano-pane { background: #444; } -.nano .nano-slider { background: #eee !important; } - -#webrx-main-container -{ - position: relative; - width: 100%; - margin: 0; - padding: 0; -} - -.webrx-error -{ - font-weight: bold; - color: #ff6262; -} - -#openwebrx-problems span -{ - background: #ff6262; - padding: 3px; - font-size: 8pt; - color: white; - font-weight: bold; - border-radius: 4px; - -moz-border-radius: 4px; - margin: 0px 2px 0px 2px; -} - -/*#webrx-freq-show -{ - visibility: hidden; - position: absolute; - top: 0px; - left: 0px; - padding: 5px; - font-weight: bold; - border-radius: 10px; - -moz-border-radius: 10px; - background-color: #999999; - color: White; - z-index:9999; /*should be higher? - -}*/ - -/* removed non-free fonts like that: */ -/*@font-face { - font-family: 'unibody_8_pro_regregular'; - src: url('gfx/unibody8pro-regular-webfont.eot'); - src: url('gfx/unibody8pro-regular-webfont.ttf'); - font-weight: normal; - font-style: normal; -}*/ - -@font-face { - font-family: 'expletus-sans-medium'; - src: url('gfx/font-expletus-sans/ExpletusSans-Medium.ttf'); - font-weight: normal; - font-style: normal; -} - -#webrx-actual-freq -{ - width: 100%; - text-align: left; - font-size: 16pt; - font-family: 'expletus-sans-medium'; - padding: 0; - margin: 0; - line-height:22px; - -} - -#webrx-mouse-freq -{ - width: 100%; - text-align: left; - font-size: 10pt; - color: #AAA; - font-family: 'expletus-sans-medium'; - margin-bottom: 5px; -} - - -.openwebrx-panel -{ - transform: perspective( 600px ) rotateX( 90deg ); - visibility: hidden; - background-color: #575757; - padding: 10px; - color: white; - position: fixed; - font-size: 10pt; - border-radius: 15px; - -moz-border-radius: 15px; -} - -.openwebrx-panel a -{ - color: #5ca8ff; - text-shadow: none; -} - -.openwebrx-panel-inner -{ - overflow-y: auto; - overflow-x: hidden; - height: 100%; -} - -.openwebrx-button -{ - background-color: #373737; - padding: 4.2px; - border-radius: 5px; - -moz-border-radius: 5px; - color: White; - font-weight: bold; - margin-right: 1px; - cursor: pointer; - background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) ); - background:-moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% ); - user-select: none; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - display: inline-block; -} - -.openwebrx-button:hover, .openwebrx-demodulator-button.highlighted -{ - /*background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #3F3F3F), color-stop(1, #777777) ); - background:-moz-linear-gradient( center top, #373737 5%, #4F4F4F 100% );*/ - background: #474747; - color: #FFFF50; -} - -.openwebrx-button:active -{ - background: #777777; - color: #FFFF50; -} - -.openwebrx-demodulator-button -{ - width: 38px; - height: 19px; - font-size: 12pt; - text-align: center; -} - -.openwebrx-square-button img -{ - height: 27px; -} - -.openwebrx-round-button -{ - margin-right: -2px; - width: 35px; - height: 35px; - border-radius: 25px; -} - -.openwebrx-round-button img -{ - height: 30px; -} - -.openwebrx-round-button-small -{ - margin-right: -3px; - width: 20px; - height: 20px; - border-radius: 25px; -} - -.openwebrx-round-button-small img -{ - height: 20px; -} - -img.openwebrx-mirror-img -{ - transform: scale(-1, 1); -} - - -.openwebrx-round-rightarrow img -{ - position: relative; - left: 12px; - top: 3px; -} - -.openwebrx-round-leftarrow img -{ - position: relative; - left: 7px; - top: 3px; -} - -#openwebrx-client-log-title -{ - margin-bottom: 5px; - font-weight: bold; -} - -.openwebrx-progressbar -{ - position: relative; - border-radius: 5px; - background-color: #003850; /*#006235;*/ - display: inline-block; - text-align: center; - font-size: 8pt; - font-weight: bold; - text-shadow: 0px 0px 4px #000000; - cursor: default; - user-select: none; - -webkit-touch-callout: none; - -webkit-user-select: none; - -khtml-user-select: none; - -moz-user-select: none; - -ms-user-select: none; -} - -.openwebrx-progressbar-bar -{ - border-radius: 5px; - height: 100%; - width: 100%; -} - -.openwebrx-progressbar-text -{ - position: absolute; - left:0px; - top:4px; - width: inherit; -} - -#openwebrx-panel-status -{ - margin: 0px; - padding: 0px; - background-color:rgba(0, 0, 0, 0); -} - -#openwebrx-panel-status div.openwebrx-progressbar -{ - width: 200px; - height: 20px; -} - -#openwebrx-main-buttons img -{ -} - -#openwebrx-main-buttons ul -{ - display: table; - margin:0; -} - - -#openwebrx-main-buttons ul li -{ - display: table-cell; - padding-left: 5px; - padding-right: 5px; - cursor:pointer; -} - -#openwebrx-main-buttons li:hover -{ - background-color: rgba(255, 255, 255, 0.3); -} - -#openwebrx-main-buttons li:active -{ - background-color: rgba(255, 255, 255, 0.55); -} - - -#openwebrx-main-buttons -{ - position: absolute; - right: 133px; - top: 3px; - margin:0; - color: white; - text-shadow: 0px 0px 4px #000000; - text-align: center; - font-size: 9pt; - font-weight: bold; -} - -#openwebrx-panel-receiver -{ - width:110px; -} - -#openwebrx-mute-on -{ - color: lime; -} - -#openwebrx-mute-off -{ - color: white; -} - -.openwebrx-panel-slider -{ - position: relative; - top: -2px; - width: 95px; -} - -.openwebrx-sliderbtn-img -{ - width: 14px; - position:relative; - top: 1px; -} - -.openwebrx-panel-line -{ - padding-top: 5px; -} - -#openwebrx-smeter-outer -{ - border-color: #888; - border-style: solid; - border-width: 0px; - width: 255px; - height: 7px; - background-color: #373737; - border-radius: 3px; - position: relative; -} -#openwebrx-smeter-bar -{ - transition: all 0.2s linear; - width: 0px; - height: 7px; - background: linear-gradient(to top, #ff5939 , #961700); - position: absolute; - margin: 0; padding: 0; left: 0; - border-radius: 3px; -} - -#openwebrx-smeter-db -{ - color: #aaa; - display: inline-block; - font-size: 10pt; - float: right; - margin-right: 5px; - margin-top: 24px; - font-family: 'expletus-sans-medium'; -} - -#openwebrx-big-grey -{ - position: fixed; - width: 100%; - height: 100%; - margin: 0; - padding: 0; - opacity: 0.8; - background-color: #777; - left: 0; - top: 0; - z-index: 1001; - display: none; - vertical-align: middle; - text-align: center; - color: white; - font-weight: bold; - font-size: 20pt; - cursor: pointer; - transition: opacity 0.3s linear; -} - -#openwebrx-big-grey img -{ - width: 150px; -} - -#openwebrx-digimode-canvas-container -{ - /*margin: -10px -10px 10px -10px;*/ - margin: -10px -10px 0px -10px; - border-radius: 15px; - height: 150px; - background-color: #333; - position: relative; - overflow: hidden; -} - -#openwebrx-digimode-canvas-container canvas -{ - position: absolute; - pointer-events: none; - transition: width 500ms, left 500ms; -} - -#openwebrx-secondary-demod-listbox -{ - width: 201px; - height: 27px; - border-radius: 5px; - background-color: #373737; - color: White; - font-weight: normal; - font-size: 13pt; - margin-right: 1px; - background:-webkit-gradient( linear, left top, left bottom, color-stop(0.0 , #373737), color-stop(1, #4F4F4F) ); - background:-moz-linear-gradient( center top, #373737 0%, #4F4F4F 100% ); - border-color: transparent; - border-width: 0px; - -moz-appearance: none; - padding-left:3px; -} - -#openwebrx-secondary-demod-listbox option -{ - border-width: 0px; - background-color: #373737; - color: White; -} - -#openwebrx-cursor-blink -{ - animation: cursor-blink 1s infinite; - /*animation: cursor-3d 2s infinite;*/ - animation-timing-function: linear; - animation-direction: alternate; - height: 1em; - width: 8px; - background-color: White; - display: inline-block; - position: relative; - top: 1px; - /*perspective: 60px;*/ - -} - -@keyframes cursor-blink -{ - 0%{ opacity: 0; } - 50% { opacity: 1; } - 100%{ opacity: 0; } -} - -@keyframes cursor-3d -{ - 0%{ transform: rotateX(0deg) rotateX(Ydeg); } - 50% { transform: rotateX(180deg) rotateY(360deg); opacity: 0.1; } - 100%{ transform: rotateX(360deg) rotateY(720deg); } -} - -#openwebrx-digimode-content -{ - word-wrap: break-word; - position: absolute; - bottom: 0; - width: 100%; -} - -#openwebrx-digimode-content-container -{ - overflow-y: hidden; - display: block; - height: 50px; - position: relative; -} - -#openwebrx-digimode-content-container .gradient -{ - width: 100%; - height: 20px; - background: linear-gradient(to top, rgba(87,87,87,0) 0%,rgba(87,87,87,1) 100%); - position: absolute; - top: 0; - z-index: 10; -} - - -#openwebrx-digimode-content .part -{ - perspective: 700px; -} - -#openwebrx-digimode-content .part -{ - animation: new-digimode-data-3d 100ms; - animation-timing-function: linear; - display: inline-block; - perspective-origin: 50% 50%; - transform-origin: 0% 50%; -} - -#openwebrx-digimode-content .part .subpart -{ -} - - -@keyframes new-digimode-data -{ - 0%{ opacity: 0; } - 100%{ opacity: 1; } -} - -@keyframes new-digimode-data-3d -{ - 0%{ transform: rotateX(0deg) rotateY(-90deg) translateX(-5px) scale(1.3); } - 100%{ transform: rotateX(0deg) rotateY(0deg) translateX(0) scale(1); } -} - -#openwebrx-digimode-select-channel -{ - transition: all 500ms; - background-color: Yellow; - display: block; - position: absolute; - pointer-events: none; - height: 100%; - width: 0px; - top: 0px; - left: 0px; - opacity: 0.7; - border-style: solid; - border-width: 0px; - border-color: Red; -} - diff --git a/htdocs/openwebrx.js b/htdocs/openwebrx.js index c7fd7c093..1ab27d3f6 100644 --- a/htdocs/openwebrx.js +++ b/htdocs/openwebrx.js @@ -3,6 +3,7 @@ This file is part of OpenWebRX, an open-source SDR receiver software with a web UI. Copyright (c) 2013-2015 by Andras Retzler + Copyright (c) 2019-2021 by Jakob Ketterl This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as @@ -21,255 +22,184 @@ */ -is_firefox=navigator.userAgent.indexOf("Firefox")!=-1; - -function arrayBufferToString(buf) { - //http://stackoverflow.com/questions/6965107/converting-between-strings-and-arraybuffers - return String.fromCharCode.apply(null, new Uint8Array(buf)); -} - -function getFirstChars(buf, num) -{ - var u8buf=new Uint8Array(buf); - var output=String(); - num=Math.min(num,u8buf.length); - for(i=0;i= 0; var bandwidth; var center_freq; -var audio_buffer_current_size_debug=0; -var audio_buffer_all_size_debug=0; -var audio_buffer_current_count_debug=0; -var audio_buffer_current_size=0; var fft_size; -var fft_fps; -var fft_compression="none"; -var fft_codec=new sdrjs.ImaAdpcm(); -var audio_compression="none"; -var waterfall_setup_done=0; -var waterfall_queue = []; -var waterfall_timer; +var fft_compression = "none"; +var fft_codec; +var waterfall_setup_done = 0; var secondary_fft_size; -/*function fade(something,from,to,time_ms,fps) -{ - something.style.opacity=from; - something.fade_i=0; - n_of_iters=time_ms/(1000/fps); - change=(to-from)/(n_of_iters-1); - - something.fade_timer=window.setInterval( - function(){ - if(something.fade_i++= waterfall_max_level) { + if (!which) { + waterfall_min_level = waterfall_max_level -1; + } else { + waterfall_max_level = waterfall_min_level + 1; + } + } + updateWaterfallSliders(); } -function updateVolume() -{ - volume = parseFloat(e("openwebrx-panel-volume").value) / 100; +function updateWaterfallSliders() { + $('#openwebrx-waterfall-color-max') + .val(waterfall_max_level) + .attr('title', 'Waterfall maximum level (' + Math.round(waterfall_max_level) + ' dB)'); + $('#openwebrx-waterfall-color-min') + .val(waterfall_min_level) + .attr('title', 'Waterfall minimum level (' + Math.round(waterfall_min_level) + ' dB)'); } -function toggleMute() -{ - if (mute) { - mute = false; - e("openwebrx-mute-on").id="openwebrx-mute-off"; - e("openwebrx-mute-img").src="gfx/openwebrx-speaker.png"; - e("openwebrx-panel-volume").disabled=false; - e("openwebrx-panel-volume").style.opacity=1.0; - e("openwebrx-panel-volume").value = volumeBeforeMute; - } else { - mute = true; - e("openwebrx-mute-off").id="openwebrx-mute-on"; - e("openwebrx-mute-img").src="gfx/openwebrx-speaker-muted.png"; - e("openwebrx-panel-volume").disabled=true; - e("openwebrx-panel-volume").style.opacity=0.5; - volumeBeforeMute = e("openwebrx-panel-volume").value; - e("openwebrx-panel-volume").value=0; - } - - updateVolume(); -} - -function zoomInOneStep () { zoom_set(zoom_level+1); } -function zoomOutOneStep () { zoom_set(zoom_level-1); } -function zoomInTotal () { zoom_set(zoom_levels.length-1); } -function zoomOutTotal () { zoom_set(0); } -function setSquelchDefault() { e("openwebrx-panel-squelch").value=0; } -function setSquelchToAuto() { e("openwebrx-panel-squelch").value=(getLogSmeterValue(smeter_level)+10).toString(); updateSquelch(); } -function updateSquelch() -{ - var sliderValue=parseInt(e("openwebrx-panel-squelch").value); - var outputValue=(sliderValue==parseInt(e("openwebrx-panel-squelch").min))?0:getLinearSmeterValue(sliderValue); - ws.send("SET squelch_level="+outputValue.toString()); +function waterfallColorsDefault() { + waterfall_min_level = waterfall_min_level_default; + waterfall_max_level = waterfall_max_level_default; + updateWaterfallSliders(); + waterfallColorsContinuousReset(); } -function updateWaterfallColors(which) -{ - wfmax=e("openwebrx-waterfall-color-max"); - wfmin=e("openwebrx-waterfall-color-min"); - if(parseInt(wfmin.value)>=parseInt(wfmax.value)) - { - if(!which) wfmin.value=(parseInt(wfmax.value)-1).toString(); - else wfmax.value=(parseInt(wfmin.value)+1).toString(); - } - waterfall_min_level=parseInt(wfmin.value); - waterfall_max_level=parseInt(wfmax.value); -} -function waterfallColorsDefault() -{ - waterfall_min_level=waterfall_min_level_default; - waterfall_max_level=waterfall_max_level_default; - e("openwebrx-waterfall-color-min").value=waterfall_min_level.toString(); - e("openwebrx-waterfall-color-max").value=waterfall_max_level.toString(); +function waterfallColorsAuto(levels) { + var min_level = levels.min - waterfall_auto_levels.min; + var max_level = levels.max + waterfall_auto_levels.max; + max_level = Math.max(min_level + (waterfall_auto_min_range || 0), max_level); + waterfall_min_level = min_level; + waterfall_max_level = max_level; + updateWaterfallSliders(); } -function waterfallColorsAuto() -{ - e("openwebrx-waterfall-color-min").value=(waterfall_measure_minmax_min-waterfall_auto_level_margin[0]).toString(); - e("openwebrx-waterfall-color-max").value=(waterfall_measure_minmax_max+waterfall_auto_level_margin[1]).toString(); - updateWaterfallColors(0); +var waterfall_continuous = { + min: -150, + max: 0 +}; + +function waterfallColorsContinuousReset() { + waterfall_continuous.min = waterfall_min_level; + waterfall_continuous.max = waterfall_max_level; } -function setSmeterRelativeValue(value) -{ - if(value<0) value=0; - if(value>1.0) value=1.0; - var bar=e("openwebrx-smeter-bar"); - var outer=e("openwebrx-smeter-outer"); - bar.style.width=(outer.offsetWidth*value).toString()+"px"; - bgRed="linear-gradient(to top, #ff5939 , #961700)"; - bgGreen="linear-gradient(to top, #22ff2f , #008908)"; - bgYellow="linear-gradient(to top, #fff720 , #a49f00)"; - bar.style.background=(value>0.9)?bgRed:((value>0.7)?bgYellow:bgGreen); - //bar.style.backgroundColor=(value>0.9)?"#ff5939":((value>0.7)?"#fff720":"#22ff2f"); -} - -function getLogSmeterValue(value) -{ - return 10*Math.log10(value); +function waterfallColorsContinuous(levels) { + if (levels.max > waterfall_continuous.max + 1) { + waterfall_continuous.max += 1; + } else if (levels.max < waterfall_continuous.max - 1) { + waterfall_continuous.max -= .1; + } + if (levels.min < waterfall_continuous.min - 1) { + waterfall_continuous.min -= 1; + } else if (levels.min > waterfall_continuous.min + 1) { + waterfall_continuous.min += .1; + } + waterfallColorsAuto(waterfall_continuous); +} + +function setSmeterRelativeValue(value) { + if (value < 0) value = 0; + if (value > 1.0) value = 1.0; + var $meter = $("#openwebrx-smeter"); + var $bar = $meter.find(".openwebrx-smeter-bar"); + $bar.css({transform: 'translate(' + ((value - 1) * 100) + '%) translateZ(0)'}); + if (value > 0.9) { + // red + $bar.css({background: 'linear-gradient(to top, #ff5939 , #961700)'}); + } else if (value > 0.7) { + // yellow + $bar.css({background: 'linear-gradient(to top, #fff720 , #a49f00)'}); + } else { + // red + $bar.css({background: 'linear-gradient(to top, #22ff2f , #008908)'}); + } } -function getLinearSmeterValue(db_value) -{ - return Math.pow(10,db_value/10); +function setSquelchSliderBackground(val) { + var $slider = $('#openwebrx-panel-receiver .openwebrx-squelch-slider'); + var min = Number($slider.attr('min')); + var max = Number($slider.attr('max')); + var sliderPosition = $slider.val(); + var relative = (val - min) / (max - min); + // use a brighter color when squelch is open + var color = val >= sliderPosition ? '#22ff2f' : '#008908'; + // we don't use the gradient, but separate the colors discretely using css tricks + var style = 'linear-gradient(90deg, ' + color + ', ' + color + ' ' + relative * 100 + '%, #B6B6B6 ' + relative * 100 + '%)'; + $slider.css('--track-background', style); } -function setSmeterAbsoluteValue(value) //the value that comes from `csdr squelch_and_smeter_cc` -{ - var logValue=getLogSmeterValue(value); - var lowLevel=waterfall_min_level-20; - var highLevel=waterfall_max_level+20; - var percent=(logValue-lowLevel)/(highLevel-lowLevel); - setSmeterRelativeValue(percent); - e("openwebrx-smeter-db").innerHTML=logValue.toFixed(1)+" dB"; +function getLogSmeterValue(value) { + return 10 * Math.log10(value); } -function typeInAnimation(element,timeout,what,onFinish) +function setSmeterAbsoluteValue(value) //the value that comes from `csdr squelch_and_smeter_cc` { - if(!what) { onFinish(); return; } - element.innerHTML+=what[0]; - window.setTimeout( function(){typeInAnimation(element,timeout,what.substring(1),onFinish);}, timeout ); + var logValue = getLogSmeterValue(value); + setSquelchSliderBackground(logValue); + var lowLevel = waterfall_min_level - 20; + var highLevel = waterfall_max_level + 20; + var percent = (logValue - lowLevel) / (highLevel - lowLevel); + setSmeterRelativeValue(percent); + $("#openwebrx-smeter-db").html(logValue.toFixed(1) + " dB"); } - - -// ======================================================== -// ================= ANIMATION ROUTINES ================= -// ======================================================== - -function animate(object,style_name,unit,from,to,accel,time_ms,fps,to_exec) -{ - //console.log(object.className); - if(typeof to_exec=="undefined") to_exec=0; - object.style[style_name]=from.toString()+unit; - object.anim_i=0; - n_of_iters=time_ms/(1000/fps); - change=(to-from)/(n_of_iters); - if(typeof object.anim_timer!="undefined") { window.clearInterval(object.anim_timer); } - object.anim_timer=window.setInterval( - function(){ - if(object.anim_i++9||unit!="px") new_val=(to+accel*remain); - else {if(Math.abs(remain)<2) new_val=to; - else new_val=to+remain-(remain/Math.abs(remain));} - object.style[style_name]=new_val.toString()+unit; - } - } - else - {object.style[style_name]=to.toString()+unit; window.clearInterval(object.anim_timer); delete object.anim_timer; } - if(to_exec!=0) to_exec(); - },1000/fps); -} - -function animate_to(object,style_name,unit,to,accel,time_ms,fps,to_exec) -{ - from=parseFloat(style_value(object,style_name)); - animate(object,style_name,unit,from,to,accel,time_ms,fps,to_exec); +function typeInAnimation(element, timeout, what, onFinish) { + if (!what) { + onFinish(); + return; + } + element.innerHTML += what[0]; + window.setTimeout(function () { + typeInAnimation(element, timeout, what.substring(1), onFinish); + }, timeout); } @@ -277,339 +207,29 @@ function animate_to(object,style_name,unit,to,accel,time_ms,fps,to_exec) // ================ DEMODULATOR ROUTINES ================ // ======================================================== -demodulators=[] - -demodulator_color_index=0; -demodulator_colors=["#ffff00", "#00ff00", "#00ffff", "#058cff", "#ff9600", "#a1ff39", "#ff4e39", "#ff5dbd"] -function demodulators_get_next_color() -{ - if(demodulator_color_index>=demodulator_colors.length) demodulator_color_index=0; - return(demodulator_colors[demodulator_color_index++]); -} - -function demod_envelope_draw(range, from, to, color, line) -{ // ____ - // Draws a standard filter envelope like this: _/ \_ - // Parameters are given in offset frequency (Hz). - // Envelope is drawn on the scale canvas. - // A "drag range" object is returned, containing information about the draggable areas of the envelope - // (beginning, ending and the line showing the offset frequency). - if(typeof color == "undefined") color="#ffff00"; //yellow - env_bounding_line_w=5; // - env_att_w=5; // _______ ___env_h2 in px ___|_____ - env_h1=17; // _/| \_ ___env_h1 in px _/ |_ \_ - env_h2=5; // |||env_att_line_w |_env_lineplus - env_lineplus=1; // ||env_bounding_line_w - env_line_click_area=6; - //range=get_visible_freq_range(); - from_px=scale_px_from_freq(from,range); - to_px=scale_px_from_freq(to,range); - if(to_pxwindow.innerWidth)) // out of screen? - { - drag_ranges.beginning={x1:from_px, x2: from_px+env_bounding_line_w+env_att_w}; - drag_ranges.ending={x1:to_px-env_bounding_line_w-env_att_w, x2: to_px}; - drag_ranges.whole_envelope={x1:from_px, x2: to_px}; - drag_ranges.envelope_on_screen=true; - scale_ctx.beginPath(); - scale_ctx.moveTo(from_px,env_h1); - scale_ctx.lineTo(from_px+env_bounding_line_w, env_h1); - scale_ctx.lineTo(from_px+env_bounding_line_w+env_att_w, env_h2); - scale_ctx.lineTo(to_px-env_bounding_line_w-env_att_w, env_h2); - scale_ctx.lineTo(to_px-env_bounding_line_w, env_h1); - scale_ctx.lineTo(to_px, env_h1); - scale_ctx.globalAlpha = 0.3; - scale_ctx.fill(); - scale_ctx.globalAlpha = 1; - scale_ctx.stroke(); - } - if(typeof line != "undefined") // out of screen? - { - line_px=scale_px_from_freq(line,range); - if(!(line_px<0||line_px>window.innerWidth)) - { - drag_ranges.line={x1:line_px-env_line_click_area/2, x2: line_px+env_line_click_area/2}; - drag_ranges.line_on_screen=true; - scale_ctx.moveTo(line_px,env_h1+env_lineplus); - scale_ctx.lineTo(line_px,env_h2-env_lineplus); - scale_ctx.stroke(); - } - } - return drag_ranges; -} - -function demod_envelope_where_clicked(x, drag_ranges, key_modifiers) -{ // Check exactly what the user has clicked based on ranges returned by demod_envelope_draw(). - in_range=function(x,range) { return range.x1<=x&&range.x2>=x; } - dr=demodulator.draggable_ranges; - - if(key_modifiers.shiftKey) - { - //Check first: shift + center drag emulates BFO knob - if(drag_ranges.line_on_screen&&in_range(x,drag_ranges.line)) return dr.bfo; - //Check second: shift + envelope drag emulates PBF knob - if(drag_ranges.envelope_on_screen&&in_range(x,drag_ranges.whole_envelope)) return dr.pbs; - } - if(drag_ranges.envelope_on_screen) - { - // For low and high cut: - if(in_range(x,drag_ranges.beginning)) return dr.beginning; - if(in_range(x,drag_ranges.ending)) return dr.ending; - // Last priority: having clicked anything else on the envelope, without holding the shift key - if(in_range(x,drag_ranges.whole_envelope)) return dr.anything_else; - } - return dr.none; //User doesn't drag the envelope for this demodulator -} - -//******* class demodulator ******* -// this can be used as a base class for ANY demodulator -demodulator=function(offset_frequency) -{ - //console.log("this too"); - this.offset_frequency=offset_frequency; - this.has_audio_output=true; - this.has_text_output=false; - this.envelope={}; - this.color=demodulators_get_next_color(); - this.stop=function(){}; +function getDemodulators() { + return [ + $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator() + ].filter(function(d) { + return !!d; + }); } -//ranges on filter envelope that can be dragged: -demodulator.draggable_ranges={none: 0, beginning:1 /*from*/, ending: 2 /*to*/, anything_else: 3, bfo: 4 /*line (while holding shift)*/, pbs: 5 } //to which parameter these correspond in demod_envelope_draw() - -//******* class demodulator_default_analog ******* -// This can be used as a base for basic audio demodulators. -// It already supports most basic modulations used for ham radio and commercial services: AM/FM/LSB/USB - -demodulator_response_time=50; -//in ms; if we don't limit the number of SETs sent to the server, audio will underrun (possibly output buffer is cleared on SETs in GNU Radio - -function demodulator_default_analog(offset_frequency,subtype) -{ - //console.log("hopefully this happens"); - //http://stackoverflow.com/questions/4152931/javascript-inheritance-call-super-constructor-or-use-prototype-chain - demodulator.call(this,offset_frequency); - this.subtype=subtype; - this.filter={ - min_passband: 100, - high_cut_limit: (audio_server_output_rate/2)-1, //audio_context.sampleRate/2, - low_cut_limit: (-audio_server_output_rate/2)+1 //-audio_context.sampleRate/2 - }; - //Subtypes only define some filter parameters and the mod string sent to server, - //so you may set these parameters in your custom child class. - //Why? As of demodulation is done on the server, difference is mainly on the server side. - this.server_mod=subtype; - if(subtype=="lsb") - { - this.low_cut=-3000; - this.high_cut=-300; - this.server_mod="ssb"; - } - else if(subtype=="usb") - { - this.low_cut=300; - this.high_cut=3000; - this.server_mod="ssb"; - } - else if(subtype=="cw") - { - this.low_cut=700; - this.high_cut=900; - this.server_mod="ssb"; - } - else if(subtype=="nfm") - { - this.low_cut=-4000; - this.high_cut=4000; - } - else if(subtype=="am") - { - this.low_cut=-4000; - this.high_cut=4000; - } - - this.wait_for_timer=false; - this.set_after=false; - this.set=function() - { //set() is a wrapper to call doset(), but it ensures that doset won't execute more frequently than demodulator_response_time. - if(!this.wait_for_timer) - { - this.doset(false); - this.set_after=false; - this.wait_for_timer=true; - timeout_this=this; //http://stackoverflow.com/a/2130411 - window.setTimeout(function() { - timeout_this.wait_for_timer=false; - if(timeout_this.set_after) timeout_this.set(); - },demodulator_response_time); - } - else - { - this.set_after=true; - } - } - - this.doset=function(first_time) - { //this function sends demodulator parameters to the server - ws.send("SET"+((first_time)?" mod="+this.server_mod:"")+ - " low_cut="+this.low_cut.toString()+" high_cut="+this.high_cut.toString()+ - " offset_freq="+this.offset_frequency.toString()); - } - this.doset(true); //we set parameters on object creation - - //******* envelope object ******* - // for drawing the filter envelope above scale - this.envelope.parent=this; - - this.envelope.draw=function(visible_range) - { - this.visible_range=visible_range; - this.drag_ranges=demod_envelope_draw(range, - center_freq+this.parent.offset_frequency+this.parent.low_cut, - center_freq+this.parent.offset_frequency+this.parent.high_cut, - this.color,center_freq+this.parent.offset_frequency); - }; - - // event handlers - this.envelope.drag_start=function(x, key_modifiers) - { - this.key_modifiers=key_modifiers; - this.dragged_range=demod_envelope_where_clicked(x,this.drag_ranges, key_modifiers); - //console.log("dragged_range: "+this.dragged_range.toString()); - this.drag_origin={ - x: x, - low_cut: this.parent.low_cut, - high_cut: this.parent.high_cut, - offset_frequency: this.parent.offset_frequency - }; - return this.dragged_range!=demodulator.draggable_ranges.none; - }; - - this.envelope.drag_move=function(x) - { - dr=demodulator.draggable_ranges; - if(this.dragged_range==dr.none) return false; // we return if user is not dragging (us) at all - freq_change=Math.round(this.visible_range.hps*(x-this.drag_origin.x)); - /*if(this.dragged_range==dr.beginning||this.dragged_range==dr.ending) - { - //we don't let the passband be too small - if(this.parent.low_cut+new_freq_change<=this.parent.high_cut-this.parent.filter.min_passband) this.freq_change=new_freq_change; - else return; - } - var new_value;*/ - - //dragging the line in the middle of the filter envelope while holding Shift does emulate - //the BFO knob on radio equipment: moving offset frequency, while passband remains unchanged - //Filter passband moves in the opposite direction than dragged, hence the minus below. - minus=(this.dragged_range==dr.bfo)?-1:1; - //dragging any other parts of the filter envelope while holding Shift does emulate the PBS knob - //(PassBand Shift) on radio equipment: PBS does move the whole passband without moving the offset - //frequency. - if(this.dragged_range==dr.beginning||this.dragged_range==dr.bfo||this.dragged_range==dr.pbs) - { - //we don't let low_cut go beyond its limits - if((new_value=this.drag_origin.low_cut+minus*freq_change)=this.parent.high_cut) return true; - this.parent.low_cut=new_value; - } - if(this.dragged_range==dr.ending||this.dragged_range==dr.bfo||this.dragged_range==dr.pbs) - { - //we don't let high_cut go beyond its limits - if((new_value=this.drag_origin.high_cut+minus*freq_change)>this.parent.filter.high_cut_limit) return true; - //nor the filter passband be too small - if(new_value-this.parent.low_cutbandwidth/2||new_value<-bandwidth/2) return true; //we don't allow tuning above Nyquist frequency :-) - this.parent.offset_frequency=new_value; - } - //now do the actual modifications: - mkenvelopes(this.visible_range); - this.parent.set(); - //will have to change this when changing to multi-demodulator mode: - e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",center_freq+this.parent.offset_frequency,1e6,4); - return true; - }; - - this.envelope.drag_end=function(x) - { //in this demodulator we've already changed values in the drag_move() function so we shouldn't do too much here. - demodulator_buttons_update(); - to_return=this.dragged_range!=demodulator.draggable_ranges.none; //this part is required for cliking anywhere on the scale to set offset - this.dragged_range=demodulator.draggable_ranges.none; - return to_return; - }; - -} - -demodulator_default_analog.prototype=new demodulator(); function mkenvelopes(visible_range) //called from mkscale { - scale_ctx.clearRect(0,0,scale_ctx.canvas.width,22); //clear the upper part of the canvas (where filter envelopes reside) - for (var i=0;ibandwidth/2||to_what<-bandwidth/2) return; - demodulators[0].offset_frequency=Math.round(to_what); - demodulators[0].set(); - mkenvelopes(get_visible_freq_range()); +function waterfallWidth() { + return $('body').width(); } @@ -620,1120 +240,865 @@ function demodulator_set_offset_frequency(which,to_what) var scale_ctx; var scale_canvas; -function scale_setup() -{ - e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(window.innerWidth/2),1e6,4); - scale_canvas=e("openwebrx-scale-canvas"); - scale_ctx=scale_canvas.getContext("2d"); - scale_canvas.addEventListener("mousedown", scale_canvas_mousedown, false); - scale_canvas.addEventListener("mousemove", scale_canvas_mousemove, false); - scale_canvas.addEventListener("mouseup", scale_canvas_mouseup, false); - resize_scale(); -} - -var scale_canvas_drag_params={ - mouse_down: false, - drag: false, - start_x: 0, - key_modifiers: {shiftKey:false, altKey: false, ctrlKey: false} +function scale_setup() { + scale_canvas = $("#openwebrx-scale-canvas")[0]; + scale_ctx = scale_canvas.getContext("2d"); + scale_canvas.addEventListener("mousedown", scale_canvas_mousedown, false); + scale_canvas.addEventListener("mousemove", scale_canvas_mousemove, false); + scale_canvas.addEventListener("mouseup", scale_canvas_mouseup, false); + resize_scale(); + var frequency_container = $("#openwebrx-frequency-container"); + frequency_container.on("mousemove", frequency_container_mousemove, false); +} + +var scale_canvas_drag_params = { + mouse_down: false, + drag: false, + start_x: 0, + key_modifiers: {shiftKey: false, altKey: false, ctrlKey: false} }; -function scale_canvas_mousedown(evt) -{ - with(scale_canvas_drag_params) - { - mouse_down=true; - drag=false; - start_x=evt.pageX; - key_modifiers.shiftKey=evt.shiftKey; - key_modifiers.altKey=evt.altKey; - key_modifiers.ctrlKey=evt.ctrlKey; - } - evt.preventDefault(); -} - -function scale_offset_freq_from_px(x, visible_range) -{ - if(typeof visible_range === "undefined") visible_range=get_visible_freq_range(); - return (visible_range.start+visible_range.bw*(x/canvas_container.clientWidth))-center_freq; +function scale_canvas_mousedown(evt) { + scale_canvas_drag_params.mouse_down = true; + scale_canvas_drag_params.drag = false; + scale_canvas_drag_params.start_x = evt.pageX; + scale_canvas_drag_params.key_modifiers.shiftKey = evt.shiftKey; + scale_canvas_drag_params.key_modifiers.altKey = evt.altKey; + scale_canvas_drag_params.key_modifiers.ctrlKey = evt.ctrlKey; + evt.preventDefault(); } -function scale_canvas_mousemove(evt) -{ - var event_handled; - if(scale_canvas_drag_params.mouse_down&&!scale_canvas_drag_params.drag&&Math.abs(evt.pageX-scale_canvas_drag_params.start_x)>canvas_drag_min_delta) - //we can use the main drag_min_delta thing of the main canvas - { - scale_canvas_drag_params.drag=true; - //call the drag_start for all demodulators (and they will decide if they're dragged, based on X coordinate) - for (var i=0;i canvas_drag_min_delta) + //we can use the main drag_min_delta thing of the main canvas + { + scale_canvas_drag_params.drag = true; + //call the drag_start for all demodulators (and they will decide if they're dragged, based on X coordinate) + for (i = 0; i < demodulators.length; i++) event_handled |= demodulators[i].envelope.drag_start(evt.pageX, scale_canvas_drag_params.key_modifiers); + scale_canvas.style.cursor = "move"; + } + else if (scale_canvas_drag_params.drag) { + //call the drag_move for all demodulators (and they will decide if they're dragged) + for (i = 0; i < demodulators.length; i++) event_handled |= demodulators[i].envelope.drag_move(evt.pageX); + if (!event_handled) demodulators[0].set_offset_frequency(scale_offset_freq_from_px(evt.pageX)); + } -function get_scale_mark_spacing(range) -{ - out={}; - fcalc=function(freq) - { - out.numlarge=(range.bw/freq); - out.large=canvas_container.clientWidth/out.numlarge; //distance between large markers (these have text) - out.ratio=5; //(ratio-1) small markers exist per large marker - out.small=out.large/out.ratio; //distance between small markers - if(out.small=scale_min_space_bw_small_markers&&freq.toString()[0]!="5") {out.small/=2; out.ratio*=2; } - out.smallbw=freq/out.ratio; - return true; - } - for(i=scale_markers_levels.length-1;i>=0;i--) - { - mp=scale_markers_levels[i]; - if (!fcalc(mp.large_marker_per_hz)) continue; - //console.log(mp.large_marker_per_hz); - //console.log(out); - if (out.large-mp.estimated_text_width>scale_min_space_bw_texts) break; - } - out.params=mp; - return out; -} - -function mkscale() -{ - //clear the lower part of the canvas (where frequency scale resides; the upper part is used by filter envelopes): - range=get_visible_freq_range(); - mkenvelopes(range); //when scale changes we will always have to redraw filter envelopes, too - scale_ctx.clearRect(0,22,scale_ctx.canvas.width,scale_ctx.canvas.height-22); - scale_ctx.strokeStyle = "#fff"; - scale_ctx.font = "bold 11px sans-serif"; - scale_ctx.textBaseline = "top"; - scale_ctx.fillStyle = "#fff"; - spacing=get_scale_mark_spacing(range); - //console.log(spacing); - marker_hz=Math.ceil(range.start/spacing.smallbw)*spacing.smallbw; - text_h_pos=22+10+((is_firefox)?3:0); - var text_to_draw; - var ftext=function(f) {text_to_draw=format_frequency(spacing.params.format,f,spacing.params.pre_divide,spacing.params.decimals);} - var last_large; - for(;;) - { - var x=scale_px_from_freq(marker_hz,range); - if(x>window.innerWidth) break; - scale_ctx.beginPath(); - scale_ctx.moveTo(x, 22); - if(marker_hz%spacing.params.large_marker_per_hz==0) - { //large marker - if(typeof first_large == "undefined") var first_large=marker_hz; - last_large=marker_hz; - scale_ctx.lineWidth=3.5; - scale_ctx.lineTo(x,22+11); - ftext(marker_hz); - var text_measured=scale_ctx.measureText(text_to_draw); - scale_ctx.textAlign = "center"; - //advanced text drawing begins - if( zoom_level==0 && (range.start+spacing.smallbw*spacing.ratio>marker_hz) && (x=scale_min_space_bw_texts) - { //and if we have enough space to draw it correctly without clipping - scale_ctx.textAlign = "left"; - scale_ctx.fillText(text_to_draw, 0, text_h_pos); - } - } - else if( zoom_level==0 && (range.end-spacing.smallbw*spacing.ratiowindow.innerWidth-text_measured.width/2) ) - { // if this is the last overall marker when zoomed out... and if it would be clipped off the screen... - if(window.innerWidth-text_measured.width-scale_px_from_freq(marker_hz-spacing.smallbw*spacing.ratio,range)>=scale_min_space_bw_texts) - { //and if we have enough space to draw it correctly without clipping - scale_ctx.textAlign = "right"; - scale_ctx.fillText(text_to_draw, window.innerWidth, text_h_pos); - } - } - else scale_ctx.fillText(text_to_draw, x, text_h_pos); //draw text normally - } - else - { //small marker - scale_ctx.lineWidth=2; - scale_ctx.lineTo(x,22+8); - } - marker_hz+=spacing.smallbw; - scale_ctx.stroke(); - } - if(zoom_level!=0) - { // if zoomed, we don't want the texts to disappear because their markers can't be seen - // on the left side - scale_ctx.textAlign = "center"; - var f=first_large-spacing.smallbw*spacing.ratio; - var x=scale_px_from_freq(f,range); - ftext(f); - var w=scale_ctx.measureText(text_to_draw).width; - if(x+w/2>0) scale_ctx.fillText(text_to_draw, x, 22+10); - // on the right side - f=last_large+spacing.smallbw*spacing.ratio; - x=scale_px_from_freq(f,range); - ftext(f); - w=scale_ctx.measureText(text_to_draw).width; - if(x-w/23) - { - out=out.substr(0,at)+","+out.substr(at); - at+=4; - decimals-=3; - } - return out; -} - -canvas_drag=false; -canvas_drag_min_delta=1; -canvas_mouse_down=false; - -function canvas_mousedown(evt) -{ - canvas_mouse_down=true; - canvas_drag=false; - canvas_drag_last_x=canvas_drag_start_x=evt.pageX; - canvas_drag_last_y=canvas_drag_start_y=evt.pageY; - evt.preventDefault(); //don't show text selection mouse pointer +function get_visible_freq_range() { + if (!bandwidth) return false; + var fcalc = function (x) { + var canvasWidth = waterfallWidth() * get_zoom(zoom_level); + return Math.round(((-zoom_offset_px + x) / canvasWidth) * bandwidth) + (center_freq - bandwidth / 2); + }; + var out = { + start: fcalc(0), + center: fcalc(waterfallWidth() / 2), + end: fcalc(waterfallWidth()), + } + out.bw = out.end - out.start; + out.hps = out.bw / waterfallWidth(); + return out; } -function canvas_mousemove(evt) -{ - if(!waterfall_setup_done) return; - //element=e("webrx-freq-show"); - relativeX=(evt.offsetX)?evt.offsetX:evt.layerX; - /*realX=(relativeX-element.clientWidth/2); - maxX=(canvases[0].clientWidth-element.clientWidth); - if(realX>maxX) realX=maxX; - if(realX<0) realX=0; - element.style.left=realX.toString()+"px";*/ - if(canvas_mouse_down) - { - if(!canvas_drag&&Math.abs(evt.pageX-canvas_drag_start_x)>canvas_drag_min_delta) - { - canvas_drag=true; - canvas_container.style.cursor="move"; - } - if(canvas_drag) - { - var deltaX=canvas_drag_last_x-evt.pageX; - var deltaY=canvas_drag_last_y-evt.pageY; - //zoom_center_where=zoom_center_where_calc(evt.pageX); - var dpx=range.hps*deltaX; - if( - !(zoom_center_rel+dpx>(bandwidth/2-canvas_container.clientWidth*(1-zoom_center_where)*range.hps)) && - !(zoom_center_rel+dpx<-bandwidth/2+canvas_container.clientWidth*zoom_center_where*range.hps) - ) { zoom_center_rel+=dpx; } -// -((canvases_new_width*(0.5+zoom_center_rel/bandwidth))-(winsize*zoom_center_where)); - resize_canvases(false); - canvas_drag_last_x=evt.pageX; - canvas_drag_last_y=evt.pageY; - mkscale(); - } - } - else e("webrx-mouse-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(relativeX),1e6,4); -} - -function canvas_container_mouseout(evt) -{ - canvas_end_drag(); +var scale_markers_levels = [ + { + "large_marker_per_hz": 10000000, //large + "estimated_text_width": 70, + "format": "{x} MHz", + "pre_divide": 1000000, + "decimals": 0 + }, + { + "large_marker_per_hz": 5000000, + "estimated_text_width": 70, + "format": "{x} MHz", + "pre_divide": 1000000, + "decimals": 0 + }, + { + "large_marker_per_hz": 1000000, + "estimated_text_width": 70, + "format": "{x} MHz", + "pre_divide": 1000000, + "decimals": 0 + }, + { + "large_marker_per_hz": 500000, + "estimated_text_width": 70, + "format": "{x} MHz", + "pre_divide": 1000000, + "decimals": 1 + }, + { + "large_marker_per_hz": 100000, + "estimated_text_width": 70, + "format": "{x} MHz", + "pre_divide": 1000000, + "decimals": 1 + }, + { + "large_marker_per_hz": 50000, + "estimated_text_width": 70, + "format": "{x} MHz", + "pre_divide": 1000000, + "decimals": 2 + }, + { + "large_marker_per_hz": 10000, + "estimated_text_width": 70, + "format": "{x} MHz", + "pre_divide": 1000000, + "decimals": 2 + }, + { + "large_marker_per_hz": 5000, + "estimated_text_width": 70, + "format": "{x} MHz", + "pre_divide": 1000000, + "decimals": 3 + }, + { + "large_marker_per_hz": 1000, + "estimated_text_width": 70, + "format": "{x} MHz", + "pre_divide": 1000000, + "decimals": 1 + } +]; +var scale_min_space_bw_texts = 50; +var scale_min_space_bw_small_markers = 7; + +function get_scale_mark_spacing(range) { + var out = {}; + var fcalc = function (freq) { + out.numlarge = (range.bw / freq); + out.large = waterfallWidth() / out.numlarge; //distance between large markers (these have text) + out.ratio = 5; //(ratio-1) small markers exist per large marker + out.small = out.large / out.ratio; //distance between small markers + if (out.small < scale_min_space_bw_small_markers) return false; + if (out.small / 2 >= scale_min_space_bw_small_markers && freq.toString()[0] !== "5") { + out.small /= 2; + out.ratio *= 2; + } + out.smallbw = freq / out.ratio; + return true; + }; + for (var i = scale_markers_levels.length - 1; i >= 0; i--) { + var mp = scale_markers_levels[i]; + if (!fcalc(mp.large_marker_per_hz)) continue; + //console.log(mp.large_marker_per_hz); + //console.log(out); + if (out.large - mp.estimated_text_width > scale_min_space_bw_texts) break; + } + out.params = mp; + return out; +} + +var range; + +function mkscale() { + //clear the lower part of the canvas (where frequency scale resides; the upper part is used by filter envelopes): + range = get_visible_freq_range(); + if (!range) return; + mkenvelopes(range); //when scale changes we will always have to redraw filter envelopes, too + scale_ctx.clearRect(0, 22, scale_ctx.canvas.width, scale_ctx.canvas.height - 22); + scale_ctx.strokeStyle = "#fff"; + scale_ctx.font = "bold 11px sans-serif"; + scale_ctx.textBaseline = "top"; + scale_ctx.fillStyle = "#fff"; + var spacing = get_scale_mark_spacing(range); + //console.log(spacing); + var marker_hz = Math.ceil(range.start / spacing.smallbw) * spacing.smallbw; + var text_h_pos = 22 + 10 + ((is_firefox) ? 3 : 0); + var text_to_draw = ''; + var ftext = function (f) { + text_to_draw = format_frequency(spacing.params.format, f, spacing.params.pre_divide, spacing.params.decimals); + }; + var last_large; + var x; + while ((x = scale_px_from_freq(marker_hz, range)) <= window.innerWidth) { + scale_ctx.beginPath(); + scale_ctx.moveTo(x, 22); + if (marker_hz % spacing.params.large_marker_per_hz === 0) { //large marker + if (typeof first_large === "undefined") var first_large = marker_hz; + last_large = marker_hz; + scale_ctx.lineWidth = 3.5; + scale_ctx.lineTo(x, 22 + 11); + ftext(marker_hz); + var text_measured = scale_ctx.measureText(text_to_draw); + scale_ctx.textAlign = "center"; + //advanced text drawing begins + if (zoom_level === 0 && (range.start + spacing.smallbw * spacing.ratio > marker_hz) && (x < text_measured.width / 2)) { //if this is the first overall marker when zoomed out... and if it would be clipped off the screen... + if (scale_px_from_freq(marker_hz + spacing.smallbw * spacing.ratio, range) - text_measured.width >= scale_min_space_bw_texts) { //and if we have enough space to draw it correctly without clipping + scale_ctx.textAlign = "left"; + scale_ctx.fillText(text_to_draw, 0, text_h_pos); + } + } + else if (zoom_level === 0 && (range.end - spacing.smallbw * spacing.ratio < marker_hz) && (x > window.innerWidth - text_measured.width / 2)) { // if this is the last overall marker when zoomed out... and if it would be clipped off the screen... + if (window.innerWidth - text_measured.width - scale_px_from_freq(marker_hz - spacing.smallbw * spacing.ratio, range) >= scale_min_space_bw_texts) { //and if we have enough space to draw it correctly without clipping + scale_ctx.textAlign = "right"; + scale_ctx.fillText(text_to_draw, window.innerWidth, text_h_pos); + } + } + else scale_ctx.fillText(text_to_draw, x, text_h_pos); //draw text normally + } + else { //small marker + scale_ctx.lineWidth = 2; + scale_ctx.lineTo(x, 22 + 8); + } + marker_hz += spacing.smallbw; + scale_ctx.stroke(); + } + if (zoom_level !== 0) { // if zoomed, we don't want the texts to disappear because their markers can't be seen + // on the left side + scale_ctx.textAlign = "center"; + var f = first_large - spacing.smallbw * spacing.ratio; + x = scale_px_from_freq(f, range); + ftext(f); + var w = scale_ctx.measureText(text_to_draw).width; + if (x + w / 2 > 0) scale_ctx.fillText(text_to_draw, x, 22 + 10); + // on the right side + f = last_large + spacing.smallbw * spacing.ratio; + x = scale_px_from_freq(f, range); + ftext(f); + w = scale_ctx.measureText(text_to_draw).width; + if (x - w / 2 < window.innerWidth) scale_ctx.fillText(text_to_draw, x, 22 + 10); + } } -//function body_mouseup() { canvas_end_drag(); console.log("body_mouseup"); } -//function window_mouseout() { canvas_end_drag(); console.log("document_mouseout"); } - -function canvas_mouseup(evt) -{ - if(!waterfall_setup_done) return; - relativeX=(evt.offsetX)?evt.offsetX:evt.layerX; - - if(!canvas_drag) - { - //ws.send("SET offset_freq="+canvas_get_freq_offset(relativeX).toString()); - demodulator_set_offset_frequency(0, canvas_get_freq_offset(relativeX)); - e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",canvas_get_frequency(relativeX),1e6,4); - } - else - { - canvas_end_drag(); - } - canvas_mouse_down=false; -} - -function canvas_end_drag() -{ - canvas_container.style.cursor="crosshair"; - canvas_mouse_down=false; +function resize_scale() { + var ratio = window.devicePixelRatio || 1; + var w = window.innerWidth; + var h = 47; + scale_canvas.style.width = w + "px"; + scale_canvas.style.height = h + "px"; + w *= ratio; + h *= ratio; + scale_canvas.width = w; + scale_canvas.height = h; + scale_ctx.scale(ratio, ratio); + mkscale(); + bookmarks.position(); } -function zoom_center_where_calc(screenposX) -{ - //return (screenposX-(window.innerWidth-canvas_container.clientWidth))/canvas_container.clientWidth; - return screenposX/canvas_container.clientWidth; +function canvas_get_freq_offset(relativeX) { + var rel = (relativeX / canvas_container.clientWidth); + return Math.round((bandwidth * rel) - (bandwidth / 2)); } -function canvas_mousewheel(evt) -{ - if(!waterfall_setup_done) return; - //var i=Math.abs(evt.wheelDelta); - //var dir=(i/evt.wheelDelta)<0; - //console.log(evt); - var relativeX=(evt.offsetX)?evt.offsetX:evt.layerX; - var dir=(evt.deltaY/Math.abs(evt.deltaY))>0; - //console.log(dir); - //i/=120; - /*while (i--)*/ zoom_step(dir, relativeX, zoom_center_where_calc(evt.pageX)); - evt.preventDefault(); - //evt.returnValue = false; //disable scrollbar move +function canvas_get_frequency(relativeX) { + return center_freq + canvas_get_freq_offset(relativeX); } -zoom_max_level_hps=33; //Hz/pixel -zoom_levels_count=14; - -function get_zoom_coeff_from_hps(hps) -{ - var shown_bw=(window.innerWidth*hps); - return bandwidth/shown_bw; +function format_frequency(format, freq_hz, pre_divide, decimals) { + var out = format.replace("{x}", (freq_hz / pre_divide).toFixed(decimals)); + var at = out.indexOf(".") + 4; + while (decimals > 3) { + out = out.substr(0, at) + "," + out.substr(at); + at += 4; + decimals -= 3; + } + return out; +} + +var canvas_drag = false; +var canvas_drag_min_delta = 1; +var canvas_mouse_down = false; +var canvas_drag_last_x; +var canvas_drag_last_y; +var canvas_drag_start_x; +var canvas_drag_start_y; + +function canvas_mousedown(evt) { + canvas_mouse_down = true; + canvas_drag = false; + canvas_drag_last_x = canvas_drag_start_x = evt.pageX; + canvas_drag_last_y = canvas_drag_start_y = evt.pageY; + evt.preventDefault(); //don't show text selection mouse pointer +} + +function canvas_mousemove(evt) { + if (!waterfall_setup_done) return; + var relativeX = get_relative_x(evt); + if (canvas_mouse_down) { + if (!canvas_drag && Math.abs(evt.pageX - canvas_drag_start_x) > canvas_drag_min_delta) { + canvas_drag = true; + canvas_container.style.cursor = "move"; + } + if (canvas_drag) { + var deltaX = canvas_drag_last_x - evt.pageX; + var dpx = range.hps * deltaX; + if ( + !(zoom_center_rel + dpx > (bandwidth / 2 - waterfallWidth() * (1 - zoom_center_where) * range.hps)) && + !(zoom_center_rel + dpx < -bandwidth / 2 + waterfallWidth() * zoom_center_where * range.hps) + ) { + zoom_center_rel += dpx; + } + resize_canvases(); + canvas_drag_last_x = evt.pageX; + canvas_drag_last_y = evt.pageY; + mkscale(); + bookmarks.position(); + } + } else { + $('#openwebrx-panel-receiver').demodulatorPanel().setMouseFrequency(canvas_get_frequency(relativeX)); + } } -zoom_levels=[1]; -zoom_level=0; -zoom_freq=0; -zoom_offset_px=0; -zoom_center_rel=0; -zoom_center_where=0; - -smeter_level=0; - -function mkzoomlevels() -{ - zoom_levels=[1]; - maxc=get_zoom_coeff_from_hps(zoom_max_level_hps); - if(maxc<1) return; - // logarithmic interpolation - zoom_ratio = Math.pow(maxc, 1/zoom_levels_count); - for(i=1;i=zoom_levels_count-1)) return; - if(out) --zoom_level; - else ++zoom_level; +function canvas_mouseup(evt) { + if (!waterfall_setup_done) return; + var relativeX = get_relative_x(evt); - zoom_center_rel=canvas_get_freq_offset(where); - //console.log("zoom_step || zlevel: "+zoom_level.toString()+" zlevel_val: "+zoom_levels[zoom_level].toString()+" zoom_center_rel: "+zoom_center_rel.toString()); - zoom_center_where=onscreen; - //console.log(zoom_center_where, zoom_center_rel, where); - resize_canvases(true); - mkscale(); -} - -function zoom_set(level) -{ - if(!(level>=0&&level<=zoom_levels.length-1)) return; - level=parseInt(level); - zoom_level = level; - //zoom_center_rel=canvas_get_freq_offset(-canvases[0].offsetLeft+canvas_container.clientWidth/2); //zoom to screen center instead of demod envelope - zoom_center_rel=demodulators[0].offset_frequency; - zoom_center_where=0.5+(zoom_center_rel/bandwidth); //this is a kind of hack - console.log(zoom_center_where, zoom_center_rel, -canvases[0].offsetLeft+canvas_container.clientWidth/2); - resize_canvases(true); - mkscale(); -} - -function zoom_calc() -{ - winsize=canvas_container.clientWidth; - var canvases_new_width=winsize*zoom_levels[zoom_level]; - zoom_offset_px=-((canvases_new_width*(0.5+zoom_center_rel/bandwidth))-(winsize*zoom_center_where)); - if(zoom_offset_px>0) zoom_offset_px=0; - if(zoom_offset_pxPlease change your operating system default settings in order to fix this.",1); - } - if(audio_server_output_rate >= output_range_min && audio_server_output_rate <= output_range_max) break; //okay, we're done - i++; - } - audio_client_resampling_factor=i; - console.log("audio_calculate_resampling() :: "+audio_client_resampling_factor.toString()+", "+audio_server_output_rate.toString()); -} - - -debug_ws_data_received=0; -max_clients_num=0; - -var COMPRESS_FFT_PAD_N=10; //should be the same as in csdr.c - -function on_ws_recv(evt) -{ - if(!(evt.data instanceof ArrayBuffer)) { divlog("on_ws_recv(): Not ArrayBuffer received...",1); return; } - // - debug_ws_data_received+=evt.data.byteLength/1000; - first4Chars=getFirstChars(evt.data,4); - first3Chars=first4Chars.slice(0,3); - if(first3Chars=="CLI") - { - var stringData=arrayBufferToString(evt.data); - if(stringData.substring(0,16)=="CLIENT DE SERVER") divlog("Server acknowledged WebSocket connection."); - - } - if(first3Chars=="AUD") - { - var audio_data; - if(audio_compression=="adpcm") audio_data=new Uint8Array(evt.data,4) - else audio_data=new Int16Array(evt.data,4); - audio_prepare(audio_data); - audio_buffer_current_size_debug+=audio_data.length; - audio_buffer_all_size_debug+=audio_data.length; - if(!(ios||is_chrome) && (audio_initialized==0 && audio_prepared_buffers.length>audio_buffering_fill_to)) audio_init() - } - else if(first3Chars=="FFT") - { - //alert("Yupee! Doing FFT"); - //if(first4Chars=="FFTS") console.log("FFTS"); - if(fft_compression=="none") waterfall_add_queue(new Float32Array(evt.data,4)); - else if(fft_compression="adpcm") - { - fft_codec.reset(); - - var waterfall_i16=fft_codec.decode(new Uint8Array(evt.data,4)); - var waterfall_f32=new Float32Array(waterfall_i16.length-COMPRESS_FFT_PAD_N); - for(var i=0;i85); - break; - case "clients": - var clients_num=parseInt(param[1]); - progressbar_set(e("openwebrx-bar-clients"),clients_num/max_clients_num,"Clients ["+param[1]+"]",clients_num>max_clients_num*0.85); - break; - case "max_clients": - max_clients_num=parseInt(param[1]); - break; - case "s": - smeter_level=parseFloat(param[1]); - setSmeterAbsoluteValue(smeter_level); - break; - } - } - /*} - catch(err) - { - divlog("Received invalid message over WebSocket."); - }*/ - } - -} - -function add_problem(what) -{ - problems_span=e("openwebrx-problems"); - for(var i=0;iMath.max(fft_fps/2,20)) //in case of emergency - { - console.log("waterfall queue length:", waterfall_queue.length); - add_problem("fft overflow"); - while(waterfall_queue.length) waterfall_add(waterfall_queue.shift()); - } -} +function canvas_mousewheel(evt) { + if (!waterfall_setup_done) return; -function on_ws_opened() -{ - ws.send("SERVER DE CLIENT openwebrx.js"); - divlog("WebSocket opened to "+ws_url); + var delta = -wheelDelta(evt); + var relativeX = get_relative_x(evt); + zoom_step(delta, relativeX, zoom_center_where_calc(evt.pageX)); + evt.preventDefault(); } -var was_error=0; -function divlog(what, is_error) -{ - is_error=!!is_error; - was_error |= is_error; - if(is_error) - { - what=""+what+""; - if(e("openwebrx-panel-log").openwebrxHidden) toggle_panel("openwebrx-panel-log"); //show panel if any error is present - } - e("openwebrx-debugdiv").innerHTML+=what+"
    "; - //var wls=e("openwebrx-log-scroll"); - //wls.scrollTop=wls.scrollHeight; //scroll to bottom - $(".nano").nanoScroller(); - $(".nano").nanoScroller({ scroll: 'bottom' }); -} - -var audio_context; -var audio_initialized=0; -var volume = 1.0; -var volumeBeforeMute = 100.0; -var mute = false; +var zoom_max_level_hps = 33; //Hz/pixel +var zoom_levels_count = 14; -var audio_received = Array(); -var audio_buffer_index = 0; -var audio_resampler; -var audio_codec=new sdrjs.ImaAdpcm(); -var audio_compression="unknown"; -var audio_node; -//var audio_received_sample_rate = 48000; -var audio_input_buffer_size; - -// Optimalise these if audio lags or is choppy: -var audio_buffer_size; -var audio_buffer_maximal_length_sec=3; //actual number of samples are calculated from sample rate -var audio_buffer_decrease_to_on_overrun_sec=2.2; -var audio_flush_interval_ms=500; //the interval in which audio_flush() is called - -var audio_prepared_buffers = Array(); -var audio_rebuffer; -var audio_last_output_buffer; -var audio_last_output_offset = 0; -var audio_buffering = false; -//var audio_buffering_fill_to=4; //on audio underrun we wait until this n*audio_buffer_size samples are present - //tnx to the hint from HA3FLT, now we have about half the response time! (original value: 10) - -function gain_ff(gain_value,data) //great! solved clicking! will have to move to sdr.js -{ - for(var i=0;iaudio_buffering_fill_to) { console.log("buffers now: "+audio_prepared_buffers.length.toString()); audio_buffering=false; } +function get_zoom(level) { + var maxc = get_zoom_coeff_from_hps(zoom_max_level_hps); + if (maxc < 1) return; + // logarithmic interpolation + var zoom_ratio = Math.pow(maxc, 1 / zoom_levels_count); + return Math.pow(zoom_ratio, level); } +function zoom_step(delta, where, onscreen) { + zoom_level += delta; + if (zoom_level < 0) { + zoom_level = 0; + } else if (zoom_level > zoom_levels_count) { + zoom_level = zoom_levels_count; + } -function audio_prepare_without_resampler(data) -{ - audio_rebuffer.push(sdrjs.ConvertI16_F(data)); - console.log("prepare",data.length,audio_rebuffer.remaining()); - while(audio_rebuffer.remaining()) - { - audio_prepared_buffers.push(audio_rebuffer.take()); - audio_buffer_current_count_debug++; - } - if(audio_buffering && audio_prepared_buffers.length>audio_buffering_fill_to) audio_buffering=false; + zoom_center_rel = canvas_get_freq_offset(where); + zoom_center_where = onscreen; + resize_canvases(); + mkscale(); + bookmarks.position(); } -function audio_prepare_old(data) -{ - //console.log("audio_prepare :: "+data.length.toString()); - //console.log("data.len = "+data.length.toString()); - var dopush=function() - { - console.log(audio_last_output_buffer); - audio_prepared_buffers.push(audio_last_output_buffer); - audio_last_output_offset=0; - audio_last_output_buffer=new Float32Array(audio_buffer_size); - audio_buffer_current_count_debug++; - }; - - var original_data_length=data.length; - var f32data=new Float32Array(data.length); - for(var i=0;iaudio_buffering_fill_to) audio_buffering=false; -} - -if (!AudioBuffer.prototype.copyToChannel) -{ //Chrome 36 does not have it, Firefox does - AudioBuffer.prototype.copyToChannel=function(input,channel) //input is Float32Array - { - var cd=this.getChannelData(channel); - for(var i=0;i zoom_levels_count) { + zoom_level = zoom_levels_count; + } else { + zoom_level = parseFloat(level); + } + //zoom_center_rel=canvas_get_freq_offset(-canvases[0].offsetLeft+waterfallWidth()/2); //zoom to screen center instead of demod envelope + var demod = $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator(); + if (demod) { + zoom_center_rel = demod.get_offset_frequency(); + } + zoom_center_where = 0.5 + (zoom_center_rel / bandwidth); //this is a kind of hack + resize_canvases(); + mkscale(); + bookmarks.position(); } -var audio_buffer_progressbar_update_disabled=false; - -var audio_buffer_total_average_level=0; -var audio_buffer_total_average_level_length=0; -var audio_overrun_cnt = 0; -var audio_underrun_cnt = 0; - -function audio_buffer_progressbar_update() -{ - if(audio_buffer_progressbar_update_disabled) return; - var audio_buffer_value=(audio_prepared_buffers.length*audio_buffer_size)/audio_context.sampleRate; - audio_buffer_total_average_level_length++; audio_buffer_total_average_level=(audio_buffer_total_average_level*((audio_buffer_total_average_level_length-1)/audio_buffer_total_average_level_length))+(audio_buffer_value/audio_buffer_total_average_level_length); - var overrun=audio_buffer_value>audio_buffer_maximal_length_sec; - var underrun=audio_prepared_buffers.length==0; - var text="buffer"; - if(overrun) { text="overrun"; console.log("audio overrun, "+(++audio_overrun_cnt).toString()); } - if(underrun) { text="underrun"; console.log("audio underrun, "+(++audio_underrun_cnt).toString()); } - if(overrun||underrun) - { - audio_buffer_progressbar_update_disabled=true; - window.setTimeout(function(){audio_buffer_progressbar_update_disabled=false; audio_buffer_progressbar_update();},1000); - } - progressbar_set(e("openwebrx-bar-audio-buffer"),(underrun)?1:audio_buffer_value/1.5,"Audio "+text+" ["+(audio_buffer_value).toFixed(1)+" s]",overrun||underrun||audio_buffer_value<0.25); +function zoom_calc() { + var winsize = waterfallWidth(); + var canvases_new_width = winsize * get_zoom(zoom_level); + zoom_offset_px = -((canvases_new_width * (0.5 + zoom_center_rel / bandwidth)) - (winsize * zoom_center_where)); + if (zoom_offset_px > 0) zoom_offset_px = 0; + if (zoom_offset_px < winsize - canvases_new_width) + zoom_offset_px = winsize - canvases_new_width; } +var networkSpeedMeasurement; +var currentprofile = { + toString: function() { + return this['sdr_id'] + '|' + this['profile_id']; + } +}; +var COMPRESS_FFT_PAD_N = 10; //should be the same as in csdr.c + +function on_ws_recv(evt) { + if (typeof evt.data === 'string') { + // text messages + networkSpeedMeasurement.add(evt.data.length); + + if (evt.data.substr(0, 16) === "CLIENT DE SERVER") { + params = Object.fromEntries( + evt.data.slice(17).split(' ').map(function(param) { + var args = param.split('='); + return [args[0], args.slice(1).join('=')] + }) + ); + var versionInfo = 'Unknown server'; + if (params.server && params.server === 'openwebrx' && params.version) { + versionInfo = 'OpenWebRX version: ' + params.version; + } + divlog('Server acknowledged WebSocket connection, ' + versionInfo); + } else { + try { + var json = JSON.parse(evt.data); + switch (json.type) { + case "config": + var config = json['value']; + if ('waterfall_colors' in config) + waterfall_colors = buildWaterfallColors(config['waterfall_colors']); + if ('waterfall_levels' in config) { + waterfall_min_level_default = config['waterfall_levels']['min']; + waterfall_max_level_default = config['waterfall_levels']['max']; + } + if ('waterfall_auto_levels' in config) + waterfall_auto_levels = config['waterfall_auto_levels']; + if ('waterfall_auto_min_range' in config) + waterfall_auto_min_range = config['waterfall_auto_min_range']; + if ('waterfall_auto_level_default_mode' in config) + waterfall_measure_minmax_continuous = config['waterfall_auto_level_default_mode']; + var waterfallAutoButton = $('#openwebrx-waterfall-colors-auto'); + waterfallAutoButton[waterfall_measure_minmax_continuous ? 'addClass' : 'removeClass']('highlighted'); + $('#openwebrx-waterfall-color-min, #openwebrx-waterfall-color-max').prop('disabled', waterfall_measure_minmax_continuous); + + waterfallColorsDefault(); + + var initial_demodulator_params = {}; + if ('start_mod' in config) + initial_demodulator_params['mod'] = config['start_mod']; + if ('start_offset_freq' in config) + initial_demodulator_params['offset_frequency'] = config['start_offset_freq']; + if ('initial_squelch_level' in config) + initial_demodulator_params['squelch_level'] = Number.isInteger(config['initial_squelch_level']) ? config['initial_squelch_level'] : -150; + + if ('samp_rate' in config) + bandwidth = config['samp_rate']; + if ('center_freq' in config) + center_freq = config['center_freq']; + if ('fft_size' in config) { + fft_size = config['fft_size']; + waterfall_clear(); + } + if ('audio_compression' in config) { + var audio_compression = config['audio_compression']; + audioEngine.setCompression(audio_compression); + divlog("Audio stream is " + ((audio_compression === "adpcm") ? "compressed" : "uncompressed") + "."); + } + if ('fft_compression' in config) { + fft_compression = config['fft_compression']; + divlog("FFT stream is " + ((fft_compression === "adpcm") ? "compressed" : "uncompressed") + "."); + } + if ('max_clients' in config) + $('#openwebrx-bar-clients').progressbar().setMaxClients(config['max_clients']); + + waterfall_init(); + + var demodulatorPanel = $('#openwebrx-panel-receiver').demodulatorPanel(); + demodulatorPanel.setCenterFrequency(center_freq); + demodulatorPanel.setInitialParams(initial_demodulator_params); + if ('squelch_auto_margin' in config) + demodulatorPanel.setSquelchMargin(config['squelch_auto_margin']); + bookmarks.loadLocalBookmarks(); + + if ('sdr_id' in config || 'profile_id' in config) { + currentprofile['sdr_id'] = config['sdr_id'] || currentprofile['sdr_id']; + currentprofile['profile_id'] = config['profile_id'] || currentprofile['profile_id']; + $('#openwebrx-sdr-profiles-listbox').val(currentprofile.toString()); + + waterfall_clear(); + zoom_set(0); + } + + if ('tuning_precision' in config) + $('#openwebrx-panel-receiver').demodulatorPanel().setTuningPrecision(config['tuning_precision']); + + if ('aircraft_tracking_service' in config) + $('#openwebrx-panel-adsb-message').adsbMessagePanel().setAircraftTrackingService(config['aircraft_tracking_service']); -function audio_flush() -{ - flushed=false; - we_have_more_than=function(sec){ return sec*audio_context.sampleRate' + profile['name'] + ""; + }).join("")); + $('#openwebrx-sdr-profiles-listbox').val(currentprofile.toString()); + // this is a bit hacky since it only makes sense if the error is actually "no sdr devices" + // the only other error condition for which the overlay is used right now is "too many users" + // so there shouldn't be a problem here + if (Object.keys(json['value']).length) { + $('#openwebrx-error-overlay').hide(); + } + break; + case "features": + Modes.setFeatures(json['value']); + $('#openwebrx-panel-metadata-wfm').metaPanel().each(function() { + this.setEnabled(!!json.value.redsea); + }); + break; + case "metadata": + $('.openwebrx-meta-panel').metaPanel().each(function(){ + this.update(json['value']); + }); + break; + case "dial_frequencies": + var as_bookmarks = json['value'].map(function (d) { + return { + name: d['mode'].toUpperCase(), + modulation: d['mode'], + frequency: d['frequency'], + underlying: d['underlying'] + }; + }); + bookmarks.replace_bookmarks(as_bookmarks, 'dial_frequencies'); + break; + case "bookmarks": + bookmarks.replace_bookmarks(json['value'], "server"); + break; + case "sdr_error": + divlog(json['value'], true); + var $overlay = $('#openwebrx-error-overlay'); + $overlay.find('.errormessage').text(json['value']); + $overlay.show(); + $("#openwebrx-panel-receiver").demodulatorPanel().stopDemodulator(); + break; + case "demodulator_error": + divlog(json['value'], true); + break; + case 'secondary_demod': + var value = json['value']; + var panels = ['wsjt', 'packet', 'pocsag', 'adsb', 'ism', 'hfdl', 'vdl2'].map(function(id) { + return $('#openwebrx-panel-' + id + '-message')[id + 'MessagePanel'](); + }); + panels.push($('#openwebrx-panel-js8-message').js8()); + if (!panels.some(function(panel) { + if (!panel.supportsMessage(value)) return false; + panel.pushMessage(value); + return true; + })) { + secondary_demod_push_data(value); + } + break; + case 'log_message': + divlog(json['value'], true); + break; + case 'backoff': + divlog("Server is currently busy: " + json['reason'], true); + var $overlay = $('#openwebrx-error-overlay'); + $overlay.find('.errormessage').text(json['reason']); + $overlay.show(); + // set a higher reconnection timeout right away to avoid additional load + reconnect_timeout = 16000; + break; + case 'modes': + Modes.setModes(json['value']); + break; + default: + console.warn('received message of unknown type: ' + json['type']); + } + } catch (e) { + // don't lose exception + console.error(e) + } + } + } else if (evt.data instanceof ArrayBuffer) { + // binary messages + networkSpeedMeasurement.add(evt.data.byteLength); + + var type = new Uint8Array(evt.data, 0, 1)[0]; + var data = evt.data.slice(1); + + var waterfall_i16; + var waterfall_f32; + var i; + + switch (type) { + case 1: + // FFT data + if (fft_compression === "none") { + waterfall_add(new Float32Array(data)); + } else if (fft_compression === "adpcm") { + fft_codec.reset(); + + waterfall_i16 = fft_codec.decode(new Uint8Array(data)); + waterfall_f32 = new Float32Array(waterfall_i16.length - COMPRESS_FFT_PAD_N); + for (i = 0; i < waterfall_i16.length; i++) waterfall_f32[i] = waterfall_i16[i + COMPRESS_FFT_PAD_N] / 100; + waterfall_add(waterfall_f32); + } + break; + case 2: + // audio data + audioEngine.pushAudio(data); + break; + case 3: + // secondary FFT + if (fft_compression === "none") { + secondary_demod_waterfall_add(new Float32Array(data)); + } else if (fft_compression === "adpcm") { + fft_codec.reset(); + + waterfall_i16 = fft_codec.decode(new Uint8Array(data)); + waterfall_f32 = new Float32Array(waterfall_i16.length - COMPRESS_FFT_PAD_N); + for (i = 0; i < waterfall_i16.length; i++) waterfall_f32[i] = waterfall_i16[i + COMPRESS_FFT_PAD_N] / 100; + secondary_demod_waterfall_add(waterfall_f32); + } + break; + case 4: + // hd audio data + audioEngine.pushHdAudio(data); + break; + default: + console.warn('unknown type of binary message: ' + type) + } + } } - -function audio_onprocess_notused(e) -{ - //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js - if(audio_received.length==0) - { add_problem("audio underrun"); return; } - output = e.outputBuffer.getChannelData(0); - int_buffer = audio_received[0]; - read_remain = audio_buffer_size; - //audio_buffer_maximal_length=120; - - obi=0; //output buffer index - debug_str="" - while(1) - { - if(int_buffer.length-audio_buffer_index>read_remain) - { - for (i=audio_buffer_index; i"+read_remain.toString()+" obi="+obi.toString()+"\n"; - audio_buffer_index+=read_remain; - break; - } - else - { - for (i=audio_buffer_index; iaudio_buffer_maximal_length) - { - add_problem("audio overrun"); - audio_received.splice(0,audio_received.length-audio_buffer_maximal_length); - } - else*/ - audio_received.splice(0,1); - //debug_str+="added remain, remain="+read_remain.toString()+" abi="+audio_buffer_index.toString()+" alen="+int_buffer.length.toString()+" i="+i.toString()+" arecva="+audio_received.length.toString()+" obi="+obi.toString()+"\n"; - audio_buffer_index = 0; - if(audio_received.length == 0 || read_remain == 0) return; - int_buffer = audio_received[0]; - } - } - //debug_str+="obi="+obi.toString(); - //alert(debug_str); -} - -function audio_flush_notused() -{ - if (audio_buffer_current_size>audio_buffer_maximal_length_sec*audio_context.sampleRate) - { - add_problem("audio overrun"); - console.log("audio_flush() :: size: "+audio_buffer_current_size.toString()+" allowed: "+(audio_buffer_maximal_length_sec*audio_context.sampleRate).toString()); - while (audio_buffer_current_size>audio_buffer_maximal_length_sec*audio_context.sampleRate*0.5) - { - audio_buffer_current_size-=audio_received[0].length; - audio_received.splice(0,1); - } - } -} - -function webrx_set_param(what, value) -{ - ws.send("SET "+what+"="+value.toString()); +function waterfall_measure_minmax_do(what) { + // Get visible range + var range = get_visible_freq_range(); + var overallStart = center_freq - bandwidth / 2; + var center = (range.center - overallStart) / bandwidth; + + // 80% of visible range + // this is based on an oversampling factor of about 1,25 + var bw = .4 * (range.bw / bandwidth); + var data = what.slice((center - bw) * what.length, (center + bw) * what.length); + + return { + min: Math.min.apply(Math, data), + max: Math.max.apply(Math, data) + }; +} + +function on_ws_opened() { + $('#openwebrx-error-overlay').hide(); + ws.send("SERVER DE CLIENT client=openwebrx.js type=receiver"); + divlog("WebSocket opened to " + ws.url); + if (!networkSpeedMeasurement) { + networkSpeedMeasurement = new Measurement(); + networkSpeedMeasurement.report(60000, 1000, function(rate){ + $('#openwebrx-bar-network-speed').progressbar().setSpeed(rate); + }); + } else { + networkSpeedMeasurement.reset(); + } + reconnect_timeout = false; + ws.send(JSON.stringify({ + "type": "connectionproperties", + "params": { + "output_rate": audioEngine.getOutputRate(), + "hd_output_rate": audioEngine.getHdOutputRate() + } + })); +} + +var was_error = 0; + +function divlog(what, is_error) { + is_error = !!is_error; + was_error |= is_error; + if (is_error) { + what = "" + what + ""; + toggle_panel("openwebrx-panel-log", true); //show panel if any error is present + } + $('#openwebrx-debugdiv')[0].innerHTML += what + "
    "; + var nano = $('.nano'); + nano.nanoScroller(); + nano.nanoScroller({scroll: 'bottom'}); } -var starting_mute = false; +var volumeBeforeMute = 100.0; +var mute = false; -function parsehash() -{ - if(h=window.location.hash) - { - h.substring(1).split(",").forEach(function(x){ - harr=x.split("="); - //console.log(harr); - if(harr[0]=="mute") toggleMute(); - else if(harr[0]=="mod") starting_mod = harr[1]; - else if(harr[0]=="sql") - { - e("openwebrx-panel-squelch").value=harr[1]; - updateSquelch(); - } - else if(harr[0]=="freq") - { - console.log(parseInt(harr[1])); - console.log(center_freq); - starting_offset_frequency = parseInt(harr[1])-center_freq; - } - }); - - } -} - -function audio_preinit() -{ - try - { - window.AudioContext = window.AudioContext||window.webkitAudioContext; - audio_context = new AudioContext(); - } - catch(e) - { - divlog('Your browser does not support Web Audio API, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser.', 1); - return; - } +// Optimalise these if audio lags or is choppy: +var audio_buffer_maximal_length_sec = 1; //actual number of samples are calculated from sample rate - if(audio_context.sampleRate<44100*2) - audio_buffer_size = 4096; - else if(audio_context.sampleRate>=44100*2 && audio_context.sampleRate<44100*4) - audio_buffer_size = 4096 * 2; - else if(audio_context.sampleRate>44100*4) - audio_buffer_size = 4096 * 4; +function onAudioStart(apiType){ + divlog('Web Audio API succesfully initialized, using ' + apiType + ' API, sample rate: ' + audioEngine.getSampleRate() + " Hz"); - audio_rebuffer = new sdrjs.Rebuffer(audio_buffer_size,sdrjs.REBUFFER_FIXED); - audio_last_output_buffer = new Float32Array(audio_buffer_size); + hideOverlay(); - //we send our setup packet - parsehash(); + // canvas_container is set after waterfall_init() has been called. we cannot initialize before. + //if (canvas_container) synchronize_demodulator_init(); - audio_calculate_resampling(audio_context.sampleRate); - audio_resampler = new sdrjs.RationalResamplerFF(audio_client_resampling_factor,1); - ws.send("SET output_rate="+audio_server_output_rate.toString()+" action=start"); //now we'll get AUD packets as well + //hide log panel in a second (if user has not hidden it yet) + window.setTimeout(function () { + toggle_panel("openwebrx-panel-log", !!was_error); + }, 2000); + //Synchronise volume with slider + updateVolume(); } -function audio_init() -{ - if(is_chrome) audio_context.resume() - if(starting_mute) toggleMute(); - - if(audio_client_resampling_factor==0) return; //if failed to find a valid resampling factor... - - audio_debug_time_start=(new Date()).getTime(); - audio_debug_time_last_start=audio_debug_time_start; - - //https://github.com/0xfe/experiments/blob/master/www/tone/js/sinewave.js - audio_initialized=1; // only tell on_ws_recv() not to call it again - - - //on Chrome v36, createJavaScriptNode has been replaced by createScriptProcessor - createjsnode_function = (audio_context.createJavaScriptNode == undefined)?audio_context.createScriptProcessor.bind(audio_context):audio_context.createJavaScriptNode.bind(audio_context); - audio_node = createjsnode_function(audio_buffer_size, 0, 1); - audio_node.onaudioprocess = audio_onprocess; - audio_node.connect(audio_context.destination); - // --- Resampling --- - //https://github.com/grantgalitz/XAudioJS/blob/master/XAudioServer.js - //audio_resampler = new Resampler(audio_received_sample_rate, audio_context.sampleRate, 1, audio_buffer_size, true); - //audio_input_buffer_size = audio_buffer_size*(audio_received_sample_rate/audio_context.sampleRate); - webrx_set_param("audio_rate",audio_context.sampleRate); //Don't try to resample //TODO remove this - - window.setInterval(audio_flush,audio_flush_interval_ms); - divlog('Web Audio API succesfully initialized, sample rate: '+audio_context.sampleRate.toString()+ " sps"); - /*audio_source=audio_context.createBufferSource(); - audio_buffer = audio_context.createBuffer(xhr.response, false); - audio_source.buffer = buffer; - audio_source.noteOn(0);*/ - demodulator_analog_replace(starting_mod); - if(starting_offset_frequency) - { - demodulators[0].offset_frequency = starting_offset_frequency; - e("webrx-actual-freq").innerHTML=format_frequency("{x} MHz",center_freq+starting_offset_frequency,1e6,4); - demodulators[0].set(); - mkscale(); - } - - //hide log panel in a second (if user has not hidden it yet) - window.setTimeout(function(){ - if(typeof e("openwebrx-panel-log").openwebrxHidden == "undefined" && !was_error) - { - toggle_panel("openwebrx-panel-log"); - //animate(e("openwebrx-panel-log"),"opacity","",1,0,0.9,1000,60); - //window.setTimeout(function(){toggle_panel("openwebrx-panel-log");e("openwebrx-panel-log").style.opacity="1";},1200) - } - },2000); - -} - -function on_ws_closed() -{ - try - { - audio_node.disconnect(); - } - catch (dont_care) {} - divlog("WebSocket has closed unexpectedly. Please reload the page.", 1); +var reconnect_timeout = false; + +function on_ws_closed() { + var demodulatorPanel = $("#openwebrx-panel-receiver").demodulatorPanel(); + demodulatorPanel.stopDemodulator(); + demodulatorPanel.resetInitialParams(); + if (reconnect_timeout) { + // max value: roundabout 8 and a half minutes + reconnect_timeout = Math.min(reconnect_timeout * 2, 512000); + } else { + // initial value: 1s + reconnect_timeout = 1000; + } + divlog("WebSocket has closed unexpectedly. Attempting to reconnect in " + reconnect_timeout / 1000 + " seconds...", 1); + + setTimeout(open_websocket, reconnect_timeout); } -function on_ws_error(event) -{ - divlog("WebSocket error.",1); +function on_ws_error() { + divlog("WebSocket error.", 1); } -String.prototype.startswith=function(str){ return this.indexOf(str) == 0; }; //http://stackoverflow.com/questions/646628/how-to-check-if-a-string-startswith-another-string +var ws; -function open_websocket() -{ - //if(ws_url.startswith("ws://localhost:")&&window.location.hostname!="127.0.0.1"&&window.location.hostname!="localhost") - //{ - //divlog("Server administrator should set server_hostname correctly, because it is left as \"localhost\". Now guessing hostname from page URL.",1); - ws_url="ws://"+(window.location.origin.split("://")[1])+"/ws/"; //guess automatically -> now default behaviour - //} - if (!("WebSocket" in window)) - divlog("Your browser does not support WebSocket, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser."); - ws = new WebSocket(ws_url+client_id); - ws.onopen = on_ws_opened; - ws.onmessage = on_ws_recv; - ws.onclose = on_ws_closed; - ws.binaryType = "arraybuffer"; - window.onbeforeunload = function() { //http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript - ws.onclose = function () {}; - ws.close(); - }; - ws.onerror = on_ws_error; -} - -function waterfall_mkcolor(db_value, waterfall_colors_arg) -{ - if(typeof waterfall_colors_arg === 'undefined') waterfall_colors_arg = waterfall_colors; - if(db_valuewaterfall_max_level) db_value=waterfall_max_level; - full_scale=waterfall_max_level-waterfall_min_level; - relative_value=db_value-waterfall_min_level; - value_percent=relative_value/full_scale; - percent_for_one_color=1/(waterfall_colors_arg.length-1); - index=Math.floor(value_percent/percent_for_one_color); - remain=(value_percent-percent_for_one_color*index)/percent_for_one_color; - return color_between(waterfall_colors_arg[index+1],waterfall_colors_arg[index],remain); -} - -function color_between(first, second, percent) -{ - output=0; - for(i=0;i<4;i++) - { - add = ((((first&(0xff<<(i*8)))>>>0)*percent) + (((second&(0xff<<(i*8)))>>>0)*(1-percent))) & (0xff<<(i*8)); - output |= add>>>0; - } - return output>>>0; +function open_websocket() { + var protocol = window.location.protocol.match(/https/) ? 'wss' : 'ws'; + + var href = window.location.href; + var index = href.lastIndexOf('/'); + if (index > 0) { + href = href.substr(0, index + 1); + } + href = href.split("://")[1]; + href = protocol + "://" + href; + if (!href.endsWith('/')) { + href += '/'; + } + var ws_url = href + "ws/"; + + if (!("WebSocket" in window)) + divlog("Your browser does not support WebSocket, which is required for WebRX to run. Please upgrade to a HTML5 compatible browser."); + ws = new WebSocket(ws_url); + ws.onopen = on_ws_opened; + ws.onmessage = on_ws_recv; + ws.onclose = on_ws_closed; + ws.binaryType = "arraybuffer"; + window.onbeforeunload = function () { //http://stackoverflow.com/questions/4812686/closing-websocket-correctly-html5-javascript + ws.onclose = function () { + }; + ws.close(); + }; + ws.onerror = on_ws_error; +} + +function waterfall_mkcolor(db_value, waterfall_colors_arg) { + waterfall_colors_arg = waterfall_colors_arg || waterfall_colors; + var value_percent = (db_value - waterfall_min_level) / (waterfall_max_level - waterfall_min_level); + value_percent = Math.max(0, Math.min(1, value_percent)); + + var scaled = value_percent * (waterfall_colors_arg.length - 1); + var index = Math.floor(scaled); + var remain = scaled - index; + if (remain === 0) return waterfall_colors_arg[index]; + return color_between(waterfall_colors_arg[index], waterfall_colors_arg[index + 1], remain);} + +function color_between(first, second, percent) { + return [ + first[0] + percent * (second[0] - first[0]), + first[1] + percent * (second[1] - first[1]), + first[2] + percent * (second[2] - first[2]) + ]; } @@ -1741,1001 +1106,478 @@ var canvas_context; var canvases = []; var canvas_default_height = 200; var canvas_container; -var canvas_phantom; - -function add_canvas() -{ - var new_canvas = document.createElement("canvas"); - new_canvas.width=fft_size; - new_canvas.height=canvas_default_height; - canvas_actual_line=canvas_default_height-1; - new_canvas.style.width=(canvas_container.clientWidth*zoom_levels[zoom_level]).toString()+"px"; - new_canvas.style.left=zoom_offset_px.toString()+"px"; - new_canvas.style.height=canvas_default_height.toString()+"px"; - new_canvas.openwebrx_top=(-canvas_default_height+1); - new_canvas.style.top=new_canvas.openwebrx_top.toString()+"px"; - canvas_context = new_canvas.getContext("2d"); - canvas_container.appendChild(new_canvas); - new_canvas.addEventListener("mouseover", canvas_mouseover, false); - new_canvas.addEventListener("mouseout", canvas_mouseout, false); - new_canvas.addEventListener("mousemove", canvas_mousemove, false); - new_canvas.addEventListener("mouseup", canvas_mouseup, false); - new_canvas.addEventListener("mousedown", canvas_mousedown, false); - new_canvas.addEventListener("wheel",canvas_mousewheel, false); - canvases.push(new_canvas); -} - - -function init_canvas_container() -{ - canvas_container=e("webrx-canvas-container"); - mathbox_container=e("openwebrx-mathbox-container"); - canvas_container.addEventListener("mouseout",canvas_container_mouseout, false); - //window.addEventListener("mouseout",window_mouseout,false); - //document.body.addEventListener("mouseup",body_mouseup,false); - canvas_phantom=e("openwebrx-phantom-canvas"); - canvas_phantom.addEventListener("mouseover", canvas_mouseover, false); - canvas_phantom.addEventListener("mouseout", canvas_mouseout, false); - canvas_phantom.addEventListener("mousemove", canvas_mousemove, false); - canvas_phantom.addEventListener("mouseup", canvas_mouseup, false); - canvas_phantom.addEventListener("mousedown", canvas_mousedown, false); - canvas_phantom.addEventListener("wheel",canvas_mousewheel, false); - canvas_phantom.style.width=canvas_container.clientWidth+"px"; - add_canvas(); -} - -canvas_maxshift=0; - -function shift_canvases() -{ - canvases.forEach(function(p) - { - p.style.top=(p.openwebrx_top++).toString()+"px"; - }); - canvas_maxshift++; - if(canvas_container.clientHeight>canvas_maxshift) - { - canvas_phantom.style.top=canvas_maxshift.toString()+"px"; - canvas_phantom.style.height=(canvas_container.clientHeight-canvas_maxshift).toString()+"px"; - canvas_phantom.style.display="block"; - } - else - canvas_phantom.style.display="none"; - - - //canvas_container.style.height=(((canvases.length-1)*canvas_default_height)+(canvas_default_height-canvas_actual_line)).toString()+"px"; - //canvas_container.style.height="100%"; -} - -function resize_canvases(zoom) -{ - if(typeof zoom == "undefined") zoom=false; - if(!zoom) mkzoomlevels(); - zoom_calc(); - new_width=(canvas_container.clientWidth*zoom_levels[zoom_level]).toString()+"px"; - var zoom_value=zoom_offset_px.toString()+"px"; - canvases.forEach(function(p) - { - p.style.width=new_width; - p.style.left=zoom_value; - }); - canvas_phantom.style.width=new_width; - canvas_phantom.style.left=zoom_value; -} - -function waterfall_init() -{ - init_canvas_container(); - waterfall_timer = window.setInterval(()=>{waterfall_dequeue(); secondary_demod_waterfall_dequeue();},900/fft_fps); - resize_waterfall_container(false); /* then */ resize_canvases(); - scale_setup(); - mkzoomlevels(); - waterfall_setup_done=1; +var canvas_actual_line = -1; + +function add_canvas() { + var new_canvas = document.createElement("canvas"); + new_canvas.width = fft_size; + new_canvas.height = canvas_default_height; + canvas_actual_line = canvas_default_height; + new_canvas.openwebrx_top = -canvas_default_height; + new_canvas.style.transform = 'translate(0, ' + new_canvas.openwebrx_top.toString() + 'px)'; + canvas_context = new_canvas.getContext("2d"); + canvas_container.appendChild(new_canvas); + canvases.push(new_canvas); + while (canvas_container && canvas_container.clientHeight + canvas_default_height * 2 < canvases.length * canvas_default_height) { + var c = canvases.shift(); + if (!c) break; + canvas_container.removeChild(c); + } } -var waterfall_dont_scale=0; -var mathbox_shift = function() -{ - if(mathbox_data_current_depth < mathbox_data_max_depth) mathbox_data_current_depth++; - if(mathbox_data_index+1>=mathbox_data_max_depth) mathbox_data_index = 0; - else mathbox_data_index++; - mathbox_data_global_index++; -} - -var mathbox_clear_data = function() -{ - mathbox_data_index = 50; - mathbox_data_current_depth = 0; +function init_canvas_container() { + canvas_container = $("#webrx-canvas-container")[0]; + canvas_container.addEventListener("mouseleave", canvas_container_mouseleave, false); + canvas_container.addEventListener("mousemove", canvas_mousemove, false); + canvas_container.addEventListener("mouseup", canvas_mouseup, false); + canvas_container.addEventListener("mousedown", canvas_mousedown, false); + canvas_container.addEventListener("wheel", canvas_mousewheel, false); + $("#openwebrx-frequency-container").each(function(){ + this.addEventListener("wheel", canvas_mousewheel, false); + }); } -//var mathbox_get_data_line = function(x) //x counts from 0 to mathbox_data_current_depth -//{ -// return (mathbox_data_max_depth + mathbox_data_index - mathbox_data_current_depth + x - 1) % mathbox_data_max_depth; -//} -// -//var mathbox_data_index_valid = function(x) //x counts from 0 to mathbox_data_current_depth -//{ -// return xmathbox_data_max_depth-mathbox_data_current_depth; +function resize_canvases() { + zoom_calc(); + $('#webrx-canvas-container').css({ + width: waterfallWidth() * get_zoom(zoom_level) + 'px', + left: zoom_offset_px + "px" + }); } +function waterfall_init() { + init_canvas_container(); + resize_canvases(); + scale_setup(); + waterfall_setup_done = 1; +} +function waterfall_add(data) { + if (!waterfall_setup_done) return; + var w = fft_size; -function waterfall_add(data) -{ - if(!waterfall_setup_done) return; - var w=fft_size; - - //waterfall_shift(); - // ==== do scaling if required ==== - /*if(waterfall_dont_scale) - { - scaled=data; - for(i=scaled.length;i1) - { - scaled[i]=data[j]*(remain/pixel_per_point)+data[j+1]*((1-remain)/pixel_per_point); - remain--; - } - else - { - j++; - scaled[i]=data[j]*(remain/pixel_per_point)+data[j+1]*((1-remain)/pixel_per_point); - remain=pixel_per_point-(1-remain); - } - } - - } - else - { //make line smaller (linear decimation, moving average) - point_per_pixel=(to-from)/w; - scaled=Array(); - j=0; - remain=point_per_pixel; - last_pixel=0; - for(i=from; i1) - { - last_pixel+=data[i]; - remain--; - } - else - { - last_pixel+=data[i]*remain; - scaled[j++]=last_pixel/point_per_pixel; - last_pixel=data[i]*(1-remain); - remain=point_per_pixel-(1-remain); //? - } - } - } - } - - //Add line to waterfall image - base=(h-1)*w*4; - for(x=0;x>>0)>>((3-i)*8))&0xff; - }*/ - - if(mathbox_mode==MATHBOX_MODES.WATERFALL) - { - //Handle mathbox - for(var i=0;i>>0)>>((3-i)*8))&0xff; - } - - //Draw image - canvas_context.putImageData(oneline_image, 0, canvas_actual_line--); - shift_canvases(); - if(canvas_actual_line<0) add_canvas(); - } + if (waterfall_measure_minmax_now) { + var levels = waterfall_measure_minmax_do(data); + waterfall_measure_minmax_now = false; + waterfallColorsAuto(levels); + waterfallColorsContinuousReset(); + } + if (waterfall_measure_minmax_continuous) { + var level = waterfall_measure_minmax_do(data); + waterfallColorsContinuous(level); + } -} + // create new canvas if the current one is full (or there isn't one) + if (canvas_actual_line <= 0) add_canvas(); -/* -function waterfall_shift() -{ - w=canvas.width; - h=canvas.height; - for(y=0; ytl.offsetLeft-20) what.style.opacity=what.style.opacity="0"; - else wet.style.opacity=wed.style.opacity="1"; - }); + //Draw image + canvas_context.putImageData(oneline_image, 0, --canvas_actual_line); + shift_canvases(); +} +function waterfall_clear() { + //delete all canvases + while (canvases.length) { + var x = canvases.shift(); + x.parentNode.removeChild(x); + } + canvas_actual_line = -1; } -var MATHBOX_MODES = -{ - UNINITIALIZED: 0, - NONE: 1, - WATERFALL: 2, - CONSTELLATION: 3 -}; -var mathbox_mode = MATHBOX_MODES.UNINITIALIZED; -var mathbox; -var mathbox_element; +function openwebrx_resize() { + resize_canvases(); + resize_scale(); +} -function mathbox_init() -{ - //mathbox_waterfall_history_length is defined in the config - mathbox_data_max_depth = fft_fps * mathbox_waterfall_history_length; //how many lines can the buffer store - mathbox_data_current_depth = 0; //how many lines are in the buffer currently - mathbox_data_index = 0; //the index of the last empty line / the line to be overwritten - mathbox_data = new Float32Array(fft_size * mathbox_data_max_depth); - mathbox_data_global_index = 0; - mathbox_correction_for_z = 0; - - mathbox = mathBox({ - plugins: ['core', 'controls', 'cursor', 'stats'], - controls: { klass: THREE.OrbitControls }, - }); - three = mathbox.three; - if(typeof three == "undefined") divlog("3D waterfall cannot be initialized because WebGL is not supported in your browser.", true); - - three.renderer.setClearColor(new THREE.Color(0x808080), 1.0); - mathbox_container.appendChild((mathbox_element=three.renderer.domElement)); - view = mathbox - .set({ - scale: 1080, - focus: 3, +function initProgressBars() { + $(".openwebrx-progressbar").each(function(){ + var bar = $(this).progressbar(); + if ('setSampleRate' in bar) { + bar.setSampleRate(audioEngine.getSampleRate()); + } }) - .camera({ - proxy: true, - position: [-2, 1, 3], - }) - .cartesian({ - range: [[-1, 1], [0, 1], [0, 1]], - scale: [2, 2/3, 1], - }); +} - view.axis({ - axis: 1, - width: 3, - color: "#fff", - }); - view.axis({ - axis: 2, - width: 3, - color: "#fff", - //offset: [0, 0, 0], - }); - view.axis({ - axis: 3, - width: 3, - color: "#fff", - }); - - view.grid({ - width: 2, - opacity: 0.5, - axes: [1, 3], - zOrder: 1, - color: "#fff", - }); +function audioReporter(stats) { + if (typeof(stats.buffersize) !== 'undefined') { + $('#openwebrx-bar-audio-buffer').progressbar().setBuffersize(stats.buffersize); + } - //var remap = function (v) { return Math.sqrt(.5 + .5 * v); }; - - - var remap = function(x,z,t) - { - var currentTimePos = mathbox_data_global_index/(fft_fps*1.0); - var realZAdd = (-(t-currentTimePos)/mathbox_waterfall_history_length); - var zAdd = realZAdd - mathbox_correction_for_z; - if(zAdd<-0.2 || zAdd>0.2) { mathbox_correction_for_z = realZAdd; } - - var xIndex = Math.trunc(((x+1)/2.0)*fft_size); //x: frequency - var zIndex = Math.trunc(z*(mathbox_data_max_depth-1)); //z: time - var realZIndex = mathbox_get_data_line(zIndex); - if(!mathbox_data_index_valid(zIndex)) return {y: undefined, dBValue: undefined, zAdd: 0 }; - //if(realZIndex>=(mathbox_data_max_depth-1)) console.log("realZIndexundef", realZIndex, zIndex); - var index = Math.trunc(xIndex + realZIndex * fft_size); - /*if(mathbox_data[index]==undefined) console.log("Undef", index, mathbox_data.length, zIndex, - realZIndex, mathbox_data_max_depth, - mathbox_data_current_depth, mathbox_data_index);*/ - var dBValue = mathbox_data[index]; - //y=1; - if(dBValue>waterfall_max_level) y = 1; - else if(dBValue>8)/255.0; - var r = ((color&0xff0000)>>16)/255.0; - emit(r, g, b, 1.0); - }, - width: mathbox_waterfall_frequency_resolution, - height: mathbox_data_max_depth - 1, - channels: 4, - axes: [1, 3], - }); + if (typeof(stats.audioRate) !== 'undefined') { + $('#openwebrx-bar-audio-output').progressbar().setAudioRate(stats.audioRate); + } +} - view.surface({ - shaded: true, - points: '<<', - colors: '<', - color: 0xFFFFFF, - }); +var bookmarks; +var audioEngine; - view.surface({ - fill: false, - lineX: false, - lineY: false, - points: '<<', - colors: '<', - color: 0xFFFFFF, - width: 2, - blending: 'add', - opacity: .25, - zBias: 5, +function openwebrx_init() { + audioEngine = new AudioEngine(audio_buffer_maximal_length_sec, audioReporter); + var $overlay = $('#openwebrx-autoplay-overlay'); + $overlay.on('click', function(){ + audioEngine.resume(); + }); + audioEngine.onStart(onAudioStart); + if (!audioEngine.isAllowed()) { + $('body').append($overlay); + $overlay.show(); + } + fft_codec = new ImaAdpcmCodec(); + initProgressBars(); + open_websocket(); + secondary_demod_init(); + digimodes_init(); + initPanels(); + $('#openwebrx-panel-receiver').demodulatorPanel(); + window.addEventListener("resize", openwebrx_resize); + bookmarks = new BookmarkBar(); + initSliders(); +} + +function initSliders() { + $('#openwebrx-panel-receiver').on('wheel', 'input[type=range]', function(ev){ + var $slider = $(this); + if (!$slider.attr('step')) return; + var val = Number($slider.val()); + // restore previous high-resolution mouse wheel delta + var mouseDelta = Number($slider.data('mouseDelta')); + if (mouseDelta) val += mouseDelta; + var step = Number($slider.attr('step')); + var newVal = val + step * -wheelDelta(ev.originalEvent); + $slider.val(newVal); + // the calculated value can have a higher resolution than the element can store, so we put the delta into the data attributes + $slider.data('mouseDelta', newVal - $slider.val()); + $slider.trigger('change'); }); - mathbox_mode = MATHBOX_MODES.NONE; - - //mathbox_element.style.width="100%"; - //mathbox_element.style.height="100%"; - -} -function mathbox_toggle() -{ + var waterfallAutoButton = $('#openwebrx-waterfall-colors-auto'); + waterfallAutoButton.on('click', function() { + waterfall_measure_minmax_now=true; + }).on('contextmenu', function(){ + waterfall_measure_minmax_continuous = !waterfall_measure_minmax_continuous; + waterfallColorsContinuousReset(); + waterfallAutoButton[waterfall_measure_minmax_continuous ? 'addClass' : 'removeClass']('highlighted'); + $('#openwebrx-waterfall-color-min, #openwebrx-waterfall-color-max').prop('disabled', waterfall_measure_minmax_continuous); - if(mathbox_mode == MATHBOX_MODES.UNINITIALIZED) mathbox_init(); - mathbox_mode = (mathbox_mode == MATHBOX_MODES.NONE) ? MATHBOX_MODES.WATERFALL : MATHBOX_MODES.NONE; - mathbox_container.style.display = (mathbox_mode == MATHBOX_MODES.WATERFALL) ? "block" : "none"; - mathbox_clear_data(); - waterfall_clear(); + return false; + }); } -function waterfall_clear() -{ - while(canvases.length) //delete all canvases - { - var x=canvases.shift(); - x.parentNode.removeChild(x); - delete x; - } - add_canvas(); -} +function digimodes_init() { + // initialze DMR timeslot muting + $('.openwebrx-dmr-timeslot-panel').click(function (e) { + $(e.currentTarget).toggleClass("muted"); + update_dmr_timeslot_filtering(); + // don't mute when the location icon is clicked + }).find('.location').click(function(e) { + e.stopPropagation(); + }); -function openwebrx_resize() -{ - resize_canvases(); - resize_waterfall_container(true); - resize_scale(); - check_top_bar_congestion(); + $('.openwebrx-meta-panel').metaPanel(); } -function openwebrx_init() -{ - if(ios||is_chrome) e("openwebrx-big-grey").style.display="table-cell"; - (opb=e("openwebrx-play-button-text")).style.marginTop=(window.innerHeight/2-opb.clientHeight/2).toString()+"px"; - init_rx_photo(); - open_websocket(); - secondary_demod_init(); - place_panels(first_show_panel); - window.setTimeout(function(){window.setInterval(debug_audio,1000);},1000); - window.addEventListener("resize",openwebrx_resize); - check_top_bar_congestion(); - - //Synchronise volume with slider - updateVolume(); - waterfallColorsDefault(); +function update_dmr_timeslot_filtering() { + var filter = $('.openwebrx-dmr-timeslot-panel').map(function (index, el) { + return (!$(el).hasClass("muted")) << index; + }).toArray().reduce(function (acc, v) { + return acc | v; + }, 0); + $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().setDmrFilter(filter); } -function iosPlayButtonClick() -{ - //On iOS, we can only start audio from a click or touch event. - audio_init(); - e("openwebrx-big-grey").style.opacity=0; - window.setTimeout(function(){ e("openwebrx-big-grey").style.display="none"; },1100); +function hideOverlay() { + var $overlay = $('#openwebrx-autoplay-overlay'); + $overlay.css('opacity', 0); + $overlay.on('transitionend', function() { + $overlay.hide(); + }); } -/* -window.setInterval(function(){ - sum=0; - for(i=0;i=(c=c.charCodeAt(0)+13)?c:c-26);}); - window.location.href="mailto:"+what; -}*/ - -var rt = function (s,n) {return s.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c<="Z"?90:122)>=(c=c.charCodeAt(0)+n)?c:c-26);});} -var irt = function (s,n) {return s.replace(/[a-zA-Z]/g,function(c){return String.fromCharCode((c>="a"?97:65)<=(c=c.charCodeAt(0)-n)?c:c+26);});} -var sendmail2 = function (s) { window.location.href="mailto:"+irt(s.replace("=",String.fromCharCode(0100)).replace("$","."),8); } - -var audio_debug_time_start=0; -var audio_debug_time_last_start=0; - -function debug_audio() -{ - if(audio_debug_time_start==0) return; //audio_init has not been called - time_now=(new Date()).getTime(); - audio_debug_time_since_last_call=(time_now-audio_debug_time_last_start)/1000; - audio_debug_time_last_start=time_now; //now - audio_debug_time_taken=(time_now-audio_debug_time_start)/1000; - kbps_mult=(audio_compression=="adpcm")?8:16; - //e("openwebrx-audio-sps").innerHTML= - // ((audio_compression=="adpcm")?"ADPCM compressed":"uncompressed")+" audio downlink:
    "+(audio_buffer_current_size_debug*kbps_mult/audio_debug_time_since_last_call).toFixed(0)+" kbps ("+ - // (audio_buffer_all_size_debug*kbps_mult/audio_debug_time_taken).toFixed(1)+" kbps avg.), feed at "+ - // ((audio_buffer_current_count_debug*audio_buffer_size)/audio_debug_time_taken).toFixed(1)+" sps output"; - - var audio_speed_value=audio_buffer_current_size_debug*kbps_mult/audio_debug_time_since_last_call; - progressbar_set(e("openwebrx-bar-audio-speed"),audio_speed_value/500000,"Audio stream ["+(audio_speed_value/1000).toFixed(0)+" kbps]",false); - - var audio_output_value=(audio_buffer_current_count_debug*audio_buffer_size)/audio_debug_time_taken; - progressbar_set(e("openwebrx-bar-audio-output"),audio_output_value/55000,"Audio output ["+(audio_output_value/1000).toFixed(1)+" ksps]",audio_output_value>55000||audio_output_value<10000); - - audio_buffer_progressbar_update(); - - var network_speed_value=debug_ws_data_received/audio_debug_time_taken; - progressbar_set(e("openwebrx-bar-network-speed"),network_speed_value*8/2000,"Network usage ["+(network_speed_value*8).toFixed(1)+" kbps]",false); - - audio_buffer_current_size_debug=0; - - if(waterfall_measure_minmax) waterfall_measure_minmax_print(); -} +var rt = function (s, n) { + return s.replace(/[a-zA-Z]/g, function (c) { + return String.fromCharCode((c <= "Z" ? 90 : 122) >= (c = c.charCodeAt(0) + n) ? c : c - 26); + }); +}; // ======================================================== // ======================= PANELS ======================= // ======================================================== -panel_margin=5.9; +function panel_displayed(el){ + return !(el.style && el.style.display && el.style.display === 'none') && !(el.movement && el.movement === 'collapse'); +} -function pop_bottommost_panel(from) -{ - min_order=parseInt(from[0].dataset.panelOrder); - min_index=0; - for(i=0;i0.5)?-90:90; - roty=0; - if(Math.random()>0.5) - { - rottemp=rotx; - rotx=roty; - roty=rottemp; - } - if(rotx!=0 && Math.random()>0.5) rotx=270; - //console.log(rotx,roty); - transformString = "perspective( 599px ) rotateX( %1deg ) rotateY( %2deg )" - .replace("%1",rotx.toString()).replace("%2",roty.toString()); - //console.log(transformString); - //console.log(panel); - panel.style.transform=transformString; - window.setTimeout(function() { - panel.style.transitionDuration="599ms"; - panel.style.transitionDelay=(Math.floor(Math.random()*500)).toString()+"ms"; - panel.style.transform="perspective( 599px ) rotateX( 0deg ) rotateY( 0deg )"; - //panel.style.transitionDuration="0ms"; - //panel.style.transitionDelay="0"; - }, 1); -} - -function place_panels(function_apply) -{ - if(function_apply == undefined) function_apply = function(x){}; - var hoffset=0; //added this because the first panel should not have such great gap below - var left_col=[]; - var right_col=[]; - var plist=e("openwebrx-panels-container").children; - for(i=0;i0) - { - p=pop_bottommost_panel(left_col); - p.style.left="0px"; - p.style.bottom=y.toString()+"px"; - p.style.visibility="visible"; - y+=p.openwebrxPanelHeight+((p.openwebrxPanelTransparent)?0:3)*panel_margin; - if(function_apply) function_apply(p); - //console.log(p.id, y, p.openwebrxPanelTransparent); - } - y=hoffset; - while(right_col.length>0) - { - p=pop_bottommost_panel(right_col); - p.style.right=(e("webrx-canvas-container").offsetWidth-e("webrx-canvas-container").clientWidth).toString()+"px"; //get scrollbar width - p.style.bottom=y.toString()+"px"; - p.style.visibility="visible"; - y+=p.openwebrxPanelHeight+((p.openwebrxPanelTransparent)?0:3)*panel_margin; - if(function_apply) function_apply(p); - } -} - -function progressbar_set(obj,val,text,over) -{ - if (val<0.05) val=0; - if (val>1) val=1; - var innerBar=null; - var innerText=null; - for(var i=0;i0) - $("#openwebrx-button-usb").addClass("highlighted"); - else $("#openwebrx-button-lsb, #openwebrx-button-usb").addClass("highlighted"); - } - break; - } -} -function demodulator_analog_replace_last() { demodulator_analog_replace(last_analog_demodulator_subtype); } + if (displayed) { + item.movement = 'collapse'; + item.style.transform = "perspective(600px) rotateX(90deg)"; + item.style.transitionProperty = 'transform'; + } else { + item.movement = 'expand'; + item.style.display = null; + setTimeout(function(){ + item.style.transitionProperty = 'transform'; + item.style.transform = 'perspective(600px) rotateX(0deg)'; + }, 20); + } + item.style.transitionDuration = "600ms"; + item.style.transitionDelay = "0ms"; +} + +function first_show_panel(panel) { + panel.style.transitionDuration = 0; + panel.style.transitionDelay = 0; + var rotx = (Math.random() > 0.5) ? -90 : 90; + var roty = 0; + if (Math.random() > 0.5) { + var rottemp = rotx; + rotx = roty; + roty = rottemp; + } + if (rotx !== 0 && Math.random() > 0.5) rotx = 270; + panel.style.transform = "perspective(600px) rotateX(%1deg) rotateY(%2deg)" + .replace("%1", rotx.toString()).replace("%2", roty.toString()); + window.setTimeout(function () { + panel.style.transitionDuration = "600ms"; + panel.style.transitionDelay = (Math.floor(Math.random() * 500)).toString() + "ms"; + panel.style.transform = "perspective(600px) rotateX(0deg) rotateY(0deg)"; + }, 1); +} + +function initPanels() { + $('#openwebrx-panels-container').find('.openwebrx-panel').each(function(){ + var el = this; + el.openwebrxPanelTransparent = (!!el.dataset.panelTransparent); + el.addEventListener('transitionend', function(ev){ + if (ev.target !== el) return; + el.style.transitionDuration = null; + el.style.transitionDelay = null; + el.style.transitionProperty = null; + if (el.movement && el.movement === 'collapse') { + el.style.display = 'none'; + } + delete el.movement; + }); + if (panel_displayed(el)) first_show_panel(el); + }); +} /* - _____ _ _ _ - | __ \(_) (_) | | - | | | |_ __ _ _ _ __ ___ ___ __| | ___ ___ + _____ _ _ _ + | __ \(_) (_) | | + | | | |_ __ _ _ _ __ ___ ___ __| | ___ ___ | | | | |/ _` | | '_ ` _ \ / _ \ / _` |/ _ \/ __| | |__| | | (_| | | | | | | | (_) | (_| | __/\__ \ |_____/|_|\__, |_|_| |_| |_|\___/ \__,_|\___||___/ - __/ | - |___/ + __/ | + |___/ */ -secondary_demod = false; -secondary_demod_offset_freq = 0; -secondary_demod_waterfall_queue = []; - -function demodulator_digital_replace_last() -{ - demodulator_digital_replace(last_digital_demodulator_subtype); - secondary_demod_listbox_update(); -} -function demodulator_digital_replace(subtype) -{ - switch(subtype) - { - case "bpsk31": - case "rtty": - secondary_demod_start(subtype); - demodulator_analog_replace('usb', true); - demodulator_buttons_update(); - break; - } - toggle_panel("openwebrx-panel-digimodes", true); -} - -function secondary_demod_create_canvas() -{ - var new_canvas = document.createElement("canvas"); - new_canvas.width=secondary_fft_size; - new_canvas.height=$(secondary_demod_canvas_container).height(); - new_canvas.style.width=$(secondary_demod_canvas_container).width()+"px"; - new_canvas.style.height=$(secondary_demod_canvas_container).height()+"px"; - console.log(new_canvas.width, new_canvas.height, new_canvas.style.width, new_canvas.style.height); - secondary_demod_current_canvas_actual_line=new_canvas.height-1; - $(secondary_demod_canvas_container).children().last().before(new_canvas); +var secondary_demod_fft_offset_db = 18; //need to calculate that later +var secondary_demod_canvases_initialized = false; +var secondary_demod_channel_freq = 1000; +var secondary_demod_waiting_for_set = false; +var secondary_demod_low_cut; +var secondary_demod_high_cut; +var secondary_demod_mousedown = false; +var secondary_demod_canvas_width; +var secondary_demod_canvas_left; +var secondary_demod_canvas_container; +var secondary_demod_current_canvas_actual_line; +var secondary_demod_current_canvas_context; +var secondary_demod_current_canvas_index; +var secondary_demod_canvases; +var secondary_bw = 31.25; +var if_samp_rate; + +function secondary_demod_create_canvas() { + var new_canvas = document.createElement("canvas"); + new_canvas.width = secondary_fft_size; + new_canvas.height = $(secondary_demod_canvas_container).height(); + new_canvas.style.width = $(secondary_demod_canvas_container).width() + "px"; + new_canvas.style.height = $(secondary_demod_canvas_container).height() + "px"; + secondary_demod_current_canvas_actual_line = new_canvas.height - 1; + $(secondary_demod_canvas_container).children().last().before(new_canvas); return new_canvas; } -function secondary_demod_remove_canvases() -{ +function secondary_demod_remove_canvases() { $(secondary_demod_canvas_container).children("canvas").remove(); } -function secondary_demod_init_canvases() -{ +function secondary_demod_init_canvases() { secondary_demod_remove_canvases(); - secondary_demod_canvases=[]; + secondary_demod_canvases = []; secondary_demod_canvases.push(secondary_demod_create_canvas()); secondary_demod_canvases.push(secondary_demod_create_canvas()); - secondary_demod_canvases[0].openwebrx_top=-$(secondary_demod_canvas_container).height(); - secondary_demod_canvases[1].openwebrx_top=0; + secondary_demod_canvases[0].openwebrx_top = -$(secondary_demod_canvas_container).height(); + secondary_demod_canvases[1].openwebrx_top = 0; secondary_demod_canvases_update_top(); secondary_demod_current_canvas_context = secondary_demod_canvases[0].getContext("2d"); - secondary_demod_current_canvas_actual_line=$(secondary_demod_canvas_container).height()-1; - secondary_demod_current_canvas_index=0; - secondary_demod_canvases_initialized=true; - //secondary_demod_update_channel_freq_from_event(); + secondary_demod_current_canvas_actual_line = $(secondary_demod_canvas_container).height() - 1; + secondary_demod_current_canvas_index = 0; + secondary_demod_canvases_initialized = true; mkscale(); //so that the secondary waterfall zoom level will be initialized } -function secondary_demod_canvases_update_top() -{ - for(var i=0;i<2;i++) secondary_demod_canvases[i].style.top=secondary_demod_canvases[i].openwebrx_top+"px"; +function secondary_demod_canvases_update_top() { + for (var i = 0; i < 2; i++) { + secondary_demod_canvases[i].style.transform = 'translate(0, ' + secondary_demod_canvases[i].openwebrx_top + 'px)'; + } } -function secondary_demod_swap_canvases() -{ - console.log("swap"); - secondary_demod_canvases[0+!secondary_demod_current_canvas_index].openwebrx_top-=$(secondary_demod_canvas_container).height()*2; - secondary_demod_current_canvas_index=0+!secondary_demod_current_canvas_index; +function secondary_demod_swap_canvases() { + secondary_demod_canvases[0 + !secondary_demod_current_canvas_index].openwebrx_top -= $(secondary_demod_canvas_container).height() * 2; + secondary_demod_current_canvas_index = 0 + !secondary_demod_current_canvas_index; secondary_demod_current_canvas_context = secondary_demod_canvases[secondary_demod_current_canvas_index].getContext("2d"); - secondary_demod_current_canvas_actual_line=$(secondary_demod_canvas_container).height()-1; + secondary_demod_current_canvas_actual_line = $(secondary_demod_canvas_container).height() - 1; } -function secondary_demod_init() -{ - $("#openwebrx-panel-digimodes")[0].openwebrxHidden = true; +function secondary_demod_init() { secondary_demod_canvas_container = $("#openwebrx-digimode-canvas-container")[0]; $(secondary_demod_canvas_container) .mousemove(secondary_demod_canvas_container_mousemove) .mouseup(secondary_demod_canvas_container_mouseup) .mousedown(secondary_demod_canvas_container_mousedown) .mouseenter(secondary_demod_canvas_container_mousein) - .mouseleave(secondary_demod_canvas_container_mouseout); -} - -function secondary_demod_start(subtype) -{ - secondary_demod_canvases_initialized = false; - ws.send("SET secondary_mod="+subtype); - secondary_demod = subtype; -} - -function secondary_demod_set() -{ - ws.send("SET secondary_offset_freq="+secondary_demod_offset_freq.toString()); -} - -function secondary_demod_stop() -{ - ws.send("SET secondary_mod=off"); - secondary_demod = false; - secondary_demod_waterfall_queue = []; -} - -function secondary_demod_waterfall_add_queue(x) -{ - secondary_demod_waterfall_queue.push(x); -} - -function secondary_demod_push_binary_data(x) -{ - secondary_demod_push_data(Array.from(x).map( y => (y)?"1":"0" ).join("")); -} - -function secondary_demod_push_data(x) -{ - x=Array.from(x).map((y)=>{ - var c=y.charCodeAt(0); - if(y=="\r") return " "; - if(y=="\n") return " "; - //if(y=="\n") return "
    "; - if(c<32||c>126) return ""; - if(y=="&") return "&"; - if(y=="<") return "<"; - if(y==">") return ">"; - if(y==" ") return " "; + .mouseleave(secondary_demod_canvas_container_mouseleave); + ['wsjt', 'packet', 'pocsag', 'adsb', 'ism', 'hfdl'].forEach(function(id){ + $('#openwebrx-panel-' + id + '-message')[id + 'MessagePanel'](); + }) + $('#openwebrx-panel-js8-message').js8(); +} + +function secondary_demod_push_data(x) { + x = Array.from(x).filter(function (y) { + var c = y.charCodeAt(0); + return (c === 10 || (c >= 32 && c <= 126)); + }).map(function (y) { + if (y === "&") + return "&"; + if (y === "<") return "<"; + if (y === ">") return ">"; + if (y === " ") return " "; + if (y === "\n") return "
    "; return y; }).join(""); - $("#openwebrx-cursor-blink").before(""+x+""); -} - -function secondary_demod_data_clear() -{ - $("#openwebrx-cursor-blink").prevAll().remove(); + $("#openwebrx-cursor-blink").before(x); } -function secondary_demod_close_window() -{ - secondary_demod_stop(); - toggle_panel("openwebrx-panel-digimodes", false); -} - -secondary_demod_fft_offset_db=30; //need to calculate that later +function secondary_demod_waterfall_add(data) { + var w = secondary_fft_size; -function secondary_demod_waterfall_add(data) -{ - if(!secondary_demod) return; - var w=secondary_fft_size; - - //Add line to waterfall image - var oneline_image = secondary_demod_current_canvas_context.createImageData(w,1); - for(x=0;x>>0)>>((3-i)*8))&0xff; - } - - //Draw image - secondary_demod_current_canvas_context.putImageData(oneline_image, 0, secondary_demod_current_canvas_actual_line--); - secondary_demod_canvases.map((x)=>{x.openwebrx_top += 1;}); - secondary_demod_canvases_update_top(); - if(secondary_demod_current_canvas_actual_line<0) secondary_demod_swap_canvases(); -} - -var secondary_demod_canvases_initialized = false; - -function secondary_demod_waterfall_dequeue() -{ - if(!secondary_demod || !secondary_demod_canvases_initialized) return; - if(secondary_demod_waterfall_queue.length) secondary_demod_waterfall_add(secondary_demod_waterfall_queue.shift()); - if(secondary_demod_waterfall_queue.length>Math.max(fft_fps/2,20)) //in case of fft overflow - { - console.log("secondary waterfall overflow, queue length:", secondary_demod_waterfall_queue.length); - while(secondary_demod_waterfall_queue.length) secondary_demod_waterfall_add(secondary_demod_waterfall_queue.shift()); - } -} - -secondary_demod_listbox_updating = false; -function secondary_demod_listbox_changed() -{ - if(secondary_demod_listbox_updating) return; - switch ($("#openwebrx-secondary-demod-listbox")[0].value) - { - case "none": - demodulator_analog_replace_last(); - break; - case "bpsk31": - demodulator_digital_replace('bpsk31'); - break; - case "rtty": - demodulator_digital_replace('rtty'); - break; + //Add line to waterfall image + var oneline_image = secondary_demod_current_canvas_context.createImageData(w, 1); + for (var x = 0; x < w; x++) { + var color = waterfall_mkcolor(data[x] + secondary_demod_fft_offset_db); + for (var i = 0; i < 3; i++) oneline_image.data[x * 4 + i] = color[i]; + oneline_image.data[x * 4 + 3] = 255; } -} -function secondary_demod_listbox_update() -{ - secondary_demod_listbox_updating = true; - $("#openwebrx-secondary-demod-listbox").val((secondary_demod)?secondary_demod:"none"); - console.log("update"); - secondary_demod_listbox_updating = false; + //Draw image + secondary_demod_current_canvas_context.putImageData(oneline_image, 0, secondary_demod_current_canvas_actual_line--); + secondary_demod_canvases.map(function (x) { + x.openwebrx_top += 1; + }) + ; + secondary_demod_canvases_update_top(); + if (secondary_demod_current_canvas_actual_line < 0) secondary_demod_swap_canvases(); } -secondary_demod_channel_freq=1000; -function secondary_demod_update_marker() -{ - var width = Math.max( (secondary_bw / (if_samp_rate/2)) * secondary_demod_canvas_width, 5); - var center_at = (secondary_demod_channel_freq / (if_samp_rate/2)) * secondary_demod_canvas_width + secondary_demod_canvas_left; - var left = center_at-width/2; - //console.log("sdum", width, left); - $("#openwebrx-digimode-select-channel").width(width).css("left",left+"px") +function secondary_demod_update_marker() { + var width = Math.max((secondary_bw / if_samp_rate) * secondary_demod_canvas_width, 5); + var center_at = ((secondary_demod_channel_freq - secondary_demod_low_cut) / if_samp_rate) * secondary_demod_canvas_width; + var left = center_at - width / 2; + $("#openwebrx-digimode-select-channel").width(width).css("left", left + "px") } -secondary_demod_waiting_for_set = false; -function secondary_demod_update_channel_freq_from_event(evt) -{ - if(typeof evt !== "undefined") - { - var relativeX=(evt.offsetX)?evt.offsetX:evt.layerX; - secondary_demod_channel_freq=secondary_demod_low_cut + - (relativeX/$(secondary_demod_canvas_container).width()) * (secondary_demod_high_cut-secondary_demod_low_cut); +function secondary_demod_update_channel_freq_from_event(evt) { + if (typeof evt !== "undefined") { + var relativeX = (evt.offsetX) ? evt.offsetX : evt.layerX; + secondary_demod_channel_freq = secondary_demod_low_cut + + (relativeX / $(secondary_demod_canvas_container).width()) * (secondary_demod_high_cut - secondary_demod_low_cut); } - //console.log("toset:", secondary_demod_channel_freq); - if(!secondary_demod_waiting_for_set) - { + if (!secondary_demod_waiting_for_set) { secondary_demod_waiting_for_set = true; - window.setTimeout(()=>{ - ws.send("SET secondary_offset_freq="+Math.floor(secondary_demod_channel_freq)); - //console.log("doneset:", secondary_demod_channel_freq); - secondary_demod_waiting_for_set = false; - }, 50); + window.setTimeout(function () { + $('#openwebrx-panel-receiver').demodulatorPanel().getDemodulator().set_secondary_offset_freq(Math.floor(secondary_demod_channel_freq)); + secondary_demod_waiting_for_set = false; + }, + 50 + ) + ; } secondary_demod_update_marker(); } -secondary_demod_mousedown=false; -function secondary_demod_canvas_container_mousein() -{ - $("#openwebrx-digimode-select-channel").css("opacity","0.7"); //.css("border-width", "1px"); +function secondary_demod_canvas_container_mousein() { + $("#openwebrx-digimode-select-channel").css("opacity", "0.7"); //.css("border-width", "1px"); } -function secondary_demod_canvas_container_mouseout() -{ - $("#openwebrx-digimode-select-channel").css("opacity","0"); +function secondary_demod_canvas_container_mouseleave() { + $("#openwebrx-digimode-select-channel").css("opacity", "0"); } -function secondary_demod_canvas_container_mousemove(evt) -{ - if(secondary_demod_mousedown) secondary_demod_update_channel_freq_from_event(evt); +function secondary_demod_canvas_container_mousemove(evt) { + if (secondary_demod_mousedown) secondary_demod_update_channel_freq_from_event(evt); } -function secondary_demod_canvas_container_mousedown(evt) -{ - if(evt.which==1) secondary_demod_mousedown=true; +function secondary_demod_canvas_container_mousedown(evt) { + if (evt.which === 1) secondary_demod_mousedown = true; } -function secondary_demod_canvas_container_mouseup(evt) -{ - if(evt.which==1) secondary_demod_mousedown=false; +function secondary_demod_canvas_container_mouseup(evt) { + if (evt.which === 1) secondary_demod_mousedown = false; secondary_demod_update_channel_freq_from_event(evt); } -function secondary_demod_waterfall_set_zoom(low_cut, high_cut) -{ - if(!secondary_demod || !secondary_demod_canvases_initialized) return; - if(low_cut<0 && high_cut<0) - { - var hctmp = high_cut; - var lctmp = low_cut; - low_cut = -hctmp; - low_cut = -lctmp; - } - else if(low_cut<0 && high_cut>0) - { - high_cut=Math.max(Math.abs(high_cut), Math.abs(low_cut)); - low_cut=0; - } +function secondary_demod_waterfall_set_zoom(low_cut, high_cut) { + if (!secondary_demod_canvases_initialized) return; secondary_demod_low_cut = low_cut; secondary_demod_high_cut = high_cut; - var shown_bw = high_cut-low_cut; - secondary_demod_canvas_width = $(secondary_demod_canvas_container).width() * (if_samp_rate/2)/shown_bw; - secondary_demod_canvas_left = -secondary_demod_canvas_width*(low_cut/(if_samp_rate/2)); - //console.log("setzoom", secondary_demod_canvas_width, secondary_demod_canvas_left, low_cut, high_cut); - secondary_demod_canvases.map((x)=>{$(x).css("left",secondary_demod_canvas_left+"px").css("width",secondary_demod_canvas_width+"px");}); + var shown_bw = high_cut - low_cut; + secondary_demod_canvas_width = $(secondary_demod_canvas_container).width() * (if_samp_rate) / shown_bw; + secondary_demod_canvas_left = (-secondary_demod_canvas_width / 2) - (low_cut / if_samp_rate) * secondary_demod_canvas_width; + secondary_demod_canvases.map(function (x) { + $(x).css({ + left: secondary_demod_canvas_left + "px", + width: secondary_demod_canvas_width + "px" + }); + }); secondary_demod_update_channel_freq_from_event(); } + +function sdr_profile_changed() { + var value = $('#openwebrx-sdr-profiles-listbox').val(); + ws.send(JSON.stringify({type: "selectprofile", params: {profile: value}})); +} diff --git a/htdocs/pwchange.html b/htdocs/pwchange.html new file mode 100644 index 000000000..e3c433ad1 --- /dev/null +++ b/htdocs/pwchange.html @@ -0,0 +1,32 @@ + + + + OpenWebRX Password change + + + + + + + + +${header} + + \ No newline at end of file diff --git a/htdocs/retry.html b/htdocs/retry.html deleted file mode 100644 index 466c7eeb6..000000000 --- a/htdocs/retry.html +++ /dev/null @@ -1,94 +0,0 @@ - - -OpenWebRX - - - - - - -
    - -
    - There are no client slots left on this server. -
    - Please wait until a client disconnects.
    We will try to reconnect in 30 seconds... -
    -
    -
    - - - diff --git a/htdocs/sdr.js b/htdocs/sdr.js deleted file mode 100644 index 0ac33ca71..000000000 --- a/htdocs/sdr.js +++ /dev/null @@ -1,11679 +0,0 @@ -/* -This file is part of libcsdr. - - Copyright (c) Andras Retzler, HA7ILM - Copyright (c) Warren Pratt, NR0V - Copyright 2006,2010,2012 Free Software Foundation, Inc. - - libcsdr is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - libcsdr is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with libcsdr. If not, see . -*/ - -// ========================================================== -// ========= THE CODE COMPILED BY EMCC STARTS HERE: ========= -// ========================================================== - -// Note: For maximum-speed code, see "Optimizing Code" on the Emscripten wiki, https://github.com/kripken/emscripten/wiki/Optimizing-Code -// Note: Some Emscripten settings may limit the speed of the generated code. -// The Module object: Our interface to the outside world. We import -// and export values on it, and do the work to get that through -// closure compiler if necessary. There are various ways Module can be used: -// 1. Not defined. We create it here -// 2. A function parameter, function(Module) { ..generated code.. } -// 3. pre-run appended it, var Module = {}; ..generated code.. -// 4. External script tag defines var Module. -// We need to do an eval in order to handle the closure compiler -// case, where this code here is minified but Module was defined -// elsewhere (e.g. case 4 above). We also need to check if Module -// already exists (e.g. case 3 above). -// Note that if you want to run closure, and also to use Module -// after the generated code, you will need to define var Module = {}; -// before the code. Then that object will be used in the code, and you -// can continue to use Module afterwards as well. -var Module; -if (!Module) Module = eval('(function() { try { return Module || {} } catch(e) { return {} } })()'); - -// Sometimes an existing Module object exists with properties -// meant to overwrite the default module functionality. Here -// we collect those properties and reapply _after_ we configure -// the current environment's defaults to avoid having to be so -// defensive during initialization. -var moduleOverrides = {}; -for (var key in Module) { - if (Module.hasOwnProperty(key)) { - moduleOverrides[key] = Module[key]; - } -} - -// The environment setup code below is customized to use Module. -// *** Environment setup code *** -var ENVIRONMENT_IS_NODE = typeof process === 'object' && typeof require === 'function'; -var ENVIRONMENT_IS_WEB = typeof window === 'object'; -var ENVIRONMENT_IS_WORKER = typeof importScripts === 'function'; -var ENVIRONMENT_IS_SHELL = !ENVIRONMENT_IS_WEB && !ENVIRONMENT_IS_NODE && !ENVIRONMENT_IS_WORKER; - -if (ENVIRONMENT_IS_NODE) { - // Expose functionality in the same simple way that the shells work - // Note that we pollute the global namespace here, otherwise we break in node - if (!Module['print']) Module['print'] = function print(x) { - process['stdout'].write(x + '\n'); - }; - if (!Module['printErr']) Module['printErr'] = function printErr(x) { - process['stderr'].write(x + '\n'); - }; - - var nodeFS = require('fs'); - var nodePath = require('path'); - - Module['read'] = function read(filename, binary) { - filename = nodePath['normalize'](filename); - var ret = nodeFS['readFileSync'](filename); - // The path is absolute if the normalized version is the same as the resolved. - if (!ret && filename != nodePath['resolve'](filename)) { - filename = path.join(__dirname, '..', 'src', filename); - ret = nodeFS['readFileSync'](filename); - } - if (ret && !binary) ret = ret.toString(); - return ret; - }; - - Module['readBinary'] = function readBinary(filename) { - return Module['read'](filename, true) - }; - - Module['load'] = function load(f) { - globalEval(read(f)); - }; - - Module['arguments'] = process['argv'].slice(2); - - module['exports'] = Module; -} else if (ENVIRONMENT_IS_SHELL) { - if (!Module['print']) Module['print'] = print; - if (typeof printErr != 'undefined') Module['printErr'] = printErr; // not present in v8 or older sm - - if (typeof read != 'undefined') { - Module['read'] = read; - } else { - Module['read'] = function read() { - throw 'no read() available (jsc?)' - }; - } - - Module['readBinary'] = function readBinary(f) { - return read(f, 'binary'); - }; - - if (typeof scriptArgs != 'undefined') { - Module['arguments'] = scriptArgs; - } else if (typeof arguments != 'undefined') { - Module['arguments'] = arguments; - } - - this['Module'] = Module; - - eval("if (typeof gc === 'function' && gc.toString().indexOf('[native code]') > 0) var gc = undefined"); // wipe out the SpiderMonkey shell 'gc' function, which can confuse closure (uses it as a minified name, and it is then initted to a non-falsey value unexpectedly) -} else if (ENVIRONMENT_IS_WEB || ENVIRONMENT_IS_WORKER) { - Module['read'] = function read(url) { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, false); - xhr.send(null); - return xhr.responseText; - }; - - if (typeof arguments != 'undefined') { - Module['arguments'] = arguments; - } - - if (typeof console !== 'undefined') { - if (!Module['print']) Module['print'] = function print(x) { - console.log(x); - }; - if (!Module['printErr']) Module['printErr'] = function printErr(x) { - console.log(x); - }; - } else { - // Probably a worker, and without console.log. We can do very little here... - var TRY_USE_DUMP = false; - if (!Module['print']) Module['print'] = (TRY_USE_DUMP && (typeof(dump) !== "undefined") ? (function(x) { - dump(x); - }) : (function(x) { - // self.postMessage(x); // enable this if you want stdout to be sent as messages - })); - } - - if (ENVIRONMENT_IS_WEB) { - this['Module'] = Module; - } else { - Module['load'] = importScripts; - } -} else { - // Unreachable because SHELL is dependant on the others - throw 'Unknown runtime environment. Where are we?'; -} - -function globalEval(x) { - eval.call(null, x); -} -if (!Module['load'] == 'undefined' && Module['read']) { - Module['load'] = function load(f) { - globalEval(Module['read'](f)); - }; -} -if (!Module['print']) { - Module['print'] = function() {}; -} -if (!Module['printErr']) { - Module['printErr'] = Module['print']; -} -if (!Module['arguments']) { - Module['arguments'] = []; -} -// *** Environment setup code *** - -// Closure helpers -Module.print = Module['print']; -Module.printErr = Module['printErr']; - -// Callbacks -Module['preRun'] = []; -Module['postRun'] = []; - -// Merge back in the overrides -for (var key in moduleOverrides) { - if (moduleOverrides.hasOwnProperty(key)) { - Module[key] = moduleOverrides[key]; - } -} - - - -// === Auto-generated preamble library stuff === - -//======================================== -// Runtime code shared with compiler -//======================================== - -var Runtime = { - stackSave: function() { - return STACKTOP; - }, - stackRestore: function(stackTop) { - STACKTOP = stackTop; - }, - forceAlign: function(target, quantum) { - quantum = quantum || 4; - if (quantum == 1) return target; - if (isNumber(target) && isNumber(quantum)) { - return Math.ceil(target / quantum) * quantum; - } else if (isNumber(quantum) && isPowerOfTwo(quantum)) { - return '(((' + target + ')+' + (quantum - 1) + ')&' + -quantum + ')'; - } - return 'Math.ceil((' + target + ')/' + quantum + ')*' + quantum; - }, - isNumberType: function(type) { - return type in Runtime.INT_TYPES || type in Runtime.FLOAT_TYPES; - }, - isPointerType: function isPointerType(type) { - return type[type.length - 1] == '*'; - }, - isStructType: function isStructType(type) { - if (isPointerType(type)) return false; - if (isArrayType(type)) return true; - if (/?/.test(type)) return true; // { i32, i8 } etc. - anonymous struct types - // See comment in isStructPointerType() - return type[0] == '%'; - }, - INT_TYPES: { - "i1": 0, - "i8": 0, - "i16": 0, - "i32": 0, - "i64": 0 - }, - FLOAT_TYPES: { - "float": 0, - "double": 0 - }, - or64: function(x, y) { - var l = (x | 0) | (y | 0); - var h = (Math.round(x / 4294967296) | Math.round(y / 4294967296)) * 4294967296; - return l + h; - }, - and64: function(x, y) { - var l = (x | 0) & (y | 0); - var h = (Math.round(x / 4294967296) & Math.round(y / 4294967296)) * 4294967296; - return l + h; - }, - xor64: function(x, y) { - var l = (x | 0) ^ (y | 0); - var h = (Math.round(x / 4294967296) ^ Math.round(y / 4294967296)) * 4294967296; - return l + h; - }, - getNativeTypeSize: function(type) { - switch (type) { - case 'i1': - case 'i8': - return 1; - case 'i16': - return 2; - case 'i32': - return 4; - case 'i64': - return 8; - case 'float': - return 4; - case 'double': - return 8; - default: - { - if (type[type.length - 1] === '*') { - return Runtime.QUANTUM_SIZE; // A pointer - } else if (type[0] === 'i') { - var bits = parseInt(type.substr(1)); - assert(bits % 8 === 0); - return bits / 8; - } else { - return 0; - } - } - } - }, - getNativeFieldSize: function(type) { - return Math.max(Runtime.getNativeTypeSize(type), Runtime.QUANTUM_SIZE); - }, - dedup: function dedup(items, ident) { - var seen = {}; - if (ident) { - return items.filter(function(item) { - if (seen[item[ident]]) return false; - seen[item[ident]] = true; - return true; - }); - } else { - return items.filter(function(item) { - if (seen[item]) return false; - seen[item] = true; - return true; - }); - } - }, - set: function set() { - var args = typeof arguments[0] === 'object' ? arguments[0] : arguments; - var ret = {}; - for (var i = 0; i < args.length; i++) { - ret[args[i]] = 0; - } - return ret; - }, - STACK_ALIGN: 8, - getAlignSize: function(type, size, vararg) { - // we align i64s and doubles on 64-bit boundaries, unlike x86 - if (vararg) return 8; - if (!vararg && (type == 'i64' || type == 'double')) return 8; - if (!type) return Math.min(size, 8); // align structures internally to 64 bits - return Math.min(size || (type ? Runtime.getNativeFieldSize(type) : 0), Runtime.QUANTUM_SIZE); - }, - calculateStructAlignment: function calculateStructAlignment(type) { - type.flatSize = 0; - type.alignSize = 0; - var diffs = []; - var prev = -1; - var index = 0; - type.flatIndexes = type.fields.map(function(field) { - index++; - var size, alignSize; - if (Runtime.isNumberType(field) || Runtime.isPointerType(field)) { - size = Runtime.getNativeTypeSize(field); // pack char; char; in structs, also char[X]s. - alignSize = Runtime.getAlignSize(field, size); - } else if (Runtime.isStructType(field)) { - if (field[1] === '0') { - // this is [0 x something]. When inside another structure like here, it must be at the end, - // and it adds no size - // XXX this happens in java-nbody for example... assert(index === type.fields.length, 'zero-length in the middle!'); - size = 0; - if (Types.types[field]) { - alignSize = Runtime.getAlignSize(null, Types.types[field].alignSize); - } else { - alignSize = type.alignSize || QUANTUM_SIZE; - } - } else { - size = Types.types[field].flatSize; - alignSize = Runtime.getAlignSize(null, Types.types[field].alignSize); - } - } else if (field[0] == 'b') { - // bN, large number field, like a [N x i8] - size = field.substr(1) | 0; - alignSize = 1; - } else if (field[0] === '<') { - // vector type - size = alignSize = Types.types[field].flatSize; // fully aligned - } else if (field[0] === 'i') { - // illegal integer field, that could not be legalized because it is an internal structure field - // it is ok to have such fields, if we just use them as markers of field size and nothing more complex - size = alignSize = parseInt(field.substr(1)) / 8; - assert(size % 1 === 0, 'cannot handle non-byte-size field ' + field); - } else { - assert(false, 'invalid type for calculateStructAlignment'); - } - if (type.packed) alignSize = 1; - type.alignSize = Math.max(type.alignSize, alignSize); - var curr = Runtime.alignMemory(type.flatSize, alignSize); // if necessary, place this on aligned memory - type.flatSize = curr + size; - if (prev >= 0) { - diffs.push(curr - prev); - } - prev = curr; - return curr; - }); - if (type.name_ && type.name_[0] === '[') { - // arrays have 2 elements, so we get the proper difference. then we scale here. that way we avoid - // allocating a potentially huge array for [999999 x i8] etc. - type.flatSize = parseInt(type.name_.substr(1)) * type.flatSize / 2; - } - type.flatSize = Runtime.alignMemory(type.flatSize, type.alignSize); - if (diffs.length == 0) { - type.flatFactor = type.flatSize; - } else if (Runtime.dedup(diffs).length == 1) { - type.flatFactor = diffs[0]; - } - type.needsFlattening = (type.flatFactor != 1); - return type.flatIndexes; - }, - generateStructInfo: function(struct, typeName, offset) { - var type, alignment; - if (typeName) { - offset = offset || 0; - type = (typeof Types === 'undefined' ? Runtime.typeInfo : Types.types)[typeName]; - if (!type) return null; - if (type.fields.length != struct.length) { - printErr('Number of named fields must match the type for ' + typeName + ': possibly duplicate struct names. Cannot return structInfo'); - return null; - } - alignment = type.flatIndexes; - } else { - var type = { - fields: struct.map(function(item) { - return item[0] - }) - }; - alignment = Runtime.calculateStructAlignment(type); - } - var ret = { - __size__: type.flatSize - }; - if (typeName) { - struct.forEach(function(item, i) { - if (typeof item === 'string') { - ret[item] = alignment[i] + offset; - } else { - // embedded struct - var key; - for (var k in item) key = k; - ret[key] = Runtime.generateStructInfo(item[key], type.fields[i], alignment[i]); - } - }); - } else { - struct.forEach(function(item, i) { - ret[item[1]] = alignment[i]; - }); - } - return ret; - }, - dynCall: function(sig, ptr, args) { - if (args && args.length) { - if (!args.splice) args = Array.prototype.slice.call(args); - args.splice(0, 0, ptr); - return Module['dynCall_' + sig].apply(null, args); - } else { - return Module['dynCall_' + sig].call(null, ptr); - } - }, - functionPointers: [], - addFunction: function(func) { - for (var i = 0; i < Runtime.functionPointers.length; i++) { - if (!Runtime.functionPointers[i]) { - Runtime.functionPointers[i] = func; - return 2 * (1 + i); - } - } - throw 'Finished up all reserved function pointers. Use a higher value for RESERVED_FUNCTION_POINTERS.'; - }, - removeFunction: function(index) { - Runtime.functionPointers[(index - 2) / 2] = null; - }, - getAsmConst: function(code, numArgs) { - // code is a constant string on the heap, so we can cache these - if (!Runtime.asmConstCache) Runtime.asmConstCache = {}; - var func = Runtime.asmConstCache[code]; - if (func) return func; - var args = []; - for (var i = 0; i < numArgs; i++) { - args.push(String.fromCharCode(36) + i); // $0, $1 etc - } - code = Pointer_stringify(code); - if (code[0] === '"') { - // tolerate EM_ASM("..code..") even though EM_ASM(..code..) is correct - if (code.indexOf('"', 1) === code.length - 1) { - code = code.substr(1, code.length - 2); - } else { - // something invalid happened, e.g. EM_ASM("..code($0)..", input) - abort('invalid EM_ASM input |' + code + '|. Please use EM_ASM(..code..) (no quotes) or EM_ASM({ ..code($0).. }, input) (to input values)'); - } - } - return Runtime.asmConstCache[code] = eval('(function(' + args.join(',') + '){ ' + code + ' })'); // new Function does not allow upvars in node - }, - warnOnce: function(text) { - if (!Runtime.warnOnce.shown) Runtime.warnOnce.shown = {}; - if (!Runtime.warnOnce.shown[text]) { - Runtime.warnOnce.shown[text] = 1; - Module.printErr(text); - } - }, - funcWrappers: {}, - getFuncWrapper: function(func, sig) { - assert(sig); - if (!Runtime.funcWrappers[func]) { - Runtime.funcWrappers[func] = function dynCall_wrapper() { - return Runtime.dynCall(sig, func, arguments); - }; - } - return Runtime.funcWrappers[func]; - }, - UTF8Processor: function() { - var buffer = []; - var needed = 0; - this.processCChar = function(code) { - code = code & 0xFF; - - if (buffer.length == 0) { - if ((code & 0x80) == 0x00) { // 0xxxxxxx - return String.fromCharCode(code); - } - buffer.push(code); - if ((code & 0xE0) == 0xC0) { // 110xxxxx - needed = 1; - } else if ((code & 0xF0) == 0xE0) { // 1110xxxx - needed = 2; - } else { // 11110xxx - needed = 3; - } - return ''; - } - - if (needed) { - buffer.push(code); - needed--; - if (needed > 0) return ''; - } - - var c1 = buffer[0]; - var c2 = buffer[1]; - var c3 = buffer[2]; - var c4 = buffer[3]; - var ret; - if (buffer.length == 2) { - ret = String.fromCharCode(((c1 & 0x1F) << 6) | (c2 & 0x3F)); - } else if (buffer.length == 3) { - ret = String.fromCharCode(((c1 & 0x0F) << 12) | ((c2 & 0x3F) << 6) | (c3 & 0x3F)); - } else { - // http://mathiasbynens.be/notes/javascript-encoding#surrogate-formulae - var codePoint = ((c1 & 0x07) << 18) | ((c2 & 0x3F) << 12) | - ((c3 & 0x3F) << 6) | (c4 & 0x3F); - ret = String.fromCharCode( - Math.floor((codePoint - 0x10000) / 0x400) + 0xD800, (codePoint - 0x10000) % 0x400 + 0xDC00); - } - buffer.length = 0; - return ret; - } - this.processJSString = function processJSString(string) { - string = unescape(encodeURIComponent(string)); - var ret = []; - for (var i = 0; i < string.length; i++) { - ret.push(string.charCodeAt(i)); - } - return ret; - } - }, - stackAlloc: function(size) { - var ret = STACKTOP; - STACKTOP = (STACKTOP + size) | 0; - STACKTOP = (((STACKTOP) + 7) & -8); - return ret; - }, - staticAlloc: function(size) { - var ret = STATICTOP; - STATICTOP = (STATICTOP + size) | 0; - STATICTOP = (((STATICTOP) + 7) & -8); - return ret; - }, - dynamicAlloc: function(size) { - var ret = DYNAMICTOP; - DYNAMICTOP = (DYNAMICTOP + size) | 0; - DYNAMICTOP = (((DYNAMICTOP) + 7) & -8); - if (DYNAMICTOP >= TOTAL_MEMORY) enlargeMemory();; - return ret; - }, - alignMemory: function(size, quantum) { - var ret = size = Math.ceil((size) / (quantum ? quantum : 8)) * (quantum ? quantum : 8); - return ret; - }, - makeBigInt: function(low, high, unsigned) { - var ret = (unsigned ? ((+((low >>> 0))) + ((+((high >>> 0))) * (+4294967296))) : ((+((low >>> 0))) + ((+((high | 0))) * (+4294967296)))); - return ret; - }, - GLOBAL_BASE: 8, - QUANTUM_SIZE: 4, - __dummy__: 0 -} - - -Module['Runtime'] = Runtime; - - - - - - - - - -//======================================== -// Runtime essentials -//======================================== - -var __THREW__ = 0; // Used in checking for thrown exceptions. - -var ABORT = false; // whether we are quitting the application. no code should run after this. set in exit() and abort() -var EXITSTATUS = 0; - -var undef = 0; -// tempInt is used for 32-bit signed values or smaller. tempBigInt is used -// for 32-bit unsigned values or more than 32 bits. TODO: audit all uses of tempInt -var tempValue, tempInt, tempBigInt, tempInt2, tempBigInt2, tempPair, tempBigIntI, tempBigIntR, tempBigIntS, tempBigIntP, tempBigIntD, tempDouble, tempFloat; -var tempI64, tempI64b; -var tempRet0, tempRet1, tempRet2, tempRet3, tempRet4, tempRet5, tempRet6, tempRet7, tempRet8, tempRet9; - -function assert(condition, text) { - if (!condition) { - abort('Assertion failed: ' + text); - } -} - -var globalScope = this; - -// C calling interface. A convenient way to call C functions (in C files, or -// defined with extern "C"). -// -// Note: LLVM optimizations can inline and remove functions, after which you will not be -// able to call them. Closure can also do so. To avoid that, add your function to -// the exports using something like -// -// -s EXPORTED_FUNCTIONS='["_main", "_myfunc"]' -// -// @param ident The name of the C function (note that C++ functions will be name-mangled - use extern "C") -// @param returnType The return type of the function, one of the JS types 'number', 'string' or 'array' (use 'number' for any C pointer, and -// 'array' for JavaScript arrays and typed arrays; note that arrays are 8-bit). -// @param argTypes An array of the types of arguments for the function (if there are no arguments, this can be ommitted). Types are as in returnType, -// except that 'array' is not possible (there is no way for us to know the length of the array) -// @param args An array of the arguments to the function, as native JS values (as in returnType) -// Note that string arguments will be stored on the stack (the JS string will become a C string on the stack). -// @return The return value, as a native JS value (as in returnType) -function ccall(ident, returnType, argTypes, args) { - return ccallFunc(getCFunc(ident), returnType, argTypes, args); -} -Module["ccall"] = ccall; - -// Returns the C function with a specified identifier (for C++, you need to do manual name mangling) -function getCFunc(ident) { - try { - var func = Module['_' + ident]; // closure exported function - if (!func) func = eval('_' + ident); // explicit lookup - } catch (e) {} - assert(func, 'Cannot call unknown function ' + ident + ' (perhaps LLVM optimizations or closure removed it?)'); - return func; -} - -// Internal function that does a C call using a function, not an identifier -function ccallFunc(func, returnType, argTypes, args) { - var stack = 0; - - function toC(value, type) { - if (type == 'string') { - if (value === null || value === undefined || value === 0) return 0; // null string - value = intArrayFromString(value); - type = 'array'; - } - if (type == 'array') { - if (!stack) stack = Runtime.stackSave(); - var ret = Runtime.stackAlloc(value.length); - writeArrayToMemory(value, ret); - return ret; - } - return value; - } - - function fromC(value, type) { - if (type == 'string') { - return Pointer_stringify(value); - } - assert(type != 'array'); - return value; - } - var i = 0; - var cArgs = args ? args.map(function(arg) { - return toC(arg, argTypes[i++]); - }) : []; - var ret = fromC(func.apply(null, cArgs), returnType); - if (stack) Runtime.stackRestore(stack); - return ret; -} - -// Returns a native JS wrapper for a C function. This is similar to ccall, but -// returns a function you can call repeatedly in a normal way. For example: -// -// var my_function = cwrap('my_c_function', 'number', ['number', 'number']); -// alert(my_function(5, 22)); -// alert(my_function(99, 12)); -// -function cwrap(ident, returnType, argTypes) { - var func = getCFunc(ident); - return function() { - return ccallFunc(func, returnType, argTypes, Array.prototype.slice.call(arguments)); - } -} -Module["cwrap"] = cwrap; - -// Sets a value in memory in a dynamic way at run-time. Uses the -// type data. This is the same as makeSetValue, except that -// makeSetValue is done at compile-time and generates the needed -// code then, whereas this function picks the right code at -// run-time. -// Note that setValue and getValue only do *aligned* writes and reads! -// Note that ccall uses JS types as for defining types, while setValue and -// getValue need LLVM types ('i8', 'i32') - this is a lower-level operation -function setValue(ptr, value, type, noSafe) { - type = type || 'i8'; - if (type.charAt(type.length - 1) === '*') type = 'i32'; // pointers are 32-bit - switch (type) { - case 'i1': - HEAP8[(ptr)] = value; - break; - case 'i8': - HEAP8[(ptr)] = value; - break; - case 'i16': - HEAP16[((ptr) >> 1)] = value; - break; - case 'i32': - HEAP32[((ptr) >> 2)] = value; - break; - case 'i64': - (tempI64 = [value >>> 0, (tempDouble = value, (+(Math_abs(tempDouble))) >= (+1) ? (tempDouble > (+0) ? ((Math_min((+(Math_floor((tempDouble) / (+4294967296)))), (+4294967295))) | 0) >>> 0 : (~~((+(Math_ceil((tempDouble - +(((~~(tempDouble))) >>> 0)) / (+4294967296)))))) >>> 0) : 0)], HEAP32[((ptr) >> 2)] = tempI64[0], HEAP32[(((ptr) + (4)) >> 2)] = tempI64[1]); - break; - case 'float': - HEAPF32[((ptr) >> 2)] = value; - break; - case 'double': - HEAPF64[((ptr) >> 3)] = value; - break; - default: - abort('invalid type for setValue: ' + type); - } -} -Module['setValue'] = setValue; - -// Parallel to setValue. -function getValue(ptr, type, noSafe) { - type = type || 'i8'; - if (type.charAt(type.length - 1) === '*') type = 'i32'; // pointers are 32-bit - switch (type) { - case 'i1': - return HEAP8[(ptr)]; - case 'i8': - return HEAP8[(ptr)]; - case 'i16': - return HEAP16[((ptr) >> 1)]; - case 'i32': - return HEAP32[((ptr) >> 2)]; - case 'i64': - return HEAP32[((ptr) >> 2)]; - case 'float': - return HEAPF32[((ptr) >> 2)]; - case 'double': - return HEAPF64[((ptr) >> 3)]; - default: - abort('invalid type for setValue: ' + type); - } - return null; -} -Module['getValue'] = getValue; - -var ALLOC_NORMAL = 0; // Tries to use _malloc() -var ALLOC_STACK = 1; // Lives for the duration of the current function call -var ALLOC_STATIC = 2; // Cannot be freed -var ALLOC_DYNAMIC = 3; // Cannot be freed except through sbrk -var ALLOC_NONE = 4; // Do not allocate -Module['ALLOC_NORMAL'] = ALLOC_NORMAL; -Module['ALLOC_STACK'] = ALLOC_STACK; -Module['ALLOC_STATIC'] = ALLOC_STATIC; -Module['ALLOC_DYNAMIC'] = ALLOC_DYNAMIC; -Module['ALLOC_NONE'] = ALLOC_NONE; - -// allocate(): This is for internal use. You can use it yourself as well, but the interface -// is a little tricky (see docs right below). The reason is that it is optimized -// for multiple syntaxes to save space in generated code. So you should -// normally not use allocate(), and instead allocate memory using _malloc(), -// initialize it with setValue(), and so forth. -// @slab: An array of data, or a number. If a number, then the size of the block to allocate, -// in *bytes* (note that this is sometimes confusing: the next parameter does not -// affect this!) -// @types: Either an array of types, one for each byte (or 0 if no type at that position), -// or a single type which is used for the entire block. This only matters if there -// is initial data - if @slab is a number, then this does not matter at all and is -// ignored. -// @allocator: How to allocate memory, see ALLOC_* -function allocate(slab, types, allocator, ptr) { - var zeroinit, size; - if (typeof slab === 'number') { - zeroinit = true; - size = slab; - } else { - zeroinit = false; - size = slab.length; - } - - var singleType = typeof types === 'string' ? types : null; - - var ret; - if (allocator == ALLOC_NONE) { - ret = ptr; - } else { - ret = [_malloc, Runtime.stackAlloc, Runtime.staticAlloc, Runtime.dynamicAlloc][allocator === undefined ? ALLOC_STATIC : allocator](Math.max(size, singleType ? 1 : types.length)); - } - - if (zeroinit) { - var ptr = ret, - stop; - assert((ret & 3) == 0); - stop = ret + (size & ~3); - for (; ptr < stop; ptr += 4) { - HEAP32[((ptr) >> 2)] = 0; - } - stop = ret + size; - while (ptr < stop) { - HEAP8[((ptr++) | 0)] = 0; - } - return ret; - } - - if (singleType === 'i8') { - if (slab.subarray || slab.slice) { - HEAPU8.set(slab, ret); - } else { - HEAPU8.set(new Uint8Array(slab), ret); - } - return ret; - } - - var i = 0, - type, typeSize, previousType; - while (i < size) { - var curr = slab[i]; - - if (typeof curr === 'function') { - curr = Runtime.getFunctionIndex(curr); - } - - type = singleType || types[i]; - if (type === 0) { - i++; - continue; - } - - if (type == 'i64') type = 'i32'; // special case: we have one i32 here, and one i32 later - - setValue(ret + i, curr, type); - - // no need to look up size unless type changes, so cache it - if (previousType !== type) { - typeSize = Runtime.getNativeTypeSize(type); - previousType = type; - } - i += typeSize; - } - - return ret; -} -Module['allocate'] = allocate; - -function Pointer_stringify(ptr, /* optional */ length) { - // TODO: use TextDecoder - // Find the length, and check for UTF while doing so - var hasUtf = false; - var t; - var i = 0; - while (1) { - t = HEAPU8[(((ptr) + (i)) | 0)]; - if (t >= 128) hasUtf = true; - else if (t == 0 && !length) break; - i++; - if (length && i == length) break; - } - if (!length) length = i; - - var ret = ''; - - if (!hasUtf) { - var MAX_CHUNK = 1024; // split up into chunks, because .apply on a huge string can overflow the stack - var curr; - while (length > 0) { - curr = String.fromCharCode.apply(String, HEAPU8.subarray(ptr, ptr + Math.min(length, MAX_CHUNK))); - ret = ret ? ret + curr : curr; - ptr += MAX_CHUNK; - length -= MAX_CHUNK; - } - return ret; - } - - var utf8 = new Runtime.UTF8Processor(); - for (i = 0; i < length; i++) { - t = HEAPU8[(((ptr) + (i)) | 0)]; - ret += utf8.processCChar(t); - } - return ret; -} -Module['Pointer_stringify'] = Pointer_stringify; - -// Given a pointer 'ptr' to a null-terminated UTF16LE-encoded string in the emscripten HEAP, returns -// a copy of that string as a Javascript String object. -function UTF16ToString(ptr) { - var i = 0; - - var str = ''; - while (1) { - var codeUnit = HEAP16[(((ptr) + (i * 2)) >> 1)]; - if (codeUnit == 0) - return str; - ++i; - // fromCharCode constructs a character from a UTF-16 code unit, so we can pass the UTF16 string right through. - str += String.fromCharCode(codeUnit); - } -} -Module['UTF16ToString'] = UTF16ToString; - -// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', -// null-terminated and encoded in UTF16LE form. The copy will require at most (str.length*2+1)*2 bytes of space in the HEAP. -function stringToUTF16(str, outPtr) { - for (var i = 0; i < str.length; ++i) { - // charCodeAt returns a UTF-16 encoded code unit, so it can be directly written to the HEAP. - var codeUnit = str.charCodeAt(i); // possibly a lead surrogate - HEAP16[(((outPtr) + (i * 2)) >> 1)] = codeUnit; - } - // Null-terminate the pointer to the HEAP. - HEAP16[(((outPtr) + (str.length * 2)) >> 1)] = 0; -} -Module['stringToUTF16'] = stringToUTF16; - -// Given a pointer 'ptr' to a null-terminated UTF32LE-encoded string in the emscripten HEAP, returns -// a copy of that string as a Javascript String object. -function UTF32ToString(ptr) { - var i = 0; - - var str = ''; - while (1) { - var utf32 = HEAP32[(((ptr) + (i * 4)) >> 2)]; - if (utf32 == 0) - return str; - ++i; - // Gotcha: fromCharCode constructs a character from a UTF-16 encoded code (pair), not from a Unicode code point! So encode the code point to UTF-16 for constructing. - if (utf32 >= 0x10000) { - var ch = utf32 - 0x10000; - str += String.fromCharCode(0xD800 | (ch >> 10), 0xDC00 | (ch & 0x3FF)); - } else { - str += String.fromCharCode(utf32); - } - } -} -Module['UTF32ToString'] = UTF32ToString; - -// Copies the given Javascript String object 'str' to the emscripten HEAP at address 'outPtr', -// null-terminated and encoded in UTF32LE form. The copy will require at most (str.length+1)*4 bytes of space in the HEAP, -// but can use less, since str.length does not return the number of characters in the string, but the number of UTF-16 code units in the string. -function stringToUTF32(str, outPtr) { - var iChar = 0; - for (var iCodeUnit = 0; iCodeUnit < str.length; ++iCodeUnit) { - // Gotcha: charCodeAt returns a 16-bit word that is a UTF-16 encoded code unit, not a Unicode code point of the character! We must decode the string to UTF-32 to the heap. - var codeUnit = str.charCodeAt(iCodeUnit); // possibly a lead surrogate - if (codeUnit >= 0xD800 && codeUnit <= 0xDFFF) { - var trailSurrogate = str.charCodeAt(++iCodeUnit); - codeUnit = 0x10000 + ((codeUnit & 0x3FF) << 10) | (trailSurrogate & 0x3FF); - } - HEAP32[(((outPtr) + (iChar * 4)) >> 2)] = codeUnit; - ++iChar; - } - // Null-terminate the pointer to the HEAP. - HEAP32[(((outPtr) + (iChar * 4)) >> 2)] = 0; -} -Module['stringToUTF32'] = stringToUTF32; - -function demangle(func) { - try { - // Special-case the entry point, since its name differs from other name mangling. - if (func == 'Object._main' || func == '_main') { - return 'main()'; - } - if (typeof func === 'number') func = Pointer_stringify(func); - if (func[0] !== '_') return func; - if (func[1] !== '_') return func; // C function - if (func[2] !== 'Z') return func; - switch (func[3]) { - case 'n': - return 'operator new()'; - case 'd': - return 'operator delete()'; - } - var i = 3; - // params, etc. - var basicTypes = { - 'v': 'void', - 'b': 'bool', - 'c': 'char', - 's': 'short', - 'i': 'int', - 'l': 'long', - 'f': 'float', - 'd': 'double', - 'w': 'wchar_t', - 'a': 'signed char', - 'h': 'unsigned char', - 't': 'unsigned short', - 'j': 'unsigned int', - 'm': 'unsigned long', - 'x': 'long long', - 'y': 'unsigned long long', - 'z': '...' - }; - - function dump(x) { - //return; - if (x) Module.print(x); - Module.print(func); - var pre = ''; - for (var a = 0; a < i; a++) pre += ' '; - Module.print(pre + '^'); - } - var subs = []; - - function parseNested() { - i++; - if (func[i] === 'K') i++; // ignore const - var parts = []; - while (func[i] !== 'E') { - if (func[i] === 'S') { // substitution - i++; - var next = func.indexOf('_', i); - var num = func.substring(i, next) || 0; - parts.push(subs[num] || '?'); - i = next + 1; - continue; - } - if (func[i] === 'C') { // constructor - parts.push(parts[parts.length - 1]); - i += 2; - continue; - } - var size = parseInt(func.substr(i)); - var pre = size.toString().length; - if (!size || !pre) { - i--; - break; - } // counter i++ below us - var curr = func.substr(i + pre, size); - parts.push(curr); - subs.push(curr); - i += pre + size; - } - i++; // skip E - return parts; - } - var first = true; - - function parse(rawList, limit, allowVoid) { // main parser - limit = limit || Infinity; - var ret = '', - list = []; - - function flushList() { - return '(' + list.join(', ') + ')'; - } - var name; - if (func[i] === 'N') { - // namespaced N-E - name = parseNested().join('::'); - limit--; - if (limit === 0) return rawList ? [name] : name; - } else { - // not namespaced - if (func[i] === 'K' || (first && func[i] === 'L')) i++; // ignore const and first 'L' - var size = parseInt(func.substr(i)); - if (size) { - var pre = size.toString().length; - name = func.substr(i + pre, size); - i += pre + size; - } - } - first = false; - if (func[i] === 'I') { - i++; - var iList = parse(true); - var iRet = parse(true, 1, true); - ret += iRet[0] + ' ' + name + '<' + iList.join(', ') + '>'; - } else { - ret = name; - } - paramLoop: while (i < func.length && limit-- > 0) { - //dump('paramLoop'); - var c = func[i++]; - if (c in basicTypes) { - list.push(basicTypes[c]); - } else { - switch (c) { - case 'P': - list.push(parse(true, 1, true)[0] + '*'); - break; // pointer - case 'R': - list.push(parse(true, 1, true)[0] + '&'); - break; // reference - case 'L': - { // literal - i++; // skip basic type - var end = func.indexOf('E', i); - var size = end - i; - list.push(func.substr(i, size)); - i += size + 2; // size + 'EE' - break; - } - case 'A': - { // array - var size = parseInt(func.substr(i)); - i += size.toString().length; - if (func[i] !== '_') throw '?'; - i++; // skip _ - list.push(parse(true, 1, true)[0] + ' [' + size + ']'); - break; - } - case 'E': - break paramLoop; - default: - ret += '?' + c; - break paramLoop; - } - } - } - if (!allowVoid && list.length === 1 && list[0] === 'void') list = []; // avoid (void) - return rawList ? list : ret + flushList(); - } - return parse(); - } catch (e) { - return func; - } -} - -function demangleAll(text) { - return text.replace(/__Z[\w\d_]+/g, function(x) { - var y = demangle(x); - return x === y ? x : (x + ' [' + y + ']') - }); -} - -function stackTrace() { - var stack = new Error().stack; - return stack ? demangleAll(stack) : '(no stack trace available)'; // Stack trace is not available at least on IE10 and Safari 6. -} - -// Memory management - -var PAGE_SIZE = 4096; - -function alignMemoryPage(x) { - return (x + 4095) & -4096; -} - -var HEAP; -var HEAP8, HEAPU8, HEAP16, HEAPU16, HEAP32, HEAPU32, HEAPF32, HEAPF64; - -var STATIC_BASE = 0, - STATICTOP = 0, - staticSealed = false; // static area -var STACK_BASE = 0, - STACKTOP = 0, - STACK_MAX = 0; // stack area -var DYNAMIC_BASE = 0, - DYNAMICTOP = 0; // dynamic area handled by sbrk - -function enlargeMemory() { - abort('Cannot enlarge memory arrays in asm.js. Either (1) compile with -s TOTAL_MEMORY=X with X higher than the current value ' + TOTAL_MEMORY + ', or (2) set Module.TOTAL_MEMORY before the program runs.'); -} - -var TOTAL_STACK = Module['TOTAL_STACK'] || 5242880; -var TOTAL_MEMORY = Module['TOTAL_MEMORY'] || 67108864; -var FAST_MEMORY = Module['FAST_MEMORY'] || 2097152; - -var totalMemory = 4096; -while (totalMemory < TOTAL_MEMORY || totalMemory < 2 * TOTAL_STACK) { - if (totalMemory < 16 * 1024 * 1024) { - totalMemory *= 2; - } else { - totalMemory += 16 * 1024 * 1024 - } -} -if (totalMemory !== TOTAL_MEMORY) { - Module.printErr('increasing TOTAL_MEMORY to ' + totalMemory + ' to be more reasonable'); - TOTAL_MEMORY = totalMemory; -} - -// Initialize the runtime's memory -// check for full engine support (use string 'subarray' to avoid closure compiler confusion) -assert(typeof Int32Array !== 'undefined' && typeof Float64Array !== 'undefined' && !!(new Int32Array(1)['subarray']) && !!(new Int32Array(1)['set']), - 'Cannot fallback to non-typed array case: Code is too specialized'); - -var buffer = new ArrayBuffer(TOTAL_MEMORY); -HEAP8 = new Int8Array(buffer); -HEAP16 = new Int16Array(buffer); -HEAP32 = new Int32Array(buffer); -HEAPU8 = new Uint8Array(buffer); -HEAPU16 = new Uint16Array(buffer); -HEAPU32 = new Uint32Array(buffer); -HEAPF32 = new Float32Array(buffer); -HEAPF64 = new Float64Array(buffer); - -// Endianness check (note: assumes compiler arch was little-endian) -HEAP32[0] = 255; -assert(HEAPU8[0] === 255 && HEAPU8[3] === 0, 'Typed arrays 2 must be run on a little-endian system'); - -Module['HEAP'] = HEAP; -Module['HEAP8'] = HEAP8; -Module['HEAP16'] = HEAP16; -Module['HEAP32'] = HEAP32; -Module['HEAPU8'] = HEAPU8; -Module['HEAPU16'] = HEAPU16; -Module['HEAPU32'] = HEAPU32; -Module['HEAPF32'] = HEAPF32; -Module['HEAPF64'] = HEAPF64; - -function callRuntimeCallbacks(callbacks) { - while (callbacks.length > 0) { - var callback = callbacks.shift(); - if (typeof callback == 'function') { - callback(); - continue; - } - var func = callback.func; - if (typeof func === 'number') { - if (callback.arg === undefined) { - Runtime.dynCall('v', func); - } else { - Runtime.dynCall('vi', func, [callback.arg]); - } - } else { - func(callback.arg === undefined ? null : callback.arg); - } - } -} - -var __ATPRERUN__ = []; // functions called before the runtime is initialized -var __ATINIT__ = []; // functions called during startup -var __ATMAIN__ = []; // functions called when main() is to be run -var __ATEXIT__ = []; // functions called during shutdown -var __ATPOSTRUN__ = []; // functions called after the runtime has exited - -var runtimeInitialized = false; - -function preRun() { - // compatibility - merge in anything from Module['preRun'] at this time - if (Module['preRun']) { - if (typeof Module['preRun'] == 'function') Module['preRun'] = [Module['preRun']]; - while (Module['preRun'].length) { - addOnPreRun(Module['preRun'].shift()); - } - } - callRuntimeCallbacks(__ATPRERUN__); -} - -function ensureInitRuntime() { - if (runtimeInitialized) return; - runtimeInitialized = true; - callRuntimeCallbacks(__ATINIT__); -} - -function preMain() { - callRuntimeCallbacks(__ATMAIN__); -} - -function exitRuntime() { - callRuntimeCallbacks(__ATEXIT__); -} - -function postRun() { - // compatibility - merge in anything from Module['postRun'] at this time - if (Module['postRun']) { - if (typeof Module['postRun'] == 'function') Module['postRun'] = [Module['postRun']]; - while (Module['postRun'].length) { - addOnPostRun(Module['postRun'].shift()); - } - } - callRuntimeCallbacks(__ATPOSTRUN__); -} - -function addOnPreRun(cb) { - __ATPRERUN__.unshift(cb); -} -Module['addOnPreRun'] = Module.addOnPreRun = addOnPreRun; - -function addOnInit(cb) { - __ATINIT__.unshift(cb); -} -Module['addOnInit'] = Module.addOnInit = addOnInit; - -function addOnPreMain(cb) { - __ATMAIN__.unshift(cb); -} -Module['addOnPreMain'] = Module.addOnPreMain = addOnPreMain; - -function addOnExit(cb) { - __ATEXIT__.unshift(cb); -} -Module['addOnExit'] = Module.addOnExit = addOnExit; - -function addOnPostRun(cb) { - __ATPOSTRUN__.unshift(cb); -} -Module['addOnPostRun'] = Module.addOnPostRun = addOnPostRun; - -// Tools - -// This processes a JS string into a C-line array of numbers, 0-terminated. -// For LLVM-originating strings, see parser.js:parseLLVMString function -function intArrayFromString(stringy, dontAddNull, length /* optional */ ) { - var ret = (new Runtime.UTF8Processor()).processJSString(stringy); - if (length) { - ret.length = length; - } - if (!dontAddNull) { - ret.push(0); - } - return ret; -} -Module['intArrayFromString'] = intArrayFromString; - -function intArrayToString(array) { - var ret = []; - for (var i = 0; i < array.length; i++) { - var chr = array[i]; - if (chr > 0xFF) { - chr &= 0xFF; - } - ret.push(String.fromCharCode(chr)); - } - return ret.join(''); -} -Module['intArrayToString'] = intArrayToString; - -// Write a Javascript array to somewhere in the heap -function writeStringToMemory(string, buffer, dontAddNull) { - var array = intArrayFromString(string, dontAddNull); - var i = 0; - while (i < array.length) { - var chr = array[i]; - HEAP8[(((buffer) + (i)) | 0)] = chr; - i = i + 1; - } -} -Module['writeStringToMemory'] = writeStringToMemory; - -function writeArrayToMemory(array, buffer) { - for (var i = 0; i < array.length; i++) { - HEAP8[(((buffer) + (i)) | 0)] = array[i]; - } -} -Module['writeArrayToMemory'] = writeArrayToMemory; - -function writeAsciiToMemory(str, buffer, dontAddNull) { - for (var i = 0; i < str.length; i++) { - HEAP8[(((buffer) + (i)) | 0)] = str.charCodeAt(i); - } - if (!dontAddNull) HEAP8[(((buffer) + (str.length)) | 0)] = 0; -} -Module['writeAsciiToMemory'] = writeAsciiToMemory; - -function unSign(value, bits, ignore, sig) { - if (value >= 0) { - return value; - } - return bits <= 32 ? 2 * Math.abs(1 << (bits - 1)) + value // Need some trickery, since if bits == 32, we are right at the limit of the bits JS uses in bitshifts - : Math.pow(2, bits) + value; -} - -function reSign(value, bits, ignore, sig) { - if (value <= 0) { - return value; - } - var half = bits <= 32 ? Math.abs(1 << (bits - 1)) // abs is needed if bits == 32 - : Math.pow(2, bits - 1); - if (value >= half && (bits <= 32 || value > half)) { // for huge values, we can hit the precision limit and always get true here. so don't do that - // but, in general there is no perfect solution here. With 64-bit ints, we get rounding and errors - // TODO: In i64 mode 1, resign the two parts separately and safely - value = -2 * half + value; // Cannot bitshift half, as it may be at the limit of the bits JS uses in bitshifts - } - return value; -} - -// check for imul support, and also for correctness ( https://bugs.webkit.org/show_bug.cgi?id=126345 ) -if (!Math['imul'] || Math['imul'](0xffffffff, 5) !== -5) Math['imul'] = function imul(a, b) { - var ah = a >>> 16; - var al = a & 0xffff; - var bh = b >>> 16; - var bl = b & 0xffff; - return (al * bl + ((ah * bl + al * bh) << 16)) | 0; -}; -Math.imul = Math['imul']; - - -var Math_abs = Math.abs; -var Math_cos = Math.cos; -var Math_sin = Math.sin; -var Math_tan = Math.tan; -var Math_acos = Math.acos; -var Math_asin = Math.asin; -var Math_atan = Math.atan; -var Math_atan2 = Math.atan2; -var Math_exp = Math.exp; -var Math_log = Math.log; -var Math_sqrt = Math.sqrt; -var Math_ceil = Math.ceil; -var Math_floor = Math.floor; -var Math_pow = Math.pow; -var Math_imul = Math.imul; -var Math_fround = Math.fround; -var Math_min = Math.min; - -// A counter of dependencies for calling run(). If we need to -// do asynchronous work before running, increment this and -// decrement it. Incrementing must happen in a place like -// PRE_RUN_ADDITIONS (used by emcc to add file preloading). -// Note that you can add dependencies in preRun, even though -// it happens right before run - run will be postponed until -// the dependencies are met. -var runDependencies = 0; -var runDependencyWatcher = null; -var dependenciesFulfilled = null; // overridden to take different actions when all run dependencies are fulfilled - -function addRunDependency(id) { - runDependencies++; - if (Module['monitorRunDependencies']) { - Module['monitorRunDependencies'](runDependencies); - } -} -Module['addRunDependency'] = addRunDependency; - -function removeRunDependency(id) { - runDependencies--; - if (Module['monitorRunDependencies']) { - Module['monitorRunDependencies'](runDependencies); - } - if (runDependencies == 0) { - if (runDependencyWatcher !== null) { - clearInterval(runDependencyWatcher); - runDependencyWatcher = null; - } - if (dependenciesFulfilled) { - var callback = dependenciesFulfilled; - dependenciesFulfilled = null; - callback(); // can add another dependenciesFulfilled - } - } -} -Module['removeRunDependency'] = removeRunDependency; - -Module["preloadedImages"] = {}; // maps url to image data -Module["preloadedAudios"] = {}; // maps url to audio data - - -var memoryInitializer = null; - -// === Body === - - - -STATIC_BASE = 8; - -STATICTOP = STATIC_BASE + 3016; - - -/* global initializers */ -__ATINIT__.push({ - func: function() { - runPostSets() - } -}); - - - - - - - - - -/* memory initializer */ -allocate([93, 59, 32, 101, 114, 114, 111, 114, 95, 118, 101, 99, 116, 111, 114, 95, 100, 98, 61, 50, 48, 42, 108, 111, 103, 49, 48, 40, 101, 114, 114, 111, 114, 95, 118, 101, 99, 116, 111, 114, 41, 59, 32, 112, 108, 111, 116, 40, 101, 114, 114, 111, 114, 95, 118, 101, 99, 116, 111, 114, 95, 100, 98, 41, 59, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 2, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 8, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 2, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 8, 0, 0, 0, 28, 231, 5, 82, 18, 219, 7, 82, 171, 207, 217, 210, 255, 248, 185, 81, 67, 26, 172, 211, 92, 100, 176, 210, 167, 29, 24, 212, 1, 58, 121, 211, 5, 161, 70, 212, 42, 210, 182, 211, 240, 134, 66, 212, 116, 10, 164, 211, 11, 46, 229, 211, 37, 173, 82, 210, 45, 128, 69, 83, 130, 129, 238, 83, 100, 212, 139, 84, 33, 133, 147, 84, 14, 87, 4, 85, 54, 41, 234, 84, 49, 183, 53, 85, 74, 117, 15, 85, 185, 20, 69, 85, 219, 25, 7, 85, 149, 8, 31, 85, 184, 136, 154, 84, 234, 77, 91, 84, 128, 81, 20, 212, 1, 159, 234, 212, 243, 146, 75, 213, 95, 236, 172, 213, 100, 44, 202, 213, 103, 156, 23, 214, 63, 161, 23, 214, 128, 90, 88, 214, 221, 79, 60, 214, 252, 178, 135, 214, 115, 177, 41, 214, 73, 44, 154, 214, 136, 253, 9, 87, 166, 141, 226, 87, 136, 253, 9, 87, 73, 44, 154, 214, 115, 177, 41, 214, 252, 178, 135, 214, 221, 79, 60, 214, 128, 90, 88, 214, 63, 161, 23, 214, 103, 156, 23, 214, 100, 44, 202, 213, 95, 236, 172, 213, 243, 146, 75, 213, 1, 159, 234, 212, 128, 81, 20, 212, 234, 77, 91, 84, 184, 136, 154, 84, 149, 8, 31, 85, 219, 25, 7, 85, 185, 20, 69, 85, 74, 117, 15, 85, 49, 183, 53, 85, 54, 41, 234, 84, 14, 87, 4, 85, 33, 133, 147, 84, 100, 212, 139, 84, 130, 129, 238, 83, 45, 128, 69, 83, 37, 173, 82, 210, 11, 46, 229, 211, 116, 10, 164, 211, 240, 134, 66, 212, 42, 210, 182, 211, 5, 161, 70, 212, 1, 58, 121, 211, 167, 29, 24, 212, 92, 100, 176, 210, 67, 26, 172, 211, 255, 248, 185, 81, 171, 207, 217, 210, 18, 219, 7, 82, 28, 231, 5, 82, 0, 0, 0, 0, 54, 48, 226, 58, 146, 125, 235, 58, 103, 152, 251, 58, 165, 142, 6, 59, 225, 4, 13, 59, 38, 144, 14, 59, 103, 147, 9, 59, 60, 177, 251, 58, 143, 77, 217, 58, 50, 249, 176, 58, 123, 7, 137, 58, 54, 86, 79, 58, 190, 7, 35, 58, 95, 252, 16, 58, 171, 157, 22, 58, 247, 87, 43, 58, 110, 118, 66, 58, 80, 3, 78, 58, 177, 6, 66, 58, 198, 99, 23, 58, 147, 77, 155, 57, 198, 133, 165, 184, 132, 132, 3, 186, 86, 199, 111, 186, 31, 120, 165, 186, 168, 52, 197, 186, 109, 19, 213, 186, 217, 231, 214, 186, 70, 11, 208, 186, 187, 54, 200, 186, 74, 193, 199, 186, 29, 174, 213, 186, 147, 247, 245, 186, 236, 65, 20, 187, 171, 131, 52, 187, 237, 240, 87, 187, 225, 230, 121, 187, 227, 255, 138, 187, 218, 143, 148, 187, 201, 29, 153, 187, 231, 85, 153, 187, 170, 248, 150, 187, 96, 122, 148, 187, 24, 117, 148, 187, 20, 11, 153, 187, 107, 95, 163, 187, 189, 69, 179, 187, 0, 61, 199, 187, 238, 187, 220, 187, 239, 190, 240, 187, 7, 61, 0, 188, 20, 3, 5, 188, 230, 111, 6, 188, 36, 12, 5, 188, 23, 20, 2, 188, 105, 106, 254, 187, 152, 89, 252, 187, 126, 93, 0, 188, 67, 116, 6, 188, 95, 51, 16, 188, 124, 114, 28, 188, 166, 86, 41, 188, 110, 184, 52, 188, 55, 165, 60, 188, 201, 216, 63, 188, 23, 26, 62, 188, 211, 92, 56, 188, 128, 156, 48, 188, 176, 117, 41, 188, 179, 144, 37, 188, 23, 251, 38, 188, 236, 150, 46, 188, 20, 198, 59, 188, 142, 104, 76, 188, 167, 54, 93, 188, 133, 108, 106, 188, 9, 168, 112, 188, 66, 198, 109, 188, 112, 148, 97, 188, 171, 26, 78, 188, 2, 111, 55, 188, 184, 1, 35, 188, 168, 120, 22, 188, 132, 80, 22, 188, 167, 127, 36, 188, 179, 102, 63, 188, 133, 85, 97, 188, 50, 99, 128, 188, 123, 181, 136, 188, 142, 242, 130, 188, 68, 247, 81, 188, 44, 158, 217, 187, 212, 209, 51, 59, 70, 233, 119, 60, 3, 184, 244, 60, 154, 24, 60, 61, 16, 180, 125, 61, 88, 146, 156, 61, 151, 75, 180, 61, 17, 154, 195, 61, 239, 227, 200, 61, 17, 154, 195, 61, 151, 75, 180, 61, 88, 146, 156, 61, 16, 180, 125, 61, 154, 24, 60, 61, 3, 184, 244, 60, 70, 233, 119, 60, 212, 209, 51, 59, 44, 158, 217, 187, 68, 247, 81, 188, 142, 242, 130, 188, 123, 181, 136, 188, 50, 99, 128, 188, 133, 85, 97, 188, 179, 102, 63, 188, 167, 127, 36, 188, 132, 80, 22, 188, 168, 120, 22, 188, 184, 1, 35, 188, 2, 111, 55, 188, 171, 26, 78, 188, 112, 148, 97, 188, 66, 198, 109, 188, 9, 168, 112, 188, 133, 108, 106, 188, 167, 54, 93, 188, 142, 104, 76, 188, 20, 198, 59, 188, 236, 150, 46, 188, 23, 251, 38, 188, 179, 144, 37, 188, 176, 117, 41, 188, 128, 156, 48, 188, 211, 92, 56, 188, 23, 26, 62, 188, 201, 216, 63, 188, 55, 165, 60, 188, 110, 184, 52, 188, 166, 86, 41, 188, 124, 114, 28, 188, 95, 51, 16, 188, 67, 116, 6, 188, 126, 93, 0, 188, 152, 89, 252, 187, 105, 106, 254, 187, 23, 20, 2, 188, 36, 12, 5, 188, 230, 111, 6, 188, 20, 3, 5, 188, 7, 61, 0, 188, 239, 190, 240, 187, 238, 187, 220, 187, 0, 61, 199, 187, 189, 69, 179, 187, 107, 95, 163, 187, 20, 11, 153, 187, 24, 117, 148, 187, 96, 122, 148, 187, 170, 248, 150, 187, 231, 85, 153, 187, 201, 29, 153, 187, 218, 143, 148, 187, 227, 255, 138, 187, 225, 230, 121, 187, 237, 240, 87, 187, 171, 131, 52, 187, 236, 65, 20, 187, 147, 247, 245, 186, 29, 174, 213, 186, 74, 193, 199, 186, 187, 54, 200, 186, 70, 11, 208, 186, 217, 231, 214, 186, 109, 19, 213, 186, 168, 52, 197, 186, 31, 120, 165, 186, 86, 199, 111, 186, 132, 132, 3, 186, 198, 133, 165, 184, 147, 77, 155, 57, 198, 99, 23, 58, 177, 6, 66, 58, 80, 3, 78, 58, 110, 118, 66, 58, 247, 87, 43, 58, 171, 157, 22, 58, 95, 252, 16, 58, 190, 7, 35, 58, 54, 86, 79, 58, 123, 7, 137, 58, 50, 249, 176, 58, 143, 77, 217, 58, 60, 177, 251, 58, 103, 147, 9, 59, 38, 144, 14, 59, 225, 4, 13, 59, 165, 142, 6, 59, 103, 152, 251, 58, 146, 125, 235, 58, 54, 48, 226, 58, 0, 0, 0, 0, 31, 224, 36, 59, 115, 56, 74, 59, 230, 137, 111, 59, 29, 135, 135, 59, 153, 60, 146, 59, 19, 29, 151, 59, 62, 4, 151, 59, 69, 68, 148, 59, 76, 15, 146, 59, 251, 153, 147, 59, 121, 52, 155, 59, 235, 152, 169, 59, 141, 162, 189, 59, 195, 136, 212, 59, 91, 146, 234, 59, 58, 21, 252, 59, 198, 63, 3, 60, 242, 143, 4, 60, 97, 184, 2, 60, 227, 203, 254, 59, 231, 145, 249, 59, 66, 250, 249, 59, 117, 110, 1, 60, 62, 101, 10, 60, 189, 221, 22, 60, 202, 186, 36, 60, 103, 75, 49, 60, 223, 4, 58, 60, 104, 64, 61, 60, 141, 196, 58, 60, 184, 243, 51, 60, 134, 137, 43, 60, 246, 237, 36, 60, 48, 73, 35, 60, 173, 144, 40, 60, 29, 212, 52, 60, 174, 2, 70, 60, 249, 67, 88, 60, 174, 220, 102, 60, 136, 113, 109, 60, 5, 86, 105, 60, 111, 144, 90, 60, 153, 59, 68, 60, 58, 26, 44, 60, 222, 86, 25, 60, 242, 163, 18, 60, 23, 35, 28, 60, 236, 141, 53, 60, 193, 31, 89, 60, 81, 164, 123, 60, 139, 228, 134, 60, 66, 159, 126, 60, 247, 221, 62, 60, 52, 158, 136, 59, 157, 147, 230, 187, 33, 62, 176, 188, 194, 133, 28, 189, 27, 184, 99, 189, 141, 28, 147, 189, 179, 58, 174, 189, 76, 247, 191, 189, 169, 34, 198, 189, 76, 247, 191, 189, 179, 58, 174, 189, 141, 28, 147, 189, 27, 184, 99, 189, 194, 133, 28, 189, 33, 62, 176, 188, 157, 147, 230, 187, 52, 158, 136, 59, 247, 221, 62, 60, 66, 159, 126, 60, 139, 228, 134, 60, 81, 164, 123, 60, 193, 31, 89, 60, 236, 141, 53, 60, 23, 35, 28, 60, 242, 163, 18, 60, 222, 86, 25, 60, 58, 26, 44, 60, 153, 59, 68, 60, 111, 144, 90, 60, 5, 86, 105, 60, 136, 113, 109, 60, 174, 220, 102, 60, 249, 67, 88, 60, 174, 2, 70, 60, 29, 212, 52, 60, 173, 144, 40, 60, 48, 73, 35, 60, 246, 237, 36, 60, 134, 137, 43, 60, 184, 243, 51, 60, 141, 196, 58, 60, 104, 64, 61, 60, 223, 4, 58, 60, 103, 75, 49, 60, 202, 186, 36, 60, 189, 221, 22, 60, 62, 101, 10, 60, 117, 110, 1, 60, 66, 250, 249, 59, 231, 145, 249, 59, 227, 203, 254, 59, 97, 184, 2, 60, 242, 143, 4, 60, 198, 63, 3, 60, 58, 21, 252, 59, 91, 146, 234, 59, 195, 136, 212, 59, 141, 162, 189, 59, 235, 152, 169, 59, 121, 52, 155, 59, 251, 153, 147, 59, 76, 15, 146, 59, 69, 68, 148, 59, 62, 4, 151, 59, 19, 29, 151, 59, 153, 60, 146, 59, 29, 135, 135, 59, 230, 137, 111, 59, 115, 56, 74, 59, 31, 224, 36, 59, 0, 0, 0, 0, 222, 82, 148, 58, 17, 222, 110, 58, 163, 210, 227, 58, 235, 251, 178, 185, 117, 168, 94, 186, 111, 92, 173, 185, 6, 130, 62, 187, 91, 45, 106, 187, 215, 43, 81, 187, 101, 237, 198, 187, 202, 1, 216, 187, 193, 29, 197, 187, 211, 66, 17, 188, 17, 56, 16, 188, 21, 252, 248, 187, 83, 103, 36, 188, 95, 206, 17, 188, 215, 183, 217, 187, 66, 217, 8, 188, 238, 187, 187, 187, 136, 81, 12, 187, 132, 53, 70, 187, 131, 50, 174, 58, 211, 116, 205, 59, 139, 117, 195, 59, 64, 206, 69, 60, 98, 208, 147, 60, 214, 96, 144, 60, 175, 228, 207, 60, 251, 170, 1, 61, 215, 132, 244, 60, 212, 33, 33, 61, 123, 176, 54, 61, 125, 184, 31, 61, 18, 139, 83, 61, 135, 202, 85, 61, 75, 112, 15, 61, 167, 37, 118, 61, 108, 162, 133, 60, 35, 191, 94, 190, 20, 207, 193, 190, 35, 191, 94, 190, 108, 162, 133, 60, 167, 37, 118, 61, 75, 112, 15, 61, 135, 202, 85, 61, 18, 139, 83, 61, 125, 184, 31, 61, 123, 176, 54, 61, 212, 33, 33, 61, 215, 132, 244, 60, 251, 170, 1, 61, 175, 228, 207, 60, 214, 96, 144, 60, 98, 208, 147, 60, 64, 206, 69, 60, 139, 117, 195, 59, 211, 116, 205, 59, 131, 50, 174, 58, 132, 53, 70, 187, 136, 81, 12, 187, 238, 187, 187, 187, 66, 217, 8, 188, 215, 183, 217, 187, 95, 206, 17, 188, 83, 103, 36, 188, 21, 252, 248, 187, 17, 56, 16, 188, 211, 66, 17, 188, 193, 29, 197, 187, 202, 1, 216, 187, 101, 237, 198, 187, 215, 43, 81, 187, 91, 45, 106, 187, 6, 130, 62, 187, 111, 92, 173, 185, 117, 168, 94, 186, 235, 251, 178, 185, 163, 210, 227, 58, 17, 222, 110, 58, 222, 82, 148, 58, 0, 0, 0, 0, 37, 103, 32, 0, 0, 0, 0, 0, 101, 114, 114, 111, 114, 95, 118, 101, 99, 116, 111, 114, 61, 91, 0, 0, 73, 78, 86, 65, 76, 73, 68, 0, 72, 65, 77, 77, 73, 78, 71, 0, 66, 76, 65, 67, 75, 77, 65, 78, 0, 0, 0, 0, 0, 0, 0, 0, 66, 79, 88, 67, 65, 82, 0, 0, 7, 0, 0, 0, 8, 0, 0, 0, 9, 0, 0, 0, 10, 0, 0, 0, 11, 0, 0, 0, 12, 0, 0, 0, 13, 0, 0, 0, 14, 0, 0, 0, 16, 0, 0, 0, 17, 0, 0, 0, 19, 0, 0, 0, 21, 0, 0, 0, 23, 0, 0, 0, 25, 0, 0, 0, 28, 0, 0, 0, 31, 0, 0, 0, 34, 0, 0, 0, 37, 0, 0, 0, 41, 0, 0, 0, 45, 0, 0, 0, 50, 0, 0, 0, 55, 0, 0, 0, 60, 0, 0, 0, 66, 0, 0, 0, 73, 0, 0, 0, 80, 0, 0, 0, 88, 0, 0, 0, 97, 0, 0, 0, 107, 0, 0, 0, 118, 0, 0, 0, 130, 0, 0, 0, 143, 0, 0, 0, 157, 0, 0, 0, 173, 0, 0, 0, 190, 0, 0, 0, 209, 0, 0, 0, 230, 0, 0, 0, 253, 0, 0, 0, 23, 1, 0, 0, 51, 1, 0, 0, 81, 1, 0, 0, 115, 1, 0, 0, 152, 1, 0, 0, 193, 1, 0, 0, 238, 1, 0, 0, 32, 2, 0, 0, 86, 2, 0, 0, 146, 2, 0, 0, 212, 2, 0, 0, 28, 3, 0, 0, 108, 3, 0, 0, 195, 3, 0, 0, 36, 4, 0, 0, 142, 4, 0, 0, 2, 5, 0, 0, 131, 5, 0, 0, 16, 6, 0, 0, 171, 6, 0, 0, 86, 7, 0, 0, 18, 8, 0, 0, 224, 8, 0, 0, 195, 9, 0, 0, 189, 10, 0, 0, 208, 11, 0, 0, 255, 12, 0, 0, 76, 14, 0, 0, 186, 15, 0, 0, 76, 17, 0, 0, 7, 19, 0, 0, 238, 20, 0, 0, 6, 23, 0, 0, 84, 25, 0, 0, 220, 27, 0, 0, 165, 30, 0, 0, 182, 33, 0, 0, 21, 37, 0, 0, 202, 40, 0, 0, 223, 44, 0, 0, 91, 49, 0, 0, 75, 54, 0, 0, 185, 59, 0, 0, 178, 65, 0, 0, 68, 72, 0, 0, 126, 79, 0, 0, 113, 87, 0, 0, 47, 96, 0, 0, 206, 105, 0, 0, 98, 116, 0, 0, 255, 127, 0, 0, 0, 0, 0, 0], "i8", ALLOC_NONE, Runtime.GLOBAL_BASE); - - - -var tempDoublePtr = Runtime.alignMemory(allocate(12, "i8", ALLOC_STATIC), 8); - -assert(tempDoublePtr % 8 == 0); - -function copyTempFloat(ptr) { // functions, because inlining this code increases code size too much - - HEAP8[tempDoublePtr] = HEAP8[ptr]; - - HEAP8[tempDoublePtr + 1] = HEAP8[ptr + 1]; - - HEAP8[tempDoublePtr + 2] = HEAP8[ptr + 2]; - - HEAP8[tempDoublePtr + 3] = HEAP8[ptr + 3]; - -} - -function copyTempDouble(ptr) { - - HEAP8[tempDoublePtr] = HEAP8[ptr]; - - HEAP8[tempDoublePtr + 1] = HEAP8[ptr + 1]; - - HEAP8[tempDoublePtr + 2] = HEAP8[ptr + 2]; - - HEAP8[tempDoublePtr + 3] = HEAP8[ptr + 3]; - - HEAP8[tempDoublePtr + 4] = HEAP8[ptr + 4]; - - HEAP8[tempDoublePtr + 5] = HEAP8[ptr + 5]; - - HEAP8[tempDoublePtr + 6] = HEAP8[ptr + 6]; - - HEAP8[tempDoublePtr + 7] = HEAP8[ptr + 7]; - -} - - - -function _strncmp(px, py, n) { - var i = 0; - while (i < n) { - var x = HEAPU8[(((px) + (i)) | 0)]; - var y = HEAPU8[(((py) + (i)) | 0)]; - if (x == y && x == 0) return 0; - if (x == 0) return -1; - if (y == 0) return 1; - if (x == y) { - i++; - continue; - } else { - return x > y ? 1 : -1; - } - } - return 0; -} - -function _strcmp(px, py) { - return _strncmp(px, py, TOTAL_MEMORY); -} - -var _cos = Math_cos; - -var _sin = Math_sin; - - - -Module["_memcpy"] = _memcpy; -var _llvm_memcpy_p0i8_p0i8_i32 = _memcpy; - -var _ceilf = Math_ceil; - -var _fabs = Math_abs; - -var _atan2 = Math_atan2; - -function _log10(x) { - return Math.log(x) / Math.LN10; -} - - - - - -var ERRNO_CODES = { - EPERM: 1, - ENOENT: 2, - ESRCH: 3, - EINTR: 4, - EIO: 5, - ENXIO: 6, - E2BIG: 7, - ENOEXEC: 8, - EBADF: 9, - ECHILD: 10, - EAGAIN: 11, - EWOULDBLOCK: 11, - ENOMEM: 12, - EACCES: 13, - EFAULT: 14, - ENOTBLK: 15, - EBUSY: 16, - EEXIST: 17, - EXDEV: 18, - ENODEV: 19, - ENOTDIR: 20, - EISDIR: 21, - EINVAL: 22, - ENFILE: 23, - EMFILE: 24, - ENOTTY: 25, - ETXTBSY: 26, - EFBIG: 27, - ENOSPC: 28, - ESPIPE: 29, - EROFS: 30, - EMLINK: 31, - EPIPE: 32, - EDOM: 33, - ERANGE: 34, - ENOMSG: 42, - EIDRM: 43, - ECHRNG: 44, - EL2NSYNC: 45, - EL3HLT: 46, - EL3RST: 47, - ELNRNG: 48, - EUNATCH: 49, - ENOCSI: 50, - EL2HLT: 51, - EDEADLK: 35, - ENOLCK: 37, - EBADE: 52, - EBADR: 53, - EXFULL: 54, - ENOANO: 55, - EBADRQC: 56, - EBADSLT: 57, - EDEADLOCK: 35, - EBFONT: 59, - ENOSTR: 60, - ENODATA: 61, - ETIME: 62, - ENOSR: 63, - ENONET: 64, - ENOPKG: 65, - EREMOTE: 66, - ENOLINK: 67, - EADV: 68, - ESRMNT: 69, - ECOMM: 70, - EPROTO: 71, - EMULTIHOP: 72, - EDOTDOT: 73, - EBADMSG: 74, - ENOTUNIQ: 76, - EBADFD: 77, - EREMCHG: 78, - ELIBACC: 79, - ELIBBAD: 80, - ELIBSCN: 81, - ELIBMAX: 82, - ELIBEXEC: 83, - ENOSYS: 38, - ENOTEMPTY: 39, - ENAMETOOLONG: 36, - ELOOP: 40, - EOPNOTSUPP: 95, - EPFNOSUPPORT: 96, - ECONNRESET: 104, - ENOBUFS: 105, - EAFNOSUPPORT: 97, - EPROTOTYPE: 91, - ENOTSOCK: 88, - ENOPROTOOPT: 92, - ESHUTDOWN: 108, - ECONNREFUSED: 111, - EADDRINUSE: 98, - ECONNABORTED: 103, - ENETUNREACH: 101, - ENETDOWN: 100, - ETIMEDOUT: 110, - EHOSTDOWN: 112, - EHOSTUNREACH: 113, - EINPROGRESS: 115, - EALREADY: 114, - EDESTADDRREQ: 89, - EMSGSIZE: 90, - EPROTONOSUPPORT: 93, - ESOCKTNOSUPPORT: 94, - EADDRNOTAVAIL: 99, - ENETRESET: 102, - EISCONN: 106, - ENOTCONN: 107, - ETOOMANYREFS: 109, - EUSERS: 87, - EDQUOT: 122, - ESTALE: 116, - ENOTSUP: 95, - ENOMEDIUM: 123, - EILSEQ: 84, - EOVERFLOW: 75, - ECANCELED: 125, - ENOTRECOVERABLE: 131, - EOWNERDEAD: 130, - ESTRPIPE: 86 -}; - -var ERRNO_MESSAGES = { - 0: "Success", - 1: "Not super-user", - 2: "No such file or directory", - 3: "No such process", - 4: "Interrupted system call", - 5: "I/O error", - 6: "No such device or address", - 7: "Arg list too long", - 8: "Exec format error", - 9: "Bad file number", - 10: "No children", - 11: "No more processes", - 12: "Not enough core", - 13: "Permission denied", - 14: "Bad address", - 15: "Block device required", - 16: "Mount device busy", - 17: "File exists", - 18: "Cross-device link", - 19: "No such device", - 20: "Not a directory", - 21: "Is a directory", - 22: "Invalid argument", - 23: "Too many open files in system", - 24: "Too many open files", - 25: "Not a typewriter", - 26: "Text file busy", - 27: "File too large", - 28: "No space left on device", - 29: "Illegal seek", - 30: "Read only file system", - 31: "Too many links", - 32: "Broken pipe", - 33: "Math arg out of domain of func", - 34: "Math result not representable", - 35: "File locking deadlock error", - 36: "File or path name too long", - 37: "No record locks available", - 38: "Function not implemented", - 39: "Directory not empty", - 40: "Too many symbolic links", - 42: "No message of desired type", - 43: "Identifier removed", - 44: "Channel number out of range", - 45: "Level 2 not synchronized", - 46: "Level 3 halted", - 47: "Level 3 reset", - 48: "Link number out of range", - 49: "Protocol driver not attached", - 50: "No CSI structure available", - 51: "Level 2 halted", - 52: "Invalid exchange", - 53: "Invalid request descriptor", - 54: "Exchange full", - 55: "No anode", - 56: "Invalid request code", - 57: "Invalid slot", - 59: "Bad font file fmt", - 60: "Device not a stream", - 61: "No data (for no delay io)", - 62: "Timer expired", - 63: "Out of streams resources", - 64: "Machine is not on the network", - 65: "Package not installed", - 66: "The object is remote", - 67: "The link has been severed", - 68: "Advertise error", - 69: "Srmount error", - 70: "Communication error on send", - 71: "Protocol error", - 72: "Multihop attempted", - 73: "Cross mount point (not really error)", - 74: "Trying to read unreadable message", - 75: "Value too large for defined data type", - 76: "Given log. name not unique", - 77: "f.d. invalid for this operation", - 78: "Remote address changed", - 79: "Can access a needed shared lib", - 80: "Accessing a corrupted shared lib", - 81: ".lib section in a.out corrupted", - 82: "Attempting to link in too many libs", - 83: "Attempting to exec a shared library", - 84: "Illegal byte sequence", - 86: "Streams pipe error", - 87: "Too many users", - 88: "Socket operation on non-socket", - 89: "Destination address required", - 90: "Message too long", - 91: "Protocol wrong type for socket", - 92: "Protocol not available", - 93: "Unknown protocol", - 94: "Socket type not supported", - 95: "Not supported", - 96: "Protocol family not supported", - 97: "Address family not supported by protocol family", - 98: "Address already in use", - 99: "Address not available", - 100: "Network interface is not configured", - 101: "Network is unreachable", - 102: "Connection reset by network", - 103: "Connection aborted", - 104: "Connection reset by peer", - 105: "No buffer space available", - 106: "Socket is already connected", - 107: "Socket is not connected", - 108: "Can't send after socket shutdown", - 109: "Too many references", - 110: "Connection timed out", - 111: "Connection refused", - 112: "Host is down", - 113: "Host is unreachable", - 114: "Socket already connected", - 115: "Connection already in progress", - 116: "Stale file handle", - 122: "Quota exceeded", - 123: "No medium (in tape drive)", - 125: "Operation canceled", - 130: "Previous owner died", - 131: "State not recoverable" -}; - - -var ___errno_state = 0; - -function ___setErrNo(value) { - // For convenient setting and returning of errno. - HEAP32[((___errno_state) >> 2)] = value; - return value; -} - -var PATH = { - splitPath: function(filename) { - var splitPathRe = /^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/; - return splitPathRe.exec(filename).slice(1); - }, - normalizeArray: function(parts, allowAboveRoot) { - // if the path tries to go above the root, `up` ends up > 0 - var up = 0; - for (var i = parts.length - 1; i >= 0; i--) { - var last = parts[i]; - if (last === '.') { - parts.splice(i, 1); - } else if (last === '..') { - parts.splice(i, 1); - up++; - } else if (up) { - parts.splice(i, 1); - up--; - } - } - // if the path is allowed to go above the root, restore leading ..s - if (allowAboveRoot) { - for (; up--; up) { - parts.unshift('..'); - } - } - return parts; - }, - normalize: function(path) { - var isAbsolute = path.charAt(0) === '/', - trailingSlash = path.substr(-1) === '/'; - // Normalize the path - path = PATH.normalizeArray(path.split('/').filter(function(p) { - return !!p; - }), !isAbsolute).join('/'); - if (!path && !isAbsolute) { - path = '.'; - } - if (path && trailingSlash) { - path += '/'; - } - return (isAbsolute ? '/' : '') + path; - }, - dirname: function(path) { - var result = PATH.splitPath(path), - root = result[0], - dir = result[1]; - if (!root && !dir) { - // No dirname whatsoever - return '.'; - } - if (dir) { - // It has a dirname, strip trailing slash - dir = dir.substr(0, dir.length - 1); - } - return root + dir; - }, - basename: function(path) { - // EMSCRIPTEN return '/'' for '/', not an empty string - if (path === '/') return '/'; - var lastSlash = path.lastIndexOf('/'); - if (lastSlash === -1) return path; - return path.substr(lastSlash + 1); - }, - extname: function(path) { - return PATH.splitPath(path)[3]; - }, - join: function() { - var paths = Array.prototype.slice.call(arguments, 0); - return PATH.normalize(paths.join('/')); - }, - join2: function(l, r) { - return PATH.normalize(l + '/' + r); - }, - resolve: function() { - var resolvedPath = '', - resolvedAbsolute = false; - for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { - var path = (i >= 0) ? arguments[i] : FS.cwd(); - // Skip empty and invalid entries - if (typeof path !== 'string') { - throw new TypeError('Arguments to path.resolve must be strings'); - } else if (!path) { - continue; - } - resolvedPath = path + '/' + resolvedPath; - resolvedAbsolute = path.charAt(0) === '/'; - } - // At this point the path should be resolved to a full absolute path, but - // handle relative paths to be safe (might happen when process.cwd() fails) - resolvedPath = PATH.normalizeArray(resolvedPath.split('/').filter(function(p) { - return !!p; - }), !resolvedAbsolute).join('/'); - return ((resolvedAbsolute ? '/' : '') + resolvedPath) || '.'; - }, - relative: function(from, to) { - from = PATH.resolve(from).substr(1); - to = PATH.resolve(to).substr(1); - - function trim(arr) { - var start = 0; - for (; start < arr.length; start++) { - if (arr[start] !== '') break; - } - var end = arr.length - 1; - for (; end >= 0; end--) { - if (arr[end] !== '') break; - } - if (start > end) return []; - return arr.slice(start, end - start + 1); - } - var fromParts = trim(from.split('/')); - var toParts = trim(to.split('/')); - var length = Math.min(fromParts.length, toParts.length); - var samePartsLength = length; - for (var i = 0; i < length; i++) { - if (fromParts[i] !== toParts[i]) { - samePartsLength = i; - break; - } - } - var outputParts = []; - for (var i = samePartsLength; i < fromParts.length; i++) { - outputParts.push('..'); - } - outputParts = outputParts.concat(toParts.slice(samePartsLength)); - return outputParts.join('/'); - } -}; - -var TTY = { - ttys: [], - init: function() { - // https://github.com/kripken/emscripten/pull/1555 - // if (ENVIRONMENT_IS_NODE) { - // // currently, FS.init does not distinguish if process.stdin is a file or TTY - // // device, it always assumes it's a TTY device. because of this, we're forcing - // // process.stdin to UTF8 encoding to at least make stdin reading compatible - // // with text files until FS.init can be refactored. - // process['stdin']['setEncoding']('utf8'); - // } - }, - shutdown: function() { - // https://github.com/kripken/emscripten/pull/1555 - // if (ENVIRONMENT_IS_NODE) { - // // inolen: any idea as to why node -e 'process.stdin.read()' wouldn't exit immediately (with process.stdin being a tty)? - // // isaacs: because now it's reading from the stream, you've expressed interest in it, so that read() kicks off a _read() which creates a ReadReq operation - // // inolen: I thought read() in that case was a synchronous operation that just grabbed some amount of buffered data if it exists? - // // isaacs: it is. but it also triggers a _read() call, which calls readStart() on the handle - // // isaacs: do process.stdin.pause() and i'd think it'd probably close the pending call - // process['stdin']['pause'](); - // } - }, - register: function(dev, ops) { - TTY.ttys[dev] = { - input: [], - output: [], - ops: ops - }; - FS.registerDevice(dev, TTY.stream_ops); - }, - stream_ops: { - open: function(stream) { - var tty = TTY.ttys[stream.node.rdev]; - if (!tty) { - throw new FS.ErrnoError(ERRNO_CODES.ENODEV); - } - stream.tty = tty; - stream.seekable = false; - }, - close: function(stream) { - // flush any pending line data - if (stream.tty.output.length) { - stream.tty.ops.put_char(stream.tty, 10); - } - }, - read: function(stream, buffer, offset, length, pos /* ignored */ ) { - if (!stream.tty || !stream.tty.ops.get_char) { - throw new FS.ErrnoError(ERRNO_CODES.ENXIO); - } - var bytesRead = 0; - for (var i = 0; i < length; i++) { - var result; - try { - result = stream.tty.ops.get_char(stream.tty); - } catch (e) { - throw new FS.ErrnoError(ERRNO_CODES.EIO); - } - if (result === undefined && bytesRead === 0) { - throw new FS.ErrnoError(ERRNO_CODES.EAGAIN); - } - if (result === null || result === undefined) break; - bytesRead++; - buffer[offset + i] = result; - } - if (bytesRead) { - stream.node.timestamp = Date.now(); - } - return bytesRead; - }, - write: function(stream, buffer, offset, length, pos) { - if (!stream.tty || !stream.tty.ops.put_char) { - throw new FS.ErrnoError(ERRNO_CODES.ENXIO); - } - for (var i = 0; i < length; i++) { - try { - stream.tty.ops.put_char(stream.tty, buffer[offset + i]); - } catch (e) { - throw new FS.ErrnoError(ERRNO_CODES.EIO); - } - } - if (length) { - stream.node.timestamp = Date.now(); - } - return i; - } - }, - default_tty_ops: { - get_char: function(tty) { - if (!tty.input.length) { - var result = null; - if (ENVIRONMENT_IS_NODE) { - result = process['stdin']['read'](); - if (!result) { - if (process['stdin']['_readableState'] && process['stdin']['_readableState']['ended']) { - return null; // EOF - } - return undefined; // no data available - } - } else if (typeof window != 'undefined' && - typeof window.prompt == 'function') { - // Browser. - result = window.prompt('Input: '); // returns null on cancel - if (result !== null) { - result += '\n'; - } - } else if (typeof readline == 'function') { - // Command line. - result = readline(); - if (result !== null) { - result += '\n'; - } - } - if (!result) { - return null; - } - tty.input = intArrayFromString(result, true); - } - return tty.input.shift(); - }, - put_char: function(tty, val) { - if (val === null || val === 10) { - Module['print'](tty.output.join('')); - tty.output = []; - } else { - tty.output.push(TTY.utf8.processCChar(val)); - } - } - }, - default_tty1_ops: { - put_char: function(tty, val) { - if (val === null || val === 10) { - Module['printErr'](tty.output.join('')); - tty.output = []; - } else { - tty.output.push(TTY.utf8.processCChar(val)); - } - } - } -}; - -var MEMFS = { - ops_table: null, - CONTENT_OWNING: 1, - CONTENT_FLEXIBLE: 2, - CONTENT_FIXED: 3, - mount: function(mount) { - return MEMFS.createNode(null, '/', 16384 | 0777, 0); - }, - createNode: function(parent, name, mode, dev) { - if (FS.isBlkdev(mode) || FS.isFIFO(mode)) { - // no supported - throw new FS.ErrnoError(ERRNO_CODES.EPERM); - } - if (!MEMFS.ops_table) { - MEMFS.ops_table = { - dir: { - node: { - getattr: MEMFS.node_ops.getattr, - setattr: MEMFS.node_ops.setattr, - lookup: MEMFS.node_ops.lookup, - mknod: MEMFS.node_ops.mknod, - mknod: MEMFS.node_ops.mknod, - rename: MEMFS.node_ops.rename, - unlink: MEMFS.node_ops.unlink, - rmdir: MEMFS.node_ops.rmdir, - readdir: MEMFS.node_ops.readdir, - symlink: MEMFS.node_ops.symlink - }, - stream: { - llseek: MEMFS.stream_ops.llseek - } - }, - file: { - node: { - getattr: MEMFS.node_ops.getattr, - setattr: MEMFS.node_ops.setattr - }, - stream: { - llseek: MEMFS.stream_ops.llseek, - read: MEMFS.stream_ops.read, - write: MEMFS.stream_ops.write, - allocate: MEMFS.stream_ops.allocate, - mmap: MEMFS.stream_ops.mmap - } - }, - link: { - node: { - getattr: MEMFS.node_ops.getattr, - setattr: MEMFS.node_ops.setattr, - readlink: MEMFS.node_ops.readlink - }, - stream: {} - }, - chrdev: { - node: { - getattr: MEMFS.node_ops.getattr, - setattr: MEMFS.node_ops.setattr - }, - stream: FS.chrdev_stream_ops - }, - }; - } - var node = FS.createNode(parent, name, mode, dev); - if (FS.isDir(node.mode)) { - node.node_ops = MEMFS.ops_table.dir.node; - node.stream_ops = MEMFS.ops_table.dir.stream; - node.contents = {}; - } else if (FS.isFile(node.mode)) { - node.node_ops = MEMFS.ops_table.file.node; - node.stream_ops = MEMFS.ops_table.file.stream; - node.contents = []; - node.contentMode = MEMFS.CONTENT_FLEXIBLE; - } else if (FS.isLink(node.mode)) { - node.node_ops = MEMFS.ops_table.link.node; - node.stream_ops = MEMFS.ops_table.link.stream; - } else if (FS.isChrdev(node.mode)) { - node.node_ops = MEMFS.ops_table.chrdev.node; - node.stream_ops = MEMFS.ops_table.chrdev.stream; - } - node.timestamp = Date.now(); - // add the new node to the parent - if (parent) { - parent.contents[name] = node; - } - return node; - }, - ensureFlexible: function(node) { - if (node.contentMode !== MEMFS.CONTENT_FLEXIBLE) { - var contents = node.contents; - node.contents = Array.prototype.slice.call(contents); - node.contentMode = MEMFS.CONTENT_FLEXIBLE; - } - }, - node_ops: { - getattr: function(node) { - var attr = {}; - // device numbers reuse inode numbers. - attr.dev = FS.isChrdev(node.mode) ? node.id : 1; - attr.ino = node.id; - attr.mode = node.mode; - attr.nlink = 1; - attr.uid = 0; - attr.gid = 0; - attr.rdev = node.rdev; - if (FS.isDir(node.mode)) { - attr.size = 4096; - } else if (FS.isFile(node.mode)) { - attr.size = node.contents.length; - } else if (FS.isLink(node.mode)) { - attr.size = node.link.length; - } else { - attr.size = 0; - } - attr.atime = new Date(node.timestamp); - attr.mtime = new Date(node.timestamp); - attr.ctime = new Date(node.timestamp); - // NOTE: In our implementation, st_blocks = Math.ceil(st_size/st_blksize), - // but this is not required by the standard. - attr.blksize = 4096; - attr.blocks = Math.ceil(attr.size / attr.blksize); - return attr; - }, - setattr: function(node, attr) { - if (attr.mode !== undefined) { - node.mode = attr.mode; - } - if (attr.timestamp !== undefined) { - node.timestamp = attr.timestamp; - } - if (attr.size !== undefined) { - MEMFS.ensureFlexible(node); - var contents = node.contents; - if (attr.size < contents.length) contents.length = attr.size; - else - while (attr.size > contents.length) contents.push(0); - } - }, - lookup: function(parent, name) { - throw FS.genericErrors[ERRNO_CODES.ENOENT]; - }, - mknod: function(parent, name, mode, dev) { - return MEMFS.createNode(parent, name, mode, dev); - }, - rename: function(old_node, new_dir, new_name) { - // if we're overwriting a directory at new_name, make sure it's empty. - if (FS.isDir(old_node.mode)) { - var new_node; - try { - new_node = FS.lookupNode(new_dir, new_name); - } catch (e) {} - if (new_node) { - for (var i in new_node.contents) { - throw new FS.ErrnoError(ERRNO_CODES.ENOTEMPTY); - } - } - } - // do the internal rewiring - delete old_node.parent.contents[old_node.name]; - old_node.name = new_name; - new_dir.contents[new_name] = old_node; - old_node.parent = new_dir; - }, - unlink: function(parent, name) { - delete parent.contents[name]; - }, - rmdir: function(parent, name) { - var node = FS.lookupNode(parent, name); - for (var i in node.contents) { - throw new FS.ErrnoError(ERRNO_CODES.ENOTEMPTY); - } - delete parent.contents[name]; - }, - readdir: function(node) { - var entries = ['.', '..'] - for (var key in node.contents) { - if (!node.contents.hasOwnProperty(key)) { - continue; - } - entries.push(key); - } - return entries; - }, - symlink: function(parent, newname, oldpath) { - var node = MEMFS.createNode(parent, newname, 0777 | 40960, 0); - node.link = oldpath; - return node; - }, - readlink: function(node) { - if (!FS.isLink(node.mode)) { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); - } - return node.link; - } - }, - stream_ops: { - read: function(stream, buffer, offset, length, position) { - var contents = stream.node.contents; - if (position >= contents.length) - return 0; - var size = Math.min(contents.length - position, length); - assert(size >= 0); - if (size > 8 && contents.subarray) { // non-trivial, and typed array - buffer.set(contents.subarray(position, position + size), offset); - } else { - for (var i = 0; i < size; i++) { - buffer[offset + i] = contents[position + i]; - } - } - return size; - }, - write: function(stream, buffer, offset, length, position, canOwn) { - var node = stream.node; - node.timestamp = Date.now(); - var contents = node.contents; - if (length && contents.length === 0 && position === 0 && buffer.subarray) { - // just replace it with the new data - if (canOwn && offset === 0) { - node.contents = buffer; // this could be a subarray of Emscripten HEAP, or allocated from some other source. - node.contentMode = (buffer.buffer === HEAP8.buffer) ? MEMFS.CONTENT_OWNING : MEMFS.CONTENT_FIXED; - } else { - node.contents = new Uint8Array(buffer.subarray(offset, offset + length)); - node.contentMode = MEMFS.CONTENT_FIXED; - } - return length; - } - MEMFS.ensureFlexible(node); - var contents = node.contents; - while (contents.length < position) contents.push(0); - for (var i = 0; i < length; i++) { - contents[position + i] = buffer[offset + i]; - } - return length; - }, - llseek: function(stream, offset, whence) { - var position = offset; - if (whence === 1) { // SEEK_CUR. - position += stream.position; - } else if (whence === 2) { // SEEK_END. - if (FS.isFile(stream.node.mode)) { - position += stream.node.contents.length; - } - } - if (position < 0) { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); - } - stream.ungotten = []; - stream.position = position; - return position; - }, - allocate: function(stream, offset, length) { - MEMFS.ensureFlexible(stream.node); - var contents = stream.node.contents; - var limit = offset + length; - while (limit > contents.length) contents.push(0); - }, - mmap: function(stream, buffer, offset, length, position, prot, flags) { - if (!FS.isFile(stream.node.mode)) { - throw new FS.ErrnoError(ERRNO_CODES.ENODEV); - } - var ptr; - var allocated; - var contents = stream.node.contents; - // Only make a new copy when MAP_PRIVATE is specified. - if (!(flags & 2) && - (contents.buffer === buffer || contents.buffer === buffer.buffer)) { - // We can't emulate MAP_SHARED when the file is not backed by the buffer - // we're mapping to (e.g. the HEAP buffer). - allocated = false; - ptr = contents.byteOffset; - } else { - // Try to avoid unnecessary slices. - if (position > 0 || position + length < contents.length) { - if (contents.subarray) { - contents = contents.subarray(position, position + length); - } else { - contents = Array.prototype.slice.call(contents, position, position + length); - } - } - allocated = true; - ptr = _malloc(length); - if (!ptr) { - throw new FS.ErrnoError(ERRNO_CODES.ENOMEM); - } - buffer.set(contents, ptr); - } - return { - ptr: ptr, - allocated: allocated - }; - } - } -}; - -var IDBFS = { - dbs: {}, - indexedDB: function() { - return window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; - }, - DB_VERSION: 20, - DB_STORE_NAME: "FILE_DATA", - mount: function(mount) { - return MEMFS.mount.apply(null, arguments); - }, - syncfs: function(mount, populate, callback) { - IDBFS.getLocalSet(mount, function(err, local) { - if (err) return callback(err); - - IDBFS.getRemoteSet(mount, function(err, remote) { - if (err) return callback(err); - - var src = populate ? remote : local; - var dst = populate ? local : remote; - - IDBFS.reconcile(src, dst, callback); - }); - }); - }, - reconcile: function(src, dst, callback) { - var total = 0; - - var create = {}; - for (var key in src.files) { - if (!src.files.hasOwnProperty(key)) continue; - var e = src.files[key]; - var e2 = dst.files[key]; - if (!e2 || e.timestamp > e2.timestamp) { - create[key] = e; - total++; - } - } - - var remove = {}; - for (var key in dst.files) { - if (!dst.files.hasOwnProperty(key)) continue; - var e = dst.files[key]; - var e2 = src.files[key]; - if (!e2) { - remove[key] = e; - total++; - } - } - - if (!total) { - // early out - return callback(null); - } - - var completed = 0; - - function done(err) { - if (err) return callback(err); - if (++completed >= total) { - return callback(null); - } - }; - - // create a single transaction to handle and IDB reads / writes we'll need to do - var db = src.type === 'remote' ? src.db : dst.db; - var transaction = db.transaction([IDBFS.DB_STORE_NAME], 'readwrite'); - transaction.onerror = function transaction_onerror() { - callback(this.error); - }; - var store = transaction.objectStore(IDBFS.DB_STORE_NAME); - - for (var path in create) { - if (!create.hasOwnProperty(path)) continue; - var entry = create[path]; - - if (dst.type === 'local') { - // save file to local - try { - if (FS.isDir(entry.mode)) { - FS.mkdir(path, entry.mode); - } else if (FS.isFile(entry.mode)) { - var stream = FS.open(path, 'w+', 0666); - FS.write(stream, entry.contents, 0, entry.contents.length, 0, true /* canOwn */ ); - FS.close(stream); - } - done(null); - } catch (e) { - return done(e); - } - } else { - // save file to IDB - var req = store.put(entry, path); - req.onsuccess = function req_onsuccess() { - done(null); - }; - req.onerror = function req_onerror() { - done(this.error); - }; - } - } - - for (var path in remove) { - if (!remove.hasOwnProperty(path)) continue; - var entry = remove[path]; - - if (dst.type === 'local') { - // delete file from local - try { - if (FS.isDir(entry.mode)) { - // TODO recursive delete? - FS.rmdir(path); - } else if (FS.isFile(entry.mode)) { - FS.unlink(path); - } - done(null); - } catch (e) { - return done(e); - } - } else { - // delete file from IDB - var req = store.delete(path); - req.onsuccess = function req_onsuccess() { - done(null); - }; - req.onerror = function req_onerror() { - done(this.error); - }; - } - } - }, - getLocalSet: function(mount, callback) { - var files = {}; - - function isRealDir(p) { - return p !== '.' && p !== '..'; - }; - - function toAbsolute(root) { - return function(p) { - return PATH.join2(root, p); - } - }; - - var check = FS.readdir(mount.mountpoint) - .filter(isRealDir) - .map(toAbsolute(mount.mountpoint)); - - while (check.length) { - var path = check.pop(); - var stat, node; - - try { - var lookup = FS.lookupPath(path); - node = lookup.node; - stat = FS.stat(path); - } catch (e) { - return callback(e); - } - - if (FS.isDir(stat.mode)) { - check.push.apply(check, FS.readdir(path) - .filter(isRealDir) - .map(toAbsolute(path))); - - files[path] = { - mode: stat.mode, - timestamp: stat.mtime - }; - } else if (FS.isFile(stat.mode)) { - files[path] = { - contents: node.contents, - mode: stat.mode, - timestamp: stat.mtime - }; - } else { - return callback(new Error('node type not supported')); - } - } - - return callback(null, { - type: 'local', - files: files - }); - }, - getDB: function(name, callback) { - // look it up in the cache - var db = IDBFS.dbs[name]; - if (db) { - return callback(null, db); - } - var req; - try { - req = IDBFS.indexedDB().open(name, IDBFS.DB_VERSION); - } catch (e) { - return onerror(e); - } - req.onupgradeneeded = function req_onupgradeneeded() { - db = req.result; - db.createObjectStore(IDBFS.DB_STORE_NAME); - }; - req.onsuccess = function req_onsuccess() { - db = req.result; - // add to the cache - IDBFS.dbs[name] = db; - callback(null, db); - }; - req.onerror = function req_onerror() { - callback(this.error); - }; - }, - getRemoteSet: function(mount, callback) { - var files = {}; - - IDBFS.getDB(mount.mountpoint, function(err, db) { - if (err) return callback(err); - - var transaction = db.transaction([IDBFS.DB_STORE_NAME], 'readonly'); - transaction.onerror = function transaction_onerror() { - callback(this.error); - }; - - var store = transaction.objectStore(IDBFS.DB_STORE_NAME); - store.openCursor().onsuccess = function store_openCursor_onsuccess(event) { - var cursor = event.target.result; - if (!cursor) { - return callback(null, { - type: 'remote', - db: db, - files: files - }); - } - - files[cursor.key] = cursor.value; - cursor.continue(); - }; - }); - } -}; - -var NODEFS = { - isWindows: false, - staticInit: function() { - NODEFS.isWindows = !!process.platform.match(/^win/); - }, - mount: function(mount) { - assert(ENVIRONMENT_IS_NODE); - return NODEFS.createNode(null, '/', NODEFS.getMode(mount.opts.root), 0); - }, - createNode: function(parent, name, mode, dev) { - if (!FS.isDir(mode) && !FS.isFile(mode) && !FS.isLink(mode)) { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); - } - var node = FS.createNode(parent, name, mode); - node.node_ops = NODEFS.node_ops; - node.stream_ops = NODEFS.stream_ops; - return node; - }, - getMode: function(path) { - var stat; - try { - stat = fs.lstatSync(path); - if (NODEFS.isWindows) { - // On Windows, directories return permission bits 'rw-rw-rw-', even though they have 'rwxrwxrwx', so - // propagate write bits to execute bits. - stat.mode = stat.mode | ((stat.mode & 146) >> 1); - } - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - return stat.mode; - }, - realPath: function(node) { - var parts = []; - while (node.parent !== node) { - parts.push(node.name); - node = node.parent; - } - parts.push(node.mount.opts.root); - parts.reverse(); - return PATH.join.apply(null, parts); - }, - flagsToPermissionStringMap: { - 0: "r", - 1: "r+", - 2: "r+", - 64: "r", - 65: "r+", - 66: "r+", - 129: "rx+", - 193: "rx+", - 514: "w+", - 577: "w", - 578: "w+", - 705: "wx", - 706: "wx+", - 1024: "a", - 1025: "a", - 1026: "a+", - 1089: "a", - 1090: "a+", - 1153: "ax", - 1154: "ax+", - 1217: "ax", - 1218: "ax+", - 4096: "rs", - 4098: "rs+" - }, - flagsToPermissionString: function(flags) { - if (flags in NODEFS.flagsToPermissionStringMap) { - return NODEFS.flagsToPermissionStringMap[flags]; - } else { - return flags; - } - }, - node_ops: { - getattr: function(node) { - var path = NODEFS.realPath(node); - var stat; - try { - stat = fs.lstatSync(path); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - // node.js v0.10.20 doesn't report blksize and blocks on Windows. Fake them with default blksize of 4096. - // See http://support.microsoft.com/kb/140365 - if (NODEFS.isWindows && !stat.blksize) { - stat.blksize = 4096; - } - if (NODEFS.isWindows && !stat.blocks) { - stat.blocks = (stat.size + stat.blksize - 1) / stat.blksize | 0; - } - return { - dev: stat.dev, - ino: stat.ino, - mode: stat.mode, - nlink: stat.nlink, - uid: stat.uid, - gid: stat.gid, - rdev: stat.rdev, - size: stat.size, - atime: stat.atime, - mtime: stat.mtime, - ctime: stat.ctime, - blksize: stat.blksize, - blocks: stat.blocks - }; - }, - setattr: function(node, attr) { - var path = NODEFS.realPath(node); - try { - if (attr.mode !== undefined) { - fs.chmodSync(path, attr.mode); - // update the common node structure mode as well - node.mode = attr.mode; - } - if (attr.timestamp !== undefined) { - var date = new Date(attr.timestamp); - fs.utimesSync(path, date, date); - } - if (attr.size !== undefined) { - fs.truncateSync(path, attr.size); - } - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - }, - lookup: function(parent, name) { - var path = PATH.join2(NODEFS.realPath(parent), name); - var mode = NODEFS.getMode(path); - return NODEFS.createNode(parent, name, mode); - }, - mknod: function(parent, name, mode, dev) { - var node = NODEFS.createNode(parent, name, mode, dev); - // create the backing node for this in the fs root as well - var path = NODEFS.realPath(node); - try { - if (FS.isDir(node.mode)) { - fs.mkdirSync(path, node.mode); - } else { - fs.writeFileSync(path, '', { - mode: node.mode - }); - } - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - return node; - }, - rename: function(oldNode, newDir, newName) { - var oldPath = NODEFS.realPath(oldNode); - var newPath = PATH.join2(NODEFS.realPath(newDir), newName); - try { - fs.renameSync(oldPath, newPath); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - }, - unlink: function(parent, name) { - var path = PATH.join2(NODEFS.realPath(parent), name); - try { - fs.unlinkSync(path); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - }, - rmdir: function(parent, name) { - var path = PATH.join2(NODEFS.realPath(parent), name); - try { - fs.rmdirSync(path); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - }, - readdir: function(node) { - var path = NODEFS.realPath(node); - try { - return fs.readdirSync(path); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - }, - symlink: function(parent, newName, oldPath) { - var newPath = PATH.join2(NODEFS.realPath(parent), newName); - try { - fs.symlinkSync(oldPath, newPath); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - }, - readlink: function(node) { - var path = NODEFS.realPath(node); - try { - return fs.readlinkSync(path); - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - } - }, - stream_ops: { - open: function(stream) { - var path = NODEFS.realPath(stream.node); - try { - if (FS.isFile(stream.node.mode)) { - stream.nfd = fs.openSync(path, NODEFS.flagsToPermissionString(stream.flags)); - } - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - }, - close: function(stream) { - try { - if (FS.isFile(stream.node.mode) && stream.nfd) { - fs.closeSync(stream.nfd); - } - } catch (e) { - if (!e.code) throw e; - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - }, - read: function(stream, buffer, offset, length, position) { - // FIXME this is terrible. - var nbuffer = new Buffer(length); - var res; - try { - res = fs.readSync(stream.nfd, nbuffer, 0, length, position); - } catch (e) { - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - if (res > 0) { - for (var i = 0; i < res; i++) { - buffer[offset + i] = nbuffer[i]; - } - } - return res; - }, - write: function(stream, buffer, offset, length, position) { - // FIXME this is terrible. - var nbuffer = new Buffer(buffer.subarray(offset, offset + length)); - var res; - try { - res = fs.writeSync(stream.nfd, nbuffer, 0, length, position); - } catch (e) { - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - return res; - }, - llseek: function(stream, offset, whence) { - var position = offset; - if (whence === 1) { // SEEK_CUR. - position += stream.position; - } else if (whence === 2) { // SEEK_END. - if (FS.isFile(stream.node.mode)) { - try { - var stat = fs.fstatSync(stream.nfd); - position += stat.size; - } catch (e) { - throw new FS.ErrnoError(ERRNO_CODES[e.code]); - } - } - } - - if (position < 0) { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); - } - - stream.position = position; - return position; - } - } -}; - -var _stdin = allocate(1, "i32*", ALLOC_STATIC); - -var _stdout = allocate(1, "i32*", ALLOC_STATIC); - -var _stderr = allocate(1, "i32*", ALLOC_STATIC); - -function _fflush(stream) { - // int fflush(FILE *stream); - // http://pubs.opengroup.org/onlinepubs/000095399/functions/fflush.html - // we don't currently perform any user-space buffering of data -} -var FS = { - root: null, - mounts: [], - devices: [null], - streams: [null], - nextInode: 1, - nameTable: null, - currentPath: "/", - initialized: false, - ignorePermissions: true, - ErrnoError: null, - genericErrors: {}, - handleFSError: function(e) { - if (!(e instanceof FS.ErrnoError)) throw e + ' : ' + stackTrace(); - return ___setErrNo(e.errno); - }, - lookupPath: function(path, opts) { - path = PATH.resolve(FS.cwd(), path); - opts = opts || { - recurse_count: 0 - }; - - if (opts.recurse_count > 8) { // max recursive lookup of 8 - throw new FS.ErrnoError(ERRNO_CODES.ELOOP); - } - - // split the path - var parts = PATH.normalizeArray(path.split('/').filter(function(p) { - return !!p; - }), false); - - // start at the root - var current = FS.root; - var current_path = '/'; - - for (var i = 0; i < parts.length; i++) { - var islast = (i === parts.length - 1); - if (islast && opts.parent) { - // stop resolving - break; - } - - current = FS.lookupNode(current, parts[i]); - current_path = PATH.join2(current_path, parts[i]); - - // jump to the mount's root node if this is a mountpoint - if (FS.isMountpoint(current)) { - current = current.mount.root; - } - - // follow symlinks - // by default, lookupPath will not follow a symlink if it is the final path component. - // setting opts.follow = true will override this behavior. - if (!islast || opts.follow) { - var count = 0; - while (FS.isLink(current.mode)) { - var link = FS.readlink(current_path); - current_path = PATH.resolve(PATH.dirname(current_path), link); - - var lookup = FS.lookupPath(current_path, { - recurse_count: opts.recurse_count - }); - current = lookup.node; - - if (count++ > 40) { // limit max consecutive symlinks to 40 (SYMLOOP_MAX). - throw new FS.ErrnoError(ERRNO_CODES.ELOOP); - } - } - } - } - - return { - path: current_path, - node: current - }; - }, - getPath: function(node) { - var path; - while (true) { - if (FS.isRoot(node)) { - var mount = node.mount.mountpoint; - if (!path) return mount; - return mount[mount.length - 1] !== '/' ? mount + '/' + path : mount + path; - } - path = path ? node.name + '/' + path : node.name; - node = node.parent; - } - }, - hashName: function(parentid, name) { - var hash = 0; - - - for (var i = 0; i < name.length; i++) { - hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0; - } - return ((parentid + hash) >>> 0) % FS.nameTable.length; - }, - hashAddNode: function(node) { - var hash = FS.hashName(node.parent.id, node.name); - node.name_next = FS.nameTable[hash]; - FS.nameTable[hash] = node; - }, - hashRemoveNode: function(node) { - var hash = FS.hashName(node.parent.id, node.name); - if (FS.nameTable[hash] === node) { - FS.nameTable[hash] = node.name_next; - } else { - var current = FS.nameTable[hash]; - while (current) { - if (current.name_next === node) { - current.name_next = node.name_next; - break; - } - current = current.name_next; - } - } - }, - lookupNode: function(parent, name) { - var err = FS.mayLookup(parent); - if (err) { - throw new FS.ErrnoError(err); - } - var hash = FS.hashName(parent.id, name); - for (var node = FS.nameTable[hash]; node; node = node.name_next) { - var nodeName = node.name; - if (node.parent.id === parent.id && nodeName === name) { - return node; - } - } - // if we failed to find it in the cache, call into the VFS - return FS.lookup(parent, name); - }, - createNode: function(parent, name, mode, rdev) { - if (!FS.FSNode) { - FS.FSNode = function(parent, name, mode, rdev) { - this.id = FS.nextInode++; - this.name = name; - this.mode = mode; - this.node_ops = {}; - this.stream_ops = {}; - this.rdev = rdev; - this.parent = null; - this.mount = null; - if (!parent) { - parent = this; // root node sets parent to itself - } - this.parent = parent; - this.mount = parent.mount; - FS.hashAddNode(this); - }; - - // compatibility - var readMode = 292 | 73; - var writeMode = 146; - - FS.FSNode.prototype = {}; - - // NOTE we must use Object.defineProperties instead of individual calls to - // Object.defineProperty in order to make closure compiler happy - Object.defineProperties(FS.FSNode.prototype, { - read: { - get: function() { - return (this.mode & readMode) === readMode; - }, - set: function(val) { - val ? this.mode |= readMode : this.mode &= ~readMode; - } - }, - write: { - get: function() { - return (this.mode & writeMode) === writeMode; - }, - set: function(val) { - val ? this.mode |= writeMode : this.mode &= ~writeMode; - } - }, - isFolder: { - get: function() { - return FS.isDir(this.mode); - }, - }, - isDevice: { - get: function() { - return FS.isChrdev(this.mode); - }, - }, - }); - } - return new FS.FSNode(parent, name, mode, rdev); - }, - destroyNode: function(node) { - FS.hashRemoveNode(node); - }, - isRoot: function(node) { - return node === node.parent; - }, - isMountpoint: function(node) { - return node.mounted; - }, - isFile: function(mode) { - return (mode & 61440) === 32768; - }, - isDir: function(mode) { - return (mode & 61440) === 16384; - }, - isLink: function(mode) { - return (mode & 61440) === 40960; - }, - isChrdev: function(mode) { - return (mode & 61440) === 8192; - }, - isBlkdev: function(mode) { - return (mode & 61440) === 24576; - }, - isFIFO: function(mode) { - return (mode & 61440) === 4096; - }, - isSocket: function(mode) { - return (mode & 49152) === 49152; - }, - flagModes: { - "r": 0, - "rs": 1052672, - "r+": 2, - "w": 577, - "wx": 705, - "xw": 705, - "w+": 578, - "wx+": 706, - "xw+": 706, - "a": 1089, - "ax": 1217, - "xa": 1217, - "a+": 1090, - "ax+": 1218, - "xa+": 1218 - }, - modeStringToFlags: function(str) { - var flags = FS.flagModes[str]; - if (typeof flags === 'undefined') { - throw new Error('Unknown file open mode: ' + str); - } - return flags; - }, - flagsToPermissionString: function(flag) { - var accmode = flag & 2097155; - var perms = ['r', 'w', 'rw'][accmode]; - if ((flag & 512)) { - perms += 'w'; - } - return perms; - }, - nodePermissions: function(node, perms) { - if (FS.ignorePermissions) { - return 0; - } - // return 0 if any user, group or owner bits are set. - if (perms.indexOf('r') !== -1 && !(node.mode & 292)) { - return ERRNO_CODES.EACCES; - } else if (perms.indexOf('w') !== -1 && !(node.mode & 146)) { - return ERRNO_CODES.EACCES; - } else if (perms.indexOf('x') !== -1 && !(node.mode & 73)) { - return ERRNO_CODES.EACCES; - } - return 0; - }, - mayLookup: function(dir) { - return FS.nodePermissions(dir, 'x'); - }, - mayCreate: function(dir, name) { - try { - var node = FS.lookupNode(dir, name); - return ERRNO_CODES.EEXIST; - } catch (e) {} - return FS.nodePermissions(dir, 'wx'); - }, - mayDelete: function(dir, name, isdir) { - var node; - try { - node = FS.lookupNode(dir, name); - } catch (e) { - return e.errno; - } - var err = FS.nodePermissions(dir, 'wx'); - if (err) { - return err; - } - if (isdir) { - if (!FS.isDir(node.mode)) { - return ERRNO_CODES.ENOTDIR; - } - if (FS.isRoot(node) || FS.getPath(node) === FS.cwd()) { - return ERRNO_CODES.EBUSY; - } - } else { - if (FS.isDir(node.mode)) { - return ERRNO_CODES.EISDIR; - } - } - return 0; - }, - mayOpen: function(node, flags) { - if (!node) { - return ERRNO_CODES.ENOENT; - } - if (FS.isLink(node.mode)) { - return ERRNO_CODES.ELOOP; - } else if (FS.isDir(node.mode)) { - if ((flags & 2097155) !== 0 || // opening for write - (flags & 512)) { - return ERRNO_CODES.EISDIR; - } - } - return FS.nodePermissions(node, FS.flagsToPermissionString(flags)); - }, - MAX_OPEN_FDS: 4096, - nextfd: function(fd_start, fd_end) { - fd_start = fd_start || 1; - fd_end = fd_end || FS.MAX_OPEN_FDS; - for (var fd = fd_start; fd <= fd_end; fd++) { - if (!FS.streams[fd]) { - return fd; - } - } - throw new FS.ErrnoError(ERRNO_CODES.EMFILE); - }, - getStream: function(fd) { - return FS.streams[fd]; - }, - createStream: function(stream, fd_start, fd_end) { - if (!FS.FSStream) { - FS.FSStream = function() {}; - FS.FSStream.prototype = {}; - // compatibility - Object.defineProperties(FS.FSStream.prototype, { - object: { - get: function() { - return this.node; - }, - set: function(val) { - this.node = val; - } - }, - isRead: { - get: function() { - return (this.flags & 2097155) !== 1; - } - }, - isWrite: { - get: function() { - return (this.flags & 2097155) !== 0; - } - }, - isAppend: { - get: function() { - return (this.flags & 1024); - } - } - }); - } - if (stream.__proto__) { - // reuse the object - stream.__proto__ = FS.FSStream.prototype; - } else { - var newStream = new FS.FSStream(); - for (var p in stream) { - newStream[p] = stream[p]; - } - stream = newStream; - } - var fd = FS.nextfd(fd_start, fd_end); - stream.fd = fd; - FS.streams[fd] = stream; - return stream; - }, - closeStream: function(fd) { - FS.streams[fd] = null; - }, - chrdev_stream_ops: { - open: function(stream) { - var device = FS.getDevice(stream.node.rdev); - // override node's stream ops with the device's - stream.stream_ops = device.stream_ops; - // forward the open call - if (stream.stream_ops.open) { - stream.stream_ops.open(stream); - } - }, - llseek: function() { - throw new FS.ErrnoError(ERRNO_CODES.ESPIPE); - } - }, - major: function(dev) { - return ((dev) >> 8); - }, - minor: function(dev) { - return ((dev) & 0xff); - }, - makedev: function(ma, mi) { - return ((ma) << 8 | (mi)); - }, - registerDevice: function(dev, ops) { - FS.devices[dev] = { - stream_ops: ops - }; - }, - getDevice: function(dev) { - return FS.devices[dev]; - }, - syncfs: function(populate, callback) { - if (typeof(populate) === 'function') { - callback = populate; - populate = false; - } - - var completed = 0; - var total = FS.mounts.length; - - function done(err) { - if (err) { - return callback(err); - } - if (++completed >= total) { - callback(null); - } - }; - - // sync all mounts - for (var i = 0; i < FS.mounts.length; i++) { - var mount = FS.mounts[i]; - if (!mount.type.syncfs) { - done(null); - continue; - } - mount.type.syncfs(mount, populate, done); - } - }, - mount: function(type, opts, mountpoint) { - var lookup; - if (mountpoint) { - lookup = FS.lookupPath(mountpoint, { - follow: false - }); - mountpoint = lookup.path; // use the absolute path - } - var mount = { - type: type, - opts: opts, - mountpoint: mountpoint, - root: null - }; - // create a root node for the fs - var root = type.mount(mount); - root.mount = mount; - mount.root = root; - // assign the mount info to the mountpoint's node - if (lookup) { - lookup.node.mount = mount; - lookup.node.mounted = true; - // compatibility update FS.root if we mount to / - if (mountpoint === '/') { - FS.root = mount.root; - } - } - // add to our cached list of mounts - FS.mounts.push(mount); - return root; - }, - lookup: function(parent, name) { - return parent.node_ops.lookup(parent, name); - }, - mknod: function(path, mode, dev) { - var lookup = FS.lookupPath(path, { - parent: true - }); - var parent = lookup.node; - var name = PATH.basename(path); - var err = FS.mayCreate(parent, name); - if (err) { - throw new FS.ErrnoError(err); - } - if (!parent.node_ops.mknod) { - throw new FS.ErrnoError(ERRNO_CODES.EPERM); - } - return parent.node_ops.mknod(parent, name, mode, dev); - }, - create: function(path, mode) { - mode = mode !== undefined ? mode : 0666; - mode &= 4095; - mode |= 32768; - return FS.mknod(path, mode, 0); - }, - mkdir: function(path, mode) { - mode = mode !== undefined ? mode : 0777; - mode &= 511 | 512; - mode |= 16384; - return FS.mknod(path, mode, 0); - }, - mkdev: function(path, mode, dev) { - if (typeof(dev) === 'undefined') { - dev = mode; - mode = 0666; - } - mode |= 8192; - return FS.mknod(path, mode, dev); - }, - symlink: function(oldpath, newpath) { - var lookup = FS.lookupPath(newpath, { - parent: true - }); - var parent = lookup.node; - var newname = PATH.basename(newpath); - var err = FS.mayCreate(parent, newname); - if (err) { - throw new FS.ErrnoError(err); - } - if (!parent.node_ops.symlink) { - throw new FS.ErrnoError(ERRNO_CODES.EPERM); - } - return parent.node_ops.symlink(parent, newname, oldpath); - }, - rename: function(old_path, new_path) { - var old_dirname = PATH.dirname(old_path); - var new_dirname = PATH.dirname(new_path); - var old_name = PATH.basename(old_path); - var new_name = PATH.basename(new_path); - // parents must exist - var lookup, old_dir, new_dir; - try { - lookup = FS.lookupPath(old_path, { - parent: true - }); - old_dir = lookup.node; - lookup = FS.lookupPath(new_path, { - parent: true - }); - new_dir = lookup.node; - } catch (e) { - throw new FS.ErrnoError(ERRNO_CODES.EBUSY); - } - // need to be part of the same mount - if (old_dir.mount !== new_dir.mount) { - throw new FS.ErrnoError(ERRNO_CODES.EXDEV); - } - // source must exist - var old_node = FS.lookupNode(old_dir, old_name); - // old path should not be an ancestor of the new path - var relative = PATH.relative(old_path, new_dirname); - if (relative.charAt(0) !== '.') { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); - } - // new path should not be an ancestor of the old path - relative = PATH.relative(new_path, old_dirname); - if (relative.charAt(0) !== '.') { - throw new FS.ErrnoError(ERRNO_CODES.ENOTEMPTY); - } - // see if the new path already exists - var new_node; - try { - new_node = FS.lookupNode(new_dir, new_name); - } catch (e) { - // not fatal - } - // early out if nothing needs to change - if (old_node === new_node) { - return; - } - // we'll need to delete the old entry - var isdir = FS.isDir(old_node.mode); - var err = FS.mayDelete(old_dir, old_name, isdir); - if (err) { - throw new FS.ErrnoError(err); - } - // need delete permissions if we'll be overwriting. - // need create permissions if new doesn't already exist. - err = new_node ? - FS.mayDelete(new_dir, new_name, isdir) : - FS.mayCreate(new_dir, new_name); - if (err) { - throw new FS.ErrnoError(err); - } - if (!old_dir.node_ops.rename) { - throw new FS.ErrnoError(ERRNO_CODES.EPERM); - } - if (FS.isMountpoint(old_node) || (new_node && FS.isMountpoint(new_node))) { - throw new FS.ErrnoError(ERRNO_CODES.EBUSY); - } - // if we are going to change the parent, check write permissions - if (new_dir !== old_dir) { - err = FS.nodePermissions(old_dir, 'w'); - if (err) { - throw new FS.ErrnoError(err); - } - } - // remove the node from the lookup hash - FS.hashRemoveNode(old_node); - // do the underlying fs rename - try { - old_dir.node_ops.rename(old_node, new_dir, new_name); - } catch (e) { - throw e; - } finally { - // add the node back to the hash (in case node_ops.rename - // changed its name) - FS.hashAddNode(old_node); - } - }, - rmdir: function(path) { - var lookup = FS.lookupPath(path, { - parent: true - }); - var parent = lookup.node; - var name = PATH.basename(path); - var node = FS.lookupNode(parent, name); - var err = FS.mayDelete(parent, name, true); - if (err) { - throw new FS.ErrnoError(err); - } - if (!parent.node_ops.rmdir) { - throw new FS.ErrnoError(ERRNO_CODES.EPERM); - } - if (FS.isMountpoint(node)) { - throw new FS.ErrnoError(ERRNO_CODES.EBUSY); - } - parent.node_ops.rmdir(parent, name); - FS.destroyNode(node); - }, - readdir: function(path) { - var lookup = FS.lookupPath(path, { - follow: true - }); - var node = lookup.node; - if (!node.node_ops.readdir) { - throw new FS.ErrnoError(ERRNO_CODES.ENOTDIR); - } - return node.node_ops.readdir(node); - }, - unlink: function(path) { - var lookup = FS.lookupPath(path, { - parent: true - }); - var parent = lookup.node; - var name = PATH.basename(path); - var node = FS.lookupNode(parent, name); - var err = FS.mayDelete(parent, name, false); - if (err) { - // POSIX says unlink should set EPERM, not EISDIR - if (err === ERRNO_CODES.EISDIR) err = ERRNO_CODES.EPERM; - throw new FS.ErrnoError(err); - } - if (!parent.node_ops.unlink) { - throw new FS.ErrnoError(ERRNO_CODES.EPERM); - } - if (FS.isMountpoint(node)) { - throw new FS.ErrnoError(ERRNO_CODES.EBUSY); - } - parent.node_ops.unlink(parent, name); - FS.destroyNode(node); - }, - readlink: function(path) { - var lookup = FS.lookupPath(path, { - follow: false - }); - var link = lookup.node; - if (!link.node_ops.readlink) { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); - } - return link.node_ops.readlink(link); - }, - stat: function(path, dontFollow) { - var lookup = FS.lookupPath(path, { - follow: !dontFollow - }); - var node = lookup.node; - if (!node.node_ops.getattr) { - throw new FS.ErrnoError(ERRNO_CODES.EPERM); - } - return node.node_ops.getattr(node); - }, - lstat: function(path) { - return FS.stat(path, true); - }, - chmod: function(path, mode, dontFollow) { - var node; - if (typeof path === 'string') { - var lookup = FS.lookupPath(path, { - follow: !dontFollow - }); - node = lookup.node; - } else { - node = path; - } - if (!node.node_ops.setattr) { - throw new FS.ErrnoError(ERRNO_CODES.EPERM); - } - node.node_ops.setattr(node, { - mode: (mode & 4095) | (node.mode & ~4095), - timestamp: Date.now() - }); - }, - lchmod: function(path, mode) { - FS.chmod(path, mode, true); - }, - fchmod: function(fd, mode) { - var stream = FS.getStream(fd); - if (!stream) { - throw new FS.ErrnoError(ERRNO_CODES.EBADF); - } - FS.chmod(stream.node, mode); - }, - chown: function(path, uid, gid, dontFollow) { - var node; - if (typeof path === 'string') { - var lookup = FS.lookupPath(path, { - follow: !dontFollow - }); - node = lookup.node; - } else { - node = path; - } - if (!node.node_ops.setattr) { - throw new FS.ErrnoError(ERRNO_CODES.EPERM); - } - node.node_ops.setattr(node, { - timestamp: Date.now() - // we ignore the uid / gid for now - }); - }, - lchown: function(path, uid, gid) { - FS.chown(path, uid, gid, true); - }, - fchown: function(fd, uid, gid) { - var stream = FS.getStream(fd); - if (!stream) { - throw new FS.ErrnoError(ERRNO_CODES.EBADF); - } - FS.chown(stream.node, uid, gid); - }, - truncate: function(path, len) { - if (len < 0) { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); - } - var node; - if (typeof path === 'string') { - var lookup = FS.lookupPath(path, { - follow: true - }); - node = lookup.node; - } else { - node = path; - } - if (!node.node_ops.setattr) { - throw new FS.ErrnoError(ERRNO_CODES.EPERM); - } - if (FS.isDir(node.mode)) { - throw new FS.ErrnoError(ERRNO_CODES.EISDIR); - } - if (!FS.isFile(node.mode)) { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); - } - var err = FS.nodePermissions(node, 'w'); - if (err) { - throw new FS.ErrnoError(err); - } - node.node_ops.setattr(node, { - size: len, - timestamp: Date.now() - }); - }, - ftruncate: function(fd, len) { - var stream = FS.getStream(fd); - if (!stream) { - throw new FS.ErrnoError(ERRNO_CODES.EBADF); - } - if ((stream.flags & 2097155) === 0) { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); - } - FS.truncate(stream.node, len); - }, - utime: function(path, atime, mtime) { - var lookup = FS.lookupPath(path, { - follow: true - }); - var node = lookup.node; - node.node_ops.setattr(node, { - timestamp: Math.max(atime, mtime) - }); - }, - open: function(path, flags, mode, fd_start, fd_end) { - flags = typeof flags === 'string' ? FS.modeStringToFlags(flags) : flags; - mode = typeof mode === 'undefined' ? 0666 : mode; - if ((flags & 64)) { - mode = (mode & 4095) | 32768; - } else { - mode = 0; - } - var node; - if (typeof path === 'object') { - node = path; - } else { - path = PATH.normalize(path); - try { - var lookup = FS.lookupPath(path, { - follow: !(flags & 131072) - }); - node = lookup.node; - } catch (e) { - // ignore - } - } - // perhaps we need to create the node - if ((flags & 64)) { - if (node) { - // if O_CREAT and O_EXCL are set, error out if the node already exists - if ((flags & 128)) { - throw new FS.ErrnoError(ERRNO_CODES.EEXIST); - } - } else { - // node doesn't exist, try to create it - node = FS.mknod(path, mode, 0); - } - } - if (!node) { - throw new FS.ErrnoError(ERRNO_CODES.ENOENT); - } - // can't truncate a device - if (FS.isChrdev(node.mode)) { - flags &= ~512; - } - // check permissions - var err = FS.mayOpen(node, flags); - if (err) { - throw new FS.ErrnoError(err); - } - // do truncation if necessary - if ((flags & 512)) { - FS.truncate(node, 0); - } - // we've already handled these, don't pass down to the underlying vfs - flags &= ~(128 | 512); - - // register the stream with the filesystem - var stream = FS.createStream({ - node: node, - path: FS.getPath(node), // we want the absolute path to the node - flags: flags, - seekable: true, - position: 0, - stream_ops: node.stream_ops, - // used by the file family libc calls (fopen, fwrite, ferror, etc.) - ungotten: [], - error: false - }, fd_start, fd_end); - // call the new stream's open function - if (stream.stream_ops.open) { - stream.stream_ops.open(stream); - } - if (Module['logReadFiles'] && !(flags & 1)) { - if (!FS.readFiles) FS.readFiles = {}; - if (!(path in FS.readFiles)) { - FS.readFiles[path] = 1; - Module['printErr']('read file: ' + path); - } - } - return stream; - }, - close: function(stream) { - try { - if (stream.stream_ops.close) { - stream.stream_ops.close(stream); - } - } catch (e) { - throw e; - } finally { - FS.closeStream(stream.fd); - } - }, - llseek: function(stream, offset, whence) { - if (!stream.seekable || !stream.stream_ops.llseek) { - throw new FS.ErrnoError(ERRNO_CODES.ESPIPE); - } - return stream.stream_ops.llseek(stream, offset, whence); - }, - read: function(stream, buffer, offset, length, position) { - if (length < 0 || position < 0) { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); - } - if ((stream.flags & 2097155) === 1) { - throw new FS.ErrnoError(ERRNO_CODES.EBADF); - } - if (FS.isDir(stream.node.mode)) { - throw new FS.ErrnoError(ERRNO_CODES.EISDIR); - } - if (!stream.stream_ops.read) { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); - } - var seeking = true; - if (typeof position === 'undefined') { - position = stream.position; - seeking = false; - } else if (!stream.seekable) { - throw new FS.ErrnoError(ERRNO_CODES.ESPIPE); - } - var bytesRead = stream.stream_ops.read(stream, buffer, offset, length, position); - if (!seeking) stream.position += bytesRead; - return bytesRead; - }, - write: function(stream, buffer, offset, length, position, canOwn) { - if (length < 0 || position < 0) { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); - } - if ((stream.flags & 2097155) === 0) { - throw new FS.ErrnoError(ERRNO_CODES.EBADF); - } - if (FS.isDir(stream.node.mode)) { - throw new FS.ErrnoError(ERRNO_CODES.EISDIR); - } - if (!stream.stream_ops.write) { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); - } - var seeking = true; - if (typeof position === 'undefined') { - position = stream.position; - seeking = false; - } else if (!stream.seekable) { - throw new FS.ErrnoError(ERRNO_CODES.ESPIPE); - } - if (stream.flags & 1024) { - // seek to the end before writing in append mode - FS.llseek(stream, 0, 2); - } - var bytesWritten = stream.stream_ops.write(stream, buffer, offset, length, position, canOwn); - if (!seeking) stream.position += bytesWritten; - return bytesWritten; - }, - allocate: function(stream, offset, length) { - if (offset < 0 || length <= 0) { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); - } - if ((stream.flags & 2097155) === 0) { - throw new FS.ErrnoError(ERRNO_CODES.EBADF); - } - if (!FS.isFile(stream.node.mode) && !FS.isDir(node.mode)) { - throw new FS.ErrnoError(ERRNO_CODES.ENODEV); - } - if (!stream.stream_ops.allocate) { - throw new FS.ErrnoError(ERRNO_CODES.EOPNOTSUPP); - } - stream.stream_ops.allocate(stream, offset, length); - }, - mmap: function(stream, buffer, offset, length, position, prot, flags) { - // TODO if PROT is PROT_WRITE, make sure we have write access - if ((stream.flags & 2097155) === 1) { - throw new FS.ErrnoError(ERRNO_CODES.EACCES); - } - if (!stream.stream_ops.mmap) { - throw new FS.ErrnoError(ERRNO_CODES.ENODEV); - } - return stream.stream_ops.mmap(stream, buffer, offset, length, position, prot, flags); - }, - ioctl: function(stream, cmd, arg) { - if (!stream.stream_ops.ioctl) { - throw new FS.ErrnoError(ERRNO_CODES.ENOTTY); - } - return stream.stream_ops.ioctl(stream, cmd, arg); - }, - readFile: function(path, opts) { - opts = opts || {}; - opts.flags = opts.flags || 'r'; - opts.encoding = opts.encoding || 'binary'; - var ret; - var stream = FS.open(path, opts.flags); - var stat = FS.stat(path); - var length = stat.size; - var buf = new Uint8Array(length); - FS.read(stream, buf, 0, length, 0); - if (opts.encoding === 'utf8') { - ret = ''; - var utf8 = new Runtime.UTF8Processor(); - for (var i = 0; i < length; i++) { - ret += utf8.processCChar(buf[i]); - } - } else if (opts.encoding === 'binary') { - ret = buf; - } else { - throw new Error('Invalid encoding type "' + opts.encoding + '"'); - } - FS.close(stream); - return ret; - }, - writeFile: function(path, data, opts) { - opts = opts || {}; - opts.flags = opts.flags || 'w'; - opts.encoding = opts.encoding || 'utf8'; - var stream = FS.open(path, opts.flags, opts.mode); - if (opts.encoding === 'utf8') { - var utf8 = new Runtime.UTF8Processor(); - var buf = new Uint8Array(utf8.processJSString(data)); - FS.write(stream, buf, 0, buf.length, 0); - } else if (opts.encoding === 'binary') { - FS.write(stream, data, 0, data.length, 0); - } else { - throw new Error('Invalid encoding type "' + opts.encoding + '"'); - } - FS.close(stream); - }, - cwd: function() { - return FS.currentPath; - }, - chdir: function(path) { - var lookup = FS.lookupPath(path, { - follow: true - }); - if (!FS.isDir(lookup.node.mode)) { - throw new FS.ErrnoError(ERRNO_CODES.ENOTDIR); - } - var err = FS.nodePermissions(lookup.node, 'x'); - if (err) { - throw new FS.ErrnoError(err); - } - FS.currentPath = lookup.path; - }, - createDefaultDirectories: function() { - FS.mkdir('/tmp'); - }, - createDefaultDevices: function() { - // create /dev - FS.mkdir('/dev'); - // setup /dev/null - FS.registerDevice(FS.makedev(1, 3), { - read: function() { - return 0; - }, - write: function() { - return 0; - } - }); - FS.mkdev('/dev/null', FS.makedev(1, 3)); - // setup /dev/tty and /dev/tty1 - // stderr needs to print output using Module['printErr'] - // so we register a second tty just for it. - TTY.register(FS.makedev(5, 0), TTY.default_tty_ops); - TTY.register(FS.makedev(6, 0), TTY.default_tty1_ops); - FS.mkdev('/dev/tty', FS.makedev(5, 0)); - FS.mkdev('/dev/tty1', FS.makedev(6, 0)); - // we're not going to emulate the actual shm device, - // just create the tmp dirs that reside in it commonly - FS.mkdir('/dev/shm'); - FS.mkdir('/dev/shm/tmp'); - }, - createStandardStreams: function() { - // TODO deprecate the old functionality of a single - // input / output callback and that utilizes FS.createDevice - // and instead require a unique set of stream ops - - // by default, we symlink the standard streams to the - // default tty devices. however, if the standard streams - // have been overwritten we create a unique device for - // them instead. - if (Module['stdin']) { - FS.createDevice('/dev', 'stdin', Module['stdin']); - } else { - FS.symlink('/dev/tty', '/dev/stdin'); - } - if (Module['stdout']) { - FS.createDevice('/dev', 'stdout', null, Module['stdout']); - } else { - FS.symlink('/dev/tty', '/dev/stdout'); - } - if (Module['stderr']) { - FS.createDevice('/dev', 'stderr', null, Module['stderr']); - } else { - FS.symlink('/dev/tty1', '/dev/stderr'); - } - - // open default streams for the stdin, stdout and stderr devices - var stdin = FS.open('/dev/stdin', 'r'); - HEAP32[((_stdin) >> 2)] = stdin.fd; - assert(stdin.fd === 1, 'invalid handle for stdin (' + stdin.fd + ')'); - - var stdout = FS.open('/dev/stdout', 'w'); - HEAP32[((_stdout) >> 2)] = stdout.fd; - assert(stdout.fd === 2, 'invalid handle for stdout (' + stdout.fd + ')'); - - var stderr = FS.open('/dev/stderr', 'w'); - HEAP32[((_stderr) >> 2)] = stderr.fd; - assert(stderr.fd === 3, 'invalid handle for stderr (' + stderr.fd + ')'); - }, - ensureErrnoError: function() { - if (FS.ErrnoError) return; - FS.ErrnoError = function ErrnoError(errno) { - this.errno = errno; - for (var key in ERRNO_CODES) { - if (ERRNO_CODES[key] === errno) { - this.code = key; - break; - } - } - this.message = ERRNO_MESSAGES[errno]; - }; - FS.ErrnoError.prototype = new Error(); - FS.ErrnoError.prototype.constructor = FS.ErrnoError; - // Some errors may happen quite a bit, to avoid overhead we reuse them (and suffer a lack of stack info) - [ERRNO_CODES.ENOENT].forEach(function(code) { - FS.genericErrors[code] = new FS.ErrnoError(code); - FS.genericErrors[code].stack = ''; - }); - }, - staticInit: function() { - FS.ensureErrnoError(); - - FS.nameTable = new Array(4096); - - FS.root = FS.createNode(null, '/', 16384 | 0777, 0); - FS.mount(MEMFS, {}, '/'); - - FS.createDefaultDirectories(); - FS.createDefaultDevices(); - }, - init: function(input, output, error) { - assert(!FS.init.initialized, 'FS.init was previously called. If you want to initialize later with custom parameters, remove any earlier calls (note that one is automatically added to the generated code)'); - FS.init.initialized = true; - - FS.ensureErrnoError(); - - // Allow Module.stdin etc. to provide defaults, if none explicitly passed to us here - Module['stdin'] = input || Module['stdin']; - Module['stdout'] = output || Module['stdout']; - Module['stderr'] = error || Module['stderr']; - - FS.createStandardStreams(); - }, - quit: function() { - FS.init.initialized = false; - for (var i = 0; i < FS.streams.length; i++) { - var stream = FS.streams[i]; - if (!stream) { - continue; - } - FS.close(stream); - } - }, - getMode: function(canRead, canWrite) { - var mode = 0; - if (canRead) mode |= 292 | 73; - if (canWrite) mode |= 146; - return mode; - }, - joinPath: function(parts, forceRelative) { - var path = PATH.join.apply(null, parts); - if (forceRelative && path[0] == '/') path = path.substr(1); - return path; - }, - absolutePath: function(relative, base) { - return PATH.resolve(base, relative); - }, - standardizePath: function(path) { - return PATH.normalize(path); - }, - findObject: function(path, dontResolveLastLink) { - var ret = FS.analyzePath(path, dontResolveLastLink); - if (ret.exists) { - return ret.object; - } else { - ___setErrNo(ret.error); - return null; - } - }, - analyzePath: function(path, dontResolveLastLink) { - // operate from within the context of the symlink's target - try { - var lookup = FS.lookupPath(path, { - follow: !dontResolveLastLink - }); - path = lookup.path; - } catch (e) {} - var ret = { - isRoot: false, - exists: false, - error: 0, - name: null, - path: null, - object: null, - parentExists: false, - parentPath: null, - parentObject: null - }; - try { - var lookup = FS.lookupPath(path, { - parent: true - }); - ret.parentExists = true; - ret.parentPath = lookup.path; - ret.parentObject = lookup.node; - ret.name = PATH.basename(path); - lookup = FS.lookupPath(path, { - follow: !dontResolveLastLink - }); - ret.exists = true; - ret.path = lookup.path; - ret.object = lookup.node; - ret.name = lookup.node.name; - ret.isRoot = lookup.path === '/'; - } catch (e) { - ret.error = e.errno; - }; - return ret; - }, - createFolder: function(parent, name, canRead, canWrite) { - var path = PATH.join2(typeof parent === 'string' ? parent : FS.getPath(parent), name); - var mode = FS.getMode(canRead, canWrite); - return FS.mkdir(path, mode); - }, - createPath: function(parent, path, canRead, canWrite) { - parent = typeof parent === 'string' ? parent : FS.getPath(parent); - var parts = path.split('/').reverse(); - while (parts.length) { - var part = parts.pop(); - if (!part) continue; - var current = PATH.join2(parent, part); - try { - FS.mkdir(current); - } catch (e) { - // ignore EEXIST - } - parent = current; - } - return current; - }, - createFile: function(parent, name, properties, canRead, canWrite) { - var path = PATH.join2(typeof parent === 'string' ? parent : FS.getPath(parent), name); - var mode = FS.getMode(canRead, canWrite); - return FS.create(path, mode); - }, - createDataFile: function(parent, name, data, canRead, canWrite, canOwn) { - var path = name ? PATH.join2(typeof parent === 'string' ? parent : FS.getPath(parent), name) : parent; - var mode = FS.getMode(canRead, canWrite); - var node = FS.create(path, mode); - if (data) { - if (typeof data === 'string') { - var arr = new Array(data.length); - for (var i = 0, len = data.length; i < len; ++i) arr[i] = data.charCodeAt(i); - data = arr; - } - // make sure we can write to the file - FS.chmod(node, mode | 146); - var stream = FS.open(node, 'w'); - FS.write(stream, data, 0, data.length, 0, canOwn); - FS.close(stream); - FS.chmod(node, mode); - } - return node; - }, - createDevice: function(parent, name, input, output) { - var path = PATH.join2(typeof parent === 'string' ? parent : FS.getPath(parent), name); - var mode = FS.getMode(!!input, !!output); - if (!FS.createDevice.major) FS.createDevice.major = 64; - var dev = FS.makedev(FS.createDevice.major++, 0); - // Create a fake device that a set of stream ops to emulate - // the old behavior. - FS.registerDevice(dev, { - open: function(stream) { - stream.seekable = false; - }, - close: function(stream) { - // flush any pending line data - if (output && output.buffer && output.buffer.length) { - output(10); - } - }, - read: function(stream, buffer, offset, length, pos /* ignored */ ) { - var bytesRead = 0; - for (var i = 0; i < length; i++) { - var result; - try { - result = input(); - } catch (e) { - throw new FS.ErrnoError(ERRNO_CODES.EIO); - } - if (result === undefined && bytesRead === 0) { - throw new FS.ErrnoError(ERRNO_CODES.EAGAIN); - } - if (result === null || result === undefined) break; - bytesRead++; - buffer[offset + i] = result; - } - if (bytesRead) { - stream.node.timestamp = Date.now(); - } - return bytesRead; - }, - write: function(stream, buffer, offset, length, pos) { - for (var i = 0; i < length; i++) { - try { - output(buffer[offset + i]); - } catch (e) { - throw new FS.ErrnoError(ERRNO_CODES.EIO); - } - } - if (length) { - stream.node.timestamp = Date.now(); - } - return i; - } - }); - return FS.mkdev(path, mode, dev); - }, - createLink: function(parent, name, target, canRead, canWrite) { - var path = PATH.join2(typeof parent === 'string' ? parent : FS.getPath(parent), name); - return FS.symlink(target, path); - }, - forceLoadFile: function(obj) { - if (obj.isDevice || obj.isFolder || obj.link || obj.contents) return true; - var success = true; - if (typeof XMLHttpRequest !== 'undefined') { - throw new Error("Lazy loading should have been performed (contents set) in createLazyFile, but it was not. Lazy loading only works in web workers. Use --embed-file or --preload-file in emcc on the main thread."); - } else if (Module['read']) { - // Command-line. - try { - // WARNING: Can't read binary files in V8's d8 or tracemonkey's js, as - // read() will try to parse UTF8. - obj.contents = intArrayFromString(Module['read'](obj.url), true); - } catch (e) { - success = false; - } - } else { - throw new Error('Cannot load without read() or XMLHttpRequest.'); - } - if (!success) ___setErrNo(ERRNO_CODES.EIO); - return success; - }, - createLazyFile: function(parent, name, url, canRead, canWrite) { - if (typeof XMLHttpRequest !== 'undefined') { - if (!ENVIRONMENT_IS_WORKER) throw 'Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc'; - // Lazy chunked Uint8Array (implements get and length from Uint8Array). Actual getting is abstracted away for eventual reuse. - function LazyUint8Array() { - this.lengthKnown = false; - this.chunks = []; // Loaded chunks. Index is the chunk number - } - LazyUint8Array.prototype.get = function LazyUint8Array_get(idx) { - if (idx > this.length - 1 || idx < 0) { - return undefined; - } - var chunkOffset = idx % this.chunkSize; - var chunkNum = Math.floor(idx / this.chunkSize); - return this.getter(chunkNum)[chunkOffset]; - } - LazyUint8Array.prototype.setDataGetter = function LazyUint8Array_setDataGetter(getter) { - this.getter = getter; - } - LazyUint8Array.prototype.cacheLength = function LazyUint8Array_cacheLength() { - // Find length - var xhr = new XMLHttpRequest(); - xhr.open('HEAD', url, false); - xhr.send(null); - if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status); - var datalength = Number(xhr.getResponseHeader("Content-length")); - var header; - var hasByteServing = (header = xhr.getResponseHeader("Accept-Ranges")) && header === "bytes"; - var chunkSize = 1024 * 1024; // Chunk size in bytes - - if (!hasByteServing) chunkSize = datalength; - - // Function to get a range from the remote URL. - var doXHR = (function(from, to) { - if (from > to) throw new Error("invalid range (" + from + ", " + to + ") or no bytes requested!"); - if (to > datalength - 1) throw new Error("only " + datalength + " bytes available! programmer error!"); - - // TODO: Use mozResponseArrayBuffer, responseStream, etc. if available. - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, false); - if (datalength !== chunkSize) xhr.setRequestHeader("Range", "bytes=" + from + "-" + to); - - // Some hints to the browser that we want binary data. - if (typeof Uint8Array != 'undefined') xhr.responseType = 'arraybuffer'; - if (xhr.overrideMimeType) { - xhr.overrideMimeType('text/plain; charset=x-user-defined'); - } - - xhr.send(null); - if (!(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304)) throw new Error("Couldn't load " + url + ". Status: " + xhr.status); - if (xhr.response !== undefined) { - return new Uint8Array(xhr.response || []); - } else { - return intArrayFromString(xhr.responseText || '', true); - } - }); - var lazyArray = this; - lazyArray.setDataGetter(function(chunkNum) { - var start = chunkNum * chunkSize; - var end = (chunkNum + 1) * chunkSize - 1; // including this byte - end = Math.min(end, datalength - 1); // if datalength-1 is selected, this is the last block - if (typeof(lazyArray.chunks[chunkNum]) === "undefined") { - lazyArray.chunks[chunkNum] = doXHR(start, end); - } - if (typeof(lazyArray.chunks[chunkNum]) === "undefined") throw new Error("doXHR failed!"); - return lazyArray.chunks[chunkNum]; - }); - - this._length = datalength; - this._chunkSize = chunkSize; - this.lengthKnown = true; - } - - var lazyArray = new LazyUint8Array(); - Object.defineProperty(lazyArray, "length", { - get: function() { - if (!this.lengthKnown) { - this.cacheLength(); - } - return this._length; - } - }); - Object.defineProperty(lazyArray, "chunkSize", { - get: function() { - if (!this.lengthKnown) { - this.cacheLength(); - } - return this._chunkSize; - } - }); - - var properties = { - isDevice: false, - contents: lazyArray - }; - } else { - var properties = { - isDevice: false, - url: url - }; - } - - var node = FS.createFile(parent, name, properties, canRead, canWrite); - // This is a total hack, but I want to get this lazy file code out of the - // core of MEMFS. If we want to keep this lazy file concept I feel it should - // be its own thin LAZYFS proxying calls to MEMFS. - if (properties.contents) { - node.contents = properties.contents; - } else if (properties.url) { - node.contents = null; - node.url = properties.url; - } - // override each stream op with one that tries to force load the lazy file first - var stream_ops = {}; - var keys = Object.keys(node.stream_ops); - keys.forEach(function(key) { - var fn = node.stream_ops[key]; - stream_ops[key] = function forceLoadLazyFile() { - if (!FS.forceLoadFile(node)) { - throw new FS.ErrnoError(ERRNO_CODES.EIO); - } - return fn.apply(null, arguments); - }; - }); - // use a custom read function - stream_ops.read = function stream_ops_read(stream, buffer, offset, length, position) { - if (!FS.forceLoadFile(node)) { - throw new FS.ErrnoError(ERRNO_CODES.EIO); - } - var contents = stream.node.contents; - if (position >= contents.length) - return 0; - var size = Math.min(contents.length - position, length); - assert(size >= 0); - if (contents.slice) { // normal array - for (var i = 0; i < size; i++) { - buffer[offset + i] = contents[position + i]; - } - } else { - for (var i = 0; i < size; i++) { // LazyUint8Array from sync binary XHR - buffer[offset + i] = contents.get(position + i); - } - } - return size; - }; - node.stream_ops = stream_ops; - return node; - }, - createPreloadedFile: function(parent, name, url, canRead, canWrite, onload, onerror, dontCreateFile, canOwn) { - Browser.init(); - // TODO we should allow people to just pass in a complete filename instead - // of parent and name being that we just join them anyways - var fullname = name ? PATH.resolve(PATH.join2(parent, name)) : parent; - - function processData(byteArray) { - function finish(byteArray) { - if (!dontCreateFile) { - FS.createDataFile(parent, name, byteArray, canRead, canWrite, canOwn); - } - if (onload) onload(); - removeRunDependency('cp ' + fullname); - } - var handled = false; - Module['preloadPlugins'].forEach(function(plugin) { - if (handled) return; - if (plugin['canHandle'](fullname)) { - plugin['handle'](byteArray, fullname, finish, function() { - if (onerror) onerror(); - removeRunDependency('cp ' + fullname); - }); - handled = true; - } - }); - if (!handled) finish(byteArray); - } - addRunDependency('cp ' + fullname); - if (typeof url == 'string') { - Browser.asyncLoad(url, function(byteArray) { - processData(byteArray); - }, onerror); - } else { - processData(url); - } - }, - indexedDB: function() { - return window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; - }, - DB_NAME: function() { - return 'EM_FS_' + window.location.pathname; - }, - DB_VERSION: 20, - DB_STORE_NAME: "FILE_DATA", - saveFilesToDB: function(paths, onload, onerror) { - onload = onload || function() {}; - onerror = onerror || function() {}; - var indexedDB = FS.indexedDB(); - try { - var openRequest = indexedDB.open(FS.DB_NAME(), FS.DB_VERSION); - } catch (e) { - return onerror(e); - } - openRequest.onupgradeneeded = function openRequest_onupgradeneeded() { - console.log('creating db'); - var db = openRequest.result; - db.createObjectStore(FS.DB_STORE_NAME); - }; - openRequest.onsuccess = function openRequest_onsuccess() { - var db = openRequest.result; - var transaction = db.transaction([FS.DB_STORE_NAME], 'readwrite'); - var files = transaction.objectStore(FS.DB_STORE_NAME); - var ok = 0, - fail = 0, - total = paths.length; - - function finish() { - if (fail == 0) onload(); - else onerror(); - } - paths.forEach(function(path) { - var putRequest = files.put(FS.analyzePath(path).object.contents, path); - putRequest.onsuccess = function putRequest_onsuccess() { - ok++; - if (ok + fail == total) finish() - }; - putRequest.onerror = function putRequest_onerror() { - fail++; - if (ok + fail == total) finish() - }; - }); - transaction.onerror = onerror; - }; - openRequest.onerror = onerror; - }, - loadFilesFromDB: function(paths, onload, onerror) { - onload = onload || function() {}; - onerror = onerror || function() {}; - var indexedDB = FS.indexedDB(); - try { - var openRequest = indexedDB.open(FS.DB_NAME(), FS.DB_VERSION); - } catch (e) { - return onerror(e); - } - openRequest.onupgradeneeded = onerror; // no database to load from - openRequest.onsuccess = function openRequest_onsuccess() { - var db = openRequest.result; - try { - var transaction = db.transaction([FS.DB_STORE_NAME], 'readonly'); - } catch (e) { - onerror(e); - return; - } - var files = transaction.objectStore(FS.DB_STORE_NAME); - var ok = 0, - fail = 0, - total = paths.length; - - function finish() { - if (fail == 0) onload(); - else onerror(); - } - paths.forEach(function(path) { - var getRequest = files.get(path); - getRequest.onsuccess = function getRequest_onsuccess() { - if (FS.analyzePath(path).exists) { - FS.unlink(path); - } - FS.createDataFile(PATH.dirname(path), PATH.basename(path), getRequest.result, true, true, true); - ok++; - if (ok + fail == total) finish(); - }; - getRequest.onerror = function getRequest_onerror() { - fail++; - if (ok + fail == total) finish() - }; - }); - transaction.onerror = onerror; - }; - openRequest.onerror = onerror; - } -}; - - - - -var _mkport = undefined; -var SOCKFS = { - mount: function(mount) { - return FS.createNode(null, '/', 16384 | 0777, 0); - }, - createSocket: function(family, type, protocol) { - var streaming = type == 1; - if (protocol) { - assert(streaming == (protocol == 6)); // if SOCK_STREAM, must be tcp - } - - // create our internal socket structure - var sock = { - family: family, - type: type, - protocol: protocol, - server: null, - peers: {}, - pending: [], - recv_queue: [], - sock_ops: SOCKFS.websocket_sock_ops - }; - - // create the filesystem node to store the socket structure - var name = SOCKFS.nextname(); - var node = FS.createNode(SOCKFS.root, name, 49152, 0); - node.sock = sock; - - // and the wrapping stream that enables library functions such - // as read and write to indirectly interact with the socket - var stream = FS.createStream({ - path: name, - node: node, - flags: FS.modeStringToFlags('r+'), - seekable: false, - stream_ops: SOCKFS.stream_ops - }); - - // map the new stream to the socket structure (sockets have a 1:1 - // relationship with a stream) - sock.stream = stream; - - return sock; - }, - getSocket: function(fd) { - var stream = FS.getStream(fd); - if (!stream || !FS.isSocket(stream.node.mode)) { - return null; - } - return stream.node.sock; - }, - stream_ops: { - poll: function(stream) { - var sock = stream.node.sock; - return sock.sock_ops.poll(sock); - }, - ioctl: function(stream, request, varargs) { - var sock = stream.node.sock; - return sock.sock_ops.ioctl(sock, request, varargs); - }, - read: function(stream, buffer, offset, length, position /* ignored */ ) { - var sock = stream.node.sock; - var msg = sock.sock_ops.recvmsg(sock, length); - if (!msg) { - // socket is closed - return 0; - } - buffer.set(msg.buffer, offset); - return msg.buffer.length; - }, - write: function(stream, buffer, offset, length, position /* ignored */ ) { - var sock = stream.node.sock; - return sock.sock_ops.sendmsg(sock, buffer, offset, length); - }, - close: function(stream) { - var sock = stream.node.sock; - sock.sock_ops.close(sock); - } - }, - nextname: function() { - if (!SOCKFS.nextname.current) { - SOCKFS.nextname.current = 0; - } - return 'socket[' + (SOCKFS.nextname.current++) + ']'; - }, - websocket_sock_ops: { - createPeer: function(sock, addr, port) { - var ws; - - if (typeof addr === 'object') { - ws = addr; - addr = null; - port = null; - } - - if (ws) { - // for sockets that've already connected (e.g. we're the server) - // we can inspect the _socket property for the address - if (ws._socket) { - addr = ws._socket.remoteAddress; - port = ws._socket.remotePort; - } - // if we're just now initializing a connection to the remote, - // inspect the url property - else { - var result = /ws[s]?:\/\/([^:]+):(\d+)/.exec(ws.url); - if (!result) { - throw new Error('WebSocket URL must be in the format ws(s)://address:port'); - } - addr = result[1]; - port = parseInt(result[2], 10); - } - } else { - // create the actual websocket object and connect - try { - var url = 'ws://' + addr + ':' + port; - // the node ws library API is slightly different than the browser's - var opts = ENVIRONMENT_IS_NODE ? { - headers: { - 'websocket-protocol': ['binary'] - } - } : ['binary']; - // If node we use the ws library. - var WebSocket = ENVIRONMENT_IS_NODE ? require('ws') : window['WebSocket']; - ws = new WebSocket(url, opts); - ws.binaryType = 'arraybuffer'; - } catch (e) { - throw new FS.ErrnoError(ERRNO_CODES.EHOSTUNREACH); - } - } - - - var peer = { - addr: addr, - port: port, - socket: ws, - dgram_send_queue: [] - }; - - SOCKFS.websocket_sock_ops.addPeer(sock, peer); - SOCKFS.websocket_sock_ops.handlePeerEvents(sock, peer); - - // if this is a bound dgram socket, send the port number first to allow - // us to override the ephemeral port reported to us by remotePort on the - // remote end. - if (sock.type === 2 && typeof sock.sport !== 'undefined') { - peer.dgram_send_queue.push(new Uint8Array([ - 255, 255, 255, 255, - 'p'.charCodeAt(0), 'o'.charCodeAt(0), 'r'.charCodeAt(0), 't'.charCodeAt(0), ((sock.sport & 0xff00) >> 8), (sock.sport & 0xff) - ])); - } - - return peer; - }, - getPeer: function(sock, addr, port) { - return sock.peers[addr + ':' + port]; - }, - addPeer: function(sock, peer) { - sock.peers[peer.addr + ':' + peer.port] = peer; - }, - removePeer: function(sock, peer) { - delete sock.peers[peer.addr + ':' + peer.port]; - }, - handlePeerEvents: function(sock, peer) { - var first = true; - - var handleOpen = function() { - try { - var queued = peer.dgram_send_queue.shift(); - while (queued) { - peer.socket.send(queued); - queued = peer.dgram_send_queue.shift(); - } - } catch (e) { - // not much we can do here in the way of proper error handling as we've already - // lied and said this data was sent. shut it down. - peer.socket.close(); - } - }; - - function handleMessage(data) { - assert(typeof data !== 'string' && data.byteLength !== undefined); // must receive an ArrayBuffer - data = new Uint8Array(data); // make a typed array view on the array buffer - - - // if this is the port message, override the peer's port with it - var wasfirst = first; - first = false; - if (wasfirst && - data.length === 10 && - data[0] === 255 && data[1] === 255 && data[2] === 255 && data[3] === 255 && - data[4] === 'p'.charCodeAt(0) && data[5] === 'o'.charCodeAt(0) && data[6] === 'r'.charCodeAt(0) && data[7] === 't'.charCodeAt(0)) { - // update the peer's port and it's key in the peer map - var newport = ((data[8] << 8) | data[9]); - SOCKFS.websocket_sock_ops.removePeer(sock, peer); - peer.port = newport; - SOCKFS.websocket_sock_ops.addPeer(sock, peer); - return; - } - - sock.recv_queue.push({ - addr: peer.addr, - port: peer.port, - data: data - }); - }; - - if (ENVIRONMENT_IS_NODE) { - peer.socket.on('open', handleOpen); - peer.socket.on('message', function(data, flags) { - if (!flags.binary) { - return; - } - handleMessage((new Uint8Array(data)).buffer); // copy from node Buffer -> ArrayBuffer - }); - peer.socket.on('error', function() { - // don't throw - }); - } else { - peer.socket.onopen = handleOpen; - peer.socket.onmessage = function peer_socket_onmessage(event) { - handleMessage(event.data); - }; - } - }, - poll: function(sock) { - if (sock.type === 1 && sock.server) { - // listen sockets should only say they're available for reading - // if there are pending clients. - return sock.pending.length ? (64 | 1) : 0; - } - - var mask = 0; - var dest = sock.type === 1 ? // we only care about the socket state for connection-based sockets - SOCKFS.websocket_sock_ops.getPeer(sock, sock.daddr, sock.dport) : - null; - - if (sock.recv_queue.length || - !dest || // connection-less sockets are always ready to read - (dest && dest.socket.readyState === dest.socket.CLOSING) || - (dest && dest.socket.readyState === dest.socket.CLOSED)) { // let recv return 0 once closed - mask |= (64 | 1); - } - - if (!dest || // connection-less sockets are always ready to write - (dest && dest.socket.readyState === dest.socket.OPEN)) { - mask |= 4; - } - - if ((dest && dest.socket.readyState === dest.socket.CLOSING) || - (dest && dest.socket.readyState === dest.socket.CLOSED)) { - mask |= 16; - } - - return mask; - }, - ioctl: function(sock, request, arg) { - switch (request) { - case 21531: - var bytes = 0; - if (sock.recv_queue.length) { - bytes = sock.recv_queue[0].data.length; - } - HEAP32[((arg) >> 2)] = bytes; - return 0; - default: - return ERRNO_CODES.EINVAL; - } - }, - close: function(sock) { - // if we've spawned a listen server, close it - if (sock.server) { - try { - sock.server.close(); - } catch (e) {} - sock.server = null; - } - // close any peer connections - var peers = Object.keys(sock.peers); - for (var i = 0; i < peers.length; i++) { - var peer = sock.peers[peers[i]]; - try { - peer.socket.close(); - } catch (e) {} - SOCKFS.websocket_sock_ops.removePeer(sock, peer); - } - return 0; - }, - bind: function(sock, addr, port) { - if (typeof sock.saddr !== 'undefined' || typeof sock.sport !== 'undefined') { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); // already bound - } - sock.saddr = addr; - sock.sport = port || _mkport(); - // in order to emulate dgram sockets, we need to launch a listen server when - // binding on a connection-less socket - // note: this is only required on the server side - if (sock.type === 2) { - // close the existing server if it exists - if (sock.server) { - sock.server.close(); - sock.server = null; - } - // swallow error operation not supported error that occurs when binding in the - // browser where this isn't supported - try { - sock.sock_ops.listen(sock, 0); - } catch (e) { - if (!(e instanceof FS.ErrnoError)) throw e; - if (e.errno !== ERRNO_CODES.EOPNOTSUPP) throw e; - } - } - }, - connect: function(sock, addr, port) { - if (sock.server) { - throw new FS.ErrnoError(ERRNO_CODS.EOPNOTSUPP); - } - - // TODO autobind - // if (!sock.addr && sock.type == 2) { - // } - - // early out if we're already connected / in the middle of connecting - if (typeof sock.daddr !== 'undefined' && typeof sock.dport !== 'undefined') { - var dest = SOCKFS.websocket_sock_ops.getPeer(sock, sock.daddr, sock.dport); - if (dest) { - if (dest.socket.readyState === dest.socket.CONNECTING) { - throw new FS.ErrnoError(ERRNO_CODES.EALREADY); - } else { - throw new FS.ErrnoError(ERRNO_CODES.EISCONN); - } - } - } - - // add the socket to our peer list and set our - // destination address / port to match - var peer = SOCKFS.websocket_sock_ops.createPeer(sock, addr, port); - sock.daddr = peer.addr; - sock.dport = peer.port; - - // always "fail" in non-blocking mode - throw new FS.ErrnoError(ERRNO_CODES.EINPROGRESS); - }, - listen: function(sock, backlog) { - if (!ENVIRONMENT_IS_NODE) { - throw new FS.ErrnoError(ERRNO_CODES.EOPNOTSUPP); - } - if (sock.server) { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); // already listening - } - var WebSocketServer = require('ws').Server; - var host = sock.saddr; - sock.server = new WebSocketServer({ - host: host, - port: sock.sport - // TODO support backlog - }); - - sock.server.on('connection', function(ws) { - if (sock.type === 1) { - var newsock = SOCKFS.createSocket(sock.family, sock.type, sock.protocol); - - // create a peer on the new socket - var peer = SOCKFS.websocket_sock_ops.createPeer(newsock, ws); - newsock.daddr = peer.addr; - newsock.dport = peer.port; - - // push to queue for accept to pick up - sock.pending.push(newsock); - } else { - // create a peer on the listen socket so calling sendto - // with the listen socket and an address will resolve - // to the correct client - SOCKFS.websocket_sock_ops.createPeer(sock, ws); - } - }); - sock.server.on('closed', function() { - sock.server = null; - }); - sock.server.on('error', function() { - // don't throw - }); - }, - accept: function(listensock) { - if (!listensock.server) { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); - } - var newsock = listensock.pending.shift(); - newsock.stream.flags = listensock.stream.flags; - return newsock; - }, - getname: function(sock, peer) { - var addr, port; - if (peer) { - if (sock.daddr === undefined || sock.dport === undefined) { - throw new FS.ErrnoError(ERRNO_CODES.ENOTCONN); - } - addr = sock.daddr; - port = sock.dport; - } else { - // TODO saddr and sport will be set for bind()'d UDP sockets, but what - // should we be returning for TCP sockets that've been connect()'d? - addr = sock.saddr || 0; - port = sock.sport || 0; - } - return { - addr: addr, - port: port - }; - }, - sendmsg: function(sock, buffer, offset, length, addr, port) { - if (sock.type === 2) { - // connection-less sockets will honor the message address, - // and otherwise fall back to the bound destination address - if (addr === undefined || port === undefined) { - addr = sock.daddr; - port = sock.dport; - } - // if there was no address to fall back to, error out - if (addr === undefined || port === undefined) { - throw new FS.ErrnoError(ERRNO_CODES.EDESTADDRREQ); - } - } else { - // connection-based sockets will only use the bound - addr = sock.daddr; - port = sock.dport; - } - - // find the peer for the destination address - var dest = SOCKFS.websocket_sock_ops.getPeer(sock, addr, port); - - // early out if not connected with a connection-based socket - if (sock.type === 1) { - if (!dest || dest.socket.readyState === dest.socket.CLOSING || dest.socket.readyState === dest.socket.CLOSED) { - throw new FS.ErrnoError(ERRNO_CODES.ENOTCONN); - } else if (dest.socket.readyState === dest.socket.CONNECTING) { - throw new FS.ErrnoError(ERRNO_CODES.EAGAIN); - } - } - - // create a copy of the incoming data to send, as the WebSocket API - // doesn't work entirely with an ArrayBufferView, it'll just send - // the entire underlying buffer - var data; - if (buffer instanceof Array || buffer instanceof ArrayBuffer) { - data = buffer.slice(offset, offset + length); - } else { // ArrayBufferView - data = buffer.buffer.slice(buffer.byteOffset + offset, buffer.byteOffset + offset + length); - } - - // if we're emulating a connection-less dgram socket and don't have - // a cached connection, queue the buffer to send upon connect and - // lie, saying the data was sent now. - if (sock.type === 2) { - if (!dest || dest.socket.readyState !== dest.socket.OPEN) { - // if we're not connected, open a new connection - if (!dest || dest.socket.readyState === dest.socket.CLOSING || dest.socket.readyState === dest.socket.CLOSED) { - dest = SOCKFS.websocket_sock_ops.createPeer(sock, addr, port); - } - dest.dgram_send_queue.push(data); - return length; - } - } - - try { - // send the actual data - dest.socket.send(data); - return length; - } catch (e) { - throw new FS.ErrnoError(ERRNO_CODES.EINVAL); - } - }, - recvmsg: function(sock, length) { - // http://pubs.opengroup.org/onlinepubs/7908799/xns/recvmsg.html - if (sock.type === 1 && sock.server) { - // tcp servers should not be recv()'ing on the listen socket - throw new FS.ErrnoError(ERRNO_CODES.ENOTCONN); - } - - var queued = sock.recv_queue.shift(); - if (!queued) { - if (sock.type === 1) { - var dest = SOCKFS.websocket_sock_ops.getPeer(sock, sock.daddr, sock.dport); - - if (!dest) { - // if we have a destination address but are not connected, error out - throw new FS.ErrnoError(ERRNO_CODES.ENOTCONN); - } else if (dest.socket.readyState === dest.socket.CLOSING || dest.socket.readyState === dest.socket.CLOSED) { - // return null if the socket has closed - return null; - } else { - // else, our socket is in a valid state but truly has nothing available - throw new FS.ErrnoError(ERRNO_CODES.EAGAIN); - } - } else { - throw new FS.ErrnoError(ERRNO_CODES.EAGAIN); - } - } - - // queued.data will be an ArrayBuffer if it's unadulterated, but if it's - // requeued TCP data it'll be an ArrayBufferView - var queuedLength = queued.data.byteLength || queued.data.length; - var queuedOffset = queued.data.byteOffset || 0; - var queuedBuffer = queued.data.buffer || queued.data; - var bytesRead = Math.min(length, queuedLength); - var res = { - buffer: new Uint8Array(queuedBuffer, queuedOffset, bytesRead), - addr: queued.addr, - port: queued.port - }; - - - // push back any unread data for TCP connections - if (sock.type === 1 && bytesRead < queuedLength) { - var bytesRemaining = queuedLength - bytesRead; - queued.data = new Uint8Array(queuedBuffer, queuedOffset + bytesRead, bytesRemaining); - sock.recv_queue.unshift(queued); - } - - return res; - } - } -}; - -function _send(fd, buf, len, flags) { - var sock = SOCKFS.getSocket(fd); - if (!sock) { - ___setErrNo(ERRNO_CODES.EBADF); - return -1; - } - // TODO honor flags - return _write(fd, buf, len); -} - -function _pwrite(fildes, buf, nbyte, offset) { - // ssize_t pwrite(int fildes, const void *buf, size_t nbyte, off_t offset); - // http://pubs.opengroup.org/onlinepubs/000095399/functions/write.html - var stream = FS.getStream(fildes); - if (!stream) { - ___setErrNo(ERRNO_CODES.EBADF); - return -1; - } - try { - var slab = HEAP8; - return FS.write(stream, slab, buf, nbyte, offset); - } catch (e) { - FS.handleFSError(e); - return -1; - } -} - -function _write(fildes, buf, nbyte) { - // ssize_t write(int fildes, const void *buf, size_t nbyte); - // http://pubs.opengroup.org/onlinepubs/000095399/functions/write.html - var stream = FS.getStream(fildes); - if (!stream) { - ___setErrNo(ERRNO_CODES.EBADF); - return -1; - } - - - try { - var slab = HEAP8; - return FS.write(stream, slab, buf, nbyte); - } catch (e) { - FS.handleFSError(e); - return -1; - } -} - -function _fwrite(ptr, size, nitems, stream) { - // size_t fwrite(const void *restrict ptr, size_t size, size_t nitems, FILE *restrict stream); - // http://pubs.opengroup.org/onlinepubs/000095399/functions/fwrite.html - var bytesToWrite = nitems * size; - if (bytesToWrite == 0) return 0; - var bytesWritten = _write(stream, ptr, bytesToWrite); - if (bytesWritten == -1) { - var streamObj = FS.getStream(stream); - if (streamObj) streamObj.error = true; - return 0; - } else { - return Math.floor(bytesWritten / size); - } -} - - - -Module["_strlen"] = _strlen; - -function __reallyNegative(x) { - return x < 0 || (x === 0 && (1 / x) === -Infinity); -} - -function __formatString(format, varargs) { - var textIndex = format; - var argIndex = 0; - - function getNextArg(type) { - // NOTE: Explicitly ignoring type safety. Otherwise this fails: - // int x = 4; printf("%c\n", (char)x); - var ret; - if (type === 'double') { - ret = HEAPF64[(((varargs) + (argIndex)) >> 3)]; - } else if (type == 'i64') { - ret = [HEAP32[(((varargs) + (argIndex)) >> 2)], - HEAP32[(((varargs) + (argIndex + 8)) >> 2)] - ]; - argIndex += 8; // each 32-bit chunk is in a 64-bit block - - } else { - type = 'i32'; // varargs are always i32, i64, or double - ret = HEAP32[(((varargs) + (argIndex)) >> 2)]; - } - argIndex += Math.max(Runtime.getNativeFieldSize(type), Runtime.getAlignSize(type, null, true)); - return ret; - } - - var ret = []; - var curr, next, currArg; - while (1) { - var startTextIndex = textIndex; - curr = HEAP8[(textIndex)]; - if (curr === 0) break; - next = HEAP8[((textIndex + 1) | 0)]; - if (curr == 37) { - // Handle flags. - var flagAlwaysSigned = false; - var flagLeftAlign = false; - var flagAlternative = false; - var flagZeroPad = false; - var flagPadSign = false; - flagsLoop: while (1) { - switch (next) { - case 43: - flagAlwaysSigned = true; - break; - case 45: - flagLeftAlign = true; - break; - case 35: - flagAlternative = true; - break; - case 48: - if (flagZeroPad) { - break flagsLoop; - } else { - flagZeroPad = true; - break; - } - case 32: - flagPadSign = true; - break; - default: - break flagsLoop; - } - textIndex++; - next = HEAP8[((textIndex + 1) | 0)]; - } - - // Handle width. - var width = 0; - if (next == 42) { - width = getNextArg('i32'); - textIndex++; - next = HEAP8[((textIndex + 1) | 0)]; - } else { - while (next >= 48 && next <= 57) { - width = width * 10 + (next - 48); - textIndex++; - next = HEAP8[((textIndex + 1) | 0)]; - } - } - - // Handle precision. - var precisionSet = false, - precision = -1; - if (next == 46) { - precision = 0; - precisionSet = true; - textIndex++; - next = HEAP8[((textIndex + 1) | 0)]; - if (next == 42) { - precision = getNextArg('i32'); - textIndex++; - } else { - while (1) { - var precisionChr = HEAP8[((textIndex + 1) | 0)]; - if (precisionChr < 48 || - precisionChr > 57) break; - precision = precision * 10 + (precisionChr - 48); - textIndex++; - } - } - next = HEAP8[((textIndex + 1) | 0)]; - } - if (precision === -1) { - precision = 6; // Standard default. - precisionSet = false; - } - - // Handle integer sizes. WARNING: These assume a 32-bit architecture! - var argSize; - switch (String.fromCharCode(next)) { - case 'h': - var nextNext = HEAP8[((textIndex + 2) | 0)]; - if (nextNext == 104) { - textIndex++; - argSize = 1; // char (actually i32 in varargs) - } else { - argSize = 2; // short (actually i32 in varargs) - } - break; - case 'l': - var nextNext = HEAP8[((textIndex + 2) | 0)]; - if (nextNext == 108) { - textIndex++; - argSize = 8; // long long - } else { - argSize = 4; // long - } - break; - case 'L': // long long - case 'q': // int64_t - case 'j': // intmax_t - argSize = 8; - break; - case 'z': // size_t - case 't': // ptrdiff_t - case 'I': // signed ptrdiff_t or unsigned size_t - argSize = 4; - break; - default: - argSize = null; - } - if (argSize) textIndex++; - next = HEAP8[((textIndex + 1) | 0)]; - - // Handle type specifier. - switch (String.fromCharCode(next)) { - case 'd': - case 'i': - case 'u': - case 'o': - case 'x': - case 'X': - case 'p': - { - // Integer. - var signed = next == 100 || next == 105; - argSize = argSize || 4; - var currArg = getNextArg('i' + (argSize * 8)); - var origArg = currArg; - var argText; - // Flatten i64-1 [low, high] into a (slightly rounded) double - if (argSize == 8) { - currArg = Runtime.makeBigInt(currArg[0], currArg[1], next == 117); - } - // Truncate to requested size. - if (argSize <= 4) { - var limit = Math.pow(256, argSize) - 1; - currArg = (signed ? reSign : unSign)(currArg & limit, argSize * 8); - } - // Format the number. - var currAbsArg = Math.abs(currArg); - var prefix = ''; - if (next == 100 || next == 105) { - if (argSize == 8 && i64Math) argText = i64Math.stringify(origArg[0], origArg[1], null); - else - argText = reSign(currArg, 8 * argSize, 1).toString(10); - } else if (next == 117) { - if (argSize == 8 && i64Math) argText = i64Math.stringify(origArg[0], origArg[1], true); - else - argText = unSign(currArg, 8 * argSize, 1).toString(10); - currArg = Math.abs(currArg); - } else if (next == 111) { - argText = (flagAlternative ? '0' : '') + currAbsArg.toString(8); - } else if (next == 120 || next == 88) { - prefix = (flagAlternative && currArg != 0) ? '0x' : ''; - if (argSize == 8 && i64Math) { - if (origArg[1]) { - argText = (origArg[1] >>> 0).toString(16); - var lower = (origArg[0] >>> 0).toString(16); - while (lower.length < 8) lower = '0' + lower; - argText += lower; - } else { - argText = (origArg[0] >>> 0).toString(16); - } - } else - if (currArg < 0) { - // Represent negative numbers in hex as 2's complement. - currArg = -currArg; - argText = (currAbsArg - 1).toString(16); - var buffer = []; - for (var i = 0; i < argText.length; i++) { - buffer.push((0xF - parseInt(argText[i], 16)).toString(16)); - } - argText = buffer.join(''); - while (argText.length < argSize * 2) argText = 'f' + argText; - } else { - argText = currAbsArg.toString(16); - } - if (next == 88) { - prefix = prefix.toUpperCase(); - argText = argText.toUpperCase(); - } - } else if (next == 112) { - if (currAbsArg === 0) { - argText = '(nil)'; - } else { - prefix = '0x'; - argText = currAbsArg.toString(16); - } - } - if (precisionSet) { - while (argText.length < precision) { - argText = '0' + argText; - } - } - - // Add sign if needed - if (currArg >= 0) { - if (flagAlwaysSigned) { - prefix = '+' + prefix; - } else if (flagPadSign) { - prefix = ' ' + prefix; - } - } - - // Move sign to prefix so we zero-pad after the sign - if (argText.charAt(0) == '-') { - prefix = '-' + prefix; - argText = argText.substr(1); - } - - // Add padding. - while (prefix.length + argText.length < width) { - if (flagLeftAlign) { - argText += ' '; - } else { - if (flagZeroPad) { - argText = '0' + argText; - } else { - prefix = ' ' + prefix; - } - } - } - - // Insert the result into the buffer. - argText = prefix + argText; - argText.split('').forEach(function(chr) { - ret.push(chr.charCodeAt(0)); - }); - break; - } - case 'f': - case 'F': - case 'e': - case 'E': - case 'g': - case 'G': - { - // Float. - var currArg = getNextArg('double'); - var argText; - if (isNaN(currArg)) { - argText = 'nan'; - flagZeroPad = false; - } else if (!isFinite(currArg)) { - argText = (currArg < 0 ? '-' : '') + 'inf'; - flagZeroPad = false; - } else { - var isGeneral = false; - var effectivePrecision = Math.min(precision, 20); - - // Convert g/G to f/F or e/E, as per: - // http://pubs.opengroup.org/onlinepubs/9699919799/functions/printf.html - if (next == 103 || next == 71) { - isGeneral = true; - precision = precision || 1; - var exponent = parseInt(currArg.toExponential(effectivePrecision).split('e')[1], 10); - if (precision > exponent && exponent >= -4) { - next = ((next == 103) ? 'f' : 'F').charCodeAt(0); - precision -= exponent + 1; - } else { - next = ((next == 103) ? 'e' : 'E').charCodeAt(0); - precision--; - } - effectivePrecision = Math.min(precision, 20); - } - - if (next == 101 || next == 69) { - argText = currArg.toExponential(effectivePrecision); - // Make sure the exponent has at least 2 digits. - if (/[eE][-+]\d$/.test(argText)) { - argText = argText.slice(0, -1) + '0' + argText.slice(-1); - } - } else if (next == 102 || next == 70) { - argText = currArg.toFixed(effectivePrecision); - if (currArg === 0 && __reallyNegative(currArg)) { - argText = '-' + argText; - } - } - - var parts = argText.split('e'); - if (isGeneral && !flagAlternative) { - // Discard trailing zeros and periods. - while (parts[0].length > 1 && parts[0].indexOf('.') != -1 && - (parts[0].slice(-1) == '0' || parts[0].slice(-1) == '.')) { - parts[0] = parts[0].slice(0, -1); - } - } else { - // Make sure we have a period in alternative mode. - if (flagAlternative && argText.indexOf('.') == -1) parts[0] += '.'; - // Zero pad until required precision. - while (precision > effectivePrecision++) parts[0] += '0'; - } - argText = parts[0] + (parts.length > 1 ? 'e' + parts[1] : ''); - - // Capitalize 'E' if needed. - if (next == 69) argText = argText.toUpperCase(); - - // Add sign. - if (currArg >= 0) { - if (flagAlwaysSigned) { - argText = '+' + argText; - } else if (flagPadSign) { - argText = ' ' + argText; - } - } - } - - // Add padding. - while (argText.length < width) { - if (flagLeftAlign) { - argText += ' '; - } else { - if (flagZeroPad && (argText[0] == '-' || argText[0] == '+')) { - argText = argText[0] + '0' + argText.slice(1); - } else { - argText = (flagZeroPad ? '0' : ' ') + argText; - } - } - } - - // Adjust case. - if (next < 97) argText = argText.toUpperCase(); - - // Insert the result into the buffer. - argText.split('').forEach(function(chr) { - ret.push(chr.charCodeAt(0)); - }); - break; - } - case 's': - { - // String. - var arg = getNextArg('i8*'); - var argLength = arg ? _strlen(arg) : '(null)'.length; - if (precisionSet) argLength = Math.min(argLength, precision); - if (!flagLeftAlign) { - while (argLength < width--) { - ret.push(32); - } - } - if (arg) { - for (var i = 0; i < argLength; i++) { - ret.push(HEAPU8[((arg++) | 0)]); - } - } else { - ret = ret.concat(intArrayFromString('(null)'.substr(0, argLength), true)); - } - if (flagLeftAlign) { - while (argLength < width--) { - ret.push(32); - } - } - break; - } - case 'c': - { - // Character. - if (flagLeftAlign) ret.push(getNextArg('i8')); - while (--width > 0) { - ret.push(32); - } - if (!flagLeftAlign) ret.push(getNextArg('i8')); - break; - } - case 'n': - { - // Write the length written so far to the next parameter. - var ptr = getNextArg('i32*'); - HEAP32[((ptr) >> 2)] = ret.length; - break; - } - case '%': - { - // Literal percent sign. - ret.push(curr); - break; - } - default: - { - // Unknown specifiers remain untouched. - for (var i = startTextIndex; i < textIndex + 2; i++) { - ret.push(HEAP8[(i)]); - } - } - } - textIndex += 2; - // TODO: Support a/A (hex float) and m (last error) specifiers. - // TODO: Support %1${specifier} for arg selection. - } else { - ret.push(curr); - textIndex += 1; - } - } - return ret; -} - -function _fprintf(stream, format, varargs) { - // int fprintf(FILE *restrict stream, const char *restrict format, ...); - // http://pubs.opengroup.org/onlinepubs/000095399/functions/printf.html - var result = __formatString(format, varargs); - var stack = Runtime.stackSave(); - var ret = _fwrite(allocate(result, 'i8', ALLOC_STACK), 1, result.length, stream); - Runtime.stackRestore(stack); - return ret; -} - -function _printf(format, varargs) { - // int printf(const char *restrict format, ...); - // http://pubs.opengroup.org/onlinepubs/000095399/functions/printf.html - var stdout = HEAP32[((_stdout) >> 2)]; - return _fprintf(stdout, format, varargs); -} - -var _sqrtf = Math_sqrt; - -var _fabsf = Math_abs; - - -function _fputs(s, stream) { - // int fputs(const char *restrict s, FILE *restrict stream); - // http://pubs.opengroup.org/onlinepubs/000095399/functions/fputs.html - return _write(stream, s, _strlen(s)); -} - -function _fputc(c, stream) { - // int fputc(int c, FILE *stream); - // http://pubs.opengroup.org/onlinepubs/000095399/functions/fputc.html - var chr = unSign(c & 0xFF); - HEAP8[((_fputc.ret) | 0)] = chr; - var ret = _write(stream, _fputc.ret, 1); - if (ret == -1) { - var streamObj = FS.getStream(stream); - if (streamObj) streamObj.error = true; - return -1; - } else { - return chr; - } -} - -function _puts(s) { - // int puts(const char *s); - // http://pubs.opengroup.org/onlinepubs/000095399/functions/puts.html - // NOTE: puts() always writes an extra newline. - var stdout = HEAP32[((_stdout) >> 2)]; - var ret = _fputs(s, stdout); - if (ret < 0) { - return ret; - } else { - var newlineRet = _fputc(10, stdout); - return (newlineRet < 0) ? -1 : ret + 1; - } -} - -function _abort() { - Module['abort'](); -} - -function ___errno_location() { - return ___errno_state; -} - -function _sbrk(bytes) { - // Implement a Linux-like 'memory area' for our 'process'. - // Changes the size of the memory area by |bytes|; returns the - // address of the previous top ('break') of the memory area - // We control the "dynamic" memory - DYNAMIC_BASE to DYNAMICTOP - var self = _sbrk; - if (!self.called) { - DYNAMICTOP = alignMemoryPage(DYNAMICTOP); // make sure we start out aligned - self.called = true; - assert(Runtime.dynamicAlloc); - self.alloc = Runtime.dynamicAlloc; - Runtime.dynamicAlloc = function() { - abort('cannot dynamically allocate, sbrk now has control') - }; - } - var ret = DYNAMICTOP; - if (bytes != 0) self.alloc(bytes); - return ret; // Previous break location. -} - -function _sysconf(name) { - // long sysconf(int name); - // http://pubs.opengroup.org/onlinepubs/009695399/functions/sysconf.html - switch (name) { - case 30: - return PAGE_SIZE; - case 132: - case 133: - case 12: - case 137: - case 138: - case 15: - case 235: - case 16: - case 17: - case 18: - case 19: - case 20: - case 149: - case 13: - case 10: - case 236: - case 153: - case 9: - case 21: - case 22: - case 159: - case 154: - case 14: - case 77: - case 78: - case 139: - case 80: - case 81: - case 79: - case 82: - case 68: - case 67: - case 164: - case 11: - case 29: - case 47: - case 48: - case 95: - case 52: - case 51: - case 46: - return 200809; - case 27: - case 246: - case 127: - case 128: - case 23: - case 24: - case 160: - case 161: - case 181: - case 182: - case 242: - case 183: - case 184: - case 243: - case 244: - case 245: - case 165: - case 178: - case 179: - case 49: - case 50: - case 168: - case 169: - case 175: - case 170: - case 171: - case 172: - case 97: - case 76: - case 32: - case 173: - case 35: - return -1; - case 176: - case 177: - case 7: - case 155: - case 8: - case 157: - case 125: - case 126: - case 92: - case 93: - case 129: - case 130: - case 131: - case 94: - case 91: - return 1; - case 74: - case 60: - case 69: - case 70: - case 4: - return 1024; - case 31: - case 42: - case 72: - return 32; - case 87: - case 26: - case 33: - return 2147483647; - case 34: - case 1: - return 47839; - case 38: - case 36: - return 99; - case 43: - case 37: - return 2048; - case 0: - return 2097152; - case 3: - return 65536; - case 28: - return 32768; - case 44: - return 32767; - case 75: - return 16384; - case 39: - return 1000; - case 89: - return 700; - case 71: - return 256; - case 40: - return 255; - case 2: - return 100; - case 180: - return 64; - case 25: - return 20; - case 5: - return 16; - case 6: - return 6; - case 73: - return 4; - case 84: - return 1; - } - ___setErrNo(ERRNO_CODES.EINVAL); - return -1; -} - -function _time(ptr) { - var ret = Math.floor(Date.now() / 1000); - if (ptr) { - HEAP32[((ptr) >> 2)] = ret; - } - return ret; -} - - - -Module["_memset"] = _memset; - - - - -var Browser = { - mainLoop: { - scheduler: null, - shouldPause: false, - paused: false, - queue: [], - pause: function() { - Browser.mainLoop.shouldPause = true; - }, - resume: function() { - if (Browser.mainLoop.paused) { - Browser.mainLoop.paused = false; - Browser.mainLoop.scheduler(); - } - Browser.mainLoop.shouldPause = false; - }, - updateStatus: function() { - if (Module['setStatus']) { - var message = Module['statusMessage'] || 'Please wait...'; - var remaining = Browser.mainLoop.remainingBlockers; - var expected = Browser.mainLoop.expectedBlockers; - if (remaining) { - if (remaining < expected) { - Module['setStatus'](message + ' (' + (expected - remaining) + '/' + expected + ')'); - } else { - Module['setStatus'](message); - } - } else { - Module['setStatus'](''); - } - } - } - }, - isFullScreen: false, - pointerLock: false, - moduleContextCreatedCallbacks: [], - workers: [], - init: function() { - if (!Module["preloadPlugins"]) Module["preloadPlugins"] = []; // needs to exist even in workers - - if (Browser.initted || ENVIRONMENT_IS_WORKER) return; - Browser.initted = true; - - try { - new Blob(); - Browser.hasBlobConstructor = true; - } catch (e) { - Browser.hasBlobConstructor = false; - console.log("warning: no blob constructor, cannot create blobs with mimetypes"); - } - Browser.BlobBuilder = typeof MozBlobBuilder != "undefined" ? MozBlobBuilder : (typeof WebKitBlobBuilder != "undefined" ? WebKitBlobBuilder : (!Browser.hasBlobConstructor ? console.log("warning: no BlobBuilder") : null)); - Browser.URLObject = typeof window != "undefined" ? (window.URL ? window.URL : window.webkitURL) : undefined; - if (!Module.noImageDecoding && typeof Browser.URLObject === 'undefined') { - console.log("warning: Browser does not support creating object URLs. Built-in browser image decoding will not be available."); - Module.noImageDecoding = true; - } - - // Support for plugins that can process preloaded files. You can add more of these to - // your app by creating and appending to Module.preloadPlugins. - // - // Each plugin is asked if it can handle a file based on the file's name. If it can, - // it is given the file's raw data. When it is done, it calls a callback with the file's - // (possibly modified) data. For example, a plugin might decompress a file, or it - // might create some side data structure for use later (like an Image element, etc.). - - var imagePlugin = {}; - imagePlugin['canHandle'] = function imagePlugin_canHandle(name) { - return !Module.noImageDecoding && /\.(jpg|jpeg|png|bmp)$/i.test(name); - }; - imagePlugin['handle'] = function imagePlugin_handle(byteArray, name, onload, onerror) { - var b = null; - if (Browser.hasBlobConstructor) { - try { - b = new Blob([byteArray], { - type: Browser.getMimetype(name) - }); - if (b.size !== byteArray.length) { // Safari bug #118630 - // Safari's Blob can only take an ArrayBuffer - b = new Blob([(new Uint8Array(byteArray)).buffer], { - type: Browser.getMimetype(name) - }); - } - } catch (e) { - Runtime.warnOnce('Blob constructor present but fails: ' + e + '; falling back to blob builder'); - } - } - if (!b) { - var bb = new Browser.BlobBuilder(); - bb.append((new Uint8Array(byteArray)).buffer); // we need to pass a buffer, and must copy the array to get the right data range - b = bb.getBlob(); - } - var url = Browser.URLObject.createObjectURL(b); - var img = new Image(); - img.onload = function img_onload() { - assert(img.complete, 'Image ' + name + ' could not be decoded'); - var canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - var ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0); - Module["preloadedImages"][name] = canvas; - Browser.URLObject.revokeObjectURL(url); - if (onload) onload(byteArray); - }; - img.onerror = function img_onerror(event) { - console.log('Image ' + url + ' could not be decoded'); - if (onerror) onerror(); - }; - img.src = url; - }; - Module['preloadPlugins'].push(imagePlugin); - - var audioPlugin = {}; - audioPlugin['canHandle'] = function audioPlugin_canHandle(name) { - return !Module.noAudioDecoding && name.substr(-4) in { - '.ogg': 1, - '.wav': 1, - '.mp3': 1 - }; - }; - audioPlugin['handle'] = function audioPlugin_handle(byteArray, name, onload, onerror) { - var done = false; - - function finish(audio) { - if (done) return; - done = true; - Module["preloadedAudios"][name] = audio; - if (onload) onload(byteArray); - } - - function fail() { - if (done) return; - done = true; - Module["preloadedAudios"][name] = new Audio(); // empty shim - if (onerror) onerror(); - } - if (Browser.hasBlobConstructor) { - try { - var b = new Blob([byteArray], { - type: Browser.getMimetype(name) - }); - } catch (e) { - return fail(); - } - var url = Browser.URLObject.createObjectURL(b); // XXX we never revoke this! - var audio = new Audio(); - audio.addEventListener('canplaythrough', function() { - finish(audio) - }, false); // use addEventListener due to chromium bug 124926 - audio.onerror = function audio_onerror(event) { - if (done) return; - console.log('warning: browser could not fully decode audio ' + name + ', trying slower base64 approach'); - - function encode64(data) { - var BASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; - var PAD = '='; - var ret = ''; - var leftchar = 0; - var leftbits = 0; - for (var i = 0; i < data.length; i++) { - leftchar = (leftchar << 8) | data[i]; - leftbits += 8; - while (leftbits >= 6) { - var curr = (leftchar >> (leftbits - 6)) & 0x3f; - leftbits -= 6; - ret += BASE[curr]; - } - } - if (leftbits == 2) { - ret += BASE[(leftchar & 3) << 4]; - ret += PAD + PAD; - } else if (leftbits == 4) { - ret += BASE[(leftchar & 0xf) << 2]; - ret += PAD; - } - return ret; - } - audio.src = 'data:audio/x-' + name.substr(-3) + ';base64,' + encode64(byteArray); - finish(audio); // we don't wait for confirmation this worked - but it's worth trying - }; - audio.src = url; - // workaround for chrome bug 124926 - we do not always get oncanplaythrough or onerror - Browser.safeSetTimeout(function() { - finish(audio); // try to use it even though it is not necessarily ready to play - }, 10000); - } else { - return fail(); - } - }; - Module['preloadPlugins'].push(audioPlugin); - - // Canvas event setup - - var canvas = Module['canvas']; - canvas.requestPointerLock = canvas['requestPointerLock'] || - canvas['mozRequestPointerLock'] || - canvas['webkitRequestPointerLock']; - canvas.exitPointerLock = document['exitPointerLock'] || - document['mozExitPointerLock'] || - document['webkitExitPointerLock'] || - function() {}; // no-op if function does not exist - canvas.exitPointerLock = canvas.exitPointerLock.bind(document); - - function pointerLockChange() { - Browser.pointerLock = document['pointerLockElement'] === canvas || - document['mozPointerLockElement'] === canvas || - document['webkitPointerLockElement'] === canvas; - } - - document.addEventListener('pointerlockchange', pointerLockChange, false); - document.addEventListener('mozpointerlockchange', pointerLockChange, false); - document.addEventListener('webkitpointerlockchange', pointerLockChange, false); - - if (Module['elementPointerLock']) { - canvas.addEventListener("click", function(ev) { - if (!Browser.pointerLock && canvas.requestPointerLock) { - canvas.requestPointerLock(); - ev.preventDefault(); - } - }, false); - } - }, - createContext: function(canvas, useWebGL, setInModule, webGLContextAttributes) { - var ctx; - try { - if (useWebGL) { - var contextAttributes = { - antialias: false, - alpha: false - }; - - if (webGLContextAttributes) { - for (var attribute in webGLContextAttributes) { - contextAttributes[attribute] = webGLContextAttributes[attribute]; - } - } - - - var errorInfo = '?'; - - function onContextCreationError(event) { - errorInfo = event.statusMessage || errorInfo; - } - canvas.addEventListener('webglcontextcreationerror', onContextCreationError, false); - try { - ['experimental-webgl', 'webgl'].some(function(webglId) { - return ctx = canvas.getContext(webglId, contextAttributes); - }); - } finally { - canvas.removeEventListener('webglcontextcreationerror', onContextCreationError, false); - } - } else { - ctx = canvas.getContext('2d'); - } - if (!ctx) throw ':('; - } catch (e) { - Module.print('Could not create canvas: ' + [errorInfo, e]); - return null; - } - if (useWebGL) { - // Set the background of the WebGL canvas to black - canvas.style.backgroundColor = "black"; - - // Warn on context loss - canvas.addEventListener('webglcontextlost', function(event) { - alert('WebGL context lost. You will need to reload the page.'); - }, false); - } - if (setInModule) { - GLctx = Module.ctx = ctx; - Module.useWebGL = useWebGL; - Browser.moduleContextCreatedCallbacks.forEach(function(callback) { - callback() - }); - Browser.init(); - } - return ctx; - }, - destroyContext: function(canvas, useWebGL, setInModule) {}, - fullScreenHandlersInstalled: false, - lockPointer: undefined, - resizeCanvas: undefined, - requestFullScreen: function(lockPointer, resizeCanvas) { - Browser.lockPointer = lockPointer; - Browser.resizeCanvas = resizeCanvas; - if (typeof Browser.lockPointer === 'undefined') Browser.lockPointer = true; - if (typeof Browser.resizeCanvas === 'undefined') Browser.resizeCanvas = false; - - var canvas = Module['canvas']; - - function fullScreenChange() { - Browser.isFullScreen = false; - if ((document['webkitFullScreenElement'] || document['webkitFullscreenElement'] || - document['mozFullScreenElement'] || document['mozFullscreenElement'] || - document['fullScreenElement'] || document['fullscreenElement']) === canvas) { - canvas.cancelFullScreen = document['cancelFullScreen'] || - document['mozCancelFullScreen'] || - document['webkitCancelFullScreen']; - canvas.cancelFullScreen = canvas.cancelFullScreen.bind(document); - if (Browser.lockPointer) canvas.requestPointerLock(); - Browser.isFullScreen = true; - if (Browser.resizeCanvas) Browser.setFullScreenCanvasSize(); - } else if (Browser.resizeCanvas) { - Browser.setWindowedCanvasSize(); - } - if (Module['onFullScreen']) Module['onFullScreen'](Browser.isFullScreen); - } - - if (!Browser.fullScreenHandlersInstalled) { - Browser.fullScreenHandlersInstalled = true; - document.addEventListener('fullscreenchange', fullScreenChange, false); - document.addEventListener('mozfullscreenchange', fullScreenChange, false); - document.addEventListener('webkitfullscreenchange', fullScreenChange, false); - } - - canvas.requestFullScreen = canvas['requestFullScreen'] || - canvas['mozRequestFullScreen'] || - (canvas['webkitRequestFullScreen'] ? function() { - canvas['webkitRequestFullScreen'](Element['ALLOW_KEYBOARD_INPUT']) - } : null); - canvas.requestFullScreen(); - }, - requestAnimationFrame: function requestAnimationFrame(func) { - if (typeof window === 'undefined') { // Provide fallback to setTimeout if window is undefined (e.g. in Node.js) - setTimeout(func, 1000 / 60); - } else { - if (!window.requestAnimationFrame) { - window.requestAnimationFrame = window['requestAnimationFrame'] || - window['mozRequestAnimationFrame'] || - window['webkitRequestAnimationFrame'] || - window['msRequestAnimationFrame'] || - window['oRequestAnimationFrame'] || - window['setTimeout']; - } - window.requestAnimationFrame(func); - } - }, - safeCallback: function(func) { - return function() { - if (!ABORT) return func.apply(null, arguments); - }; - }, - safeRequestAnimationFrame: function(func) { - return Browser.requestAnimationFrame(function() { - if (!ABORT) func(); - }); - }, - safeSetTimeout: function(func, timeout) { - return setTimeout(function() { - if (!ABORT) func(); - }, timeout); - }, - safeSetInterval: function(func, timeout) { - return setInterval(function() { - if (!ABORT) func(); - }, timeout); - }, - getMimetype: function(name) { - return { - 'jpg': 'image/jpeg', - 'jpeg': 'image/jpeg', - 'png': 'image/png', - 'bmp': 'image/bmp', - 'ogg': 'audio/ogg', - 'wav': 'audio/wav', - 'mp3': 'audio/mpeg' - }[name.substr(name.lastIndexOf('.') + 1)]; - }, - getUserMedia: function(func) { - if (!window.getUserMedia) { - window.getUserMedia = navigator['getUserMedia'] || - navigator['mozGetUserMedia']; - } - window.getUserMedia(func); - }, - getMovementX: function(event) { - return event['movementX'] || - event['mozMovementX'] || - event['webkitMovementX'] || - 0; - }, - getMovementY: function(event) { - return event['movementY'] || - event['mozMovementY'] || - event['webkitMovementY'] || - 0; - }, - mouseX: 0, - mouseY: 0, - mouseMovementX: 0, - mouseMovementY: 0, - calculateMouseEvent: function(event) { // event should be mousemove, mousedown or mouseup - if (Browser.pointerLock) { - // When the pointer is locked, calculate the coordinates - // based on the movement of the mouse. - // Workaround for Firefox bug 764498 - if (event.type != 'mousemove' && - ('mozMovementX' in event)) { - Browser.mouseMovementX = Browser.mouseMovementY = 0; - } else { - Browser.mouseMovementX = Browser.getMovementX(event); - Browser.mouseMovementY = Browser.getMovementY(event); - } - - // check if SDL is available - if (typeof SDL != "undefined") { - Browser.mouseX = SDL.mouseX + Browser.mouseMovementX; - Browser.mouseY = SDL.mouseY + Browser.mouseMovementY; - } else { - // just add the mouse delta to the current absolut mouse position - // FIXME: ideally this should be clamped against the canvas size and zero - Browser.mouseX += Browser.mouseMovementX; - Browser.mouseY += Browser.mouseMovementY; - } - } else { - // Otherwise, calculate the movement based on the changes - // in the coordinates. - var rect = Module["canvas"].getBoundingClientRect(); - var x, y; - - // Neither .scrollX or .pageXOffset are defined in a spec, but - // we prefer .scrollX because it is currently in a spec draft. - // (see: http://www.w3.org/TR/2013/WD-cssom-view-20131217/) - var scrollX = ((typeof window.scrollX !== 'undefined') ? window.scrollX : window.pageXOffset); - var scrollY = ((typeof window.scrollY !== 'undefined') ? window.scrollY : window.pageYOffset); - if (event.type == 'touchstart' || - event.type == 'touchend' || - event.type == 'touchmove') { - var t = event.touches.item(0); - if (t) { - x = t.pageX - (scrollX + rect.left); - y = t.pageY - (scrollY + rect.top); - } else { - return; - } - } else { - x = event.pageX - (scrollX + rect.left); - y = event.pageY - (scrollY + rect.top); - } - - // the canvas might be CSS-scaled compared to its backbuffer; - // SDL-using content will want mouse coordinates in terms - // of backbuffer units. - var cw = Module["canvas"].width; - var ch = Module["canvas"].height; - x = x * (cw / rect.width); - y = y * (ch / rect.height); - - Browser.mouseMovementX = x - Browser.mouseX; - Browser.mouseMovementY = y - Browser.mouseY; - Browser.mouseX = x; - Browser.mouseY = y; - } - }, - xhrLoad: function(url, onload, onerror) { - var xhr = new XMLHttpRequest(); - xhr.open('GET', url, true); - xhr.responseType = 'arraybuffer'; - xhr.onload = function xhr_onload() { - if (xhr.status == 200 || (xhr.status == 0 && xhr.response)) { // file URLs can return 0 - onload(xhr.response); - } else { - onerror(); - } - }; - xhr.onerror = onerror; - xhr.send(null); - }, - asyncLoad: function(url, onload, onerror, noRunDep) { - Browser.xhrLoad(url, function(arrayBuffer) { - assert(arrayBuffer, 'Loading data file "' + url + '" failed (no arrayBuffer).'); - onload(new Uint8Array(arrayBuffer)); - if (!noRunDep) removeRunDependency('al ' + url); - }, function(event) { - if (onerror) { - onerror(); - } else { - throw 'Loading data file "' + url + '" failed.'; - } - }); - if (!noRunDep) addRunDependency('al ' + url); - }, - resizeListeners: [], - updateResizeListeners: function() { - var canvas = Module['canvas']; - Browser.resizeListeners.forEach(function(listener) { - listener(canvas.width, canvas.height); - }); - }, - setCanvasSize: function(width, height, noUpdates) { - var canvas = Module['canvas']; - canvas.width = width; - canvas.height = height; - if (!noUpdates) Browser.updateResizeListeners(); - }, - windowedWidth: 0, - windowedHeight: 0, - setFullScreenCanvasSize: function() { - var canvas = Module['canvas']; - this.windowedWidth = canvas.width; - this.windowedHeight = canvas.height; - canvas.width = screen.width; - canvas.height = screen.height; - // check if SDL is available - if (typeof SDL != "undefined") { - var flags = HEAPU32[((SDL.screen + Runtime.QUANTUM_SIZE * 0) >> 2)]; - flags = flags | 0x00800000; // set SDL_FULLSCREEN flag - HEAP32[((SDL.screen + Runtime.QUANTUM_SIZE * 0) >> 2)] = flags - } - Browser.updateResizeListeners(); - }, - setWindowedCanvasSize: function() { - var canvas = Module['canvas']; - canvas.width = this.windowedWidth; - canvas.height = this.windowedHeight; - // check if SDL is available - if (typeof SDL != "undefined") { - var flags = HEAPU32[((SDL.screen + Runtime.QUANTUM_SIZE * 0) >> 2)]; - flags = flags & ~0x00800000; // clear SDL_FULLSCREEN flag - HEAP32[((SDL.screen + Runtime.QUANTUM_SIZE * 0) >> 2)] = flags - } - Browser.updateResizeListeners(); - } -}; -FS.staticInit(); -__ATINIT__.unshift({ - func: function() { - if (!Module["noFSInit"] && !FS.init.initialized) FS.init() - } -}); -__ATMAIN__.push({ - func: function() { - FS.ignorePermissions = false - } -}); -__ATEXIT__.push({ - func: function() { - FS.quit() - } -}); -Module["FS_createFolder"] = FS.createFolder; -Module["FS_createPath"] = FS.createPath; -Module["FS_createDataFile"] = FS.createDataFile; -Module["FS_createPreloadedFile"] = FS.createPreloadedFile; -Module["FS_createLazyFile"] = FS.createLazyFile; -Module["FS_createLink"] = FS.createLink; -Module["FS_createDevice"] = FS.createDevice; -___errno_state = Runtime.staticAlloc(4); -HEAP32[((___errno_state) >> 2)] = 0; -__ATINIT__.unshift({ - func: function() { - TTY.init() - } -}); -__ATEXIT__.push({ - func: function() { - TTY.shutdown() - } -}); -TTY.utf8 = new Runtime.UTF8Processor(); -if (ENVIRONMENT_IS_NODE) { - var fs = require("fs"); - NODEFS.staticInit(); -} -__ATINIT__.push({ - func: function() { - SOCKFS.root = FS.mount(SOCKFS, {}, null); - } -}); -_fputc.ret = allocate([0], "i8", ALLOC_STATIC); -Module["requestFullScreen"] = function Module_requestFullScreen(lockPointer, resizeCanvas) { - Browser.requestFullScreen(lockPointer, resizeCanvas) -}; -Module["requestAnimationFrame"] = function Module_requestAnimationFrame(func) { - Browser.requestAnimationFrame(func) -}; -Module["setCanvasSize"] = function Module_setCanvasSize(width, height, noUpdates) { - Browser.setCanvasSize(width, height, noUpdates) -}; -Module["pauseMainLoop"] = function Module_pauseMainLoop() { - Browser.mainLoop.pause() -}; -Module["resumeMainLoop"] = function Module_resumeMainLoop() { - Browser.mainLoop.resume() -}; -Module["getUserMedia"] = function Module_getUserMedia() { - Browser.getUserMedia() -} -STACK_BASE = STACKTOP = Runtime.alignMemory(STATICTOP); - -staticSealed = true; // seal the static portion of memory - -STACK_MAX = STACK_BASE + 5242880; - -DYNAMIC_BASE = DYNAMICTOP = Runtime.alignMemory(STACK_MAX); - -assert(DYNAMIC_BASE < TOTAL_MEMORY, "TOTAL_MEMORY not big enough for stack"); - - -var Math_min = Math.min; - -function invoke_vi(index, a1) { - try { - Module["dynCall_vi"](index, a1); - } catch (e) { - if (typeof e !== 'number' && e !== 'longjmp') throw e; - asm["setThrew"](1, 0); - } -} - -function invoke_vii(index, a1, a2) { - try { - Module["dynCall_vii"](index, a1, a2); - } catch (e) { - if (typeof e !== 'number' && e !== 'longjmp') throw e; - asm["setThrew"](1, 0); - } -} - -function invoke_ii(index, a1) { - try { - return Module["dynCall_ii"](index, a1); - } catch (e) { - if (typeof e !== 'number' && e !== 'longjmp') throw e; - asm["setThrew"](1, 0); - } -} - -function invoke_ff(index, a1) { - try { - return Module["dynCall_ff"](index, a1); - } catch (e) { - if (typeof e !== 'number' && e !== 'longjmp') throw e; - asm["setThrew"](1, 0); - } -} - -function invoke_v(index) { - try { - Module["dynCall_v"](index); - } catch (e) { - if (typeof e !== 'number' && e !== 'longjmp') throw e; - asm["setThrew"](1, 0); - } -} - -function invoke_iii(index, a1, a2) { - try { - return Module["dynCall_iii"](index, a1, a2); - } catch (e) { - if (typeof e !== 'number' && e !== 'longjmp') throw e; - asm["setThrew"](1, 0); - } -} - -function asmPrintInt(x, y) { - Module.print('int ' + x + ',' + y); // + ' ' + new Error().stack); -} - -function asmPrintFloat(x, y) { - Module.print('float ' + x + ',' + y); // + ' ' + new Error().stack); - } - // EMSCRIPTEN_START_ASM -var asm = (function(global, env, buffer) { - "use asm"; - var a = new global.Int8Array(buffer); - var b = new global.Int16Array(buffer); - var c = new global.Int32Array(buffer); - var d = new global.Uint8Array(buffer); - var e = new global.Uint16Array(buffer); - var f = new global.Uint32Array(buffer); - var g = new global.Float32Array(buffer); - var h = new global.Float64Array(buffer); - var i = env.STACKTOP | 0; - var j = env.STACK_MAX | 0; - var k = env.tempDoublePtr | 0; - var l = env.ABORT | 0; - var m = +env.NaN; - var n = +env.Infinity; - var o = 0; - var p = 0; - var q = 0; - var r = 0; - var s = 0, - t = 0, - u = 0, - v = 0, - w = 0.0, - x = 0, - y = 0, - z = 0, - A = 0.0; - var B = 0; - var C = 0; - var D = 0; - var E = 0; - var F = 0; - var G = 0; - var H = 0; - var I = 0; - var J = 0; - var K = 0; - var L = global.Math.floor; - var M = global.Math.abs; - var N = global.Math.sqrt; - var O = global.Math.pow; - var P = global.Math.cos; - var Q = global.Math.sin; - var R = global.Math.tan; - var S = global.Math.acos; - var T = global.Math.asin; - var U = global.Math.atan; - var V = global.Math.atan2; - var W = global.Math.exp; - var X = global.Math.log; - var Y = global.Math.ceil; - var Z = global.Math.imul; - var _ = env.abort; - var $ = env.assert; - var aa = env.asmPrintInt; - var ba = env.asmPrintFloat; - var ca = env.min; - var da = env.invoke_vi; - var ea = env.invoke_vii; - var fa = env.invoke_ii; - var ga = env.invoke_ff; - var ha = env.invoke_v; - var ia = env.invoke_iii; - var ja = env._strncmp; - var ka = env._fabsf; - var la = env._sysconf; - var ma = env._abort; - var na = env._fprintf; - var oa = env._printf; - var pa = env._fflush; - var qa = env.__reallyNegative; - var ra = env._sqrtf; - var sa = env._fputc; - var ta = env._fabs; - var ua = env.___setErrNo; - var va = env._fwrite; - var wa = env._send; - var xa = env._write; - var ya = env._fputs; - var za = env._log10; - var Aa = env._sin; - var Ba = env._ceilf; - var Ca = env.__formatString; - var Da = env._cos; - var Ea = env._pwrite; - var Fa = env._puts; - var Ga = env._sbrk; - var Ha = env.___errno_location; - var Ia = env._atan2; - var Ja = env._time; - var Ka = env._strcmp; - var La = 0.0; - // EMSCRIPTEN_START_FUNCS - function Sa(a) { - a = a | 0; - var b = 0; - b = i; - i = i + a | 0; - i = i + 7 & -8; - return b | 0 - } - - function Ta() { - return i | 0 - } - - function Ua(a) { - a = a | 0; - i = a - } - - function Va(a, b) { - a = a | 0; - b = b | 0; - if ((o | 0) == 0) { - o = a; - p = b - } - } - - function Wa(b) { - b = b | 0; - a[k] = a[b]; - a[k + 1 | 0] = a[b + 1 | 0]; - a[k + 2 | 0] = a[b + 2 | 0]; - a[k + 3 | 0] = a[b + 3 | 0] - } - - function Xa(b) { - b = b | 0; - a[k] = a[b]; - a[k + 1 | 0] = a[b + 1 | 0]; - a[k + 2 | 0] = a[b + 2 | 0]; - a[k + 3 | 0] = a[b + 3 | 0]; - a[k + 4 | 0] = a[b + 4 | 0]; - a[k + 5 | 0] = a[b + 5 | 0]; - a[k + 6 | 0] = a[b + 6 | 0]; - a[k + 7 | 0] = a[b + 7 | 0] - } - - function Ya(a) { - a = a | 0; - B = a - } - - function Za(a) { - a = a | 0; - C = a - } - - function _a(a) { - a = a | 0; - D = a - } - - function $a(a) { - a = a | 0; - E = a - } - - function ab(a) { - a = a | 0; - F = a - } - - function bb(a) { - a = a | 0; - G = a - } - - function cb(a) { - a = a | 0; - H = a - } - - function db(a) { - a = a | 0; - I = a - } - - function eb(a) { - a = a | 0; - J = a - } - - function fb(a) { - a = a | 0; - K = a - } - - function gb() {} - - function hb(a) { - a = a | 0; - ac(c[a + 12 >> 2] | 0); - return - } - - function ib(a) { - a = a | 0; - if ((Ka(2160, a | 0) | 0) == 0) { - return 0 - } else { - a = (Ka(2144, a | 0) | 0) == 0; - return (a ? 1 : 2) | 0 - } - return 0 - } - - function jb(a) { - a = a | 0; - if ((a | 0) == 1) { - a = 2144 - } else if ((a | 0) == 2) { - a = 2136 - } else if ((a | 0) == 0) { - a = 2160 - } else { - a = 2128 - } - return a | 0 - } - - function kb(a) { - a = +a; - a = a * .5 + .5; - return +(.42 - +P(a * 6.2831854820251465) * .5 + +P(a * 12.566370964050293) * .08) - } - - function lb(a) { - a = +a; - return +(.54 - +P((a * .5 + .5) * 6.2831854820251465) * .46) - } - - function mb(a) { - a = +a; - return +1.0 - } - - function nb(a, b, c, d) { - a = a | 0; - b = b | 0; - c = +c; - d = d | 0; - var e = 0, - f = 0.0, - h = 0, - i = 0.0, - j = 0.0; - e = (b | 0) / 2 | 0; - if ((d | 0) == 1) { - d = 6 - } else if ((d | 0) == 0) { - d = 2 - } else { - d = 4 - } - c = c * 6.2831854820251465; - g[a + (e << 2) >> 2] = c * +Pa[d & 7](0.0); - if ((b | 0) >= 2) { - f = +(e | 0); - h = 1; - do { - i = +(h | 0); - j = +Q(c * i) / +(h | 0); - i = j * +Pa[d & 7](i / f); - g[a + (h + e << 2) >> 2] = i; - g[a + (e - h << 2) >> 2] = i; - h = h + 1 | 0; - } while ((e | 0) >= (h | 0)) - } - e = (b | 0) > 0; - if (e) { - c = 0.0; - d = 0 - } else { - return - } - do { - c = c + +g[a + (d << 2) >> 2]; - d = d + 1 | 0; - } while ((d | 0) < (b | 0)); - if (e) { - e = 0 - } else { - return - } - do { - h = a + (e << 2) | 0; - g[h >> 2] = +g[h >> 2] / c; - e = e + 1 | 0; - } while ((e | 0) < (b | 0)); - return - } - - function ob(a, b, c, d, e) { - a = a | 0; - b = b | 0; - c = +c; - d = +d; - e = e | 0; - var f = 0, - h = 0, - i = 0.0, - j = 0.0, - k = 0, - l = 0.0, - m = 0.0; - f = bc(b << 2) | 0; - i = (d - c) * .5; - h = (b | 0) / 2 | 0; - if ((e | 0) == 0) { - e = 2 - } else if ((e | 0) == 1) { - e = 6 - } else { - e = 4 - } - j = i * 6.2831854820251465; - g[f + (h << 2) >> 2] = j * +Pa[e & 7](0.0); - if ((b | 0) >= 2) { - i = +(h | 0); - k = 1; - do { - l = +(k | 0); - m = +Q(j * l) / +(k | 0); - l = m * +Pa[e & 7](l / i); - g[f + (k + h << 2) >> 2] = l; - g[f + (h - k << 2) >> 2] = l; - k = k + 1 | 0; - } while ((h | 0) >= (k | 0)) - } - h = (b | 0) > 0; - if (h) { - i = 0.0; - e = 0 - } else { - return - } - while (1) { - i = i + +g[f + (e << 2) >> 2]; - e = e + 1 | 0; - if ((e | 0) >= (b | 0)) { - e = 0; - break - } - } - do { - k = f + (e << 2) | 0; - g[k >> 2] = +g[k >> 2] / i; - e = e + 1 | 0; - } while ((e | 0) < (b | 0)); - if (!h) { - return - } - c = (c + d) * .5 * 6.2831854820251465; - j = 0.0; - h = 0; - do { - d = j; - i = +P(d); - d = +Q(d); - j = c + j; - if (j > 6.2831854820251465) { - do { - j = j + -6.2831854820251465; - } while (j > 6.2831854820251465) - } - if (j < 0.0) { - do { - j = j + 6.2831854820251465; - } while (j < 0.0) - } - m = +g[f + (h << 2) >> 2]; - k = a + (h << 3) | 0; - g[k >> 2] = i * m; - g[k + 4 >> 2] = d * m; - h = h + 1 | 0; - } while ((h | 0) < (b | 0)); - return - } - - function pb(a) { - a = +a; - var b = 0; - b = ~~(4.0 / a); - return (b & 1 ^ 1) + b | 0 - } - - function qb(a, b, c, d, e) { - a = a | 0; - b = b | 0; - c = c | 0; - d = +d; - e = +e; - var f = 0, - h = 0, - i = 0, - j = 0.0, - k = 0, - l = 0.0; - d = d * 2.0 * 3.1415927410125732; - if ((c | 0) > 0) { - f = 0 - } else { - return +e - } - do { - l = e; - j = +P(l); - l = +Q(l); - k = a + (f << 3) | 0; - i = k + 4 | 0; - h = b + (f << 3) | 0; - g[h >> 2] = j * +g[k >> 2] - l * +g[i >> 2]; - g[h + 4 >> 2] = l * +g[k >> 2] + j * +g[i >> 2]; - e = d + e; - if (e > 6.2831854820251465) { - do { - e = e + -6.2831854820251465; - } while (e > 6.2831854820251465) - } - if (e < 0.0) { - do { - e = e + 6.2831854820251465; - } while (e < 0.0) - } - f = f + 1 | 0; - } while ((f | 0) < (c | 0)); - return +e - } - - function rb(a, b) { - a = a | 0; - b = b | 0; - var d = 0, - e = 0.0, - f = 0; - d = bc(b << 2) | 0; - if ((b | 0) > 0) { - e = +(b | 0); - f = 0; - do { - g[d + (f << 2) >> 2] = +Q(+(f | 0) / e * 1.5707963705062866); - f = f + 1 | 0; - } while ((f | 0) < (b | 0)) - } - c[a >> 2] = d; - c[a + 4 >> 2] = b; - return - } - - function sb(a) { - a = a | 0; - var b = 0, - d = 0; - b = i; - d = a; - a = i; - i = i + 8 | 0; - c[a >> 2] = c[d >> 2]; - c[a + 4 >> 2] = c[d + 4 >> 2]; - cc(c[a >> 2] | 0); - i = b; - return - } - - function tb(a, b, d, e, f, h) { - a = a | 0; - b = b | 0; - d = d | 0; - e = +e; - f = f | 0; - h = +h; - var j = 0, - k = 0.0, - l = 0, - m = 0, - n = 0.0, - o = 0, - p = 0, - q = 0, - r = 0.0, - s = 0; - j = i; - s = f; - f = i; - i = i + 8 | 0; - c[f >> 2] = c[s >> 2]; - c[f + 4 >> 2] = c[s + 4 >> 2]; - e = e * 2.0 * 3.1415927410125732; - if ((d | 0) <= 0) { - r = h; - i = j; - return +r - } - l = c[f + 4 >> 2] | 0; - k = +(l | 0); - l = l - 1 | 0; - f = c[f >> 2] | 0; - m = 0; - do { - s = ~~(h / 1.5707963705062866); - o = ~~((h - +(s | 0) * 1.5707963705062866) / 1.5707963705062866 * k); - p = l - o | 0; - q = (s & 1 | 0) == 0; - if ((s | 0) == 0) { - n = 1.0 - } else { - n = (s | 0) < 3 ? -1.0 : 1.0 - } - r = ((s | 0) > 1 ? -1.0 : 1.0) * +g[f + ((q ? o : p) << 2) >> 2]; - n = n * +g[f + ((q ? p : o) << 2) >> 2]; - p = a + (m << 3) | 0; - q = p + 4 | 0; - s = b + (m << 3) | 0; - g[s >> 2] = n * +g[p >> 2] - r * +g[q >> 2]; - g[s + 4 >> 2] = r * +g[p >> 2] + n * +g[q >> 2]; - h = e + h; - if (h > 6.2831854820251465) { - do { - h = h + -6.2831854820251465; - } while (h > 6.2831854820251465) - } - if (h < 0.0) { - do { - h = h + 6.2831854820251465; - } while (h < 0.0) - } - m = m + 1 | 0; - } while ((m | 0) < (d | 0)); - i = j; - return +h - } - - function ub(a, b, c, d, e, f) { - a = a | 0; - b = b | 0; - c = c | 0; - d = d | 0; - e = e | 0; - f = f | 0; - var h = 0, - i = 0, - j = 0.0, - k = 0.0, - l = 0; - if ((c | 0) < 1 | (f | 0) > (c | 0)) { - l = 0; - return l | 0 - } - if ((f | 0) > 0) { - i = 0; - h = 0 - } else { - i = 0; - a = 0; - do { - l = b + (i << 3) | 0; - g[l >> 2] = 0.0; - g[l + 4 >> 2] = 0.0; - i = i + 1 | 0; - a = a + d | 0; - } while (!((a | 0) >= (c | 0) | (a + f | 0) > (c | 0))); - return i | 0 - } - do { - j = 0.0; - l = 0; - while (1) { - j = j + +g[a + (l + h << 3) >> 2] * +g[e + (l << 2) >> 2]; - l = l + 1 | 0; - if ((l | 0) >= (f | 0)) { - k = 0.0; - l = 0; - break - } - } - do { - k = k + +g[a + (l + h << 3) + 4 >> 2] * +g[e + (l << 2) >> 2]; - l = l + 1 | 0; - } while ((l | 0) < (f | 0)); - l = b + (i << 3) | 0; - g[l >> 2] = j; - g[l + 4 >> 2] = k; - i = i + 1 | 0; - h = h + d | 0; - } while (!((h | 0) >= (c | 0) | (h + f | 0) > (c | 0))); - return i | 0 - } - - function vb(a, b, d, e, f, h, i, j, k) { - a = a | 0; - b = b | 0; - d = d | 0; - e = e | 0; - f = f | 0; - h = h | 0; - i = i | 0; - j = j | 0; - k = k | 0; - var l = 0, - m = 0, - n = 0.0, - o = 0, - p = 0, - q = 0, - r = 0, - s = 0, - t = 0, - u = 0.0; - q = (Z(f, e) | 0) / (h | 0) | 0; - a: do { - if ((q | 0) > 0) { - r = f - 1 - k | 0; - p = (j | 0) / (f | 0) | 0; - n = +(f | 0); - l = 0; - do { - m = Z(l, h) | 0; - o = (r + m | 0) / (f | 0) | 0; - m = (k - m + (Z(o, f) | 0) | 0) % (f | 0) | 0; - if ((p + o | 0) >= (e | 0)) { - break a - } - s = (j - m | 0) / (f | 0) | 0; - if ((s | 0) > 0) { - u = 0.0; - t = 0; - do { - u = u + +g[b + (t + o << 2) >> 2] * +g[i + ((Z(t, f) | 0) + m << 2) >> 2]; - t = t + 1 | 0; - } while ((t | 0) < (s | 0)) - } else { - u = 0.0 - } - g[d + (l << 2) >> 2] = n * u; - l = l + 1 | 0; - } while ((l | 0) < (q | 0)) - } else { - o = 0; - m = 0; - l = 0 - } - } while (0); - c[a >> 2] = o; - c[a + 4 >> 2] = l; - c[a + 8 >> 2] = m; - return - } - - function wb(a, b, c, d, e) { - a = a | 0; - b = b | 0; - c = c | 0; - d = d | 0; - e = e | 0; - var f = 0.0, - h = 0.0, - i = 0.0, - j = 0.0; - f = 1.0 / +(c | 0); - h = 1.0 / +(d | 0); - f = (f < h ? f : h) * .5; - d = (b | 0) / 2 | 0; - if ((e | 0) == 1) { - e = 6 - } else if ((e | 0) == 0) { - e = 2 - } else { - e = 4 - } - h = f * 6.2831854820251465; - g[a + (d << 2) >> 2] = h * +Pa[e & 7](0.0); - if ((b | 0) >= 2) { - f = +(d | 0); - c = 1; - do { - i = +(c | 0); - j = +Q(h * i) / +(c | 0); - i = j * +Pa[e & 7](i / f); - g[a + (c + d << 2) >> 2] = i; - g[a + (d - c << 2) >> 2] = i; - c = c + 1 | 0; - } while ((d | 0) >= (c | 0)) - } - if ((b | 0) > 0) { - f = 0.0; - d = 0 - } else { - return - } - while (1) { - f = f + +g[a + (d << 2) >> 2]; - d = d + 1 | 0; - if ((d | 0) >= (b | 0)) { - d = 0; - break - } - } - do { - c = a + (d << 2) | 0; - g[c >> 2] = +g[c >> 2] / f; - d = d + 1 | 0; - } while ((d | 0) < (b | 0)); - return - } - - function xb(a, b, d, e, f, h, j, k) { - a = a | 0; - b = b | 0; - d = d | 0; - e = e | 0; - f = +f; - h = h | 0; - j = j | 0; - k = k | 0; - var l = 0, - m = 0, - n = 0, - o = 0, - p = 0.0, - q = 0, - r = 0, - s = 0, - t = 0, - u = 0, - v = 0.0, - w = 0, - x = 0.0, - y = 0.0; - l = i; - w = k; - k = i; - i = i + 12 | 0; - i = i + 7 & -8; - c[k >> 2] = c[w >> 2]; - c[k + 4 >> 2] = c[w + 4 >> 2]; - c[k + 8 >> 2] = c[w + 8 >> 2]; - if (f <= 1.0) { - w = a; - u = k; - c[w >> 2] = c[u >> 2]; - c[w + 4 >> 2] = c[u + 4 >> 2]; - c[w + 8 >> 2] = c[u + 8 >> 2]; - i = l; - return - } - m = k | 0; - p = +g[m >> 2]; - if (p == 0.0) { - if ((j | 0) > 0) { - v = 0.0; - o = 0; - do { - v = v + +g[h + (o << 2) >> 2] * +g[b + (o << 2) >> 2]; - o = o + 1 | 0; - } while ((o | 0) < (j | 0)) - } else { - v = 0.0 - } - g[d >> 2] = v; - q = 1; - p = p + f - } else { - q = 0 - } - r = ~~+Y(p); - t = r - 1 | 0; - if ((r + j | 0) < (e | 0)) { - o = (j | 0) > 0; - v = 0.0; - s = -1; - while (1) { - do { - if ((s | 0) == (t | 0)) { - n = 12 - } else { - s = r - 1 | 0; - if (o) { - v = 0.0; - n = 0 - } else { - x = 0.0; - v = 0.0; - break - } - while (1) { - v = v + +g[h + (n << 2) >> 2] * +g[b + (s + n << 2) >> 2]; - n = n + 1 | 0; - if ((n | 0) >= (j | 0)) { - n = 12; - break - } - } - } - } while (0); - do { - if ((n | 0) == 12) { - n = 0; - if (o) { - x = 0.0; - s = 0 - } else { - x = 0.0; - break - } - do { - x = x + +g[h + (s << 2) >> 2] * +g[b + (s + r << 2) >> 2]; - s = s + 1 | 0; - } while ((s | 0) < (j | 0)) - } - } while (0); - y = p - +(r | 0) + 1.0; - u = q + 1 | 0; - g[d + (q << 2) >> 2] = (1.0 - y) * v + y * x; - p = p + f; - w = ~~+Y(p); - t = w - 1 | 0; - if ((w + j | 0) < (e | 0)) { - v = x; - s = r; - q = u; - r = w - } else { - q = u; - break - } - } - } - c[k + 4 >> 2] = t; - g[m >> 2] = p - +(t | 0); - c[k + 8 >> 2] = q; - w = a; - u = k; - c[w >> 2] = c[u >> 2]; - c[w + 4 >> 2] = c[u + 4 >> 2]; - c[w + 8 >> 2] = c[u + 8 >> 2]; - i = l; - return - } - - function yb(a, b, d, e, f) { - a = a | 0; - b = b | 0; - d = d | 0; - e = e | 0; - f = f | 0; - var h = 0, - i = 0, - j = 0, - k = 0, - l = 0, - m = 0, - n = 0, - o = 0, - p = 0; - hb(a); - h = a | 0; - if ((c[h >> 2] | 0) > 0) { - a = c[a + 8 >> 2] | 0; - i = c[b + 4 >> 2] | 0; - j = 0; - do { - p = j << 1; - o = a + (p << 2) | 0; - l = d + (j << 3) | 0; - k = p | 1; - m = a + (k << 2) | 0; - n = l + 4 | 0; - g[i + (p << 2) >> 2] = +g[o >> 2] * +g[l >> 2] - +g[m >> 2] * +g[n >> 2]; - g[i + (k << 2) >> 2] = +g[o >> 2] * +g[n >> 2] + +g[m >> 2] * +g[l >> 2]; - j = j + 1 | 0; - } while ((j | 0) < (c[h >> 2] | 0)) - } - hb(b); - d = c[b + 8 >> 2] | 0; - i = c[h >> 2] | 0; - if ((i | 0) > 0) { - a = d; - b = 0; - do { - p = b << 1; - o = a + (p << 2) | 0; - g[o >> 2] = +g[o >> 2] / +(i | 0); - p = a + ((p | 1) << 2) | 0; - g[p >> 2] = +g[p >> 2] / +(c[h >> 2] | 0); - b = b + 1 | 0; - i = c[h >> 2] | 0; - } while ((b | 0) < (i | 0)) - } - if ((f | 0) <= 0) { - return - } - h = 0; - do { - p = h << 1; - n = d + (p << 2) | 0; - o = e + (h << 3) | 0; - g[n >> 2] = +g[n >> 2] + +g[o >> 2]; - p = d + ((p | 1) << 2) | 0; - g[p >> 2] = +g[p >> 2] + +g[o + 4 >> 2]; - h = h + 1 | 0; - } while ((h | 0) < (f | 0)); - return - } - - function zb(a, b, c) { - a = a | 0; - b = b | 0; - c = c | 0; - var d = 0, - e = 0, - f = 0.0, - h = 0.0, - i = 0; - d = (c | 0) > 0; - if (d) { - e = 0 - } else { - return - } - do { - i = a + (e << 3) | 0; - h = +g[i >> 2]; - f = +g[i + 4 >> 2]; - g[b + (e << 2) >> 2] = h * h + f * f; - e = e + 1 | 0; - } while ((e | 0) < (c | 0)); - if (d) { - a = 0 - } else { - return - } - do { - i = b + (a << 2) | 0; - g[i >> 2] = +N(+g[i >> 2]); - a = a + 1 | 0; - } while ((a | 0) < (c | 0)); - return - } - - function Ab(a, b, c, d, e) { - a = a | 0; - b = b | 0; - c = c | 0; - d = +d; - e = +e; - var f = 0, - h = 0.0, - i = 0, - j = 0.0; - f = d == 0.0; - d = f ? .9475436210632324 : d; - e = f ? .39248543977737427 : e; - if ((c | 0) > 0) { - f = 0 - } else { - return - } - do { - i = a + (f << 3) | 0; - h = +g[i >> 2]; - if (h < 0.0) { - h = -0.0 - h - } - j = +g[i + 4 >> 2]; - if (j < 0.0) { - j = -0.0 - j - } - g[b + (f << 2) >> 2] = d * (j > h ? j : h) + e * (j < h ? j : h); - f = f + 1 | 0; - } while ((f | 0) < (c | 0)); - return - } - - function Bb(a, b, d, e, f, h) { - a = a | 0; - b = b | 0; - d = d | 0; - e = e | 0; - f = +f; - h = h | 0; - var j = 0, - k = 0, - l = 0, - m = 0, - n = 0.0; - j = i; - m = h; - h = i; - i = i + 8 | 0; - c[h >> 2] = c[m >> 2]; - c[h + 4 >> 2] = c[m + 4 >> 2]; - f = f == 0.0 ? .9990000128746033 : f; - k = h | 0; - l = h + 4 | 0; - n = +g[b >> 2] - +g[k >> 2] + f * +g[l >> 2]; - g[d >> 2] = n; - if ((e | 0) > 1) { - m = 1; - do { - n = +g[b + (m << 2) >> 2] - +g[b + (m - 1 << 2) >> 2] + f * n; - g[d + (m << 2) >> 2] = n; - m = m + 1 | 0; - } while ((m | 0) < (e | 0)) - } - m = e - 1 | 0; - g[k >> 2] = +g[b + (m << 2) >> 2]; - g[l >> 2] = +g[d + (m << 2) >> 2]; - k = h; - m = a; - l = c[k + 4 >> 2] | 0; - c[m >> 2] = c[k >> 2]; - c[m + 4 >> 2] = l; - i = j; - return - } - - function Cb(a, b, c, d) { - a = a | 0; - b = b | 0; - c = c | 0; - d = +d; - var e = 0.0, - f = 0, - h = 0.0, - i = 0.0, - j = 0; - f = (c | 0) > 0; - if (f) { - j = 0; - h = 0.0 - } else { - i = 0.0 / +(c | 0); - return +i - } - do { - h = h + +g[a + (j << 2) >> 2]; - j = j + 1 | 0; - } while ((j | 0) < (c | 0)); - e = +(c | 0); - h = h / e; - i = h - d; - if (f) { - f = 0 - } else { - i = h; - return +i - } - do { - g[b + (f << 2) >> 2] = +g[a + (f << 2) >> 2] - (i * (+(f | 0) / e) + d); - f = f + 1 | 0; - } while ((f | 0) < (c | 0)); - return +h - } - - function Db(a, b) { - a = a | 0; - b = b | 0; - var d = 0, - e = 0, - f = 0.0, - h = 0, - i = 0, - j = 0, - k = 0.0, - l = 0, - m = 0.0, - n = 0, - o = 0.0; - d = a + 20 | 0; - e = c[d >> 2] | 0; - j = (e | 0) > 0; - if (j) { - h = c[a + 8 >> 2] | 0; - i = 0; - f = 0.0; - do { - k = +M(+(+g[h + (i << 2) >> 2])); - f = k > f ? k : f; - i = i + 1 | 0; - } while ((i | 0) < (e | 0)) - } else { - f = 0.0 - } - i = a + 16 | 0; - m = +g[i >> 2]; - o = f < m ? m : f; - h = a + 12 | 0; - k = +g[h >> 2]; - k = +g[a + 24 >> 2] / (o < k ? k : o); - k = k > 50.0 ? 50.0 : k; - if (j) { - j = a + 28 | 0; - l = a | 0; - n = 0; - do { - o = +(n | 0) / +(e | 0); - g[b + (n << 2) >> 2] = +g[(c[l >> 2] | 0) + (n << 2) >> 2] * (k * o + +g[j >> 2] * (1.0 - o)); - n = n + 1 | 0; - e = c[d >> 2] | 0; - } while ((n | 0) < (e | 0)); - m = +g[i >> 2] - } else { - l = a | 0; - j = a + 28 | 0 - } - e = c[l >> 2] | 0; - b = a + 4 | 0; - c[l >> 2] = c[b >> 2]; - g[h >> 2] = m; - n = a + 8 | 0; - c[b >> 2] = c[n >> 2]; - g[i >> 2] = f; - c[n >> 2] = e; - g[j >> 2] = k; - return - } - - function Eb(a, b, c, d) { - a = a | 0; - b = b | 0; - c = c | 0; - d = +d; - var e = 0.0, - f = 0, - h = 0; - if ((c | 0) > 0) { - f = 0 - } else { - return +d - } - while (1) { - h = a + (f << 3) | 0; - e = +V(+(+g[h + 4 >> 2]), +(+g[h >> 2])); - d = e - d; - if (d < -3.1415927410125732) { - d = d + 6.2831854820251465 - } - if (d > 3.1415927410125732) { - d = d + -6.2831854820251465 - } - g[b + (f << 2) >> 2] = d / 3.1415927410125732; - f = f + 1 | 0; - if ((f | 0) < (c | 0)) { - d = e - } else { - break - } - } - return +e - } - - function Fb(a, b, d, e, f) { - a = a | 0; - b = b | 0; - d = d | 0; - e = e | 0; - f = f | 0; - var h = 0, - j = 0, - k = 0.0, - l = 0.0, - m = 0, - n = 0; - h = i; - m = f; - j = i; - i = i + 8 | 0; - c[j >> 2] = c[m >> 2]; - c[j + 4 >> 2] = c[m + 4 >> 2]; - f = b | 0; - l = +g[f >> 2]; - k = +g[f + 4 >> 2]; - g[d >> 2] = (l * (k - +g[j + 4 >> 2]) - k * (l - +g[j >> 2])) * .340447550238101 / (l * l + k * k); - if ((e | 0) > 1) { - j = 1; - do { - n = b + (j << 3) | 0; - k = +g[n + 4 >> 2]; - m = j << 1; - l = +g[n >> 2]; - g[d + (j << 2) >> 2] = (l * (k - +g[f + (m - 1 << 2) >> 2]) - k * (l - +g[f + (m - 2 << 2) >> 2])) * .340447550238101 / (k * k + l * l); - j = j + 1 | 0; - } while ((j | 0) < (e | 0)) - } - j = b + (e - 1 << 3) | 0; - n = a; - m = c[j + 4 >> 2] | 0; - c[n >> 2] = c[j >> 2]; - c[n + 4 >> 2] = m; - i = h; - return - } - - function Gb(a, b, d, e, f, h) { - a = a | 0; - b = b | 0; - d = d | 0; - e = e | 0; - f = f | 0; - h = h | 0; - var j = 0, - k = 0, - l = 0, - m = 0, - n = 0, - o = 0.0, - p = 0.0; - j = i; - m = h; - h = i; - i = i + 8 | 0; - c[h >> 2] = c[m >> 2]; - c[h + 4 >> 2] = c[m + 4 >> 2]; - m = f + (e << 2) | 0; - k = b | 0; - g[f >> 2] = +g[k + 4 >> 2] - +g[h + 4 >> 2]; - l = (e | 0) > 1; - do { - if (l) { - n = 1; - do { - g[f + (n << 2) >> 2] = +g[b + (n << 3) + 4 >> 2] - +g[k + ((n << 1) - 1 << 2) >> 2]; - n = n + 1 | 0; - } while ((n | 0) < (e | 0)); - g[m >> 2] = +g[k >> 2] - +g[h >> 2]; - if (l) { - l = 1 - } else { - break - } - do { - g[f + (l + e << 2) >> 2] = +g[b + (l << 3) >> 2] - +g[k + ((l << 1) - 2 << 2) >> 2]; - l = l + 1 | 0; - } while ((l | 0) < (e | 0)) - } else { - g[m >> 2] = +g[k >> 2] - +g[h >> 2] - } - } while (0); - k = (e | 0) > 0; - do { - if (k) { - l = 0; - do { - n = b + (l << 3) | 0; - g[d + (l << 2) >> 2] = +g[n >> 2] * +g[f + (l << 2) >> 2] - +g[n + 4 >> 2] * +g[f + (l + e << 2) >> 2]; - l = l + 1 | 0; - } while ((l | 0) < (e | 0)); - if (k) { - l = 0 - } else { - break - } - do { - n = b + (l << 3) | 0; - p = +g[n >> 2]; - o = +g[n + 4 >> 2]; - g[f + (l << 2) >> 2] = p * p + o * o; - l = l + 1 | 0; - } while ((l | 0) < (e | 0)); - if (k) { - k = 0 - } else { - break - } - do { - o = +g[f + (k << 2) >> 2]; - l = d + (k << 2) | 0; - if (o != 0.0) { - o = +g[l >> 2] * .340447550238101 / o - } else { - o = 0.0 - } - g[l >> 2] = o; - k = k + 1 | 0; - } while ((k | 0) < (e | 0)) - } - } while (0); - h = b + (e - 1 << 3) | 0; - n = a; - m = c[h + 4 >> 2] | 0; - c[n >> 2] = c[h >> 2]; - c[n + 4 >> 2] = m; - i = j; - return - } - - function Hb(a, b, d, e, f, h) { - a = a | 0; - b = b | 0; - d = d | 0; - e = +e; - f = f | 0; - h = +h; - var i = 0.0; - i = 1.0 / +(f | 0); - e = i / (i + e); - f = (g[k >> 2] = h, c[k >> 2] | 0); - if ((f & 2139095040 | 0) == 2139095040) { - i = (f & 8388607 | 0) != 0 ? 0.0 : h - } else { - i = h - } - h = 1.0 - e; - i = h * i + e * +g[a >> 2]; - g[b >> 2] = i; - if ((d | 0) > 1) { - f = 1 - } else { - f = d - 1 | 0; - f = b + (f << 2) | 0; - i = +g[f >> 2]; - return +i - } - do { - i = e * +g[a + (f << 2) >> 2] + h * i; - g[b + (f << 2) >> 2] = i; - f = f + 1 | 0; - } while ((f | 0) < (d | 0)); - f = d - 1 | 0; - f = b + (f << 2) | 0; - i = +g[f >> 2]; - return +i - } - - function Ib(a, b, c, d) { - a = a | 0; - b = b | 0; - c = c | 0; - d = d | 0; - var e = 0, - f = 0, - h = 0, - i = 0, - j = 0.0; - f = (d | 0) == 44100; - i = (d | 0) == 8e3; - h = (d | 0) == 11025; - e = h ? 1776 : i ? 144 : f ? 1280 : 472; - d = h ? 81 : i ? 81 : f ? 123 : (d | 0) == 48e3 ? 201 : 0; - if ((d | 0) == 0) { - i = 0; - return i | 0 - } - c = c - d | 0; - if ((c | 0) > 0) { - f = 0 - } else { - i = 0; - return i | 0 - } - do { - j = 0.0; - h = 0; - do { - j = j + +g[e + (h << 2) >> 2] * +g[a + (h + f << 2) >> 2]; - h = h + 1 | 0; - } while ((h | 0) < (d | 0)); - g[b + (f << 2) >> 2] = j; - f = f + 1 | 0; - } while ((f | 0) < (c | 0)); - return c | 0 - } - - function Jb(a, b, c, d) { - a = a | 0; - b = b | 0; - c = c | 0; - d = +d; - var e = 0.0, - f = 0, - h = 0.0; - if ((c | 0) <= 0) { - return - } - e = -0.0 - d; - f = 0; - do { - h = +g[a + (f << 2) >> 2]; - h = h > d ? d : h; - g[b + (f << 2) >> 2] = h < e ? e : h; - f = f + 1 | 0; - } while ((f | 0) < (c | 0)); - return - } - - function Kb(a, b, c, d) { - a = a | 0; - b = b | 0; - c = c | 0; - d = +d; - var e = 0; - if ((c | 0) > 0) { - e = 0 - } else { - return - } - do { - g[b + (e << 2) >> 2] = +g[a + (e << 2) >> 2] * d; - e = e + 1 | 0; - } while ((e | 0) < (c | 0)); - return - } - - function Lb(a) { - a = a | 0; - var b = 0, - c = 0; - c = -1; - b = 0; - while (1) { - if ((1 << b & a | 0) != 0) { - if ((c | 0) == -1) { - c = b - } else { - c = -1; - a = 5; - break - } - } - b = b + 1 | 0; - if ((b | 0) >= 31) { - a = 5; - break - } - } - if ((a | 0) == 5) { - return c | 0 - } - return 0 - } - - function Mb(a) { - a = a | 0; - var b = 0, - c = 0; - c = 0; - while (1) { - b = 1 << c; - c = c + 1 | 0; - if ((b | 0) > (a | 0)) { - break - } - if ((c | 0) >= 31) { - b = -1; - break - } - } - return b | 0 - } - - function Nb(a, b, c, d) { - a = a | 0; - b = b | 0; - c = c | 0; - d = d | 0; - var e = 0.0, - f = 0, - h = 0, - i = 0.0, - j = 0.0, - k = 0; - if ((d | 0) == 1) { - d = 6 - } else if ((d | 0) == 0) { - d = 2 - } else { - d = 4 - } - if ((c | 0) <= 0) { - return - } - e = +(c - 1 | 0); - f = 0; - do { - k = a + (f << 3) | 0; - j = +g[k >> 2]; - i = +(f | 0) / e * 2.0 + 1.0; - h = b + (f << 3) | 0; - g[h >> 2] = j * +Pa[d & 7](i); - j = +g[k + 4 >> 2]; - g[h + 4 >> 2] = j * +Pa[d & 7](i); - f = f + 1 | 0; - } while ((f | 0) < (c | 0)); - return - } - - function Ob(a, b, c, d) { - a = a | 0; - b = b | 0; - c = c | 0; - d = d | 0; - var e = 0.0, - f = 0, - h = 0.0; - if ((d | 0) == 1) { - d = 6 - } else if ((d | 0) == 0) { - d = 2 - } else { - d = 4 - } - if ((c | 0) <= 0) { - return - } - e = +(c - 1 | 0); - f = 0; - do { - h = +g[a + (f << 2) >> 2]; - g[b + (f << 2) >> 2] = h * +Pa[d & 7](+(f | 0) / e * 2.0 + 1.0); - f = f + 1 | 0; - } while ((f | 0) < (c | 0)); - return - } - - function Pb(a, b, c, d) { - a = a | 0; - b = b | 0; - c = c | 0; - d = +d; - var e = 0, - f = 0, - h = 0.0, - i = 0.0, - j = 0; - e = (c | 0) > 0; - if (e) { - f = 0 - } else { - return - } - do { - j = a + (f << 3) | 0; - i = +g[j >> 2]; - h = +g[j + 4 >> 2]; - g[b + (f << 2) >> 2] = i * i + h * h; - f = f + 1 | 0; - } while ((f | 0) < (c | 0)); - if (e) { - a = 0 - } else { - return - } - do { - j = b + (a << 2) | 0; - g[j >> 2] = +za(+(+g[j >> 2])); - a = a + 1 | 0; - } while ((a | 0) < (c | 0)); - if (e) { - e = 0 - } else { - return - } - do { - j = b + (e << 2) | 0; - g[j >> 2] = +g[j >> 2] * 10.0 + d; - e = e + 1 | 0; - } while ((e | 0) < (c | 0)); - return - } - - function Qb(a, b, c) { - a = a | 0; - b = b | 0; - c = c | 0; - var e = 0; - if ((c | 0) > 0) { - e = 0 - } else { - return - } - do { - g[b + (e << 2) >> 2] = +((d[a + e | 0] | 0) >>> 0) / 127.5 + -1.0; - e = e + 1 | 0; - } while ((e | 0) < (c | 0)); - return - } - - function Rb(b, c, d) { - b = b | 0; - c = c | 0; - d = d | 0; - var e = 0; - if ((d | 0) > 0) { - e = 0 - } else { - return - } - do { - a[c + e | 0] = ~~(+g[b + (e << 2) >> 2] * 255.0 * .5 + 128.0); - e = e + 1 | 0; - } while ((e | 0) < (d | 0)); - return - } - - function Sb(a, c, d) { - a = a | 0; - c = c | 0; - d = d | 0; - var e = 0; - if ((d | 0) > 0) { - e = 0 - } else { - return - } - do { - g[c + (e << 2) >> 2] = +(b[a + (e << 1) >> 1] | 0) / 32767.0; - e = e + 1 | 0; - } while ((e | 0) < (d | 0)); - return - } - - function Tb(a, c, d) { - a = a | 0; - c = c | 0; - d = d | 0; - var e = 0; - if ((d | 0) > 0) { - e = 0 - } else { - return - } - do { - b[c + (e << 1) >> 1] = ~~(+g[a + (e << 2) >> 2] * 32767.0); - e = e + 1 | 0; - } while ((e | 0) < (d | 0)); - return - } - - function Ub(a, b, d, e, f) { - a = a | 0; - b = b | 0; - d = d | 0; - e = e | 0; - f = +f; - var h = 0, - j = 0.0, - k = 0.0, - l = 0.0, - m = 0.0, - n = 0.0, - o = 0, - p = 0, - q = 0, - r = 0; - h = i; - o = e; - e = i; - i = i + 12 | 0; - i = i + 7 & -8; - c[e >> 2] = c[o >> 2]; - c[e + 4 >> 2] = c[o + 4 >> 2]; - c[e + 8 >> 2] = c[o + 8 >> 2]; - j = f; - if ((d | 0) > 0) { - m = +Q(j); - n = +P(j); - k = +g[e + 4 >> 2]; - l = +g[e >> 2]; - o = 0; - while (1) { - r = a + (o << 3) | 0; - q = r + 4 | 0; - p = b + (o << 3) | 0; - g[p >> 2] = n * +g[r >> 2] - m * +g[q >> 2]; - g[p + 4 >> 2] = m * +g[r >> 2] + n * +g[q >> 2]; - j = n * k - m * l; - o = o + 1 | 0; - if ((o | 0) < (d | 0)) { - m = m * k + n * l; - n = j - } else { - break - } - } - } - f = +(d | 0) * +g[e + 8 >> 2] * 3.1415927410125732 + f; - if (f > 3.1415927410125732) { - do { - f = f + -6.2831854820251465; - } while (f > 3.1415927410125732) - } - if (f >= -3.1415927410125732) { - n = f; - i = h; - return +n - } - do { - f = f + 6.2831854820251465; - } while (f < -3.1415927410125732); - i = h; - return +f - } - - function Vb(a, b) { - a = a | 0; - b = +b; - var c = 0.0, - d = 0.0; - b = b * 2.0; - d = b * 3.1415927410125732; - c = +P(d); - g[a >> 2] = +Q(d); - g[a + 4 >> 2] = c; - g[a + 8 >> 2] = b; - return - } - - function Wb(a) { - a = a | 0; - var b = 0, - d = 0.0, - e = 0.0, - f = 0.0, - j = 0.0, - k = 0.0, - l = 0, - m = 0.0, - n = 0.0, - o = 0.0, - p = 0, - q = 0.0, - r = 0; - b = i; - l = a; - p = i; - i = i + 12 | 0; - i = i + 7 & -8; - c[p >> 2] = c[l >> 2]; - c[p + 4 >> 2] = c[l + 4 >> 2]; - c[p + 8 >> 2] = c[l + 8 >> 2]; - e = +g[p + 8 >> 2]; - a = ~~(2.0 / e + 1.0); - oa(2112, (l = i, i = i + 1 | 0, i = i + 7 & -8, c[l >> 2] = 0, l) | 0) | 0; - i = l; - j = +g[p + 4 >> 2]; - f = +g[p >> 2]; - e = e * 3.1415927410125732; - d = +(a | 0); - m = 0.0; - q = 1.0; - n = 0.0; - p = 0; - o = 0.0; - l = 0; - while (1) { - k = q * j - m * f; - m = m * j + q * f; - n = n + e; - if (n > 6.2831854820251465) { - do { - n = n + -6.2831854820251465; - } while (n > 6.2831854820251465) - } - r = ((l >>> 0) % 1e4 | 0 | 0) == 0; - p = r ? a : p; - o = r ? 0.0 : o; - do { - if ((p | 0) == 0) { - p = 0 - } else { - o = o + +M(+(k - +P(n))); - p = p - 1 | 0; - if ((p | 0) != 0) { - break - } - oa(2104, (p = i, i = i + 8 | 0, h[p >> 3] = o / d, p) | 0) | 0; - i = p; - p = 0 - } - } while (0); - l = l + 1 | 0; - if (l >>> 0 < 5e5 >>> 0) { - q = k - } else { - break - } - } - Fa(8) | 0; - i = b; - return - } - - function Xb(a, b, c) { - a = a | 0; - b = +b; - c = c | 0; - var d = 0.0, - e = 0.0; - b = +(c | 0) * b * 2.0; - e = b * 3.1415927410125732; - d = +P(e); - g[a >> 2] = +Q(e); - g[a + 4 >> 2] = d; - g[a + 8 >> 2] = b; - return - } - - function Yb(a, b, d, e, f, h, j) { - a = a | 0; - b = b | 0; - d = d | 0; - e = e | 0; - f = f | 0; - h = h | 0; - j = j | 0; - var k = 0, - l = 0, - m = 0, - n = 0.0, - o = 0.0, - p = 0.0, - q = 0.0, - r = 0.0, - s = 0, - t = 0, - u = 0.0, - v = 0, - w = 0, - x = 0; - l = i; - k = f; - f = i; - i = i + 12 | 0; - i = i + 7 & -8; - c[f >> 2] = c[k >> 2]; - c[f + 4 >> 2] = c[k + 4 >> 2]; - c[f + 8 >> 2] = c[k + 8 >> 2]; - k = j; - j = i; - i = i + 12 | 0; - i = i + 7 & -8; - c[j >> 2] = c[k >> 2]; - c[j + 4 >> 2] = c[k + 4 >> 2]; - c[j + 8 >> 2] = c[k + 8 >> 2]; - k = j + 4 | 0; - n = +g[k >> 2]; - o = n; - m = j | 0; - t = c[m >> 2] | 0; - if ((t | 0) < (e | 0)) { - p = +Q(o); - o = +P(o); - q = +g[f + 4 >> 2]; - r = +g[f >> 2]; - s = 0; - while (1) { - x = b + (t << 3) | 0; - w = x + 4 | 0; - v = d + (s << 3) | 0; - g[v >> 2] = o * +g[x >> 2] - p * +g[w >> 2]; - g[v + 4 >> 2] = p * +g[x >> 2] + o * +g[w >> 2]; - s = s + 1 | 0; - u = o * q - p * r; - t = t + h | 0; - if ((t | 0) < (e | 0)) { - p = p * q + o * r; - o = u - } else { - break - } - } - } else { - s = 0 - } - c[m >> 2] = t - e; - n = n + +(s | 0) * +g[f + 8 >> 2] * 3.1415927410125732; - g[k >> 2] = n; - c[j + 8 >> 2] = s; - if (n > 3.1415927410125732) { - do { - n = n + -6.2831854820251465; - } while (n > 3.1415927410125732); - g[k >> 2] = n - } - if (n >= -3.1415927410125732) { - x = a; - w = j; - c[x >> 2] = c[w >> 2]; - c[x + 4 >> 2] = c[w + 4 >> 2]; - c[x + 8 >> 2] = c[w + 8 >> 2]; - i = l; - return - } - do { - n = n + 6.2831854820251465; - } while (n < -3.1415927410125732); - g[k >> 2] = n; - x = a; - w = j; - c[x >> 2] = c[w >> 2]; - c[x + 4 >> 2] = c[w + 4 >> 2]; - c[x + 8 >> 2] = c[w + 8 >> 2]; - i = l; - return - } - - function Zb(a, b, c, d, e, f, h, i, j, k, l) { - a = a | 0; - b = b | 0; - c = c | 0; - d = +d; - e = +e; - f = +f; - h = +h; - i = i | 0; - j = j | 0; - k = +k; - l = +l; - var m = 0.0, - n = 0, - o = 0, - p = 0, - q = 0.0, - r = 0.0, - s = 0, - t = 0.0; - g[b >> 2] = +g[a >> 2] * l; - if ((c | 0) <= 1) { - t = l; - return +t - } - o = 0; - m = l; - q = d / l; - p = 0; - n = 1; - do { - l = +g[a + (n << 2) >> 2]; - t = +M(+l); - r = d / t - m; - if (l != 0.0) { - do { - if (r < 0.0) { - s = q < t; - o = s ? j : o; - q = s ? t : q; - if (o << 16 >> 16 > 0) { - r = 0.0; - o = o - 1 & 65535; - break - } else { - r = r * e; - p = i; - break - } - } else { - if (p << 16 >> 16 > 0) { - r = 0.0; - p = p - 1 & 65535; - break - } else { - r = r * f; - break - } - } - } while (0); - r = m + r - } else { - r = m - } - r = r > h ? h : r; - m = m + (r < 0.0 ? 0.0 : r) - m * k; - g[b + (n << 2) >> 2] = l * m; - n = n + 1 | 0; - } while ((n | 0) < (c | 0)); - return +m - } - - function _b(d, e, f, g, h) { - d = d | 0; - e = e | 0; - f = f | 0; - g = g | 0; - h = h | 0; - var j = 0, - k = 0, - l = 0, - m = 0, - n = 0, - o = 0, - p = 0, - q = 0, - r = 0, - s = 0, - t = 0, - u = 0, - v = 0; - j = i; - k = h; - h = i; - i = i + 8 | 0; - c[h >> 2] = c[k >> 2]; - c[h + 4 >> 2] = c[k + 4 >> 2]; - k = (g | 0) / 2 | 0; - if ((g | 0) <= 1) { - u = h; - v = d; - s = u | 0; - s = c[s >> 2] | 0; - u = u + 4 | 0; - u = c[u >> 2] | 0; - t = v | 0; - c[t >> 2] = s; - v = v + 4 | 0; - c[v >> 2] = u; - i = j; - return - } - g = h + 4 | 0; - l = h | 0; - m = 0; - p = c[g >> 2] | 0; - o = c[l >> 2] | 0; - while (1) { - q = m << 1; - n = (b[e + (q << 1) >> 1] | 0) - p | 0; - r = c[2168 + (o << 2) >> 2] | 0; - s = (n | 0) < 0 ? -n | 0 : n; - n = n >> 31 & 8; - if ((s | 0) < (r | 0)) { - t = s - } else { - n = n | 4; - t = s - r | 0 - } - s = r >> 1; - if ((t | 0) >= (s | 0)) { - n = n | 2; - t = t - s | 0 - } - u = r >> 2; - if ((t | 0) < (u | 0)) { - n = n & 255 - } else { - n = (n | 1) & 255 - } - t = n & 255; - r = (r >> 3) + ((t & 1 | 0) == 0 ? 0 : u) + ((t & 2 | 0) == 0 ? 0 : s) + ((t & 4 | 0) == 0 ? 0 : r) | 0; - p = ((t & 8 | 0) == 0 ? r : -r | 0) + p | 0; - if ((p | 0) > 32767) { - p = 32767 - } else { - p = (p | 0) < -32768 ? -32768 : p - } - o = (c[80 + (t << 2) >> 2] | 0) + o | 0; - if ((o | 0) < 0) { - r = 0 - } else { - r = (o | 0) > 88 ? 88 : o - } - o = f + m | 0; - a[o] = n; - s = (b[e + ((q | 1) << 1) >> 1] | 0) - p | 0; - q = c[2168 + (r << 2) >> 2] | 0; - u = (s | 0) < 0 ? -s | 0 : s; - s = s >> 31 & 8; - if ((u | 0) >= (q | 0)) { - s = s | 4; - u = u - q | 0 - } - t = q >> 1; - if ((u | 0) >= (t | 0)) { - s = s | 2; - u = u - t | 0 - } - v = q >> 2; - if ((u | 0) < (v | 0)) { - s = s & 255 - } else { - s = (s | 1) & 255 - } - u = s & 255; - q = (q >> 3) + ((u & 1 | 0) == 0 ? 0 : v) + ((u & 2 | 0) == 0 ? 0 : t) + ((u & 4 | 0) == 0 ? 0 : q) | 0; - p = ((u & 8 | 0) == 0 ? q : -q | 0) + p | 0; - if ((p | 0) > 32767) { - p = 32767 - } else { - p = (p | 0) < -32768 ? -32768 : p - } - q = (c[80 + (u << 2) >> 2] | 0) + r | 0; - if ((q | 0) < 0) { - q = 0 - } else { - q = (q | 0) > 88 ? 88 : q - } - m = m + 1 | 0; - a[o] = n | s << 4; - if ((m | 0) < (k | 0)) { - o = q - } else { - break - } - } - c[g >> 2] = p; - c[l >> 2] = q; - u = h; - v = d; - s = u | 0; - s = c[s >> 2] | 0; - u = u + 4 | 0; - u = c[u >> 2] | 0; - t = v | 0; - c[t >> 2] = s; - v = v + 4 | 0; - c[v >> 2] = u; - i = j; - return - } - - function $b(e, f, g, h, j) { - e = e | 0; - f = f | 0; - g = g | 0; - h = h | 0; - j = j | 0; - var k = 0, - l = 0, - m = 0, - n = 0, - o = 0, - p = 0, - q = 0, - r = 0, - s = 0, - t = 0, - u = 0; - n = i; - u = j; - j = i; - i = i + 8 | 0; - c[j >> 2] = c[u >> 2]; - c[j + 4 >> 2] = c[u + 4 >> 2]; - if ((h | 0) <= 0) { - t = j; - u = e; - r = t | 0; - r = c[r >> 2] | 0; - t = t + 4 | 0; - t = c[t >> 2] | 0; - s = u | 0; - c[s >> 2] = r; - u = u + 4 | 0; - c[u >> 2] = t; - i = n; - return - } - k = j | 0; - l = j + 4 | 0; - m = 0; - o = 0; - q = c[k >> 2] | 0; - r = c[l >> 2] | 0; - while (1) { - p = f + o | 0; - t = c[2168 + (q << 2) >> 2] | 0; - u = t >> 3; - s = a[p] & 15; - if ((s & 1 | 0) != 0) { - u = u + (t >> 2) | 0 - } - if ((s & 2 | 0) != 0) { - u = u + (t >> 1) | 0 - } - t = u + ((s & 4 | 0) == 0 ? 0 : t) | 0; - u = ((s & 8 | 0) == 0 ? t : -t | 0) + r | 0; - if ((u | 0) > 32767) { - t = 32767; - r = 32767 - } else { - r = (u | 0) < -32768; - t = r ? -32768 : u & 65535; - r = r ? -32768 : u - } - q = (c[80 + (s << 2) >> 2] | 0) + q | 0; - if ((q | 0) < 0) { - q = 0 - } else { - q = (q | 0) > 88 ? 88 : q - } - b[g + (m << 1) >> 1] = t; - s = c[2168 + (q << 2) >> 2] | 0; - t = s >> 3; - p = (d[p] | 0) >>> 4 & 255; - if ((p & 1 | 0) != 0) { - t = t + (s >> 2) | 0 - } - if ((p & 2 | 0) != 0) { - t = t + (s >> 1) | 0 - } - s = t + ((p & 4 | 0) == 0 ? 0 : s) | 0; - r = ((p & 8 | 0) == 0 ? s : -s | 0) + r | 0; - if ((r | 0) > 32767) { - s = 32767; - r = 32767 - } else { - t = (r | 0) < -32768; - s = t ? -32768 : r & 65535; - r = t ? -32768 : r - } - p = (c[80 + (p << 2) >> 2] | 0) + q | 0; - if ((p | 0) < 0) { - q = 0 - } else { - q = (p | 0) > 88 ? 88 : p - } - b[g + ((m | 1) << 1) >> 1] = s; - o = o + 1 | 0; - if ((o | 0) < (h | 0)) { - m = m + 2 | 0 - } else { - break - } - } - c[k >> 2] = q; - c[l >> 2] = r; - t = j; - u = e; - r = t | 0; - r = c[r >> 2] | 0; - t = t + 4 | 0; - t = c[t >> 2] | 0; - s = u | 0; - c[s >> 2] = r; - u = u + 4 | 0; - c[u >> 2] = t; - i = n; - return - } - - function ac(a) { - a = a | 0; - var b = 0, - d = 0; - b = i; - d = c[a >> 2] | 0; - Na[c[c[d >> 2] >> 2] & 1](d, c[a + 4 >> 2] | 0); - i = b; - return - } - - function bc(a) { - a = a | 0; - var b = 0, - d = 0, - e = 0, - f = 0, - g = 0, - h = 0, - i = 0, - j = 0, - k = 0, - l = 0, - m = 0, - n = 0, - o = 0, - p = 0, - q = 0; - do { - if (a >>> 0 < 245 >>> 0) { - if (a >>> 0 < 11 >>> 0) { - a = 16 - } else { - a = a + 11 & -8 - } - f = a >>> 3; - e = c[638] | 0; - b = e >>> (f >>> 0); - if ((b & 3 | 0) != 0) { - h = (b & 1 ^ 1) + f | 0; - a = h << 1; - d = 2592 + (a << 2) | 0; - a = 2592 + (a + 2 << 2) | 0; - g = c[a >> 2] | 0; - f = g + 8 | 0; - b = c[f >> 2] | 0; - do { - if ((d | 0) == (b | 0)) { - c[638] = e & ~(1 << h) - } else { - if (b >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } - e = b + 12 | 0; - if ((c[e >> 2] | 0) == (g | 0)) { - c[e >> 2] = d; - c[a >> 2] = b; - break - } else { - ma(); - return 0 - } - } - } while (0); - q = h << 3; - c[g + 4 >> 2] = q | 3; - q = g + (q | 4) | 0; - c[q >> 2] = c[q >> 2] | 1; - q = f; - return q | 0 - } - if (a >>> 0 <= (c[640] | 0) >>> 0) { - break - } - if ((b | 0) != 0) { - i = 2 << f; - i = b << f & (i | -i); - i = (i & -i) - 1 | 0; - b = i >>> 12 & 16; - i = i >>> (b >>> 0); - h = i >>> 5 & 8; - i = i >>> (h >>> 0); - f = i >>> 2 & 4; - i = i >>> (f >>> 0); - g = i >>> 1 & 2; - i = i >>> (g >>> 0); - d = i >>> 1 & 1; - d = (h | b | f | g | d) + (i >>> (d >>> 0)) | 0; - i = d << 1; - g = 2592 + (i << 2) | 0; - i = 2592 + (i + 2 << 2) | 0; - f = c[i >> 2] | 0; - b = f + 8 | 0; - h = c[b >> 2] | 0; - do { - if ((g | 0) == (h | 0)) { - c[638] = e & ~(1 << d) - } else { - if (h >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } - e = h + 12 | 0; - if ((c[e >> 2] | 0) == (f | 0)) { - c[e >> 2] = g; - c[i >> 2] = h; - break - } else { - ma(); - return 0 - } - } - } while (0); - q = d << 3; - d = q - a | 0; - c[f + 4 >> 2] = a | 3; - e = f + a | 0; - c[f + (a | 4) >> 2] = d | 1; - c[f + q >> 2] = d; - f = c[640] | 0; - if ((f | 0) != 0) { - a = c[643] | 0; - i = f >>> 3; - g = i << 1; - f = 2592 + (g << 2) | 0; - h = c[638] | 0; - i = 1 << i; - do { - if ((h & i | 0) == 0) { - c[638] = h | i; - h = f; - g = 2592 + (g + 2 << 2) | 0 - } else { - g = 2592 + (g + 2 << 2) | 0; - h = c[g >> 2] | 0; - if (h >>> 0 >= (c[642] | 0) >>> 0) { - break - } - ma(); - return 0 - } - } while (0); - c[g >> 2] = a; - c[h + 12 >> 2] = a; - c[a + 8 >> 2] = h; - c[a + 12 >> 2] = f - } - c[640] = d; - c[643] = e; - q = b; - return q | 0 - } - b = c[639] | 0; - if ((b | 0) == 0) { - break - } - e = (b & -b) - 1 | 0; - p = e >>> 12 & 16; - e = e >>> (p >>> 0); - o = e >>> 5 & 8; - e = e >>> (o >>> 0); - q = e >>> 2 & 4; - e = e >>> (q >>> 0); - b = e >>> 1 & 2; - e = e >>> (b >>> 0); - d = e >>> 1 & 1; - d = c[2856 + ((o | p | q | b | d) + (e >>> (d >>> 0)) << 2) >> 2] | 0; - e = d; - b = d; - d = (c[d + 4 >> 2] & -8) - a | 0; - while (1) { - h = c[e + 16 >> 2] | 0; - if ((h | 0) == 0) { - h = c[e + 20 >> 2] | 0; - if ((h | 0) == 0) { - break - } - } - g = (c[h + 4 >> 2] & -8) - a | 0; - f = g >>> 0 < d >>> 0; - e = h; - b = f ? h : b; - d = f ? g : d - } - f = b; - h = c[642] | 0; - if (f >>> 0 < h >>> 0) { - ma(); - return 0 - } - q = f + a | 0; - e = q; - if (f >>> 0 >= q >>> 0) { - ma(); - return 0 - } - g = c[b + 24 >> 2] | 0; - i = c[b + 12 >> 2] | 0; - do { - if ((i | 0) == (b | 0)) { - j = b + 20 | 0; - i = c[j >> 2] | 0; - if ((i | 0) == 0) { - j = b + 16 | 0; - i = c[j >> 2] | 0; - if ((i | 0) == 0) { - i = 0; - break - } - } - while (1) { - l = i + 20 | 0; - k = c[l >> 2] | 0; - if ((k | 0) != 0) { - i = k; - j = l; - continue - } - k = i + 16 | 0; - l = c[k >> 2] | 0; - if ((l | 0) == 0) { - break - } else { - i = l; - j = k - } - } - if (j >>> 0 < h >>> 0) { - ma(); - return 0 - } else { - c[j >> 2] = 0; - break - } - } else { - j = c[b + 8 >> 2] | 0; - if (j >>> 0 < h >>> 0) { - ma(); - return 0 - } - h = j + 12 | 0; - if ((c[h >> 2] | 0) != (b | 0)) { - ma(); - return 0 - } - k = i + 8 | 0; - if ((c[k >> 2] | 0) == (b | 0)) { - c[h >> 2] = i; - c[k >> 2] = j; - break - } else { - ma(); - return 0 - } - } - } while (0); - a: do { - if ((g | 0) != 0) { - j = b + 28 | 0; - h = 2856 + (c[j >> 2] << 2) | 0; - do { - if ((b | 0) == (c[h >> 2] | 0)) { - c[h >> 2] = i; - if ((i | 0) != 0) { - break - } - c[639] = c[639] & ~(1 << c[j >> 2]); - break a - } else { - if (g >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } - h = g + 16 | 0; - if ((c[h >> 2] | 0) == (b | 0)) { - c[h >> 2] = i - } else { - c[g + 20 >> 2] = i - } - if ((i | 0) == 0) { - break a - } - } - } while (0); - if (i >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } - c[i + 24 >> 2] = g; - g = c[b + 16 >> 2] | 0; - do { - if ((g | 0) != 0) { - if (g >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } else { - c[i + 16 >> 2] = g; - c[g + 24 >> 2] = i; - break - } - } - } while (0); - g = c[b + 20 >> 2] | 0; - if ((g | 0) == 0) { - break - } - if (g >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } else { - c[i + 20 >> 2] = g; - c[g + 24 >> 2] = i; - break - } - } - } while (0); - if (d >>> 0 < 16 >>> 0) { - q = d + a | 0; - c[b + 4 >> 2] = q | 3; - q = f + (q + 4) | 0; - c[q >> 2] = c[q >> 2] | 1 - } else { - c[b + 4 >> 2] = a | 3; - c[f + (a | 4) >> 2] = d | 1; - c[f + (d + a) >> 2] = d; - f = c[640] | 0; - if ((f | 0) != 0) { - a = c[643] | 0; - h = f >>> 3; - g = h << 1; - f = 2592 + (g << 2) | 0; - i = c[638] | 0; - h = 1 << h; - do { - if ((i & h | 0) == 0) { - c[638] = i | h; - h = f; - g = 2592 + (g + 2 << 2) | 0 - } else { - g = 2592 + (g + 2 << 2) | 0; - h = c[g >> 2] | 0; - if (h >>> 0 >= (c[642] | 0) >>> 0) { - break - } - ma(); - return 0 - } - } while (0); - c[g >> 2] = a; - c[h + 12 >> 2] = a; - c[a + 8 >> 2] = h; - c[a + 12 >> 2] = f - } - c[640] = d; - c[643] = e - } - q = b + 8 | 0; - return q | 0 - } else { - if (a >>> 0 > 4294967231 >>> 0) { - a = -1; - break - } - b = a + 11 | 0; - a = b & -8; - f = c[639] | 0; - if ((f | 0) == 0) { - break - } - e = -a | 0; - b = b >>> 8; - do { - if ((b | 0) == 0) { - g = 0 - } else { - if (a >>> 0 > 16777215 >>> 0) { - g = 31; - break - } - p = (b + 1048320 | 0) >>> 16 & 8; - q = b << p; - o = (q + 520192 | 0) >>> 16 & 4; - q = q << o; - g = (q + 245760 | 0) >>> 16 & 2; - g = 14 - (o | p | g) + (q << g >>> 15) | 0; - g = a >>> ((g + 7 | 0) >>> 0) & 1 | g << 1 - } - } while (0); - h = c[2856 + (g << 2) >> 2] | 0; - b: do { - if ((h | 0) == 0) { - b = 0; - j = 0 - } else { - if ((g | 0) == 31) { - i = 0 - } else { - i = 25 - (g >>> 1) | 0 - } - b = 0; - i = a << i; - j = 0; - while (1) { - l = c[h + 4 >> 2] & -8; - k = l - a | 0; - if (k >>> 0 < e >>> 0) { - if ((l | 0) == (a | 0)) { - b = h; - e = k; - j = h; - break b - } else { - b = h; - e = k - } - } - k = c[h + 20 >> 2] | 0; - h = c[h + 16 + (i >>> 31 << 2) >> 2] | 0; - j = (k | 0) == 0 | (k | 0) == (h | 0) ? j : k; - if ((h | 0) == 0) { - break - } else { - i = i << 1 - } - } - } - } while (0); - if ((j | 0) == 0 & (b | 0) == 0) { - q = 2 << g; - f = f & (q | -q); - if ((f | 0) == 0) { - break - } - q = (f & -f) - 1 | 0; - n = q >>> 12 & 16; - q = q >>> (n >>> 0); - m = q >>> 5 & 8; - q = q >>> (m >>> 0); - o = q >>> 2 & 4; - q = q >>> (o >>> 0); - p = q >>> 1 & 2; - q = q >>> (p >>> 0); - j = q >>> 1 & 1; - j = c[2856 + ((m | n | o | p | j) + (q >>> (j >>> 0)) << 2) >> 2] | 0 - } - if ((j | 0) != 0) { - while (1) { - g = (c[j + 4 >> 2] & -8) - a | 0; - f = g >>> 0 < e >>> 0; - e = f ? g : e; - b = f ? j : b; - f = c[j + 16 >> 2] | 0; - if ((f | 0) != 0) { - j = f; - continue - } - j = c[j + 20 >> 2] | 0; - if ((j | 0) == 0) { - break - } - } - } - if ((b | 0) == 0) { - break - } - if (e >>> 0 >= ((c[640] | 0) - a | 0) >>> 0) { - break - } - d = b; - i = c[642] | 0; - if (d >>> 0 < i >>> 0) { - ma(); - return 0 - } - g = d + a | 0; - f = g; - if (d >>> 0 >= g >>> 0) { - ma(); - return 0 - } - h = c[b + 24 >> 2] | 0; - j = c[b + 12 >> 2] | 0; - do { - if ((j | 0) == (b | 0)) { - k = b + 20 | 0; - j = c[k >> 2] | 0; - if ((j | 0) == 0) { - k = b + 16 | 0; - j = c[k >> 2] | 0; - if ((j | 0) == 0) { - j = 0; - break - } - } - while (1) { - l = j + 20 | 0; - m = c[l >> 2] | 0; - if ((m | 0) != 0) { - j = m; - k = l; - continue - } - l = j + 16 | 0; - m = c[l >> 2] | 0; - if ((m | 0) == 0) { - break - } else { - j = m; - k = l - } - } - if (k >>> 0 < i >>> 0) { - ma(); - return 0 - } else { - c[k >> 2] = 0; - break - } - } else { - k = c[b + 8 >> 2] | 0; - if (k >>> 0 < i >>> 0) { - ma(); - return 0 - } - i = k + 12 | 0; - if ((c[i >> 2] | 0) != (b | 0)) { - ma(); - return 0 - } - l = j + 8 | 0; - if ((c[l >> 2] | 0) == (b | 0)) { - c[i >> 2] = j; - c[l >> 2] = k; - break - } else { - ma(); - return 0 - } - } - } while (0); - c: do { - if ((h | 0) != 0) { - i = b + 28 | 0; - k = 2856 + (c[i >> 2] << 2) | 0; - do { - if ((b | 0) == (c[k >> 2] | 0)) { - c[k >> 2] = j; - if ((j | 0) != 0) { - break - } - c[639] = c[639] & ~(1 << c[i >> 2]); - break c - } else { - if (h >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } - i = h + 16 | 0; - if ((c[i >> 2] | 0) == (b | 0)) { - c[i >> 2] = j - } else { - c[h + 20 >> 2] = j - } - if ((j | 0) == 0) { - break c - } - } - } while (0); - if (j >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } - c[j + 24 >> 2] = h; - h = c[b + 16 >> 2] | 0; - do { - if ((h | 0) != 0) { - if (h >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } else { - c[j + 16 >> 2] = h; - c[h + 24 >> 2] = j; - break - } - } - } while (0); - h = c[b + 20 >> 2] | 0; - if ((h | 0) == 0) { - break - } - if (h >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } else { - c[j + 20 >> 2] = h; - c[h + 24 >> 2] = j; - break - } - } - } while (0); - d: do { - if (e >>> 0 < 16 >>> 0) { - q = e + a | 0; - c[b + 4 >> 2] = q | 3; - q = d + (q + 4) | 0; - c[q >> 2] = c[q >> 2] | 1 - } else { - c[b + 4 >> 2] = a | 3; - c[d + (a | 4) >> 2] = e | 1; - c[d + (e + a) >> 2] = e; - h = e >>> 3; - if (e >>> 0 < 256 >>> 0) { - g = h << 1; - e = 2592 + (g << 2) | 0; - i = c[638] | 0; - h = 1 << h; - do { - if ((i & h | 0) == 0) { - c[638] = i | h; - h = e; - g = 2592 + (g + 2 << 2) | 0 - } else { - g = 2592 + (g + 2 << 2) | 0; - h = c[g >> 2] | 0; - if (h >>> 0 >= (c[642] | 0) >>> 0) { - break - } - ma(); - return 0 - } - } while (0); - c[g >> 2] = f; - c[h + 12 >> 2] = f; - c[d + (a + 8) >> 2] = h; - c[d + (a + 12) >> 2] = e; - break - } - f = e >>> 8; - do { - if ((f | 0) == 0) { - h = 0 - } else { - if (e >>> 0 > 16777215 >>> 0) { - h = 31; - break - } - p = (f + 1048320 | 0) >>> 16 & 8; - q = f << p; - o = (q + 520192 | 0) >>> 16 & 4; - q = q << o; - h = (q + 245760 | 0) >>> 16 & 2; - h = 14 - (o | p | h) + (q << h >>> 15) | 0; - h = e >>> ((h + 7 | 0) >>> 0) & 1 | h << 1 - } - } while (0); - f = 2856 + (h << 2) | 0; - c[d + (a + 28) >> 2] = h; - c[d + (a + 20) >> 2] = 0; - c[d + (a + 16) >> 2] = 0; - j = c[639] | 0; - i = 1 << h; - if ((j & i | 0) == 0) { - c[639] = j | i; - c[f >> 2] = g; - c[d + (a + 24) >> 2] = f; - c[d + (a + 12) >> 2] = g; - c[d + (a + 8) >> 2] = g; - break - } - f = c[f >> 2] | 0; - if ((h | 0) == 31) { - h = 0 - } else { - h = 25 - (h >>> 1) | 0 - } - e: do { - if ((c[f + 4 >> 2] & -8 | 0) != (e | 0)) { - h = e << h; - while (1) { - i = f + 16 + (h >>> 31 << 2) | 0; - j = c[i >> 2] | 0; - if ((j | 0) == 0) { - break - } - if ((c[j + 4 >> 2] & -8 | 0) == (e | 0)) { - f = j; - break e - } else { - f = j; - h = h << 1 - } - } - if (i >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } else { - c[i >> 2] = g; - c[d + (a + 24) >> 2] = f; - c[d + (a + 12) >> 2] = g; - c[d + (a + 8) >> 2] = g; - break d - } - } - } while (0); - h = f + 8 | 0; - e = c[h >> 2] | 0; - q = c[642] | 0; - if (f >>> 0 >= q >>> 0 & e >>> 0 >= q >>> 0) { - c[e + 12 >> 2] = g; - c[h >> 2] = g; - c[d + (a + 8) >> 2] = e; - c[d + (a + 12) >> 2] = f; - c[d + (a + 24) >> 2] = 0; - break - } else { - ma(); - return 0 - } - } - } while (0); - q = b + 8 | 0; - return q | 0 - } - } while (0); - b = c[640] | 0; - if (b >>> 0 >= a >>> 0) { - d = b - a | 0; - e = c[643] | 0; - if (d >>> 0 > 15 >>> 0) { - q = e; - c[643] = q + a; - c[640] = d; - c[q + (a + 4) >> 2] = d | 1; - c[q + b >> 2] = d; - c[e + 4 >> 2] = a | 3 - } else { - c[640] = 0; - c[643] = 0; - c[e + 4 >> 2] = b | 3; - q = e + (b + 4) | 0; - c[q >> 2] = c[q >> 2] | 1 - } - q = e + 8 | 0; - return q | 0 - } - b = c[641] | 0; - if (b >>> 0 > a >>> 0) { - o = b - a | 0; - c[641] = o; - q = c[644] | 0; - p = q; - c[644] = p + a; - c[p + (a + 4) >> 2] = o | 1; - c[q + 4 >> 2] = a | 3; - q = q + 8 | 0; - return q | 0 - } - do { - if ((c[632] | 0) == 0) { - b = la(30) | 0; - if ((b - 1 & b | 0) == 0) { - c[634] = b; - c[633] = b; - c[635] = -1; - c[636] = -1; - c[637] = 0; - c[749] = 0; - c[632] = (Ja(0) | 0) & -16 ^ 1431655768; - break - } else { - ma(); - return 0 - } - } - } while (0); - h = a + 48 | 0; - e = c[634] | 0; - g = a + 47 | 0; - b = e + g | 0; - e = -e | 0; - f = b & e; - if (f >>> 0 <= a >>> 0) { - q = 0; - return q | 0 - } - i = c[748] | 0; - do { - if ((i | 0) != 0) { - p = c[746] | 0; - q = p + f | 0; - if (q >>> 0 <= p >>> 0 | q >>> 0 > i >>> 0) { - a = 0 - } else { - break - } - return a | 0 - } - } while (0); - f: do { - if ((c[749] & 4 | 0) == 0) { - k = c[644] | 0; - g: do { - if ((k | 0) == 0) { - d = 181 - } else { - l = 3e3; - while (1) { - j = l | 0; - m = c[j >> 2] | 0; - if (m >>> 0 <= k >>> 0) { - i = l + 4 | 0; - if ((m + (c[i >> 2] | 0) | 0) >>> 0 > k >>> 0) { - break - } - } - l = c[l + 8 >> 2] | 0; - if ((l | 0) == 0) { - d = 181; - break g - } - } - if ((l | 0) == 0) { - d = 181; - break - } - e = b - (c[641] | 0) & e; - if (e >>> 0 >= 2147483647 >>> 0) { - e = 0; - break - } - b = Ga(e | 0) | 0; - if ((b | 0) == ((c[j >> 2] | 0) + (c[i >> 2] | 0) | 0)) { - d = 190 - } else { - d = 191 - } - } - } while (0); - do { - if ((d | 0) == 181) { - i = Ga(0) | 0; - if ((i | 0) == -1) { - e = 0; - break - } - e = i; - b = c[633] | 0; - j = b - 1 | 0; - if ((j & e | 0) == 0) { - e = f - } else { - e = f - e + (j + e & -b) | 0 - } - j = c[746] | 0; - b = j + e | 0; - if (!(e >>> 0 > a >>> 0 & e >>> 0 < 2147483647 >>> 0)) { - e = 0; - break - } - k = c[748] | 0; - if ((k | 0) != 0) { - if (b >>> 0 <= j >>> 0 | b >>> 0 > k >>> 0) { - e = 0; - break - } - } - b = Ga(e | 0) | 0; - if ((b | 0) == (i | 0)) { - b = i; - d = 190 - } else { - d = 191 - } - } - } while (0); - h: do { - if ((d | 0) == 190) { - if ((b | 0) != -1) { - d = 201; - break f - } - } else if ((d | 0) == 191) { - d = -e | 0; - do { - if ((b | 0) != -1 & e >>> 0 < 2147483647 >>> 0 & h >>> 0 > e >>> 0) { - q = c[634] | 0; - g = g - e + q & -q; - if (g >>> 0 >= 2147483647 >>> 0) { - break - } - if ((Ga(g | 0) | 0) == -1) { - Ga(d | 0) | 0; - e = 0; - break h - } else { - e = g + e | 0; - break - } - } - } while (0); - if ((b | 0) == -1) { - e = 0 - } else { - d = 201; - break f - } - } - } while (0); - c[749] = c[749] | 4; - d = 198 - } else { - e = 0; - d = 198 - } - } while (0); - do { - if ((d | 0) == 198) { - if (f >>> 0 >= 2147483647 >>> 0) { - break - } - b = Ga(f | 0) | 0; - f = Ga(0) | 0; - if (!((b | 0) != -1 & (f | 0) != -1 & b >>> 0 < f >>> 0)) { - break - } - g = f - b | 0; - f = g >>> 0 > (a + 40 | 0) >>> 0; - if (f) { - e = f ? g : e; - d = 201 - } - } - } while (0); - do { - if ((d | 0) == 201) { - f = (c[746] | 0) + e | 0; - c[746] = f; - if (f >>> 0 > (c[747] | 0) >>> 0) { - c[747] = f - } - f = c[644] | 0; - i: do { - if ((f | 0) == 0) { - q = c[642] | 0; - if ((q | 0) == 0 | b >>> 0 < q >>> 0) { - c[642] = b - } - c[750] = b; - c[751] = e; - c[753] = 0; - c[647] = c[632]; - c[646] = -1; - d = 0; - do { - q = d << 1; - p = 2592 + (q << 2) | 0; - c[2592 + (q + 3 << 2) >> 2] = p; - c[2592 + (q + 2 << 2) >> 2] = p; - d = d + 1 | 0; - } while (d >>> 0 < 32 >>> 0); - d = b + 8 | 0; - if ((d & 7 | 0) == 0) { - d = 0 - } else { - d = -d & 7 - } - q = e - 40 - d | 0; - c[644] = b + d; - c[641] = q; - c[b + (d + 4) >> 2] = q | 1; - c[b + (e - 36) >> 2] = 40; - c[645] = c[636] - } else { - g = 3e3; - do { - j = c[g >> 2] | 0; - i = g + 4 | 0; - h = c[i >> 2] | 0; - if ((b | 0) == (j + h | 0)) { - d = 213; - break - } - g = c[g + 8 >> 2] | 0; - } while ((g | 0) != 0); - do { - if ((d | 0) == 213) { - if ((c[g + 12 >> 2] & 8 | 0) != 0) { - break - } - q = f; - if (!(q >>> 0 >= j >>> 0 & q >>> 0 < b >>> 0)) { - break - } - c[i >> 2] = h + e; - q = c[644] | 0; - b = (c[641] | 0) + e | 0; - d = q; - e = q + 8 | 0; - if ((e & 7 | 0) == 0) { - e = 0 - } else { - e = -e & 7 - } - q = b - e | 0; - c[644] = d + e; - c[641] = q; - c[d + (e + 4) >> 2] = q | 1; - c[d + (b + 4) >> 2] = 40; - c[645] = c[636]; - break i - } - } while (0); - if (b >>> 0 < (c[642] | 0) >>> 0) { - c[642] = b - } - g = b + e | 0; - i = 3e3; - do { - h = i | 0; - if ((c[h >> 2] | 0) == (g | 0)) { - d = 223; - break - } - i = c[i + 8 >> 2] | 0; - } while ((i | 0) != 0); - do { - if ((d | 0) == 223) { - if ((c[i + 12 >> 2] & 8 | 0) != 0) { - break - } - c[h >> 2] = b; - d = i + 4 | 0; - c[d >> 2] = (c[d >> 2] | 0) + e; - d = b + 8 | 0; - if ((d & 7 | 0) == 0) { - d = 0 - } else { - d = -d & 7 - } - f = b + (e + 8) | 0; - if ((f & 7 | 0) == 0) { - j = 0 - } else { - j = -f & 7 - } - m = b + (j + e) | 0; - l = m; - f = d + a | 0; - h = b + f | 0; - g = h; - i = m - (b + d) - a | 0; - c[b + (d + 4) >> 2] = a | 3; - j: do { - if ((l | 0) == (c[644] | 0)) { - q = (c[641] | 0) + i | 0; - c[641] = q; - c[644] = g; - c[b + (f + 4) >> 2] = q | 1 - } else { - if ((l | 0) == (c[643] | 0)) { - q = (c[640] | 0) + i | 0; - c[640] = q; - c[643] = g; - c[b + (f + 4) >> 2] = q | 1; - c[b + (q + f) >> 2] = q; - break - } - k = e + 4 | 0; - o = c[b + (k + j) >> 2] | 0; - if ((o & 3 | 0) == 1) { - a = o & -8; - n = o >>> 3; - k: do { - if (o >>> 0 < 256 >>> 0) { - k = c[b + ((j | 8) + e) >> 2] | 0; - m = c[b + (e + 12 + j) >> 2] | 0; - o = 2592 + (n << 1 << 2) | 0; - do { - if ((k | 0) != (o | 0)) { - if (k >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } - if ((c[k + 12 >> 2] | 0) == (l | 0)) { - break - } - ma(); - return 0 - } - } while (0); - if ((m | 0) == (k | 0)) { - c[638] = c[638] & ~(1 << n); - break - } - do { - if ((m | 0) == (o | 0)) { - n = m + 8 | 0 - } else { - if (m >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } - n = m + 8 | 0; - if ((c[n >> 2] | 0) == (l | 0)) { - break - } - ma(); - return 0 - } - } while (0); - c[k + 12 >> 2] = m; - c[n >> 2] = k - } else { - l = c[b + ((j | 24) + e) >> 2] | 0; - n = c[b + (e + 12 + j) >> 2] | 0; - do { - if ((n | 0) == (m | 0)) { - p = j | 16; - o = b + (k + p) | 0; - n = c[o >> 2] | 0; - if ((n | 0) == 0) { - o = b + (p + e) | 0; - n = c[o >> 2] | 0; - if ((n | 0) == 0) { - n = 0; - break - } - } - while (1) { - q = n + 20 | 0; - p = c[q >> 2] | 0; - if ((p | 0) != 0) { - n = p; - o = q; - continue - } - p = n + 16 | 0; - q = c[p >> 2] | 0; - if ((q | 0) == 0) { - break - } else { - n = q; - o = p - } - } - if (o >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } else { - c[o >> 2] = 0; - break - } - } else { - q = c[b + ((j | 8) + e) >> 2] | 0; - if (q >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } - o = q + 12 | 0; - if ((c[o >> 2] | 0) != (m | 0)) { - ma(); - return 0 - } - p = n + 8 | 0; - if ((c[p >> 2] | 0) == (m | 0)) { - c[o >> 2] = n; - c[p >> 2] = q; - break - } else { - ma(); - return 0 - } - } - } while (0); - if ((l | 0) == 0) { - break - } - o = b + (e + 28 + j) | 0; - p = 2856 + (c[o >> 2] << 2) | 0; - do { - if ((m | 0) == (c[p >> 2] | 0)) { - c[p >> 2] = n; - if ((n | 0) != 0) { - break - } - c[639] = c[639] & ~(1 << c[o >> 2]); - break k - } else { - if (l >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } - o = l + 16 | 0; - if ((c[o >> 2] | 0) == (m | 0)) { - c[o >> 2] = n - } else { - c[l + 20 >> 2] = n - } - if ((n | 0) == 0) { - break k - } - } - } while (0); - if (n >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } - c[n + 24 >> 2] = l; - m = j | 16; - l = c[b + (m + e) >> 2] | 0; - do { - if ((l | 0) != 0) { - if (l >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } else { - c[n + 16 >> 2] = l; - c[l + 24 >> 2] = n; - break - } - } - } while (0); - k = c[b + (k + m) >> 2] | 0; - if ((k | 0) == 0) { - break - } - if (k >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } else { - c[n + 20 >> 2] = k; - c[k + 24 >> 2] = n; - break - } - } - } while (0); - l = b + ((a | j) + e) | 0; - i = a + i | 0 - } - e = l + 4 | 0; - c[e >> 2] = c[e >> 2] & -2; - c[b + (f + 4) >> 2] = i | 1; - c[b + (i + f) >> 2] = i; - e = i >>> 3; - if (i >>> 0 < 256 >>> 0) { - h = e << 1; - a = 2592 + (h << 2) | 0; - i = c[638] | 0; - e = 1 << e; - do { - if ((i & e | 0) == 0) { - c[638] = i | e; - e = a; - h = 2592 + (h + 2 << 2) | 0 - } else { - h = 2592 + (h + 2 << 2) | 0; - e = c[h >> 2] | 0; - if (e >>> 0 >= (c[642] | 0) >>> 0) { - break - } - ma(); - return 0 - } - } while (0); - c[h >> 2] = g; - c[e + 12 >> 2] = g; - c[b + (f + 8) >> 2] = e; - c[b + (f + 12) >> 2] = a; - break - } - a = i >>> 8; - do { - if ((a | 0) == 0) { - e = 0 - } else { - if (i >>> 0 > 16777215 >>> 0) { - e = 31; - break - } - p = (a + 1048320 | 0) >>> 16 & 8; - q = a << p; - o = (q + 520192 | 0) >>> 16 & 4; - q = q << o; - e = (q + 245760 | 0) >>> 16 & 2; - e = 14 - (o | p | e) + (q << e >>> 15) | 0; - e = i >>> ((e + 7 | 0) >>> 0) & 1 | e << 1 - } - } while (0); - a = 2856 + (e << 2) | 0; - c[b + (f + 28) >> 2] = e; - c[b + (f + 20) >> 2] = 0; - c[b + (f + 16) >> 2] = 0; - j = c[639] | 0; - g = 1 << e; - if ((j & g | 0) == 0) { - c[639] = j | g; - c[a >> 2] = h; - c[b + (f + 24) >> 2] = a; - c[b + (f + 12) >> 2] = h; - c[b + (f + 8) >> 2] = h; - break - } - a = c[a >> 2] | 0; - if ((e | 0) == 31) { - e = 0 - } else { - e = 25 - (e >>> 1) | 0 - } - l: do { - if ((c[a + 4 >> 2] & -8 | 0) != (i | 0)) { - j = i << e; - while (1) { - g = a + 16 + (j >>> 31 << 2) | 0; - e = c[g >> 2] | 0; - if ((e | 0) == 0) { - break - } - if ((c[e + 4 >> 2] & -8 | 0) == (i | 0)) { - a = e; - break l - } else { - a = e; - j = j << 1 - } - } - if (g >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } else { - c[g >> 2] = h; - c[b + (f + 24) >> 2] = a; - c[b + (f + 12) >> 2] = h; - c[b + (f + 8) >> 2] = h; - break j - } - } - } while (0); - e = a + 8 | 0; - g = c[e >> 2] | 0; - q = c[642] | 0; - if (a >>> 0 >= q >>> 0 & g >>> 0 >= q >>> 0) { - c[g + 12 >> 2] = h; - c[e >> 2] = h; - c[b + (f + 8) >> 2] = g; - c[b + (f + 12) >> 2] = a; - c[b + (f + 24) >> 2] = 0; - break - } else { - ma(); - return 0 - } - } - } while (0); - q = b + (d | 8) | 0; - return q | 0 - } - } while (0); - d = f; - j = 3e3; - while (1) { - i = c[j >> 2] | 0; - if (i >>> 0 <= d >>> 0) { - h = c[j + 4 >> 2] | 0; - g = i + h | 0; - if (g >>> 0 > d >>> 0) { - break - } - } - j = c[j + 8 >> 2] | 0 - } - j = i + (h - 39) | 0; - if ((j & 7 | 0) == 0) { - j = 0 - } else { - j = -j & 7 - } - h = i + (h - 47 + j) | 0; - h = h >>> 0 < (f + 16 | 0) >>> 0 ? d : h; - i = h + 8 | 0; - j = b + 8 | 0; - if ((j & 7 | 0) == 0) { - j = 0 - } else { - j = -j & 7 - } - q = e - 40 - j | 0; - c[644] = b + j; - c[641] = q; - c[b + (j + 4) >> 2] = q | 1; - c[b + (e - 36) >> 2] = 40; - c[645] = c[636]; - c[h + 4 >> 2] = 27; - c[i >> 2] = c[750]; - c[i + 4 >> 2] = c[751]; - c[i + 8 >> 2] = c[752]; - c[i + 12 >> 2] = c[753]; - c[750] = b; - c[751] = e; - c[753] = 0; - c[752] = i; - e = h + 28 | 0; - c[e >> 2] = 7; - if ((h + 32 | 0) >>> 0 < g >>> 0) { - while (1) { - b = e + 4 | 0; - c[b >> 2] = 7; - if ((e + 8 | 0) >>> 0 < g >>> 0) { - e = b - } else { - break - } - } - } - if ((h | 0) == (d | 0)) { - break - } - e = h - f | 0; - g = d + (e + 4) | 0; - c[g >> 2] = c[g >> 2] & -2; - c[f + 4 >> 2] = e | 1; - c[d + e >> 2] = e; - g = e >>> 3; - if (e >>> 0 < 256 >>> 0) { - d = g << 1; - b = 2592 + (d << 2) | 0; - e = c[638] | 0; - g = 1 << g; - do { - if ((e & g | 0) == 0) { - c[638] = e | g; - e = b; - d = 2592 + (d + 2 << 2) | 0 - } else { - d = 2592 + (d + 2 << 2) | 0; - e = c[d >> 2] | 0; - if (e >>> 0 >= (c[642] | 0) >>> 0) { - break - } - ma(); - return 0 - } - } while (0); - c[d >> 2] = f; - c[e + 12 >> 2] = f; - c[f + 8 >> 2] = e; - c[f + 12 >> 2] = b; - break - } - b = f; - d = e >>> 8; - do { - if ((d | 0) == 0) { - d = 0 - } else { - if (e >>> 0 > 16777215 >>> 0) { - d = 31; - break - } - p = (d + 1048320 | 0) >>> 16 & 8; - q = d << p; - o = (q + 520192 | 0) >>> 16 & 4; - q = q << o; - d = (q + 245760 | 0) >>> 16 & 2; - d = 14 - (o | p | d) + (q << d >>> 15) | 0; - d = e >>> ((d + 7 | 0) >>> 0) & 1 | d << 1 - } - } while (0); - g = 2856 + (d << 2) | 0; - c[f + 28 >> 2] = d; - c[f + 20 >> 2] = 0; - c[f + 16 >> 2] = 0; - i = c[639] | 0; - h = 1 << d; - if ((i & h | 0) == 0) { - c[639] = i | h; - c[g >> 2] = b; - c[f + 24 >> 2] = g; - c[f + 12 >> 2] = f; - c[f + 8 >> 2] = f; - break - } - i = c[g >> 2] | 0; - if ((d | 0) == 31) { - g = 0 - } else { - g = 25 - (d >>> 1) | 0 - } - m: do { - if ((c[i + 4 >> 2] & -8 | 0) != (e | 0)) { - d = i; - h = e << g; - while (1) { - g = d + 16 + (h >>> 31 << 2) | 0; - i = c[g >> 2] | 0; - if ((i | 0) == 0) { - break - } - if ((c[i + 4 >> 2] & -8 | 0) == (e | 0)) { - break m - } else { - d = i; - h = h << 1 - } - } - if (g >>> 0 < (c[642] | 0) >>> 0) { - ma(); - return 0 - } else { - c[g >> 2] = b; - c[f + 24 >> 2] = d; - c[f + 12 >> 2] = f; - c[f + 8 >> 2] = f; - break i - } - } - } while (0); - d = i + 8 | 0; - e = c[d >> 2] | 0; - q = c[642] | 0; - if (i >>> 0 >= q >>> 0 & e >>> 0 >= q >>> 0) { - c[e + 12 >> 2] = b; - c[d >> 2] = b; - c[f + 8 >> 2] = e; - c[f + 12 >> 2] = i; - c[f + 24 >> 2] = 0; - break - } else { - ma(); - return 0 - } - } - } while (0); - b = c[641] | 0; - if (b >>> 0 <= a >>> 0) { - break - } - o = b - a | 0; - c[641] = o; - q = c[644] | 0; - p = q; - c[644] = p + a; - c[p + (a + 4) >> 2] = o | 1; - c[q + 4 >> 2] = a | 3; - q = q + 8 | 0; - return q | 0 - } - } while (0); - c[(Ha() | 0) >> 2] = 12; - q = 0; - return q | 0 - } - - function cc(a) { - a = a | 0; - var b = 0, - d = 0, - e = 0, - f = 0, - g = 0, - h = 0, - i = 0, - j = 0, - k = 0, - l = 0, - m = 0, - n = 0, - o = 0, - p = 0, - q = 0, - r = 0, - s = 0, - t = 0, - u = 0, - v = 0, - w = 0; - if ((a | 0) == 0) { - return - } - p = a - 8 | 0; - r = p; - q = c[642] | 0; - if (p >>> 0 < q >>> 0) { - ma() - } - n = c[a - 4 >> 2] | 0; - m = n & 3; - if ((m | 0) == 1) { - ma() - } - h = n & -8; - k = a + (h - 8) | 0; - i = k; - a: do { - if ((n & 1 | 0) == 0) { - u = c[p >> 2] | 0; - if ((m | 0) == 0) { - return - } - p = -8 - u | 0; - r = a + p | 0; - m = r; - n = u + h | 0; - if (r >>> 0 < q >>> 0) { - ma() - } - if ((m | 0) == (c[643] | 0)) { - b = a + (h - 4) | 0; - if ((c[b >> 2] & 3 | 0) != 3) { - b = m; - l = n; - break - } - c[640] = n; - c[b >> 2] = c[b >> 2] & -2; - c[a + (p + 4) >> 2] = n | 1; - c[k >> 2] = n; - return - } - t = u >>> 3; - if (u >>> 0 < 256 >>> 0) { - b = c[a + (p + 8) >> 2] | 0; - l = c[a + (p + 12) >> 2] | 0; - o = 2592 + (t << 1 << 2) | 0; - do { - if ((b | 0) != (o | 0)) { - if (b >>> 0 < q >>> 0) { - ma() - } - if ((c[b + 12 >> 2] | 0) == (m | 0)) { - break - } - ma() - } - } while (0); - if ((l | 0) == (b | 0)) { - c[638] = c[638] & ~(1 << t); - b = m; - l = n; - break - } - do { - if ((l | 0) == (o | 0)) { - s = l + 8 | 0 - } else { - if (l >>> 0 < q >>> 0) { - ma() - } - o = l + 8 | 0; - if ((c[o >> 2] | 0) == (m | 0)) { - s = o; - break - } - ma() - } - } while (0); - c[b + 12 >> 2] = l; - c[s >> 2] = b; - b = m; - l = n; - break - } - s = c[a + (p + 24) >> 2] | 0; - u = c[a + (p + 12) >> 2] | 0; - do { - if ((u | 0) == (r | 0)) { - u = a + (p + 20) | 0; - t = c[u >> 2] | 0; - if ((t | 0) == 0) { - u = a + (p + 16) | 0; - t = c[u >> 2] | 0; - if ((t | 0) == 0) { - o = 0; - break - } - } - while (1) { - w = t + 20 | 0; - v = c[w >> 2] | 0; - if ((v | 0) != 0) { - t = v; - u = w; - continue - } - v = t + 16 | 0; - w = c[v >> 2] | 0; - if ((w | 0) == 0) { - break - } else { - t = w; - u = v - } - } - if (u >>> 0 < q >>> 0) { - ma() - } else { - c[u >> 2] = 0; - o = t; - break - } - } else { - t = c[a + (p + 8) >> 2] | 0; - if (t >>> 0 < q >>> 0) { - ma() - } - q = t + 12 | 0; - if ((c[q >> 2] | 0) != (r | 0)) { - ma() - } - v = u + 8 | 0; - if ((c[v >> 2] | 0) == (r | 0)) { - c[q >> 2] = u; - c[v >> 2] = t; - o = u; - break - } else { - ma() - } - } - } while (0); - if ((s | 0) == 0) { - b = m; - l = n; - break - } - q = a + (p + 28) | 0; - t = 2856 + (c[q >> 2] << 2) | 0; - do { - if ((r | 0) == (c[t >> 2] | 0)) { - c[t >> 2] = o; - if ((o | 0) != 0) { - break - } - c[639] = c[639] & ~(1 << c[q >> 2]); - b = m; - l = n; - break a - } else { - if (s >>> 0 < (c[642] | 0) >>> 0) { - ma() - } - q = s + 16 | 0; - if ((c[q >> 2] | 0) == (r | 0)) { - c[q >> 2] = o - } else { - c[s + 20 >> 2] = o - } - if ((o | 0) == 0) { - b = m; - l = n; - break a - } - } - } while (0); - if (o >>> 0 < (c[642] | 0) >>> 0) { - ma() - } - c[o + 24 >> 2] = s; - q = c[a + (p + 16) >> 2] | 0; - do { - if ((q | 0) != 0) { - if (q >>> 0 < (c[642] | 0) >>> 0) { - ma() - } else { - c[o + 16 >> 2] = q; - c[q + 24 >> 2] = o; - break - } - } - } while (0); - p = c[a + (p + 20) >> 2] | 0; - if ((p | 0) == 0) { - b = m; - l = n; - break - } - if (p >>> 0 < (c[642] | 0) >>> 0) { - ma() - } else { - c[o + 20 >> 2] = p; - c[p + 24 >> 2] = o; - b = m; - l = n; - break - } - } else { - b = r; - l = h - } - } while (0); - m = b; - if (m >>> 0 >= k >>> 0) { - ma() - } - n = a + (h - 4) | 0; - o = c[n >> 2] | 0; - if ((o & 1 | 0) == 0) { - ma() - } - do { - if ((o & 2 | 0) == 0) { - if ((i | 0) == (c[644] | 0)) { - w = (c[641] | 0) + l | 0; - c[641] = w; - c[644] = b; - c[b + 4 >> 2] = w | 1; - if ((b | 0) != (c[643] | 0)) { - return - } - c[643] = 0; - c[640] = 0; - return - } - if ((i | 0) == (c[643] | 0)) { - w = (c[640] | 0) + l | 0; - c[640] = w; - c[643] = b; - c[b + 4 >> 2] = w | 1; - c[m + w >> 2] = w; - return - } - l = (o & -8) + l | 0; - n = o >>> 3; - b: do { - if (o >>> 0 < 256 >>> 0) { - g = c[a + h >> 2] | 0; - h = c[a + (h | 4) >> 2] | 0; - a = 2592 + (n << 1 << 2) | 0; - do { - if ((g | 0) != (a | 0)) { - if (g >>> 0 < (c[642] | 0) >>> 0) { - ma() - } - if ((c[g + 12 >> 2] | 0) == (i | 0)) { - break - } - ma() - } - } while (0); - if ((h | 0) == (g | 0)) { - c[638] = c[638] & ~(1 << n); - break - } - do { - if ((h | 0) == (a | 0)) { - j = h + 8 | 0 - } else { - if (h >>> 0 < (c[642] | 0) >>> 0) { - ma() - } - a = h + 8 | 0; - if ((c[a >> 2] | 0) == (i | 0)) { - j = a; - break - } - ma() - } - } while (0); - c[g + 12 >> 2] = h; - c[j >> 2] = g - } else { - i = c[a + (h + 16) >> 2] | 0; - n = c[a + (h | 4) >> 2] | 0; - do { - if ((n | 0) == (k | 0)) { - n = a + (h + 12) | 0; - j = c[n >> 2] | 0; - if ((j | 0) == 0) { - n = a + (h + 8) | 0; - j = c[n >> 2] | 0; - if ((j | 0) == 0) { - g = 0; - break - } - } - while (1) { - p = j + 20 | 0; - o = c[p >> 2] | 0; - if ((o | 0) != 0) { - j = o; - n = p; - continue - } - o = j + 16 | 0; - p = c[o >> 2] | 0; - if ((p | 0) == 0) { - break - } else { - j = p; - n = o - } - } - if (n >>> 0 < (c[642] | 0) >>> 0) { - ma() - } else { - c[n >> 2] = 0; - g = j; - break - } - } else { - o = c[a + h >> 2] | 0; - if (o >>> 0 < (c[642] | 0) >>> 0) { - ma() - } - p = o + 12 | 0; - if ((c[p >> 2] | 0) != (k | 0)) { - ma() - } - j = n + 8 | 0; - if ((c[j >> 2] | 0) == (k | 0)) { - c[p >> 2] = n; - c[j >> 2] = o; - g = n; - break - } else { - ma() - } - } - } while (0); - if ((i | 0) == 0) { - break - } - j = a + (h + 20) | 0; - n = 2856 + (c[j >> 2] << 2) | 0; - do { - if ((k | 0) == (c[n >> 2] | 0)) { - c[n >> 2] = g; - if ((g | 0) != 0) { - break - } - c[639] = c[639] & ~(1 << c[j >> 2]); - break b - } else { - if (i >>> 0 < (c[642] | 0) >>> 0) { - ma() - } - j = i + 16 | 0; - if ((c[j >> 2] | 0) == (k | 0)) { - c[j >> 2] = g - } else { - c[i + 20 >> 2] = g - } - if ((g | 0) == 0) { - break b - } - } - } while (0); - if (g >>> 0 < (c[642] | 0) >>> 0) { - ma() - } - c[g + 24 >> 2] = i; - i = c[a + (h + 8) >> 2] | 0; - do { - if ((i | 0) != 0) { - if (i >>> 0 < (c[642] | 0) >>> 0) { - ma() - } else { - c[g + 16 >> 2] = i; - c[i + 24 >> 2] = g; - break - } - } - } while (0); - h = c[a + (h + 12) >> 2] | 0; - if ((h | 0) == 0) { - break - } - if (h >>> 0 < (c[642] | 0) >>> 0) { - ma() - } else { - c[g + 20 >> 2] = h; - c[h + 24 >> 2] = g; - break - } - } - } while (0); - c[b + 4 >> 2] = l | 1; - c[m + l >> 2] = l; - if ((b | 0) != (c[643] | 0)) { - break - } - c[640] = l; - return - } else { - c[n >> 2] = o & -2; - c[b + 4 >> 2] = l | 1; - c[m + l >> 2] = l - } - } while (0); - g = l >>> 3; - if (l >>> 0 < 256 >>> 0) { - a = g << 1; - d = 2592 + (a << 2) | 0; - h = c[638] | 0; - g = 1 << g; - do { - if ((h & g | 0) == 0) { - c[638] = h | g; - f = d; - e = 2592 + (a + 2 << 2) | 0 - } else { - h = 2592 + (a + 2 << 2) | 0; - g = c[h >> 2] | 0; - if (g >>> 0 >= (c[642] | 0) >>> 0) { - f = g; - e = h; - break - } - ma() - } - } while (0); - c[e >> 2] = b; - c[f + 12 >> 2] = b; - c[b + 8 >> 2] = f; - c[b + 12 >> 2] = d; - return - } - e = b; - f = l >>> 8; - do { - if ((f | 0) == 0) { - f = 0 - } else { - if (l >>> 0 > 16777215 >>> 0) { - f = 31; - break - } - v = (f + 1048320 | 0) >>> 16 & 8; - w = f << v; - u = (w + 520192 | 0) >>> 16 & 4; - w = w << u; - f = (w + 245760 | 0) >>> 16 & 2; - f = 14 - (u | v | f) + (w << f >>> 15) | 0; - f = l >>> ((f + 7 | 0) >>> 0) & 1 | f << 1 - } - } while (0); - g = 2856 + (f << 2) | 0; - c[b + 28 >> 2] = f; - c[b + 20 >> 2] = 0; - c[b + 16 >> 2] = 0; - a = c[639] | 0; - h = 1 << f; - c: do { - if ((a & h | 0) == 0) { - c[639] = a | h; - c[g >> 2] = e; - c[b + 24 >> 2] = g; - c[b + 12 >> 2] = b; - c[b + 8 >> 2] = b - } else { - h = c[g >> 2] | 0; - if ((f | 0) == 31) { - g = 0 - } else { - g = 25 - (f >>> 1) | 0 - } - d: do { - if ((c[h + 4 >> 2] & -8 | 0) == (l | 0)) { - d = h - } else { - f = h; - h = l << g; - while (1) { - a = f + 16 + (h >>> 31 << 2) | 0; - g = c[a >> 2] | 0; - if ((g | 0) == 0) { - break - } - if ((c[g + 4 >> 2] & -8 | 0) == (l | 0)) { - d = g; - break d - } else { - f = g; - h = h << 1 - } - } - if (a >>> 0 < (c[642] | 0) >>> 0) { - ma() - } else { - c[a >> 2] = e; - c[b + 24 >> 2] = f; - c[b + 12 >> 2] = b; - c[b + 8 >> 2] = b; - break c - } - } - } while (0); - f = d + 8 | 0; - g = c[f >> 2] | 0; - w = c[642] | 0; - if (d >>> 0 >= w >>> 0 & g >>> 0 >= w >>> 0) { - c[g + 12 >> 2] = e; - c[f >> 2] = e; - c[b + 8 >> 2] = g; - c[b + 12 >> 2] = d; - c[b + 24 >> 2] = 0; - break - } else { - ma() - } - } - } while (0); - w = (c[646] | 0) - 1 | 0; - c[646] = w; - if ((w | 0) == 0) { - b = 3008 - } else { - return - } - while (1) { - b = c[b >> 2] | 0; - if ((b | 0) == 0) { - break - } else { - b = b + 8 | 0 - } - } - c[646] = -1; - return - } - - function dc(b, d, e) { - b = b | 0; - d = d | 0; - e = e | 0; - var f = 0; - f = b | 0; - if ((b & 3) == (d & 3)) { - while (b & 3) { - if ((e | 0) == 0) return f | 0; - a[b] = a[d] | 0; - b = b + 1 | 0; - d = d + 1 | 0; - e = e - 1 | 0 - } - while ((e | 0) >= 4) { - c[b >> 2] = c[d >> 2]; - b = b + 4 | 0; - d = d + 4 | 0; - e = e - 4 | 0 - } - } - while ((e | 0) > 0) { - a[b] = a[d] | 0; - b = b + 1 | 0; - d = d + 1 | 0; - e = e - 1 | 0 - } - return f | 0 - } - - function ec(b) { - b = b | 0; - var c = 0; - c = b; - while (a[c] | 0) { - c = c + 1 | 0 - } - return c - b | 0 - } - - function fc(b, d, e) { - b = b | 0; - d = d | 0; - e = e | 0; - var f = 0, - g = 0, - h = 0, - i = 0; - f = b + e | 0; - if ((e | 0) >= 20) { - d = d & 255; - i = b & 3; - h = d | d << 8 | d << 16 | d << 24; - g = f & ~3; - if (i) { - i = b + 4 - i | 0; - while ((b | 0) < (i | 0)) { - a[b] = d; - b = b + 1 | 0 - } - } - while ((b | 0) < (g | 0)) { - c[b >> 2] = h; - b = b + 4 | 0 - } - } - while ((b | 0) < (f | 0)) { - a[b] = d; - b = b + 1 | 0 - } - return b - e | 0 - } - - function gc(a, b) { - a = a | 0; - b = b | 0; - Ma[a & 1](b | 0) - } - - function hc(a, b, c) { - a = a | 0; - b = b | 0; - c = c | 0; - Na[a & 1](b | 0, c | 0) - } - - function ic(a, b) { - a = a | 0; - b = b | 0; - return Oa[a & 1](b | 0) | 0 - } - - function jc(a, b) { - a = a | 0; - b = +b; - return +Pa[a & 7](+b) - } - - function kc(a) { - a = a | 0; - Qa[a & 1]() - } - - function lc(a, b, c) { - a = a | 0; - b = b | 0; - c = c | 0; - return Ra[a & 1](b | 0, c | 0) | 0 - } - - function mc(a) { - a = a | 0; - _(0) - } - - function nc(a, b) { - a = a | 0; - b = b | 0; - _(1) - } - - function oc(a) { - a = a | 0; - _(2); - return 0 - } - - function pc(a) { - a = +a; - _(3); - return 0.0 - } - - function qc() { - _(4) - } - - function rc(a, b) { - a = a | 0; - b = b | 0; - _(5); - return 0 - } - - - - - // EMSCRIPTEN_END_FUNCS - var Ma = [mc, mc]; - var Na = [nc, nc]; - var Oa = [oc, oc]; - var Pa = [pc, pc, mb, pc, lb, pc, kb, pc]; - var Qa = [qc, qc]; - var Ra = [rc, rc]; - return { - _firdes_get_window_from_string: ib, - _strlen: ec, - _firdes_lowpass_f: nb, - _next_pow2: Mb, - _gain_ff: Kb, - _shift_table_cc: tb, - _firdes_wkernel_hamming: lb, - _fir_decimate_cc: ub, - _encode_ima_adpcm_i16_u8: _b, - _convert_i16_f: Sb, - _shift_addition_init: Vb, - _decimating_shift_addition_cc: Yb, - _decimating_shift_addition_init: Xb, - _shift_table_init: rb, - _convert_u8_f: Qb, - _fastagc_ff: Db, - _memset: fc, - _fmdemod_quadri_cf: Gb, - _amdemod_cf: zb, - _log2n: Lb, - _convert_f_u8: Rb, - _rational_resampler_get_lowpass_f: wb, - _apply_fir_fft_cc: yb, - _fractional_decimator_ff: xb, - _amdemod_estimator_cf: Ab, - _limit_ff: Jb, - _fmdemod_atan_cf: Eb, - _fmdemod_quadri_novect_cf: Fb, - _shift_addition_cc: Ub, - _firdes_wkernel_blackman: kb, - _deemphasis_wfm_ff: Hb, - _firdes_filter_len: pb, - _decode_ima_adpcm_u8_i16: $b, - _firdes_wkernel_boxcar: mb, - _shift_math_cc: qb, - _agc_ff: Zb, - _dcblock_ff: Bb, - _free: cc, - _fastdcblock_ff: Cb, - _firdes_get_string_from_window: jb, - _rational_resampler_ff: vb, - _convert_f_i16: Tb, - _logpower_cf: Pb, - _shift_addition_cc_test: Wb, - _malloc: bc, - _memcpy: dc, - _deemphasis_nfm_ff: Ib, - _apply_window_c: Nb, - _shift_table_deinit: sb, - _apply_window_f: Ob, - _firdes_bandpass_c: ob, - runPostSets: gb, - stackAlloc: Sa, - stackSave: Ta, - stackRestore: Ua, - setThrew: Va, - setTempRet0: Ya, - setTempRet1: Za, - setTempRet2: _a, - setTempRet3: $a, - setTempRet4: ab, - setTempRet5: bb, - setTempRet6: cb, - setTempRet7: db, - setTempRet8: eb, - setTempRet9: fb, - dynCall_vi: gc, - dynCall_vii: hc, - dynCall_ii: ic, - dynCall_ff: jc, - dynCall_v: kc, - dynCall_iii: lc - } -}) - - -// EMSCRIPTEN_END_ASM -({ - "Math": Math, - "Int8Array": Int8Array, - "Int16Array": Int16Array, - "Int32Array": Int32Array, - "Uint8Array": Uint8Array, - "Uint16Array": Uint16Array, - "Uint32Array": Uint32Array, - "Float32Array": Float32Array, - "Float64Array": Float64Array -}, { - "abort": abort, - "assert": assert, - "asmPrintInt": asmPrintInt, - "asmPrintFloat": asmPrintFloat, - "min": Math_min, - "invoke_vi": invoke_vi, - "invoke_vii": invoke_vii, - "invoke_ii": invoke_ii, - "invoke_ff": invoke_ff, - "invoke_v": invoke_v, - "invoke_iii": invoke_iii, - "_strncmp": _strncmp, - "_fabsf": _fabsf, - "_sysconf": _sysconf, - "_abort": _abort, - "_fprintf": _fprintf, - "_printf": _printf, - "_fflush": _fflush, - "__reallyNegative": __reallyNegative, - "_sqrtf": _sqrtf, - "_fputc": _fputc, - "_fabs": _fabs, - "___setErrNo": ___setErrNo, - "_fwrite": _fwrite, - "_send": _send, - "_write": _write, - "_fputs": _fputs, - "_log10": _log10, - "_sin": _sin, - "_ceilf": _ceilf, - "__formatString": __formatString, - "_cos": _cos, - "_pwrite": _pwrite, - "_puts": _puts, - "_sbrk": _sbrk, - "___errno_location": ___errno_location, - "_atan2": _atan2, - "_time": _time, - "_strcmp": _strcmp, - "STACKTOP": STACKTOP, - "STACK_MAX": STACK_MAX, - "tempDoublePtr": tempDoublePtr, - "ABORT": ABORT, - "NaN": NaN, - "Infinity": Infinity -}, buffer); -var _firdes_get_window_from_string = Module["_firdes_get_window_from_string"] = asm["_firdes_get_window_from_string"]; -var _strlen = Module["_strlen"] = asm["_strlen"]; -var _firdes_lowpass_f = Module["_firdes_lowpass_f"] = asm["_firdes_lowpass_f"]; -var _next_pow2 = Module["_next_pow2"] = asm["_next_pow2"]; -var _gain_ff = Module["_gain_ff"] = asm["_gain_ff"]; -var _shift_table_cc = Module["_shift_table_cc"] = asm["_shift_table_cc"]; -var _firdes_wkernel_hamming = Module["_firdes_wkernel_hamming"] = asm["_firdes_wkernel_hamming"]; -var _fir_decimate_cc = Module["_fir_decimate_cc"] = asm["_fir_decimate_cc"]; -var _encode_ima_adpcm_i16_u8 = Module["_encode_ima_adpcm_i16_u8"] = asm["_encode_ima_adpcm_i16_u8"]; -var _convert_i16_f = Module["_convert_i16_f"] = asm["_convert_i16_f"]; -var _shift_addition_init = Module["_shift_addition_init"] = asm["_shift_addition_init"]; -var _decimating_shift_addition_cc = Module["_decimating_shift_addition_cc"] = asm["_decimating_shift_addition_cc"]; -var _decimating_shift_addition_init = Module["_decimating_shift_addition_init"] = asm["_decimating_shift_addition_init"]; -var _shift_table_init = Module["_shift_table_init"] = asm["_shift_table_init"]; -var _convert_u8_f = Module["_convert_u8_f"] = asm["_convert_u8_f"]; -var _fastagc_ff = Module["_fastagc_ff"] = asm["_fastagc_ff"]; -var _memset = Module["_memset"] = asm["_memset"]; -var _fmdemod_quadri_cf = Module["_fmdemod_quadri_cf"] = asm["_fmdemod_quadri_cf"]; -var _amdemod_cf = Module["_amdemod_cf"] = asm["_amdemod_cf"]; -var _log2n = Module["_log2n"] = asm["_log2n"]; -var _convert_f_u8 = Module["_convert_f_u8"] = asm["_convert_f_u8"]; -var _rational_resampler_get_lowpass_f = Module["_rational_resampler_get_lowpass_f"] = asm["_rational_resampler_get_lowpass_f"]; -var _apply_fir_fft_cc = Module["_apply_fir_fft_cc"] = asm["_apply_fir_fft_cc"]; -var _fractional_decimator_ff = Module["_fractional_decimator_ff"] = asm["_fractional_decimator_ff"]; -var _amdemod_estimator_cf = Module["_amdemod_estimator_cf"] = asm["_amdemod_estimator_cf"]; -var _limit_ff = Module["_limit_ff"] = asm["_limit_ff"]; -var _fmdemod_atan_cf = Module["_fmdemod_atan_cf"] = asm["_fmdemod_atan_cf"]; -var _fmdemod_quadri_novect_cf = Module["_fmdemod_quadri_novect_cf"] = asm["_fmdemod_quadri_novect_cf"]; -var _shift_addition_cc = Module["_shift_addition_cc"] = asm["_shift_addition_cc"]; -var _firdes_wkernel_blackman = Module["_firdes_wkernel_blackman"] = asm["_firdes_wkernel_blackman"]; -var _deemphasis_wfm_ff = Module["_deemphasis_wfm_ff"] = asm["_deemphasis_wfm_ff"]; -var _firdes_filter_len = Module["_firdes_filter_len"] = asm["_firdes_filter_len"]; -var _decode_ima_adpcm_u8_i16 = Module["_decode_ima_adpcm_u8_i16"] = asm["_decode_ima_adpcm_u8_i16"]; -var _firdes_wkernel_boxcar = Module["_firdes_wkernel_boxcar"] = asm["_firdes_wkernel_boxcar"]; -var _shift_math_cc = Module["_shift_math_cc"] = asm["_shift_math_cc"]; -var _agc_ff = Module["_agc_ff"] = asm["_agc_ff"]; -var _dcblock_ff = Module["_dcblock_ff"] = asm["_dcblock_ff"]; -var _free = Module["_free"] = asm["_free"]; -var _fastdcblock_ff = Module["_fastdcblock_ff"] = asm["_fastdcblock_ff"]; -var _firdes_get_string_from_window = Module["_firdes_get_string_from_window"] = asm["_firdes_get_string_from_window"]; -var _rational_resampler_ff = Module["_rational_resampler_ff"] = asm["_rational_resampler_ff"]; -var _convert_f_i16 = Module["_convert_f_i16"] = asm["_convert_f_i16"]; -var _logpower_cf = Module["_logpower_cf"] = asm["_logpower_cf"]; -var _shift_addition_cc_test = Module["_shift_addition_cc_test"] = asm["_shift_addition_cc_test"]; -var _malloc = Module["_malloc"] = asm["_malloc"]; -var _memcpy = Module["_memcpy"] = asm["_memcpy"]; -var _deemphasis_nfm_ff = Module["_deemphasis_nfm_ff"] = asm["_deemphasis_nfm_ff"]; -var _apply_window_c = Module["_apply_window_c"] = asm["_apply_window_c"]; -var _shift_table_deinit = Module["_shift_table_deinit"] = asm["_shift_table_deinit"]; -var _apply_window_f = Module["_apply_window_f"] = asm["_apply_window_f"]; -var _firdes_bandpass_c = Module["_firdes_bandpass_c"] = asm["_firdes_bandpass_c"]; -var runPostSets = Module["runPostSets"] = asm["runPostSets"]; -var dynCall_vi = Module["dynCall_vi"] = asm["dynCall_vi"]; -var dynCall_vii = Module["dynCall_vii"] = asm["dynCall_vii"]; -var dynCall_ii = Module["dynCall_ii"] = asm["dynCall_ii"]; -var dynCall_ff = Module["dynCall_ff"] = asm["dynCall_ff"]; -var dynCall_v = Module["dynCall_v"] = asm["dynCall_v"]; -var dynCall_iii = Module["dynCall_iii"] = asm["dynCall_iii"]; - -Runtime.stackAlloc = function(size) { - return asm['stackAlloc'](size) -}; -Runtime.stackSave = function() { - return asm['stackSave']() -}; -Runtime.stackRestore = function(top) { - asm['stackRestore'](top) -}; - -// Warning: printing of i64 values may be slightly rounded! No deep i64 math used, so precise i64 code not included -var i64Math = null; - -// === Auto-generated postamble setup entry stuff === - -if (memoryInitializer) { - function applyData(data) { - HEAPU8.set(data, STATIC_BASE); - } - if (ENVIRONMENT_IS_NODE || ENVIRONMENT_IS_SHELL) { - applyData(Module['readBinary'](memoryInitializer)); - } else { - addRunDependency('memory initializer'); - Browser.asyncLoad(memoryInitializer, function(data) { - applyData(data); - removeRunDependency('memory initializer'); - }, function(data) { - throw 'could not load memory initializer ' + memoryInitializer; - }); - } -} - -function ExitStatus(status) { - this.name = "ExitStatus"; - this.message = "Program terminated with exit(" + status + ")"; - this.status = status; -}; -ExitStatus.prototype = new Error(); -ExitStatus.prototype.constructor = ExitStatus; - -var initialStackTop; -var preloadStartTime = null; -var calledMain = false; - -dependenciesFulfilled = function runCaller() { - // If run has never been called, and we should call run (INVOKE_RUN is true, and Module.noInitialRun is not false) - if (!Module['calledRun'] && shouldRunNow) run(); - if (!Module['calledRun']) dependenciesFulfilled = runCaller; // try this again later, after new deps are fulfilled -} - -Module['callMain'] = Module.callMain = function callMain(args) { - assert(runDependencies == 0, 'cannot call main when async dependencies remain! (listen on __ATMAIN__)'); - assert(__ATPRERUN__.length == 0, 'cannot call main when preRun functions remain to be called'); - - args = args || []; - - if (ENVIRONMENT_IS_WEB && preloadStartTime !== null) { - Module.printErr('preload time: ' + (Date.now() - preloadStartTime) + ' ms'); - } - - ensureInitRuntime(); - - var argc = args.length + 1; - - function pad() { - for (var i = 0; i < 4 - 1; i++) { - argv.push(0); - } - } - var argv = [allocate(intArrayFromString("/bin/this.program"), 'i8', ALLOC_NORMAL)]; - pad(); - for (var i = 0; i < argc - 1; i = i + 1) { - argv.push(allocate(intArrayFromString(args[i]), 'i8', ALLOC_NORMAL)); - pad(); - } - argv.push(0); - argv = allocate(argv, 'i32', ALLOC_NORMAL); - - initialStackTop = STACKTOP; - - try { - - var ret = Module['_main'](argc, argv, 0); - - - // if we're not running an evented main loop, it's time to exit - if (!Module['noExitRuntime']) { - exit(ret); - } - } catch (e) { - if (e instanceof ExitStatus) { - // exit() throws this once it's done to make sure execution - // has been stopped completely - return; - } else if (e == 'SimulateInfiniteLoop') { - // running an evented main loop, don't immediately exit - Module['noExitRuntime'] = true; - return; - } else { - if (e && typeof e === 'object' && e.stack) Module.printErr('exception thrown: ' + [e, e.stack]); - throw e; - } - } finally { - calledMain = true; - } -} - - - - -function run(args) { - args = args || Module['arguments']; - - if (preloadStartTime === null) preloadStartTime = Date.now(); - - if (runDependencies > 0) { - Module.printErr('run() called, but dependencies remain, so not running'); - return; - } - - preRun(); - - if (runDependencies > 0) return; // a preRun added a dependency, run will be called later - if (Module['calledRun']) return; // run may have just been called through dependencies being fulfilled just in this very frame - - function doRun() { - if (Module['calledRun']) return; // run may have just been called while the async setStatus time below was happening - Module['calledRun'] = true; - - ensureInitRuntime(); - - preMain(); - - if (Module['_main'] && shouldRunNow) { - Module['callMain'](args); - } - - postRun(); - } - - if (Module['setStatus']) { - Module['setStatus']('Running...'); - setTimeout(function() { - setTimeout(function() { - Module['setStatus'](''); - }, 1); - if (!ABORT) doRun(); - }, 1); - } else { - doRun(); - } -} -Module['run'] = Module.run = run; - -function exit(status) { - ABORT = true; - EXITSTATUS = status; - STACKTOP = initialStackTop; - - // exit the runtime - exitRuntime(); - - // TODO We should handle this differently based on environment. - // In the browser, the best we can do is throw an exception - // to halt execution, but in node we could process.exit and - // I'd imagine SM shell would have something equivalent. - // This would let us set a proper exit status (which - // would be great for checking test exit statuses). - // https://github.com/kripken/emscripten/issues/1371 - - // throw an exception to halt the current execution - throw new ExitStatus(status); -} -Module['exit'] = Module.exit = exit; - -function abort(text) { - if (text) { - Module.print(text); - Module.printErr(text); - } - - ABORT = true; - EXITSTATUS = 1; - - throw 'abort() at ' + stackTrace(); -} -Module['abort'] = Module.abort = abort; - -// {{PRE_RUN_ADDITIONS}} - -if (Module['preInit']) { - if (typeof Module['preInit'] == 'function') Module['preInit'] = [Module['preInit']]; - while (Module['preInit'].length > 0) { - Module['preInit'].pop()(); - } -} - -// shouldRunNow refers to calling main(), not run(). -var shouldRunNow = true; -if (Module['noInitialRun']) { - shouldRunNow = false; -} - -run(); - -// {{POST_RUN_ADDITIONS}} - - - - - - -// {{MODULE_ADDITIONS}} - - - - - - -// ========================================================== -// ========= / THE CODE COMPILED BY EMCC ENDS HERE ========== -// ========================================================== - -asm$ = { - malloc: function(type, size) { - real_size = size * type.BYTES_PER_ELEMENT; - pointer = Module._malloc(real_size); - heap = new Uint8Array(Module.HEAPU8.buffer, pointer, real_size); - return { - asm$: true, - ptr: heap.byteOffset, - free: function() { - Module._free(this.ptr); - }, - arr: new type(heap.buffer, heap.byteOffset, size), - size: size - }; - }, - cpy: function(dst, dst_offset, src, src_offset, size) { - if (typeof dst.asm$ != 'undefined') dst = dst.arr; - if (typeof src.asm$ != 'undefined') src = src.arr; - for (var i = 0; i < size; i++) - dst[dst_offset + i] = src[src_offset + i]; - } -}; - -// void firdes_lowpass_f(float *output, int length, float cutoff_rate, window_t window) -firdes_lowpass_f = Module.cwrap('firdes_lowpass_f', null, ['number', 'number', 'number', 'number']); - -// rational_resampler_ff_t rational_resampler_ff(float *input, float *output, int input_size, int interpolation, int decimation, float *taps, int taps_length, int last_taps_delay) -rational_resampler_ff = Module.cwrap('rational_resampler_ff', 'struct', ['number', 'number', 'number', 'number', 'number', 'number', 'number', 'number']); - - -rational_resampler_ff = function(pinput, poutput, input_length, interpolation, decimation, ptaps, taps_length, last_taps_delay) { - stackbase = STACKTOP; - STACKTOP += 4 * 3; - _rational_resampler_ff(stackbase, pinput, poutput, input_length, interpolation, decimation, ptaps, taps_length, last_taps_delay); - returnstruct = { - input_processed: getValue(stackbase, 'i32'), - output_size: getValue(stackbase + 4, 'i32'), - last_taps_delay: getValue(stackbase + 8, 'i32') - }; - STACKTOP = stackbase; - return returnstruct; -} - -sdrjs = {}; - -sdrjs.WINDOW_BOXCAR = 0; -sdrjs.WINDOW_BLACKMAN = 1; -sdrjs.WINDOW_HAMMING = 2; - -//this will be impportant whil converting arrays -//http://stackoverflow.com/questions/25839216/convert-float32array-to-int16array - -/*sdrjs.prototype.FirdesLowpassF=function(taps_length,transition_bw,window) -{ - this.calculate=function(){} - this.get_output=function(){} - this.get_output_heap=function(){} -};*/ - - -sdrjs.ConvertI16_F = function(i16data) { - var f32data = new Float32Array(i16data.length); - for (var i = 0; i < i16data.length; i++) f32data[i] = i16data[i] / 32768; - return f32data; -} - -ima_adpcm_codec = function(encode, pinput, poutput, input_length, state) { - myfunc = (encode) ? _encode_ima_adpcm_i16_u8 : _decode_ima_adpcm_u8_i16; - stackbase = STACKTOP; - STACKTOP += 4 * 2; //sizeof(int)*2 - myfunc(stackbase, pinput, poutput, input_length, state.ptr); - state.arr[0] = getValue(stackbase + 0, 'i32'); - state.arr[1] = getValue(stackbase + 4, 'i32'); - STACKTOP = stackbase; -}; - -sdrjs.ImaAdpcm = function() { - this.BUFSIZE = 1024 * 64; - this.ima_adpcm_state = asm$.malloc(Int32Array, 2); - this.i16_buffer = asm$.malloc(Int16Array, this.BUFSIZE * 2); - this.u8_buffer = asm$.malloc(Uint8Array, this.BUFSIZE); - this.ima_adpcm_state.arr[0] = 0; - this.ima_adpcm_state.arr[1] = 0; - - this.encode = function(data) { - //not_tested_yet - asm$.cpy(this.i16_buffer.arr, 0, data, 0, data.length); - ima_adpcm_codec(true, this.i16_buffer.ptr, this.u8_buffer.ptr, data.length, this.ima_adpcm_state); - out = new Uint8Array(data.length / 2); - asm$.cpy(out, 0, this.u8_buffer, 0, data.length / 2); - return out; - }; - - this.decode = function(data) { - asm$.cpy(this.u8_buffer.arr, 0, data, 0, data.length); - ima_adpcm_codec(false, this.u8_buffer.ptr, this.i16_buffer.ptr, data.length, this.ima_adpcm_state); - out = new Int16Array(data.length * 2); - asm$.cpy(out, 0, this.i16_buffer.arr, 0, data.length * 2); - return out; - }; - this.reset = function() { - this.ima_adpcm_state.arr[0] = this.ima_adpcm_state.arr[1] = 0 | 0; - } -}; - -sdrjs.REBUFFER_FIXED = 0; //rebuffer should return arrays of fixed size -sdrjs.REBUFFER_MAX = 1; //rebuffer should return arrays with a maximal size of the parameter size - -sdrjs.Rebuffer = function(size, mode) { - this.mode = mode; - this.size = size; - this.total_size = 0; - this.arrays = []; - this.last_arr = []; - this.last_arr_offset = 0; - this.push = function(data) { - this.total_size += data.length; - this.arrays.push(data); - }; - this.remaining = function() { - var fixed_bufs_num = Math.floor(this.total_size / this.size); - if (!this.mode) return fixed_bufs_num; - else return fixed_bufs_num + (!!(this.total_size - fixed_bufs_num * this.size)); //if REBUFFER_MAX, add one if we could return one more buffer (smaller than the fixed size) - }; - this.take = function() { - var a = this._take(); /*console.log(a);*/ - return a; - }; - this._take = function() { - var remain = this.size; - var offset = 0; - var obuf = new Float32Array(size); - //console.log("==== get new obuf ====", size); - while (remain) { - if (this.last_arr_offset == this.last_arr.length) { - if (this.arrays.length == 0) { - //console.log("this should not happen"); - if (this.mode) //REBUFFER_MAX - { - this.total_size = 0; - return obuf.subarray(0, offset); - } else return new Float32Array(0); //REBUFFER_FIXED - } - //console.log("pick new last_arr"); - this.last_arr = this.arrays.shift(); - this.last_arr_offset = 0; - } - var rwithin = this.last_arr.length - this.last_arr_offset; - //console.log("b :: ","remain", remain, "rwithin",rwithin,"last_arr.length",this.last_arr.length,"larroffset",this.last_arr_offset,"offset",offset); - if (remain < rwithin) { - //console.log("remain < rwithin"); //seems problematic @Andris - for (var i = 0; i < remain; i++) obuf[offset++] = this.last_arr[this.last_arr_offset++]; - remain = 0; - } else { - //console.log("remain > rwithin"); - for (var i = 0; i < rwithin; i++) obuf[offset++] = this.last_arr[this.last_arr_offset++]; - remain -= rwithin; - } - //console.log("e :: ","remain", remain, "rwithin",rwithin,"last_arr.length",this.last_arr.length,"larroffset",this.last_arr_offset,"offset",offset); - } - - this.total_size -= obuf.length; - //console.log("return _take"); - return obuf; - }; -}; - -sdrjs.RationalResamplerFF = function(interpolation, decimation, transition_bw, window) { - this.interpolation = interpolation; - this.decimation = decimation; - this.transition_bw = (typeof transition_bw == 'undefined') ? 0.05 : transition_bw; - this.window = (typeof window == 'undefined') ? 1 : window; - this.buffer_size = 1024 * 512; - this.output_buffer_size = Math.floor((this.buffer_size * interpolation) / decimation); - this.input_buffer = asm$.malloc(Float32Array, this.buffer_size); - this.output_buffer = asm$.malloc(Float32Array, this.output_buffer_size); - //Calculate filter - this.taps_length = Math.floor(4 / this.transition_bw); - this.taps = asm$.malloc(Float32Array, this.taps_length); - var cutoff_for_interpolation = 1.0 / interpolation; - var cutoff_for_decimation = 1.0 / decimation; - var cutoff = (cutoff_for_interpolation < cutoff_for_decimation) ? cutoff_for_interpolation : cutoff_for_decimation; //get the lower - firdes_lowpass_f(this.taps.ptr, this.taps_length, cutoff / 2, window); - - this.remain = 0; - this.remain_offset = 0; - this.last_taps_delay = 0; - - this.process = function(input) { - - if (input.length + this.remain > this.buffer_size) { - return new Float32Array(0); - console.log("sdrjs.RationalResamplerFF: critical audio buffering error"); //This should not happen... - /* console.log("RationalResamplerFF: splitting..."); //TODO: this branch has not been checked - output_buffers=Array(); - new_buffer_size=this.buffer_size/2; - i=0; - //process the input in chunks of new_buffer_size, and add the output product Float32Array-s to output_buffers. - while((i++)*new_buffer_size<=input.length) - { - output_buffers.push(this._process_noheapcheck(input.subarray(i*new_buffer_size,(i+1)*new_buffer_size))); - } - //add up the sizes of the output_buffer-s. - total_output_length=0; - output_buffers.forEach(function(a){total_output_length+=a.length;}); - //create one big buffer from concatenating the output_buffer-s - output=new Float32Array(total_output_length); - output_pos=0; - output_buffers.forEach(function(a){ - asm$.cpy(output,output_pos,a,0,a.length); - output_pos+=a.length; - }); - return output;*/ - } else return this._process_noheapcheck(input); - }; - this._process_noheapcheck = function(input) //if we are sure we have enough space in the buffers - { - asm$.cpy(this.input_buffer.arr, 0, this.input_buffer.arr, this.remain_offset, this.remain); - asm$.cpy(this.input_buffer.arr, this.remain, input, 0, input.length); - var total_input_size = input.length + this.remain; - d = rational_resampler_ff(this.input_buffer.ptr, this.output_buffer.ptr, total_input_size, this.interpolation, this.decimation, this.taps.ptr, this.taps_length, this.last_taps_delay); - this.last_taps_delay = d.last_taps_delay; - this.remain = total_input_size - d.input_processed; - this.remain_offset = d.input_processed; - var output_copy_arr = new Float32Array(d.output_size); - asm$.cpy(output_copy_arr, 0, this.output_buffer.arr, 0, d.output_size); - return output_copy_arr; - }; -}; - - -_sdrjs_logb = function(what) { - document.body.innerHTML += what + "
    "; -} - - -function test_firdes_lowpass_f_original() { - //Original method explained over here: - //http://kapadia.github.io/emscripten/2013/09/13/emscripten-pointers-and-pointers.html - _sdrjs_logb("test_firdes_lowpass_f_original():"); - _sdrjs_logb("Now designing FIR filter with firdes_lowpass_f in sdr.js..."); - _sdrjs_logb("output should be the same as: csdr firdes_lowpass_f 0.1 101 HAMMING"); - - var outputSize = 101 * 4; - var outputPtr = Module._malloc(outputSize); - var outputHeap = new Uint8Array(Module.HEAPU8.buffer, outputPtr, outputSize); - firdes_lowpass_f(outputHeap.byteOffset, 101, 0.1, 2); - var output = new Float32Array(outputHeap.buffer, outputHeap.byteOffset, 101); - outputStr = String(); - for (i = 0; i < output.length; i++) outputStr += output[i].toFixed(6) + ", "; - Module._free(outputHeap.byteOffset); - _sdrjs_logb(outputStr); -} - - -function test_firdes_lowpass_f_new() { - //This is much simpler, using asm$ - _sdrjs_logb("test_firdes_lowpass_f_new():"); - _sdrjs_logb("Now designing FIR filter with firdes_lowpass_f in sdr.js..."); - _sdrjs_logb("output should be the same as: csdr firdes_lowpass_f 0.1 101 HAMMING"); - - output = asm$.malloc(Float32Array, 101); - firdes_lowpass_f(output.ptr, 101, 0.1, 2); - outputStr = String(); - for (i = 0; i < output.arr.length; i++) outputStr += (output.arr[i]).toFixed(6) + ", "; - output.free(); - _sdrjs_logb(outputStr); -} - -function test_struct_return_value() { - v = STACKTOP; - STACKTOP += 4 * 3; - _shift_addition_init(v, 0.2); - console.log( - "sinval=", getValue(v, 'float'), - "cosval=", getValue(v + 4, 'float'), - "rate=", getValue(v + 8, 'float') - ); - STACKTOP = v; -} \ No newline at end of file diff --git a/htdocs/settings.html b/htdocs/settings.html new file mode 100644 index 000000000..1bdaff402 --- /dev/null +++ b/htdocs/settings.html @@ -0,0 +1,41 @@ + + + + OpenWebRX Settings + + + + + + + +${header} + + \ No newline at end of file diff --git a/htdocs/settings.js b/htdocs/settings.js new file mode 100644 index 000000000..b1bc7361a --- /dev/null +++ b/htdocs/settings.js @@ -0,0 +1,12 @@ +$(function(){ + $('.map-input').mapInput(); + $('.imageupload').imageUpload(); + $('.bookmarks').bookmarktable(); + $('.wsjt-decoding-depths').wsjtDecodingDepthsInput(); + $('#waterfall_scheme').waterfallDropdown(); + $('#rf_gain').gainInput(); + $('.optional-section').optionalSection(); + $('#scheduler').schedulerInput(); + $('.exponential-input').exponentialInput(); + $('.device-log-messages').logMessages(); +}); \ No newline at end of file diff --git a/htdocs/settings/bookmarks.html b/htdocs/settings/bookmarks.html new file mode 100644 index 000000000..046015bb9 --- /dev/null +++ b/htdocs/settings/bookmarks.html @@ -0,0 +1,69 @@ + + + + OpenWebRX Settings + + + + + + + +${header} +
    + ${breadcrumb} +
    +

    Bookmarks

    +
    +
    +
    Double-click the values in the table to edit them.
    +
    +
    + ${bookmarks} +
    + + +
    +
    + ${breadcrumb} +
    + + + \ No newline at end of file diff --git a/htdocs/settings/general.html b/htdocs/settings/general.html new file mode 100644 index 000000000..4a71fd9aa --- /dev/null +++ b/htdocs/settings/general.html @@ -0,0 +1,23 @@ + + + + OpenWebRX Settings + + + + + + + +${header} +
    + ${breadcrumb} + ${error} +
    +

    ${title}

    +
    + ${content} + ${breadcrumb} +
    +${modal} + \ No newline at end of file diff --git a/htdocs/upgrade.html b/htdocs/upgrade.html deleted file mode 100644 index 09b5aabbe..000000000 --- a/htdocs/upgrade.html +++ /dev/null @@ -1,95 +0,0 @@ - - -OpenWebRX - - - - - - -
    - -
    - Only the latest Google Chrome browser is supported at the moment.
    - Please download and install Google Chrome.
    -
    - Alternatively, you may proceed to OpenWebRX, but it's not supposed to work as expected.
    - Click here if you still want to try OpenWebRX. -
    -
    -
    - - - diff --git a/inkscape files/favicon.svg b/inkscape files/favicon.svg new file mode 100644 index 000000000..a7a6aa503 --- /dev/null +++ b/inkscape files/favicon.svg @@ -0,0 +1,2388 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inkscape files/google_maps_pin.svg b/inkscape files/google_maps_pin.svg new file mode 100644 index 000000000..1dd49619e --- /dev/null +++ b/inkscape files/google_maps_pin.svg @@ -0,0 +1,76 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-bookmark.svg b/inkscape files/openwebrx-bookmark.svg new file mode 100644 index 000000000..39b786ed2 --- /dev/null +++ b/inkscape files/openwebrx-bookmark.svg @@ -0,0 +1,56 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/inkscape files/openwebrx-directcall.svg b/inkscape files/openwebrx-directcall.svg new file mode 100644 index 000000000..248965232 --- /dev/null +++ b/inkscape files/openwebrx-directcall.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-edit.svg b/inkscape files/openwebrx-edit.svg new file mode 100644 index 000000000..6cec2f213 --- /dev/null +++ b/inkscape files/openwebrx-edit.svg @@ -0,0 +1,52 @@ + +to editimage/svg+xmlShannon E Thomashttp://www.toicon.com/icons/lines-and-angles_editimage/svg+xmlto edit diff --git a/inkscape files/openwebrx-groupcall.svg b/inkscape files/openwebrx-groupcall.svg new file mode 100644 index 000000000..877587802 --- /dev/null +++ b/inkscape files/openwebrx-groupcall.svg @@ -0,0 +1,277 @@ + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-logo.svg b/inkscape files/openwebrx-logo.svg new file mode 100644 index 000000000..6b352c25b --- /dev/null +++ b/inkscape files/openwebrx-logo.svg @@ -0,0 +1,161 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-mute.svg b/inkscape files/openwebrx-mute.svg new file mode 100644 index 000000000..0c24051c1 --- /dev/null +++ b/inkscape files/openwebrx-mute.svg @@ -0,0 +1,142 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-panel-log.svg b/inkscape files/openwebrx-panel-log.svg new file mode 100644 index 000000000..6648abbc2 --- /dev/null +++ b/inkscape files/openwebrx-panel-log.svg @@ -0,0 +1,138 @@ + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-panel-map.svg b/inkscape files/openwebrx-panel-map.svg new file mode 100644 index 000000000..21ef46ed8 --- /dev/null +++ b/inkscape files/openwebrx-panel-map.svg @@ -0,0 +1,173 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-panel-receiver.svg b/inkscape files/openwebrx-panel-receiver.svg new file mode 100644 index 000000000..247276041 --- /dev/null +++ b/inkscape files/openwebrx-panel-receiver.svg @@ -0,0 +1,181 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-panel-settings.svg b/inkscape files/openwebrx-panel-settings.svg new file mode 100644 index 000000000..c8ba6a61d --- /dev/null +++ b/inkscape files/openwebrx-panel-settings.svg @@ -0,0 +1,115 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-panel-status.svg b/inkscape files/openwebrx-panel-status.svg new file mode 100644 index 000000000..049c5648b --- /dev/null +++ b/inkscape files/openwebrx-panel-status.svg @@ -0,0 +1,146 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-play-button.svg b/inkscape files/openwebrx-play-button.svg new file mode 100644 index 000000000..5d7b7561a --- /dev/null +++ b/inkscape files/openwebrx-play-button.svg @@ -0,0 +1,67 @@ + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-rx-details-arrow-down.svg b/inkscape files/openwebrx-rx-details-arrow-down.svg new file mode 100644 index 000000000..4b0755d7e --- /dev/null +++ b/inkscape files/openwebrx-rx-details-arrow-down.svg @@ -0,0 +1,84 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/inkscape files/openwebrx-rx-details-arrow-up.svg b/inkscape files/openwebrx-rx-details-arrow-up.svg new file mode 100644 index 000000000..56a6a9c2c --- /dev/null +++ b/inkscape files/openwebrx-rx-details-arrow-up.svg @@ -0,0 +1,82 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/inkscape files/openwebrx-speake-mutedr.svg b/inkscape files/openwebrx-speake-mutedr.svg new file mode 100644 index 000000000..fa25ccd4c --- /dev/null +++ b/inkscape files/openwebrx-speake-mutedr.svg @@ -0,0 +1,129 @@ + + + + + + + + image/svg+xml + + + + + + + + + + diff --git a/inkscape files/openwebrx-speaker.svg b/inkscape files/openwebrx-speaker.svg new file mode 100644 index 000000000..ab5726ae4 --- /dev/null +++ b/inkscape files/openwebrx-speaker.svg @@ -0,0 +1,168 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-squelch.svg b/inkscape files/openwebrx-squelch.svg new file mode 100644 index 000000000..c6be1eed0 --- /dev/null +++ b/inkscape files/openwebrx-squelch.svg @@ -0,0 +1,145 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-trashcan.svg b/inkscape files/openwebrx-trashcan.svg new file mode 100644 index 000000000..661fe509d --- /dev/null +++ b/inkscape files/openwebrx-trashcan.svg @@ -0,0 +1,63 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/inkscape files/openwebrx-waterfall-auto.svg b/inkscape files/openwebrx-waterfall-auto.svg new file mode 100644 index 000000000..34ec3cdc2 --- /dev/null +++ b/inkscape files/openwebrx-waterfall-auto.svg @@ -0,0 +1,84 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/inkscape files/openwebrx-waterfall-continuous.svg b/inkscape files/openwebrx-waterfall-continuous.svg new file mode 100644 index 000000000..f95c97e34 --- /dev/null +++ b/inkscape files/openwebrx-waterfall-continuous.svg @@ -0,0 +1,120 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-waterfall-default.svg b/inkscape files/openwebrx-waterfall-default.svg new file mode 100644 index 000000000..51b5bebea --- /dev/null +++ b/inkscape files/openwebrx-waterfall-default.svg @@ -0,0 +1,123 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-zoom-in-total.svg b/inkscape files/openwebrx-zoom-in-total.svg new file mode 100644 index 000000000..dddb2bfda --- /dev/null +++ b/inkscape files/openwebrx-zoom-in-total.svg @@ -0,0 +1,149 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-zoom-in.svg b/inkscape files/openwebrx-zoom-in.svg new file mode 100644 index 000000000..d8dd9f8c1 --- /dev/null +++ b/inkscape files/openwebrx-zoom-in.svg @@ -0,0 +1,157 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-zoom-out-total.svg b/inkscape files/openwebrx-zoom-out-total.svg new file mode 100644 index 000000000..cd4b8240a --- /dev/null +++ b/inkscape files/openwebrx-zoom-out-total.svg @@ -0,0 +1,169 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/inkscape files/openwebrx-zoom-out.svg b/inkscape files/openwebrx-zoom-out.svg new file mode 100644 index 000000000..3096d4ee9 --- /dev/null +++ b/inkscape files/openwebrx-zoom-out.svg @@ -0,0 +1,158 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/openwebrx.conf b/openwebrx.conf new file mode 100644 index 000000000..6131a8d81 --- /dev/null +++ b/openwebrx.conf @@ -0,0 +1,16 @@ +[core] +data_directory = /var/lib/openwebrx +temporary_directory = /tmp +log_level = INFO + +[web] +port = 8073 +ipv6 = true +# Uncomment bind_address to bind OpenWebRX to a specific IP-address. +# By default, OpenWebRX will bind to all interfaces. +# Use ::1 for localhost only, or any other configured address to bind to that address only. +#bind_address = ::1 + +[aprs] +# path to the aprs symbols repository (get it here: https://github.com/hessu/aprs-symbols) +symbols_path = /usr/share/aprs-symbols/png diff --git a/openwebrx.py b/openwebrx.py index 07c91e1de..6aa13dd0a 100755 --- a/openwebrx.py +++ b/openwebrx.py @@ -1,737 +1,7 @@ -#!/usr/bin/python2 -print "" # python2.7 is required to run OpenWebRX instead of python3. Please run me by: python2 openwebrx.py -""" +#!/usr/bin/env python3 - This file is part of OpenWebRX, - an open-source SDR receiver software with a web UI. - Copyright (c) 2013-2015 by Andras Retzler - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as - published by the Free Software Foundation, either version 3 of the - License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -""" -sw_version="v0.17" -#0.15 (added nmux) - -import os -import code -import importlib -import csdr -import thread -import time -import datetime -import subprocess -import os -from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer -from SocketServer import ThreadingMixIn -import fcntl -import time -import md5 -import random -import threading import sys -import traceback -from collections import namedtuple -import Queue -import ctypes - -#import rtl_mus -import rxws -import uuid -import signal -import socket - -try: import sdrhu -except: sdrhu=False -avatar_ctime="" - -#pypy compatibility -try: import dl -except: pass -try: import __pypy__ -except: pass -pypy="__pypy__" in globals() - -""" -def import_all_plugins(directory): - for subdir in os.listdir(directory): - if os.path.isdir(directory+subdir) and not subdir[0]=="_": - exact_path=directory+subdir+"/plugin.py" - if os.path.isfile(exact_path): - importname=(directory+subdir+"/plugin").replace("/",".") - print "[openwebrx-import] Found plugin:",importname - importlib.import_module(importname) -""" - -class MultiThreadHTTPServer(ThreadingMixIn, HTTPServer): - pass - -def handle_signal(sig, frame): - global spectrum_dsp - if sig == signal.SIGUSR1: - print "[openwebrx] Verbose status information on USR1 signal" - print - print "time.time() =", time.time() - print "clients_mutex.locked() =", clients_mutex.locked() - print "clients_mutex_locker =", clients_mutex_locker - if server_fail: print "server_fail = ", server_fail - print "spectrum_thread_watchdog_last_tick =", spectrum_thread_watchdog_last_tick - print - print "clients:",len(clients) - for client in clients: - print - for key in client._fields: - print "\t%s = %s"%(key,str(getattr(client,key))) - elif sig == signal.SIGUSR2: - code.interact(local=globals()) - else: - print "[openwebrx] Ctrl+C: aborting." - cleanup_clients(True) - spectrum_dsp.stop() - os._exit(1) #not too graceful exit - -def access_log(data): - global logs - logs.access_log.write("["+datetime.datetime.now().isoformat()+"] "+data+"\n") - logs.access_log.flush() - -receiver_failed=spectrum_thread_watchdog_last_tick=rtl_thread=spectrum_dsp=server_fail=None - -def main(): - global clients, clients_mutex, pypy, lock_try_time, avatar_ctime, cfg, logs - global serverfail, rtl_thread - print - print "OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package" - print "_________________________________________________________________________________________________" - print - print "Author contact info: Andras Retzler, HA7ILM " - print - - no_arguments=len(sys.argv)==1 - if no_arguments: print "[openwebrx-main] Configuration script not specified. I will use: \"config_webrx.py\"" - cfg=__import__("config_webrx" if no_arguments else sys.argv[1]) - for option in ("access_log","csdr_dynamic_bufsize","csdr_print_bufsizes","csdr_through"): - if not option in dir(cfg): setattr(cfg, option, False) #initialize optional config parameters - - #Open log files - logs = type("logs_class", (object,), {"access_log":open(cfg.access_log if cfg.access_log else "/dev/null","a"), "error_log":""})() - - #Set signal handler - signal.signal(signal.SIGINT, handle_signal) #http://stackoverflow.com/questions/1112343/how-do-i-capture-sigint-in-python - signal.signal(signal.SIGUSR1, handle_signal) - signal.signal(signal.SIGUSR2, handle_signal) - - #Pypy - if pypy: print "pypy detected (and now something completely different: c code is expected to run at a speed of 3*10^8 m/s?)" - - #Change process name to "openwebrx" (to be seen in ps) - try: - for libcpath in ["/lib/i386-linux-gnu/libc.so.6","/lib/libc.so.6"]: - if os.path.exists(libcpath): - libc = dl.open(libcpath) - libc.call("prctl", 15, "openwebrx", 0, 0, 0) - break - except: - pass - - #Start rtl thread - if os.system("csdr 2> /dev/null") == 32512: #check for csdr - print "[openwebrx-main] You need to install \"csdr\" to run OpenWebRX!\n" - return - if os.system("nmux --help 2> /dev/null") == 32512: #check for nmux - print "[openwebrx-main] You need to install an up-to-date version of \"csdr\" that contains the \"nmux\" tool to run OpenWebRX! Please upgrade \"csdr\"!\n" - return - if cfg.start_rtl_thread: - nmux_bufcnt = nmux_bufsize = 0 - while nmux_bufsize < cfg.samp_rate/4: nmux_bufsize += 4096 - while nmux_bufsize * nmux_bufcnt < cfg.nmux_memory * 1e6: nmux_bufcnt += 1 - if nmux_bufcnt == 0 or nmux_bufsize == 0: - print "[openwebrx-main] Error: nmux_bufsize or nmux_bufcnt is zero. These depend on nmux_memory and samp_rate options in config_webrx.py" - return - print "[openwebrx-main] nmux_bufsize = %d, nmux_bufcnt = %d" % (nmux_bufsize, nmux_bufcnt) - cfg.start_rtl_command += "| nmux --bufsize %d --bufcnt %d --port %d --address 127.0.0.1" % (nmux_bufsize, nmux_bufcnt, cfg.iq_server_port) - rtl_thread=threading.Thread(target = lambda:subprocess.Popen(cfg.start_rtl_command, shell=True), args=()) - rtl_thread.start() - print "[openwebrx-main] Started rtl_thread: "+cfg.start_rtl_command - print "[openwebrx-main] Waiting for I/Q server to start..." - while True: - testsock=socket.socket() - try: testsock.connect(("127.0.0.1", cfg.iq_server_port)) - except: - time.sleep(0.1) - continue - testsock.close() - break - print "[openwebrx-main] I/Q server started." - - #Initialize clients - clients=[] - clients_mutex=threading.Lock() - lock_try_time=0 - - #Start watchdog thread - print "[openwebrx-main] Starting watchdog threads." - mutex_test_thread=threading.Thread(target = mutex_test_thread_function, args = ()) - mutex_test_thread.start() - mutex_watchdog_thread=threading.Thread(target = mutex_watchdog_thread_function, args = ()) - mutex_watchdog_thread.start() - - - #Start spectrum thread - print "[openwebrx-main] Starting spectrum thread." - spectrum_thread=threading.Thread(target = spectrum_thread_function, args = ()) - spectrum_thread.start() - #spectrum_watchdog_thread=threading.Thread(target = spectrum_watchdog_thread_function, args = ()) - #spectrum_watchdog_thread.start() - - get_cpu_usage() - bcastmsg_thread=threading.Thread(target = bcastmsg_thread_function, args = ()) - bcastmsg_thread.start() - - #threading.Thread(target = measure_thread_function, args = ()).start() - - #Start sdr.hu update thread - if sdrhu and cfg.sdrhu_key and cfg.sdrhu_public_listing: - print "[openwebrx-main] Starting sdr.hu update thread..." - avatar_ctime=str(os.path.getctime("htdocs/gfx/openwebrx-avatar.png")) - sdrhu_thread=threading.Thread(target = sdrhu.run, args = ()) - sdrhu_thread.start() - - #Start HTTP thread - httpd = MultiThreadHTTPServer(('', cfg.web_port), WebRXHandler) - print('[openwebrx-main] Starting HTTP server.') - access_log("Starting OpenWebRX...") - httpd.serve_forever() - - -# This is a debug function below: -measure_value=0 -def measure_thread_function(): - global measure_value - while True: - print "[openwebrx-measure] value is",measure_value - measure_value=0 - time.sleep(1) - -def bcastmsg_thread_function(): - global clients - while True: - time.sleep(3) - try: cpu_usage=get_cpu_usage() - except: cpu_usage=0 - cma("bcastmsg_thread") - for i in range(0,len(clients)): - clients[i].bcastmsg="MSG cpu_usage={0} clients={1}".format(int(cpu_usage*100),len(clients)) - cmr() - -def mutex_test_thread_function(): - global clients_mutex, lock_try_time - while True: - time.sleep(0.5) - lock_try_time=time.time() - clients_mutex.acquire() - clients_mutex.release() - lock_try_time=0 - -def cma(what): #clients_mutex acquire - global clients_mutex - global clients_mutex_locker - if not clients_mutex.locked(): clients_mutex_locker = what - clients_mutex.acquire() - -def cmr(): - global clients_mutex - global clients_mutex_locker - clients_mutex_locker = None - clients_mutex.release() - -def mutex_watchdog_thread_function(): - global lock_try_time - global clients_mutex_locker - global clients_mutex - while True: - if lock_try_time != 0 and time.time()-lock_try_time > 3.0: - #if 3 seconds pass without unlock - print "[openwebrx-mutex-watchdog] Mutex unlock timeout. Locker: \""+str(clients_mutex_locker)+"\" Now unlocking..." - clients_mutex.release() - time.sleep(0.5) - -def spectrum_watchdog_thread_function(): - global spectrum_thread_watchdog_last_tick, receiver_failed - while True: - time.sleep(60) - if spectrum_thread_watchdog_last_tick and time.time()-spectrum_thread_watchdog_last_tick > 60.0: - print "[openwebrx-spectrum-watchdog] Spectrum timeout. Seems like no I/Q data is coming from the receiver.\nIf you're using RTL-SDR, the receiver hardware may randomly fail under some circumstances:\n1) high temperature,\n2) insufficient current available from the USB port." - print "[openwebrx-spectrum-watchdog] Deactivating receiver." - receiver_failed="spectrum" - return - -def check_server(): - global spectrum_dsp, server_fail, rtl_thread - if server_fail: return server_fail - #print spectrum_dsp.process.poll() - if spectrum_dsp and spectrum_dsp.process.poll()!=None: server_fail = "spectrum_thread dsp subprocess failed" - #if rtl_thread and not rtl_thread.is_alive(): server_fail = "rtl_thread failed" - if server_fail: print "[openwebrx-check_server] >>>>>>> ERROR:", server_fail - return server_fail - -def apply_csdr_cfg_to_dsp(dsp): - dsp.csdr_dynamic_bufsize = cfg.csdr_dynamic_bufsize - dsp.csdr_print_bufsizes = cfg.csdr_print_bufsizes - dsp.csdr_through = cfg.csdr_through - -def spectrum_thread_function(): - global clients, spectrum_dsp, spectrum_thread_watchdog_last_tick - spectrum_dsp=dsp=csdr.dsp() - dsp.nc_port=cfg.iq_server_port - dsp.set_demodulator("fft") - dsp.set_samp_rate(cfg.samp_rate) - dsp.set_fft_size(cfg.fft_size) - dsp.set_fft_fps(cfg.fft_fps) - dsp.set_fft_averages(int(round(1.0 * cfg.samp_rate / cfg.fft_size / cfg.fft_fps / (1.0 - cfg.fft_voverlap_factor))) if cfg.fft_voverlap_factor>0 else 0) - dsp.set_fft_compression(cfg.fft_compression) - dsp.set_format_conversion(cfg.format_conversion) - apply_csdr_cfg_to_dsp(dsp) - sleep_sec=0.87/cfg.fft_fps - print "[openwebrx-spectrum] Spectrum thread initialized successfully." - dsp.start() - if cfg.csdr_dynamic_bufsize: - dsp.read(8) #dummy read to skip bufsize & preamble - print "[openwebrx-spectrum] Note: CSDR_DYNAMIC_BUFSIZE_ON = 1" - print "[openwebrx-spectrum] Spectrum thread started." - bytes_to_read=int(dsp.get_fft_bytes_to_read()) - spectrum_thread_counter=0 - while True: - data=dsp.read(bytes_to_read) - #print "gotcha",len(data),"bytes of spectrum data via spectrum_thread_function()" - if spectrum_thread_counter >= cfg.fft_fps: - spectrum_thread_counter=0 - spectrum_thread_watchdog_last_tick = time.time() #once every second - else: spectrum_thread_counter+=1 - cma("spectrum_thread") - correction=0 - for i in range(0,len(clients)): - i-=correction - if (clients[i].ws_started): - if clients[i].spectrum_queue.full(): - print "[openwebrx-spectrum] client spectrum queue full, closing it." - close_client(i, False) - correction+=1 - else: - clients[i].spectrum_queue.put([data]) # add new string by "reference" to all clients - cmr() - -def get_client_by_id(client_id, use_mutex=True): - global clients - output=-1 - if use_mutex: cma("get_client_by_id") - for i in range(0,len(clients)): - if(clients[i].id==client_id): - output=i - break - if use_mutex: cmr() - if output==-1: - raise ClientNotFoundException - else: - return output - -def log_client(client, what): - print "[openwebrx-httpd] client {0}#{1} :: {2}".format(client.ip,client.id,what) - -def cleanup_clients(end_all=False): - # - if a client doesn't open websocket for too long time, we drop it - # - or if end_all is true, we drop all clients - global clients - cma("cleanup_clients") - correction=0 - for i in range(0,len(clients)): - i-=correction - #print "cleanup_clients:: len(clients)=", len(clients), "i=", i - if end_all or ((not clients[i].ws_started) and (time.time()-clients[i].gen_time)>45): - if not end_all: print "[openwebrx] cleanup_clients :: client timeout to open WebSocket" - close_client(i, False) - correction+=1 - cmr() - -def generate_client_id(ip): - #add a client - global clients - new_client=namedtuple("ClientStruct", "id gen_time ws_started sprectum_queue ip closed bcastmsg dsp loopstat") - new_client.id=md5.md5(str(random.random())).hexdigest() - new_client.gen_time=time.time() - new_client.ws_started=False # to check whether client has ever tried to open the websocket - new_client.spectrum_queue=Queue.Queue(1000) - new_client.ip=ip - new_client.bcastmsg="" - new_client.closed=[False] #byref, not exactly sure if required - new_client.dsp=None - cma("generate_client_id") - clients.append(new_client) - log_client(new_client,"client added. Clients now: {0}".format(len(clients))) - cmr() - cleanup_clients() - return new_client.id - -def close_client(i, use_mutex=True): - global clients - log_client(clients[i],"client being closed.") - if use_mutex: cma("close_client") - try: - clients[i].dsp.stop() - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - print "[openwebrx] close_client dsp.stop() :: error -",exc_type,exc_value - traceback.print_tb(exc_traceback) - clients[i].closed[0]=True - access_log("Stopped streaming to client: "+clients[i].ip+"#"+str(clients[i].id)+" (users now: "+str(len(clients)-1)+")") - del clients[i] - if use_mutex: cmr() - -# http://www.codeproject.com/Articles/462525/Simple-HTTP-Server-and-Client-in-Python -# some ideas are used from the artice above - -class WebRXHandler(BaseHTTPRequestHandler): - def proc_read_thread(): - pass - - def send_302(self,what): - self.send_response(302) - self.send_header('Content-type','text/html') - self.send_header("Location", "http://{0}:{1}/{2}".format(cfg.server_hostname,cfg.web_port,what)) - self.end_headers() - self.wfile.write("

    Object moved

    Please click here to continue.".format(what)) - - - def do_GET(self): - self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - global dsp_plugin, clients_mutex, clients, avatar_ctime, sw_version, receiver_failed - rootdir = 'htdocs' - self.path=self.path.replace("..","") - path_temp_parts=self.path.split("?") - self.path=path_temp_parts[0] - request_param=path_temp_parts[1] if(len(path_temp_parts)>1) else "" - access_log("GET "+self.path+" from "+self.client_address[0]) - try: - if self.path=="/": - self.path="/index.wrx" - # there's even another cool tip at http://stackoverflow.com/questions/4419650/how-to-implement-timeout-in-basehttpserver-basehttprequesthandler-python - #if self.path[:5]=="/lock": cma("do_GET /lock/") # to test mutex_watchdog_thread. Do not uncomment in production environment! - if self.path[:4]=="/ws/": - print "[openwebrx-ws] Client requested WebSocket connection" - if receiver_failed: self.send_error(500,"Internal server error") - try: - # ========= WebSocket handshake ========= - ws_success=True - try: - rxws.handshake(self) - cma("do_GET /ws/") - client_i=get_client_by_id(self.path[4:], False) - myclient=clients[client_i] - except rxws.WebSocketException: ws_success=False - except ClientNotFoundException: ws_success=False - finally: - if clients_mutex.locked(): cmr() - if not ws_success: - self.send_error(400, 'Bad request.') - return - - # ========= Client handshake ========= - if myclient.ws_started: - print "[openwebrx-httpd] error: second WS connection with the same client id, throwing it." - self.send_error(400, 'Bad request.') #client already started - return - rxws.send(self, "CLIENT DE SERVER openwebrx.py") - client_ans=rxws.recv(self, True) - if client_ans[:16]!="SERVER DE CLIENT": - rxws.send("ERR Bad answer.") - return - myclient.ws_started=True - #send default parameters - rxws.send(self, "MSG center_freq={0} bandwidth={1} fft_size={2} fft_fps={3} audio_compression={4} fft_compression={5} max_clients={6} setup".format(str(cfg.shown_center_freq),str(cfg.samp_rate),cfg.fft_size,cfg.fft_fps,cfg.audio_compression,cfg.fft_compression,cfg.max_clients)) - - # ========= Initialize DSP ========= - dsp=csdr.dsp() - dsp_initialized=False - dsp.set_audio_compression(cfg.audio_compression) - dsp.set_fft_compression(cfg.fft_compression) #used by secondary chains - dsp.set_format_conversion(cfg.format_conversion) - dsp.set_offset_freq(0) - dsp.set_bpf(-4000,4000) - dsp.set_secondary_fft_size(cfg.digimodes_fft_size) - dsp.nc_port=cfg.iq_server_port - apply_csdr_cfg_to_dsp(dsp) - myclient.dsp=dsp - do_secondary_demod=False - access_log("Started streaming to client: "+self.client_address[0]+"#"+myclient.id+" (users now: "+str(len(clients))+")") - - while True: - myclient.loopstat=0 - if myclient.closed[0]: - print "[openwebrx-httpd:ws] client closed by other thread" - break - - # ========= send audio ========= - if dsp_initialized: - myclient.loopstat=10 - temp_audio_data=dsp.read(256) - myclient.loopstat=11 - rxws.send(self, temp_audio_data, "AUD ") - - # ========= send spectrum ========= - while not myclient.spectrum_queue.empty(): - myclient.loopstat=20 - spectrum_data=myclient.spectrum_queue.get() - #spectrum_data_mid=len(spectrum_data[0])/2 - #rxws.send(self, spectrum_data[0][spectrum_data_mid:]+spectrum_data[0][:spectrum_data_mid], "FFT ") - # (it seems GNU Radio exchanges the first and second part of the FFT output, we correct it) - myclient.loopstat=21 - rxws.send(self, spectrum_data[0],"FFT ") - - # ========= send smeter_level ========= - smeter_level=None - while True: - try: - myclient.loopstat=30 - smeter_level=dsp.get_smeter_level() - if smeter_level == None: break - except: - break - if smeter_level!=None: - myclient.loopstat=31 - rxws.send(self, "MSG s={0}".format(smeter_level)) - - # ========= send bcastmsg ========= - if myclient.bcastmsg!="": - myclient.loopstat=40 - rxws.send(self,myclient.bcastmsg) - myclient.bcastmsg="" - - # ========= send secondary ========= - if do_secondary_demod: - myclient.loopstat=41 - while True: - try: - secondary_spectrum_data=dsp.read_secondary_fft(dsp.get_secondary_fft_bytes_to_read()) - if len(secondary_spectrum_data) == 0: break - # print "len(secondary_spectrum_data)", len(secondary_spectrum_data) #TODO digimodes - rxws.send(self, secondary_spectrum_data, "FFTS") - except: break - myclient.loopstat=42 - while True: - try: - myclient.loopstat=422 - secondary_demod_data=dsp.read_secondary_demod(1) - myclient.loopstat=423 - if len(secondary_demod_data) == 0: break - # print "len(secondary_demod_data)", len(secondary_demod_data), secondary_demod_data #TODO digimodes - rxws.send(self, secondary_demod_data, "DAT ") - except: break - - # ========= process commands ========= - while True: - myclient.loopstat=50 - rdata=rxws.recv(self, False) - myclient.loopstat=51 - #try: - if not rdata: break - elif rdata[:3]=="SET": - print "[openwebrx-httpd:ws,%d] command: %s"%(client_i,rdata) - pairs=rdata[4:].split(" ") - bpf_set=False - new_bpf=dsp.get_bpf() - filter_limit=dsp.get_output_rate()/2 - for pair in pairs: - param_name, param_value = pair.split("=") - if param_name == "low_cut" and -filter_limit <= int(param_value) <= filter_limit: - bpf_set=True - new_bpf[0]=int(param_value) - elif param_name == "high_cut" and -filter_limit <= int(param_value) <= filter_limit: - bpf_set=True - new_bpf[1]=int(param_value) - elif param_name == "offset_freq" and -cfg.samp_rate/2 <= int(param_value) <= cfg.samp_rate/2: - myclient.loopstat=510 - dsp.set_offset_freq(int(param_value)) - elif param_name == "squelch_level" and float(param_value) >= 0: - myclient.loopstat=520 - dsp.set_squelch_level(float(param_value)) - elif param_name=="mod": - if (dsp.get_demodulator()!=param_value): - myclient.loopstat=530 - if dsp_initialized: dsp.stop() - dsp.set_demodulator(param_value) - if dsp_initialized: dsp.start() - elif param_name == "output_rate": - if not dsp_initialized: - myclient.loopstat=540 - dsp.set_output_rate(int(param_value)) - myclient.loopstat=541 - dsp.set_samp_rate(cfg.samp_rate) - elif param_name=="action" and param_value=="start": - if not dsp_initialized: - myclient.loopstat=550 - dsp.start() - dsp_initialized=True - elif param_name=="secondary_mod" and cfg.digimodes_enable: - if (dsp.get_secondary_demodulator() != param_value): - if dsp_initialized: dsp.stop() - if param_value == "off": - dsp.set_secondary_demodulator(None) - do_secondary_demod = False - else: - dsp.set_secondary_demodulator(param_value) - do_secondary_demod = True - rxws.send(self, "MSG secondary_fft_size={0} if_samp_rate={1} secondary_bw={2} secondary_setup".format(cfg.digimodes_fft_size, dsp.if_samp_rate(), dsp.secondary_bw())) - if dsp_initialized: dsp.start() - elif param_name=="secondary_offset_freq" and 0 <= int(param_value) <= dsp.if_samp_rate()/2 and cfg.digimodes_enable: - dsp.set_secondary_offset_freq(int(param_value)) - else: - print "[openwebrx-httpd:ws] invalid parameter" - if bpf_set: - myclient.loopstat=560 - dsp.set_bpf(*new_bpf) - #code.interact(local=locals()) - except: - myclient.loopstat=990 - exc_type, exc_value, exc_traceback = sys.exc_info() - print "[openwebrx-httpd:ws] exception: ",exc_type,exc_value - traceback.print_tb(exc_traceback) #TODO digimodes - #if exc_value[0]==32: #"broken pipe", client disconnected - # pass - #elif exc_value[0]==11: #"resource unavailable" on recv, client disconnected - # pass - #else: - # print "[openwebrx-httpd] error in /ws/ handler: ",exc_type,exc_value - # traceback.print_tb(exc_traceback) - - #stop dsp for the disconnected client - myclient.loopstat=991 - try: - dsp.stop() - del dsp - except: - print "[openwebrx-httpd] error in dsp.stop()" - - #delete disconnected client - myclient.loopstat=992 - try: - cma("do_GET /ws/ delete disconnected") - id_to_close=get_client_by_id(myclient.id,False) - close_client(id_to_close,False) - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - print "[openwebrx-httpd] client cannot be closed: ",exc_type,exc_value - traceback.print_tb(exc_traceback) - finally: - cmr() - myclient.loopstat=1000 - return - elif self.path in ("/status", "/status/"): - #self.send_header('Content-type','text/plain') - getbands=lambda: str(int(cfg.shown_center_freq-cfg.samp_rate/2))+"-"+str(int(cfg.shown_center_freq+cfg.samp_rate/2)) - self.wfile.write("status="+("inactive" if receiver_failed else "active")+"\nname="+cfg.receiver_name+"\nsdr_hw="+cfg.receiver_device+"\nop_email="+cfg.receiver_admin+"\nbands="+getbands()+"\nusers="+str(len(clients))+"\nusers_max="+str(cfg.max_clients)+"\navatar_ctime="+avatar_ctime+"\ngps="+str(cfg.receiver_gps)+"\nasl="+str(cfg.receiver_asl)+"\nloc="+cfg.receiver_location+"\nsw_version="+sw_version+"\nantenna="+cfg.receiver_ant+"\n") - print "[openwebrx-httpd] GET /status/ from",self.client_address[0] - else: - f=open(rootdir+self.path) - data=f.read() - extension=self.path[(len(self.path)-4):len(self.path)] - extension=extension[2:] if extension[1]=='.' else extension[1:] - checkresult=check_server() - if extension == "wrx" and (checkresult or receiver_failed): - self.send_302("inactive.html") - return - anyStringsPresentInUserAgent=lambda a: reduce(lambda x,y:x or y, map(lambda b:self.headers['user-agent'].count(b), a), False) - if extension == "wrx" and ( (not anyStringsPresentInUserAgent(("Chrome","Firefox","Googlebot","iPhone","iPad","iPod"))) if 'user-agent' in self.headers.keys() else True ) and (not request_param.count("unsupported")): - self.send_302("upgrade.html") - return - if extension == "wrx": - cleanup_clients(False) - if cfg.max_clients<=len(clients): - self.send_302("retry.html") - return - self.send_response(200) - if(("wrx","html","htm").count(extension)): - self.send_header('Content-type','text/html') - elif(extension=="js"): - self.send_header('Content-type','text/javascript') - elif(extension=="css"): - self.send_header('Content-type','text/css') - self.end_headers() - if extension == "wrx": - replace_dictionary=( - ("%[RX_PHOTO_DESC]",cfg.photo_desc), - ("%[CLIENT_ID]", generate_client_id(self.client_address[0])) if "%[CLIENT_ID]" in data else "", - ("%[WS_URL]","ws://"+cfg.server_hostname+":"+str(cfg.web_port)+"/ws/"), - ("%[RX_TITLE]",cfg.receiver_name), - ("%[RX_LOC]",cfg.receiver_location), - ("%[RX_QRA]",cfg.receiver_qra), - ("%[RX_ASL]",str(cfg.receiver_asl)), - ("%[RX_GPS]",str(cfg.receiver_gps[0])+","+str(cfg.receiver_gps[1])), - ("%[RX_PHOTO_HEIGHT]",str(cfg.photo_height)),("%[RX_PHOTO_TITLE]",cfg.photo_title), - ("%[RX_ADMIN]",cfg.receiver_admin), - ("%[RX_ANT]",cfg.receiver_ant), - ("%[RX_DEVICE]",cfg.receiver_device), - ("%[AUDIO_BUFSIZE]",str(cfg.client_audio_buffer_size)), - ("%[START_OFFSET_FREQ]",str(cfg.start_freq-cfg.center_freq)), - ("%[START_MOD]",cfg.start_mod), - ("%[WATERFALL_COLORS]",cfg.waterfall_colors), - ("%[WATERFALL_MIN_LEVEL]",str(cfg.waterfall_min_level)), - ("%[WATERFALL_MAX_LEVEL]",str(cfg.waterfall_max_level)), - ("%[WATERFALL_AUTO_LEVEL_MARGIN]","[%d,%d]"%cfg.waterfall_auto_level_margin), - ("%[DIGIMODES_ENABLE]",("true" if cfg.digimodes_enable else "false")), - ("%[MATHBOX_WATERFALL_FRES]",str(cfg.mathbox_waterfall_frequency_resolution)), - ("%[MATHBOX_WATERFALL_THIST]",str(cfg.mathbox_waterfall_history_length)), - ("%[MATHBOX_WATERFALL_COLORS]",cfg.mathbox_waterfall_colors) - ) - for rule in replace_dictionary: - while data.find(rule[0])!=-1: - data=data.replace(rule[0],rule[1]) - self.wfile.write(data) - f.close() - return - except IOError: - self.send_error(404, 'Invalid path.') - except: - exc_type, exc_value, exc_traceback = sys.exc_info() - print "[openwebrx-httpd] error (@outside):", exc_type, exc_value - traceback.print_tb(exc_traceback) - - -class ClientNotFoundException(Exception): - pass - -last_worktime=0 -last_idletime=0 - -def get_cpu_usage(): - global last_worktime, last_idletime - try: - f=open("/proc/stat","r") - except: - return 0 #Workaround, possibly we're on a Mac - line="" - while not "cpu " in line: line=f.readline() - f.close() - spl=line.split(" ") - worktime=int(spl[2])+int(spl[3])+int(spl[4]) - idletime=int(spl[5]) - dworktime=(worktime-last_worktime) - didletime=(idletime-last_idletime) - rate=float(dworktime)/(didletime+dworktime) - last_worktime=worktime - last_idletime=idletime - if(last_worktime==0): return 0 - return rate - +from owrx.__main__ import main -if __name__=="__main__": - main() +if __name__ == "__main__": + sys.exit(main()) diff --git a/owrx/__main__.py b/owrx/__main__.py new file mode 100644 index 000000000..d48d5de7c --- /dev/null +++ b/owrx/__main__.py @@ -0,0 +1,152 @@ +import logging + +# the linter will complain about this, but the logging must be configured before importing all the other modules +# loglevel will be adjusted later, INFO is just for the startup + +logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s") +logger = logging.getLogger(__name__) + +from http.server import HTTPServer +from owrx.http import RequestHandler +from owrx.config.core import CoreConfig +from owrx.config import Config +from owrx.config.commands import MigrateCommand +from owrx.feature import FeatureDetector +from owrx.sdr import SdrService +from socketserver import ThreadingMixIn +from owrx.service import Services +from owrx.websocket import WebSocketConnection +from owrx.reporting import ReportingEngine +from owrx.version import openwebrx_version +from owrx.audio.queue import DecoderQueue +from owrx.admin import add_admin_parser, run_admin_action +from pathlib import Path +import signal +import argparse +import socket + + +class ThreadedHttpServer(ThreadingMixIn, HTTPServer): + def __init__(self, web_port, RequestHandlerClass, use_ipv6, bind_address=None): + if bind_address is None: + bind_address = "::" if use_ipv6 else "0.0.0.0" + if use_ipv6: + self.address_family = socket.AF_INET6 + super().__init__((bind_address, web_port), RequestHandlerClass) + + +class SignalException(Exception): + pass + + +def handleSignal(sig, frame): + raise SignalException("Received Signal {sig}".format(sig=sig)) + + +def main(): + parser = argparse.ArgumentParser(description="OpenWebRX - Open Source SDR Web App for Everyone!") + parser.add_argument( + "-c", + "--config", + action="store", + help="Read core configuration from specified file", + metavar="configfile", + type=Path, + ) + parser.add_argument("-v", "--version", action="store_true", help="Show the software version") + parser.add_argument("--debug", action="store_true", help="Set loglevel to DEBUG") + + moduleparser = parser.add_subparsers(title="Modules", dest="module") + adminparser = moduleparser.add_parser("admin", help="Administration actions") + add_admin_parser(adminparser) + + configparser = moduleparser.add_parser("config", help="Configuration actions") + configcommandparser = configparser.add_subparsers(title="Commands", dest="command") + + migrateparser = configcommandparser.add_parser("migrate", help="Migrate configuration files") + migrateparser.set_defaults(cls=MigrateCommand) + + args = parser.parse_args() + + if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + + if args.version: + print("OpenWebRX version {version}".format(version=openwebrx_version)) + return 0 + + CoreConfig.load(args.config) + + if args.module == "admin": + return run_admin_action(adminparser, args) + + if args.module == "config": + return run_admin_action(configparser, args) + + return start_receiver(loglevel=logging.DEBUG if args.debug else None) + + +def start_receiver(loglevel=None): + print( + """ + +OpenWebRX - Open Source SDR Web App for Everyone! | for license see LICENSE file in the package +_________________________________________________________________________________________________ + +Author contact info: Jakob Ketterl, DD5JFK +Documentation: https://github.com/jketterl/openwebrx/wiki +Support and info: https://groups.io/g/openwebrx + + """, + flush=True + ) + + logger.info("OpenWebRX version {0} starting up...".format(openwebrx_version)) + + for sig in [signal.SIGINT, signal.SIGTERM]: + signal.signal(sig, handleSignal) + + coreConfig = CoreConfig() + + # passed loglevel takes priority (used for the --debug argument) + logging.getLogger().setLevel(coreConfig.get_log_level() if loglevel is None else loglevel) + + # config warmup + Config.validateConfig() + + featureDetector = FeatureDetector() + failed = featureDetector.get_failed_requirements("core") + if failed: + logger.error( + "you are missing required dependencies to run openwebrx. " + "please check that the following core requirements are installed and up to date: %s", + ", ".join(failed) + ) + for f in failed: + description = featureDetector.get_requirement_description(f) + if description: + logger.error("description for %s:\n%s", f, description) + return 1 + + # Get error messages about unknown / unavailable features as soon as possible + # start up "always-on" sources right away + SdrService.getAllSources() + + Services.start() + + try: + server = ThreadedHttpServer( + coreConfig.get_web_port(), RequestHandler, coreConfig.get_web_ipv6(), coreConfig.get_web_bind_address() + ) + logger.info("Ready to serve requests.") + server.serve_forever() + except SignalException: + pass + + WebSocketConnection.closeAll() + Services.stop() + SdrService.stopAllSources() + ReportingEngine.stopAll() + DecoderQueue.stopAll() + + return 0 diff --git a/owrx/admin/__init__.py b/owrx/admin/__init__.py new file mode 100644 index 000000000..30b64bba1 --- /dev/null +++ b/owrx/admin/__init__.py @@ -0,0 +1,59 @@ +from owrx.admin.commands import NewUser, DeleteUser, ResetPassword, ListUsers, DisableUser, EnableUser, HasUser +import sys +import traceback + + +def add_admin_parser(moduleparser): + subparsers = moduleparser.add_subparsers(title="Commands", dest="command") + + adduser_parser = subparsers.add_parser("adduser", help="Add a new user") + adduser_parser.add_argument("user", help="Username to be added") + adduser_parser.set_defaults(cls=NewUser) + + removeuser_parser = subparsers.add_parser("removeuser", help="Remove an existing user") + removeuser_parser.add_argument("user", help="Username to be remvoed") + removeuser_parser.set_defaults(cls=DeleteUser) + + resetpassword_parser = subparsers.add_parser("resetpassword", help="Reset a user's password") + resetpassword_parser.add_argument("user", help="Username to be remvoed") + resetpassword_parser.set_defaults(cls=ResetPassword) + + listusers_parser = subparsers.add_parser("listusers", help="List enabled users") + listusers_parser.add_argument("-a", "--all", action="store_true", help="Show all users (including disabled ones)") + listusers_parser.set_defaults(cls=ListUsers) + + disableuser_parser = subparsers.add_parser("disableuser", help="Disable a user") + disableuser_parser.add_argument("user", help="Username to be disabled") + disableuser_parser.set_defaults(cls=DisableUser) + + enableuser_parser = subparsers.add_parser("enableuser", help="Enable a user") + enableuser_parser.add_argument("user", help="Username to be enabled") + enableuser_parser.set_defaults(cls=EnableUser) + + hasuser_parser = subparsers.add_parser("hasuser", help="Test if a user exists") + hasuser_parser.add_argument("user", help="Username to be checked") + hasuser_parser.set_defaults(cls=HasUser) + + moduleparser.add_argument( + "--noninteractive", action="store_true", help="Don't ask for any user input (useful for automation)" + ) + moduleparser.add_argument("--silent", action="store_true", help="Ignore errors (useful for automation)") + + +def run_admin_action(parser, args): + if hasattr(args, "cls"): + command = args.cls() + else: + if not hasattr(args, "silent") or not args.silent: + parser.print_help() + return 1 + return 0 + + try: + return command.run(args) + except Exception: + if not hasattr(args, "silent") or not args.silent: + print("Error running command:") + traceback.print_exc() + return 1 + return 0 diff --git a/owrx/admin/commands.py b/owrx/admin/commands.py new file mode 100644 index 000000000..b6215c3f3 --- /dev/null +++ b/owrx/admin/commands.py @@ -0,0 +1,115 @@ +from abc import ABC, ABCMeta, abstractmethod +from getpass import getpass +from owrx.users import UserList, User, DefaultPasswordClass +import sys +import random +import string +import os + + +class Command(ABC): + @abstractmethod + def run(self, args): + pass + + +class UserCommand(Command, metaclass=ABCMeta): + def getPassword(self, args, username): + if args.noninteractive: + if "OWRX_PASSWORD" in os.environ: + password = os.environ["OWRX_PASSWORD"] + generated = False + else: + print("Generating password for user {username}...".format(username=username)) + password = self.getRandomPassword() + generated = True + print('Password for {username} is "{password}".'.format(username=username, password=password)) + print('This password is suitable for initial setup only, you will be asked to reset it on initial use.') + print('This password cannot be recovered from the system, please copy it now.') + else: + password = getpass("Please enter the new password for {username}: ".format(username=username)) + confirm = getpass("Please confirm the new password: ") + if password != confirm: + raise ValueError("Password mismatch") + generated = False + return password, generated + + def getRandomPassword(self, length=10): + printable = list(string.ascii_letters) + list(string.digits) + return ''.join(random.choices(printable, k=length)) + + +class NewUser(UserCommand): + def run(self, args): + username = args.user + userList = UserList() + # early test to bypass the password stuff if the user already exists + if username in userList: + raise KeyError("User {username} already exists".format(username=username)) + + password, generated = self.getPassword(args, username) + + print("Creating user {username}...".format(username=username)) + user = User(name=username, enabled=True, password=DefaultPasswordClass(password), must_change_password=generated) + userList.addUser(user) + + +class DeleteUser(UserCommand): + def run(self, args): + username = args.user + print("Deleting user {username}...".format(username=username)) + userList = UserList() + userList.deleteUser(username) + + +class ResetPassword(UserCommand): + def run(self, args): + username = args.user + password, generated = self.getPassword(args, username) + userList = UserList() + userList[username].setPassword(DefaultPasswordClass(password), must_change_password=generated) + # this is a change to an object in the list, not the list itself + # in this case, store() is explicit + userList.store() + + +class DisableUser(UserCommand): + def run(self, args): + username = args.user + userList = UserList() + userList[username].disable() + userList.store() + + +class EnableUser(UserCommand): + def run(self, args): + username = args.user + userList = UserList() + userList[username].enable() + userList.store() + + +class ListUsers(Command): + def run(self, args): + userList = UserList() + print("List of enabled users:") + for u in userList.values(): + if args.all or u.enabled: + print(" {name}".format(name=u.name)) + + +class HasUser(Command): + """ + internal command used by the debian config scripts to test if the admin user has already been created + """ + def run(self, args): + userList = UserList() + if args.user in userList: + if not args.silent: + print('User "{name}" exists.'.format(name=args.user)) + return 0 + else: + if not args.silent: + print('User "{name}" does not exist.'.format(name=args.user)) + # in bash, a return code > 0 is interpreted as "false" + return 1 diff --git a/owrx/adsb/dump1090.py b/owrx/adsb/dump1090.py new file mode 100644 index 000000000..dfac3583d --- /dev/null +++ b/owrx/adsb/dump1090.py @@ -0,0 +1,31 @@ +from pycsdr.modules import ExecModule +from pycsdr.types import Format +from csdr.module import LineBasedModule + +import logging + +logger = logging.getLogger(__name__) + + +class Dump1090Module(ExecModule): + def __init__(self): + super().__init__( + Format.COMPLEX_SHORT, + Format.CHAR, + ["dump1090", "--ifile", "-", "--iformat", "SC16", "--raw"], + # send some data on decoder shutdown since the dump1090 internal reader locks up otherwise + # dump1090 reads chunks of 100ms, which equals to 240k samples at 2.4MS/s + # some extra should not hurt + flushSize=300000 + ) + + +class RawDeframer(LineBasedModule): + def process(self, line: bytes): + if line.startswith(b'*') and line.endswith(b';') and len(line) in [16, 30]: + return bytes.fromhex(line[1:-1].decode()) + elif line == b"*0000;": + # heartbeat message. not a valid message, but known. do not log. + return + else: + logger.warning("invalid raw message: %s", line) diff --git a/owrx/adsb/modes.py b/owrx/adsb/modes.py new file mode 100644 index 000000000..87b566ec7 --- /dev/null +++ b/owrx/adsb/modes.py @@ -0,0 +1,394 @@ +from csdr.module import PickleModule +from math import sqrt, atan2, pi, floor, acos, cos +from owrx.map import IncrementalUpdate, Location, Map, Source +from owrx.metrics import Metrics, CounterMetric +from owrx.aeronautical import AirplaneLocation, IcaoSource +from owrx.reporting import ReportingEngine +from datetime import datetime, timedelta +from enum import Enum + +FEET_PER_METER = 3.28084 + + +class AdsbLocation(IncrementalUpdate, AirplaneLocation): + mapKeys = [ + "lat", + "lon", + "altitude", + "heading", + "groundtrack", + "groundspeed", + "verticalspeed", + "identification", + "TAS", + "IAS", + "heading", + ] + + def __init__(self, message): + self.history = [] + self.timestamp = datetime.now() + super().__init__(message) + + def update(self, previousLocation: Location): + if isinstance(previousLocation, AdsbLocation): + history = previousLocation.history + now = datetime.now() + history = [p for p in history if now - p["timestamp"] < self.getTTL()] + else: + history = [] + + history += [{ + "timestamp": self.timestamp, + "props": self.props, + }] + self.history = history + + merged = {} + for p in self.history: + merged.update(p["props"]) + + self.props = merged + if "lat" in merged: + self.lat = merged["lat"] + if "lon" in merged: + self.lon = merged["lon"] + + def getTTL(self) -> timedelta: + # fixed ttl for adsb-locations for now + return timedelta(seconds=30) + + +class CprRecordType(Enum): + AIR = ("air", 360) + GROUND = ("ground", 90) + + def __new__(cls, *args, **kwargs): + name, baseAngle = args + obj = object.__new__(cls) + obj._value_ = name + obj.baseAngle = baseAngle + return obj + + +class CprCache: + def __init__(self): + self.airRecords = {} + self.groundRecords = {} + + def __getRecords(self, cprType: CprRecordType): + if cprType is CprRecordType.AIR: + return self.airRecords + elif cprType is CprRecordType.GROUND: + return self.groundRecords + + def getRecentData(self, icao: str, cprType: CprRecordType): + records = self.__getRecords(cprType) + if icao not in records: + return [] + now = datetime.now() + filtered = [r for r in records[icao] if now - r["timestamp"] < timedelta(seconds=10)] + records_sorted = sorted(filtered, key=lambda r: r["timestamp"]) + records[icao] = records_sorted + return [r["data"] for r in records_sorted] + + def addRecord(self, icao: str, data: any, cprType: CprRecordType): + records = self.__getRecords(cprType) + if icao not in records: + records[icao] = [] + records[icao].append({"timestamp": datetime.now(), "data": data}) + + +class ModeSParser(PickleModule): + def __init__(self): + self.cprCache = CprCache() + name = "dump1090.decodes.adsb" + self.metrics = Metrics.getSharedInstance().getMetric(name) + if self.metrics is None: + self.metrics = CounterMetric() + Metrics.getSharedInstance().addMetric(name, self.metrics) + super().__init__() + + def process(self, input): + format = (input[0] & 0b11111000) >> 3 + message = { + "mode": "ADSB", + "format": format + } + if format == 17: + message["capability"] = input[0] & 0b111 + message["icao"] = icao = input[1:4].hex() + type = (input[4] & 0b11111000) >> 3 + message["adsb_type"] = type + + if type in [1, 2, 3, 4]: + # identification message + id = [ + (input[5] & 0b11111100) >> 2, + ((input[5] & 0b00000011) << 4) | ((input[6] & 0b11110000) >> 4), + ((input[6] & 0b00001111) << 2) | ((input[7] & 0b11000000) >> 6), + input[7] & 0b00111111, + (input[8] & 0b11111100) >> 2, + ((input[8] & 0b00000011) << 4) | ((input[9] & 0b11110000) >> 4), + ((input[9] & 0b00001111) << 2) | ((input[10] & 0b11000000) >> 6), + input[10] & 0b00111111 + ] + + message["identification"] = bytes(b + (0x40 if b < 27 else 0) for b in id).decode("ascii").strip() + + elif type in [5, 6, 7, 8]: + # surface position + # there's no altitude data in this message type, but the type implies the aircraft is on ground + message["altitude"] = "ground" + + movement = ((input[4] & 0b00000111) << 4) | ((input[5] & 0b11110000) >> 4) + if movement == 1: + message["groundspeed"] = 0 + elif 2 <= movement < 9: + message["groundspeed"] = (movement - 1) * .0125 + elif 9 <= movement < 13: + message["groundspeed"] = 1 + (movement - 8) * .25 + elif 13 <= movement < 39: + message["groundspeed"] = 2 + (movement - 12) * .5 + elif 39 <= movement < 94: + message["groundspeed"] = 15 + (movement - 38) # * 1 + elif 94 <= movement < 109: + message["groundspeed"] = 70 + (movement - 108) * 2 + elif 109 <= movement < 124: + message["groundspeeed"] = 100 + (movement - 123) * 5 + + if (input[5] & 0b00001000) >> 3: + track = ((input[5] & 0b00000111) << 3) | ((input[6] & 0b11110000) >> 4) + message["groundtrack"] = (360 * track) / 128 + + cpr = self.__getCprData(icao, input, CprRecordType.GROUND) + if cpr is not None: + lat, lon = cpr + message["lat"] = lat + message["lon"] = lon + + elif type in [9, 10, 11, 12, 13, 14, 15, 16, 17, 18]: + # airborne position (w/ baro altitude) + + cpr = self.__getCprData(icao, input, CprRecordType.AIR) + if cpr is not None: + lat, lon = cpr + message["lat"] = lat + message["lon"] = lon + + q = (input[5] & 0b1) + altitude = ((input[5] & 0b11111110) << 3) | ((input[6] & 0b11110000) >> 4) + if q: + message["altitude"] = altitude * 25 - 1000 + elif altitude > 0: + altitude = self._gillhamDecode(altitude) + if altitude is not None: + message["altitude"] = altitude + + elif type == 19: + # airborne velocity + subtype = input[4] & 0b111 + if subtype in [1, 2]: + # velocity is reported in an east/west and a north/south component + # vew = velocity east / west + vew = ((input[5] & 0b00000011) << 8) | input[6] + # vns = velocity north / south + vns = ((input[7] & 0b01111111) << 3) | ((input[8] & 0b1110000000) >> 5) + + # 0 means no data + if vew != 0 and vns != 0: + # dew = direction east/west (0 = to east, 1 = to west) + dew = (input[5] & 0b00000100) >> 2 + # dns = direction north/south (0 = to north, 1 = to south) + dns = (input[7] & 0b10000000) >> 7 + + vx = vew - 1 + if dew: + vx *= -1 + vy = vns - 1 + if dns: + vy *= -1 + # supersonic + if subtype == 2: + vx *= 4 + vy *= 4 + message["groundspeed"] = sqrt(vx ** 2 + vy ** 2) + message["groundtrack"] = (atan2(vx, vy) * 360 / (2 * pi)) % 360 + + # vertical rate + vr = ((input[8] & 0b00000111) << 6) | ((input[9] & 0b11111100) >> 2) + if vr != 0: + # vertical speed sign (1 = negative) + svr = ((input[8] & 0b00001000) >> 3) + # vertical speed + vs = 64 * (vr - 1) + if svr: + vs *= -1 + message["verticalspeed"] = vs + + elif subtype in [3, 4]: + sh = (input[5] & 0b00000100) >> 2 + if sh: + hdg = ((input[5] & 0b00000011) << 8) | input[6] + message["heading"] = hdg * 360 / 1024 + airspeed = ((input[7] & 0b01111111) << 3) | ((input[8] & 0b11100000) >> 5) + if airspeed != 0: + airspeed -= 1 + # supersonic + if subtype == 4: + airspeed *= 4 + airspeed_type = (input[7] & 0b10000000) >> 7 + if airspeed_type: + message["TAS"] = airspeed + else: + message["IAS"] = airspeed + + elif type in [20, 21, 22]: + # airborne position (w/GNSS height) + + cpr = self.__getCprData(icao, input, CprRecordType.AIR) + if cpr is not None: + lat, lon = cpr + message["lat"] = lat + message["lon"] = lon + + altitude = (input[5] << 4) | ((input[6] & 0b1111) >> 4) + message["altitude"] = altitude * FEET_PER_METER + + elif type == 28: + # aircraft status + pass + + elif type == 29: + # target state and status information + pass + + elif type == 31: + # aircraft operation status + pass + + elif format == 11: + # Mode-S All-call reply + message["icao"] = input[1:4].hex() + + self.metrics.inc() + + if "icao" in message and AdsbLocation.mapKeys & message.keys(): + data = {k: message[k] for k in AdsbLocation.mapKeys if k in message} + loc = AdsbLocation(data) + Map.getSharedInstance().updateLocation(IcaoSource(message['icao']), loc, "ADS-B", None) + ReportingEngine.getSharedInstance().spot(message) + + return message + + def __getCprData(self, icao: str, input, cprType: CprRecordType): + self.cprCache.addRecord(icao, { + "cpr_format": (input[6] & 0b00000100) >> 2, + "lat_cpr": ((input[6] & 0b00000011) << 15) | (input[7] << 7) | ((input[8] & 0b11111110) >> 1), + "lon_cpr": ((input[8] & 0b00000001) << 16) | (input[9] << 8) | (input[10]), + }, cprType) + + records = self.cprCache.getRecentData(icao, cprType) + + try: + # records are sorted by timestamp, last should be newest + odd = next(r for r in reversed(records) if r["cpr_format"]) + even = next(r for r in reversed(records) if not r["cpr_format"]) + newest = next(reversed(records)) + + lat_cpr_even = even["lat_cpr"] / 2 ** 17 + lat_cpr_odd = odd["lat_cpr"] / 2 ** 17 + + # latitude zone index + j = floor(59 * lat_cpr_even - 60 * lat_cpr_odd + .5) + + nz = 15 + d_lat_even = cprType.baseAngle / (4 * nz) + d_lat_odd = cprType.baseAngle / (4 * nz - 1) + + lat_even = d_lat_even * ((j % 60) + lat_cpr_even) + lat_odd = d_lat_odd * ((j % 59) + lat_cpr_odd) + + if lat_even >= 270: + lat_even -= 360 + if lat_odd >= 270: + lat_odd -= 360 + + def nl(lat): + if lat == 0: + return 59 + elif lat == 87: + return 2 + elif lat == -87: + return 2 + elif lat > 87: + return 1 + elif lat < -87: + return 1 + else: + return floor((2 * pi) / acos(1 - (1 - cos(pi / (2 * nz))) / (cos((pi / 180) * abs(lat)) ** 2))) + + if nl(lat_even) != nl(lat_odd): + # latitude zone mismatch. + return + + lat = lat_odd if newest["cpr_format"] else lat_even + + lon_cpr_even = even["lon_cpr"] / 2 ** 17 + lon_cpr_odd = odd["lon_cpr"] / 2 ** 17 + + # longitude zone index + nl_lat = nl(lat) + m = floor(lon_cpr_even * (nl_lat - 1) - lon_cpr_odd * nl_lat + .5) + + n_even = max(nl_lat, 1) + n_odd = max(nl_lat - 1, 1) + + d_lon_even = cprType.baseAngle / n_even + d_lon_odd = cprType.baseAngle / n_odd + + lon_even = d_lon_even * (m % n_even + lon_cpr_even) + lon_odd = d_lon_odd * (m % n_odd + lon_cpr_odd) + + lon = lon_odd if newest["cpr_format"] else lon_even + if lon >= 180: + lon -= 360 + + return lat, lon + + except StopIteration: + # we don't have both CPR records. better luck next time. + pass + + def _grayDecode(self, input: int): + l = input.bit_length() + previous_bit = 0 + output = 0 + for i in reversed(range(0, l)): + bit = (previous_bit ^ ((input >> i) & 1)) + output |= bit << i + previous_bit = bit + return output + + gianniTable = [None, -200, 0, -100, 200, None, 100, None] + + def _gillhamDecode(self, input: int): + c = ((input & 0b10000000000) >> 8) | ((input & 0b00100000000) >> 7) | ((input & 0b00001000000) >> 6) + b = ((input & 0b00000010000) >> 2) | ((input & 0b00000001000) >> 2) | ((input & 0b00000000010) >> 1) + a = ((input & 0b01000000000) >> 7) | ((input & 0b00010000000) >> 6) | ((input & 0b00000100000) >> 5) + d = ((input & 0b00000000100) >> 1) | (input & 0b00000000001) + + dab = (d << 6) | (a << 3) | b + parity = dab.bit_count() % 2 + + offset = self.gianniTable[c] + + if offset is None: + # invalid decode... + return None + + if parity: + offset *= -1 + + altitude = self._grayDecode(dab) * 500 + offset - 1000 + return altitude diff --git a/owrx/aeronautical.py b/owrx/aeronautical.py new file mode 100644 index 000000000..49ce56f98 --- /dev/null +++ b/owrx/aeronautical.py @@ -0,0 +1,89 @@ +from owrx.map import Map, LatLngLocation, Source +from csdr.module import JsonParser +from abc import ABCMeta +import re + + +class AirplaneLocation(LatLngLocation): + def __init__(self, message): + self.props = message + if "lat" in message and "lon" in message: + super().__init__(message["lat"], message["lon"]) + else: + self.lat = None + self.lon = None + + def __dict__(self): + res = super().__dict__() + res.update(self.props) + return res + + +class IcaoSource(Source): + def __init__(self, icao: str, flight: str = None): + self.icao = icao.upper() + self.flight = flight + + def getKey(self) -> str: + return "icao:{}".format(self.icao) + + def __dict__(self): + d = {"icao": self.icao} + if self.flight is not None: + d["flight"] = self.flight + return d + + +class FlightSource(Source): + def __init__(self, flight): + self.flight = flight + + def getKey(self) -> str: + return "flight:{}".format(self.flight) + + def __dict__(self): + return {"flight": self.flight} + + +class AcarsProcessor(JsonParser, metaclass=ABCMeta): + flightRegex = re.compile("^([0-9A-Z]{2})0*([0-9A-Z]+$)") + + def processAcars(self, acars: dict, icao: str = None): + if "flight" in acars: + flight_id = self.processFlight(acars["flight"]) + elif "reg" in acars: + flight_id = acars['reg'].lstrip(".") + else: + return + + if "arinc622" in acars: + arinc622 = acars["arinc622"] + if "adsc" in arinc622: + adsc = arinc622["adsc"] + if "tags" in adsc and adsc["tags"]: + msg = {} + for tag in adsc["tags"]: + if "basic_report" in tag: + basic_report = tag["basic_report"] + msg.update({ + "lat": basic_report["lat"], + "lon": basic_report["lon"], + "altitude": basic_report["alt"], + }) + if "earth_ref_data" in tag: + earth_ref_data = tag["earth_ref_data"] + msg.update({ + "groundtrack": earth_ref_data["true_trk_deg"], + "groundspeed": earth_ref_data["gnd_spd_kts"], + "verticalspeed": earth_ref_data["vspd_ftmin"], + }) + if icao is not None: + source = IcaoSource(icao, flight=flight_id) + else: + source = FlightSource(flight_id) + Map.getSharedInstance().updateLocation( + source, AirplaneLocation(msg), "ACARS over {}".format(self.mode) + ) + + def processFlight(self, raw): + return self.flightRegex.sub(r"\g<1>\g<2>", raw) diff --git a/owrx/aprs/__init__.py b/owrx/aprs/__init__.py new file mode 100644 index 000000000..c07bbde21 --- /dev/null +++ b/owrx/aprs/__init__.py @@ -0,0 +1,618 @@ +from owrx.map import Map, LatLngLocation, Source +from owrx.metrics import Metrics, CounterMetric +from owrx.bands import Bandplan +from owrx.reporting import ReportingEngine +from datetime import datetime, timezone +from csdr.module import PickleModule +import re +import logging + +logger = logging.getLogger(__name__) + + +# speed is in knots... convert to metric (km/h) +knotsToKilometers = 1.852 +feetToMeters = 0.3048 +milesToKilometers = 1.609344 +inchesToMilimeters = 25.4 + + +def fahrenheitToCelsius(f): + return (f - 32) * 5 / 9 + + +# not sure what the correct encoding is. it seems TAPR has set utf-8 as a standard, but not everybody is following it. +encoding = "utf-8" + +# regex for altitute in comment field +altitudeRegex = re.compile("(^.*)\\/A=([0-9]{6})(.*$)") + +# regex for parsing third-party headers +thirdpartyeRegex = re.compile("^([a-zA-Z0-9-]+)>((([a-zA-Z0-9-]+\\*?,)*)([a-zA-Z0-9-]+\\*?)):(.*)$") + +# regex for getting the message id out of message +messageIdRegex = re.compile("^(.*){([0-9]{1,5})$") + +# regex to filter pseudo "WIDE" path elements +widePattern = re.compile("^WIDE[0-9]$") + + +def decodeBase91(input): + base = decodeBase91(input[:-1]) * 91 if len(input) > 1 else 0 + return base + (ord(input[-1]) - 33) + + +def getSymbolData(symbol, table): + return {"symbol": symbol, "table": table, "index": ord(symbol) - 33, "tableindex": ord(table) - 33} + + +class Ax25Parser(PickleModule): + def process(self, ax25frame): + control_pid = ax25frame.find(bytes([0x03, 0xF0])) + if control_pid % 7 > 0: + logger.warning("aprs packet framing error: control/pid position not aligned with 7-octet callsign data") + + def chunks(l, n): + """Yield successive n-sized chunks from l.""" + for i in range(0, len(l), n): + yield l[i:i + n] + + try: + return { + "destination": self.extractCallsign(ax25frame[0:7]), + "source": self.extractCallsign(ax25frame[7:14]), + "path": [self.extractCallsign(c) for c in chunks(ax25frame[14:control_pid], 7)], + "data": ax25frame[control_pid + 2 :], + } + except (ValueError, IndexError): + logger.exception("error parsing ax25 frame") + + def extractCallsign(self, input): + cs = { + "callsign": bytes([b >> 1 for b in input[0:6]]).decode(encoding, "replace").strip(), + } + ssid = (input[6] & 0b00011110) >> 1 + if ssid > 0: + cs["ssid"] = ssid + return cs + + +class WeatherMapping(object): + def __init__(self, char, key, length, scale=None): + self.char = char + self.key = key + self.length = length + self.scale = scale + + def matches(self, input): + return self.char == input[0] and len(input) > self.length + + def updateWeather(self, weather, input): + def deepApply(obj, key, v): + keys = key.split(".") + if len(keys) > 1: + if not keys[0] in obj: + obj[keys[0]] = {} + deepApply(obj[keys[0]], ".".join(keys[1:]), v) + else: + obj[key] = v + + try: + value = int(input[1 : 1 + self.length]) + if self.scale: + value = self.scale(value) + deepApply(weather, self.key, value) + except ValueError: + pass + remain = input[1 + self.length :] + return weather, remain + + +class WeatherParser(object): + mappings = [ + WeatherMapping("c", "wind.direction", 3), + WeatherMapping("s", "wind.speed", 3, lambda x: x * milesToKilometers), + WeatherMapping("g", "wind.gust", 3, lambda x: x * milesToKilometers), + WeatherMapping("t", "temperature", 3, fahrenheitToCelsius), + WeatherMapping("r", "rain.hour", 3, lambda x: x / 100 * inchesToMilimeters), + WeatherMapping("p", "rain.day", 3, lambda x: x / 100 * inchesToMilimeters), + WeatherMapping("P", "rain.sincemidnight", 3, lambda x: x / 100 * inchesToMilimeters), + WeatherMapping("h", "humidity", 2), + WeatherMapping("b", "barometricpressure", 5, lambda x: x / 10), + WeatherMapping("s", "snowfall", 3, lambda x: x * 25.4), + ] + + def __init__(self, data, weather=None): + self.data = data + self.weather = {} if weather is None else weather + + def getWeather(self): + doWork = True + weather = self.weather + while doWork: + mapping = next((m for m in WeatherParser.mappings if m.matches(self.data)), None) + if mapping: + (weather, remain) = mapping.updateWeather(weather, self.data) + self.data = remain + doWork = len(self.data) > 0 + else: + doWork = False + return weather + + def getRemainder(self): + return self.data + + +class AprsLocation(LatLngLocation): + def __init__(self, data): + super().__init__(data["lat"], data["lon"]) + self.data = data + + def __dict__(self): + res = super(AprsLocation, self).__dict__() + for key in ["comment", "symbol", "course", "speed"]: + if key in self.data: + res[key] = self.data[key] + return res + + +class AprsSource(Source): + def __init__(self, source): + self.source = source + + def getKey(self) -> str: + callsign = self.source["callsign"] + if "ssid" in self.source: + callsign += "-{}".format(self.source["ssid"]) + return "aprs:{}".format(callsign) + + def __dict__(self): + return self.source + + +class AprsParser(PickleModule): + def __init__(self): + super().__init__() + self.metrics = {} + self.band = None + + def setDialFrequency(self, freq): + self.band = Bandplan.getSharedInstance().findBand(freq) + + def getMetric(self, category): + if category not in self.metrics: + band = "unknown" + if self.band is not None: + band = self.band.getName() + name = "aprs.decodes.{band}.aprs.{category}".format(band=band, category=category) + metrics = Metrics.getSharedInstance() + self.metrics[category] = metrics.getMetric(name) + if self.metrics[category] is None: + self.metrics[category] = CounterMetric() + metrics.addMetric(name, self.metrics[category]) + return self.metrics[category] + + def isDirect(self, aprsData): + if "path" in aprsData and len(aprsData["path"]) > 0: + hops = [host for host in aprsData["path"] if widePattern.match(host["callsign"]) is None] + if len(hops) > 0: + return False + if "type" in aprsData and aprsData["type"] in ["thirdparty", "item", "object"]: + return False + return True + + def process(self, data): + try: + # TODO how can we tell if this is an APRS frame at all? + aprsData = self.parseAprsData(data) + + logger.debug("decoded APRS data: %s", aprsData) + self.updateMap(aprsData) + self.getMetric("total").inc() + if self.isDirect(aprsData): + self.getMetric("direct").inc() + + # the frontend uses this to distinguish messages from the different parsers + aprsData["mode"] = "APRS" + + ReportingEngine.getSharedInstance().spot(aprsData) + return aprsData + except Exception: + logger.exception("exception while parsing aprs data") + + def updateMap(self, mapData): + if "type" in mapData and mapData["type"] == "thirdparty" and "data" in mapData: + mapData = mapData["data"] + if "lat" in mapData and "lon" in mapData: + loc = AprsLocation(mapData) + source = mapData["source"].copy() + # these are special packets, sent on behalf of other entities + if "type" in mapData: + if mapData["type"] == "item" and "item" in mapData: + source["item"] = mapData["item"] + elif mapData["type"] == "object" and "object" in mapData: + source["object"] = mapData["object"] + Map.getSharedInstance().updateLocation(AprsSource(source), loc, "APRS", self.band) + + def hasCompressedCoordinates(self, raw): + return raw[0] == "/" or raw[0] == "\\" + + def parseUncompressedCoordinates(self, raw): + lat = int(raw[0:2]) + float(raw[2:7]) / 60 + if raw[7] == "S": + lat *= -1 + lon = int(raw[9:12]) + float(raw[12:17]) / 60 + if raw[17] == "W": + lon *= -1 + return {"lat": lat, "lon": lon, "symbol": getSymbolData(raw[18], raw[8])} + + def parseCompressedCoordinates(self, raw): + return { + "lat": 90 - decodeBase91(raw[1:5]) / 380926, + "lon": -180 + decodeBase91(raw[5:9]) / 190463, + "symbol": getSymbolData(raw[9], raw[0]), + } + + def parseTimestamp(self, raw): + now = datetime.now() + if raw[6] == "h": + ts = datetime.strptime(raw[0:6], "%H%M%S") + ts = ts.replace(year=now.year, month=now.month, day=now.month, tzinfo=timezone.utc) + else: + ts = datetime.strptime(raw[0:6], "%d%H%M") + ts = ts.replace(year=now.year, month=now.month) + if raw[6] == "z": + ts = ts.replace(tzinfo=timezone.utc) + elif raw[6] == "/": + ts = ts.replace(tzinfo=now.tzinfo) + else: + logger.warning("invalid timezone info byte: %s", raw[6]) + return int(ts.timestamp() * 1000) + + def parseStatusUpate(self, raw): + res = {"type": "status"} + if raw[6] == "z": + res["timestamp"] = self.parseTimestamp(raw[0:7]) + res["comment"] = raw[7:] + else: + res["comment"] = raw + return res + + def parseAprsData(self, data): + information = data["data"] + + # forward some of the ax25 data + aprsData = {"source": data["source"], "destination": data["destination"], "path": data["path"]} + + if information[0] == 0x1C or information[0] == ord("`") or information[0] == ord("'"): + aprsData.update(MicEParser().parse(data)) + return aprsData + + information = information.decode(encoding, "replace") + + # APRS data type identifier + dti = information[0] + + if dti == "!" or dti == "=": + # position without timestamp + aprsData.update(self.parseRegularAprsData(information[1:])) + elif dti == "/" or dti == "@": + # position with timestamp + aprsData["timestamp"] = self.parseTimestamp(information[1:8]) + aprsData.update(self.parseRegularAprsData(information[8:])) + elif dti == ">": + # status update + aprsData.update(self.parseStatusUpate(information[1:])) + elif dti == "}": + # third party + aprsData.update(self.parseThirdpartyAprsData(information[1:])) + elif dti == ":": + # message + aprsData.update(self.parseMessage(information[1:])) + elif dti == ";": + # object + aprsData.update(self.parseObject(information[1:])) + elif dti == ")": + # item + aprsData.update(self.parseItem(information[1:])) + + return aprsData + + def parseObject(self, information): + result = {"type": "object"} + if len(information) > 16: + result["object"] = information[0:9].strip() + result["live"] = information[9] == "*" + result["timestamp"] = self.parseTimestamp(information[10:17]) + result.update(self.parseRegularAprsData(information[17:])) + # override type, losing information about compression + result["type"] = "object" + return result + + def parseItem(self, information): + result = {"type": "item"} + if len(information) > 3: + indexes = [information[0:10].find(p) for p in ["!", "_"]] + filtered = [i for i in indexes if i >= 3] + filtered.sort() + if len(filtered): + index = filtered[0] + result["item"] = information[0:index] + result["live"] = information[index] == "!" + result.update(self.parseRegularAprsData(information[index + 1 :])) + # override type, losing information about compression + result["type"] = "item" + return result + + def parseMessage(self, information): + result = {"type": "message"} + if len(information) > 9 and information[9] == ":": + result["adressee"] = information[0:9] + message = information[10:] + if len(message) > 3 and message[0:3] == "ack": + result["type"] = "messageacknowledgement" + result["messageid"] = int(message[3:8]) + elif len(message) > 3 and message[0:3] == "rej": + result["type"] = "messagerejection" + result["messageid"] = int(message[3:8]) + else: + matches = messageIdRegex.match(message) + if matches: + result["messageid"] = int(matches.group(2)) + message = matches.group(1) + result["message"] = message + return result + + def parseThirdpartyAprsData(self, information): + # in thirdparty packets, the callsign is passed as a string with -SSID suffix... + # this seems to be the only case where parsing is necessary, hence this function is inline + def parseCallsign(callsign): + el = callsign.split('-') + result = {"callsign": el[0]} + if len(el) > 1: + result["ssid"] = int(el[1]) + return result + + matches = thirdpartyeRegex.match(information) + if matches: + path = matches.group(2).split(",") + destination = next((c.strip("*").upper() for c in path if c.endswith("*")), None) + data = self.parseAprsData( + { + "source": parseCallsign(matches.group(1).upper()), + "destination": parseCallsign(destination), + "path": [parseCallsign(c) for c in path], + "data": matches.group(6).encode(encoding), + } + ) + return {"type": "thirdparty", "data": data} + + return {"type": "thirdparty"} + + def parseRegularAprsData(self, information): + if self.hasCompressedCoordinates(information): + aprsData = self.parseCompressedCoordinates(information[0:10]) + aprsData["type"] = "compressed" + if information[10] != " ": + if information[10] == "{": + # pre-calculated radio range + aprsData["range"] = 2 * 1.08 ** (ord(information[11]) - 33) * milesToKilometers + else: + aprsData["course"] = (ord(information[10]) - 33) * 4 + # speed is in knots... convert to metric (km/h) + aprsData["speed"] = (1.08 ** (ord(information[11]) - 33) - 1) * knotsToKilometers + # compression type + t = ord(information[12]) + aprsData["fix"] = (t & 0b00100000) > 0 + sources = ["other", "GLL", "GGA", "RMC"] + aprsData["nmeasource"] = sources[(t & 0b00011000) >> 3] + origins = [ + "Compressed", + "TNC BText", + "Software", + "[tbd]", + "KPC3", + "Pico", + "Other tracker", + "Digipeater conversion", + ] + aprsData["compressionorigin"] = origins[t & 0b00000111] + comment = information[13:] + else: + aprsData = self.parseUncompressedCoordinates(information[0:19]) + aprsData["type"] = "regular" + comment = information[19:] + + def decodeHeightGainDirectivity(comment): + res = {"height": 2 ** int(comment[4]) * 10 * feetToMeters, "gain": int(comment[5])} + directivity = int(comment[6]) + if directivity == 0: + res["directivity"] = "omni" + elif 0 < directivity < 9: + res["directivity"] = directivity * 45 + return res + + # aprs data extensions + # yes, weather stations are officially identified by their symbols. go figure... + if "symbol" in aprsData and aprsData["symbol"]["index"] == 62: + # weather report + weather = {} + if len(comment) > 6 and comment[3] == "/": + try: + weather["wind"] = {"direction": int(comment[0:3]), "speed": int(comment[4:7]) * milesToKilometers} + except ValueError: + pass + comment = comment[7:] + + parser = WeatherParser(comment, weather) + aprsData["weather"] = parser.getWeather() + comment = parser.getRemainder() + elif len(comment) > 6: + if comment[3] == "/": + # course and speed + # for a weather report, this would be wind direction and speed + try: + aprsData["course"] = int(comment[0:3]) + aprsData["speed"] = int(comment[4:7]) * knotsToKilometers + except ValueError: + pass + comment = comment[7:] + elif comment[0:3] == "PHG": + # station power and effective antenna height/gain/directivity + try: + powerCodes = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] + aprsData["power"] = powerCodes[int(comment[3])] + aprsData.update(decodeHeightGainDirectivity(comment)) + except ValueError: + pass + comment = comment[7:] + elif comment[0:3] == "RNG": + # pre-calculated radio range + try: + aprsData["range"] = int(comment[3:7]) * milesToKilometers + except ValueError: + pass + comment = comment[7:] + elif comment[0:3] == "DFS": + # direction finding signal strength and antenna height/gain + try: + aprsData["strength"] = int(comment[3]) + aprsData.update(decodeHeightGainDirectivity(comment)) + except ValueError: + pass + comment = comment[7:] + + matches = altitudeRegex.match(comment) + if matches: + aprsData["altitude"] = int(matches.group(2)) * feetToMeters + comment = matches.group(1) + matches.group(3) + + aprsData["comment"] = comment + + return aprsData + + +class MicEParser(object): + def extractNumber(self, input): + n = ord(input) + if n >= ord("P"): + return n - ord("P") + if n >= ord("A"): + return n - ord("A") + return n - ord("0") + + def listToNumber(self, input): + base = self.listToNumber(input[:-1]) * 10 if len(input) > 1 else 0 + return base + input[-1] + + def extractAltitude(self, comment): + if len(comment) < 4 or comment[3] != "}": + return (comment, None) + return comment[4:], decodeBase91(comment[:3]) - 10000 + + def extractDevice(self, comment): + if len(comment) > 0: + if comment[0] == ">": + if len(comment) > 1: + if comment[-1] == "=": + return comment[1:-1], {"manufacturer": "Kenwood", "device": "TH-D72"} + if comment[-1] == "^": + return comment[1:-1], {"manufacturer": "Kenwood", "device": "TH-D74"} + return comment[1:], {"manufacturer": "Kenwood", "device": "TH-D7A"} + if comment[0] == "]": + if len(comment) > 1 and comment[-1] == "=": + return comment[1:-1], {"manufacturer": "Kenwood", "device": "TM-D710"} + return comment[1:], {"manufacturer": "Kenwood", "device": "TM-D700"} + if len(comment) > 2 and (comment[0] == "`" or comment[0] == "'"): + if comment[-2] == "_": + devices = { + "b": "VX-8", + '"': "FTM-350", + "#": "VX-8G", + "$": "FT1D", + "%": "FTM-400DR", + ")": "FTM-100D", + "(": "FT2D", + "0": "FT3D", + } + return comment[1:-2], {"manufacturer": "Yaesu", "device": devices.get(comment[-1], "Unknown")} + if comment[-2:] == " X": + return comment[1:-2], {"manufacturer": "SainSonic", "device": "AP510"} + if comment[-2] == "(": + devices = {"5": "D578UV", "8": "D878UV"} + return comment[1:-2], {"manufacturer": "Anytone", "device": devices.get(comment[-1], "Unknown")} + if comment[-2] == "|": + devices = {"3": "TinyTrack3", "4": "TinyTrack4"} + return comment[1:-2], {"manufacturer": "Byonics", "device": devices.get(comment[-1], "Unknown")} + if comment[-2:] == "^v": + return comment[1:-2], {"manufacturer": "HinzTec", "device": "anyfrog"} + if comment[-2] == ":": + devices = {"4": "P4dragon DR-7400 modem", "8": "P4dragon DR-7800 modem"} + return ( + comment[1:-2], + {"manufacturer": "SCS GmbH & Co.", "device": devices.get(comment[-1], "Unknown")}, + ) + if comment[-2:] == "~v": + return comment[1:-2], {"manufacturer": "Other", "device": "Other"} + return comment[1:-2], None + return comment, None + + def parse(self, data): + information = data["data"] + destination = data["destination"]["callsign"] + + rawLatitude = [self.extractNumber(c) for c in destination[0:6]] + lat = self.listToNumber(rawLatitude[0:2]) + self.listToNumber(rawLatitude[2:6]) / 6000 + if ord(destination[3]) <= ord("9"): + lat *= -1 + + lon = information[1] - 28 + if ord(destination[4]) >= ord("P"): + lon += 100 + if 180 <= lon <= 189: + lon -= 80 + if 190 <= lon <= 199: + lon -= 190 + + minutes = information[2] - 28 + if minutes >= 60: + minutes -= 60 + + lon += minutes / 60 + (information[3] - 28) / 6000 + + if ord(destination[5]) >= ord("P"): + lon *= -1 + + speed = (information[4] - 28) * 10 + dc28 = information[5] - 28 + speed += int(dc28 / 10) + course = (dc28 % 10) * 100 + course += information[6] - 28 + if speed >= 800: + speed -= 800 + if course >= 400: + course -= 400 + # speed is in knots... convert to metric (km/h) + speed *= knotsToKilometers + + comment = information[9:].decode(encoding, "replace").strip() + (comment, altitude) = self.extractAltitude(comment) + + (comment, device) = self.extractDevice(comment) + + # altitude might be inside the device string, so repeat and choose one + (comment, insideAltitude) = self.extractAltitude(comment) + altitude = next((a for a in [altitude, insideAltitude] if a is not None), None) + + return { + "fix": information[0] == ord("`") or information[0] == 0x1C, + "lat": lat, + "lon": lon, + "comment": comment, + "altitude": altitude, + "speed": speed, + "course": course, + "device": device, + "type": "Mic-E", + "symbol": getSymbolData(chr(information[7]), chr(information[8])), + } diff --git a/owrx/aprs/direwolf.py b/owrx/aprs/direwolf.py new file mode 100644 index 000000000..9a533ea2b --- /dev/null +++ b/owrx/aprs/direwolf.py @@ -0,0 +1,208 @@ +from pycsdr.types import Format +from pycsdr.modules import Writer, TcpSource, ExecModule, Buffer +from csdr.module import LogReader +from owrx.config.core import CoreConfig +from owrx.config import Config +from abc import ABC, abstractmethod +import time +import os +import random +import socket + +import logging + +logger = logging.getLogger(__name__) + +FEET_PER_METER = 3.28084 + + +class DirewolfConfigSubscriber(ABC): + @abstractmethod + def onConfigChanged(self): + pass + + +class DirewolfConfig: + config_keys = [ + "aprs_callsign", + "aprs_igate_enabled", + "aprs_igate_server", + "aprs_igate_password", + "receiver_gps", + "aprs_igate_symbol", + "aprs_igate_beacon", + "aprs_igate_gain", + "aprs_igate_dir", + "aprs_igate_comment", + "aprs_igate_height", + ] + + def __init__(self): + self.subscribers = [] + self.configSub = None + self.port = None + + def wire(self, subscriber: DirewolfConfigSubscriber): + self.subscribers.append(subscriber) + if self.configSub is None: + pm = Config.get() + self.configSub = pm.filter(*DirewolfConfig.config_keys).wire(self._fireChanged) + + def unwire(self, subscriber: DirewolfConfigSubscriber): + self.subscribers.remove(subscriber) + if not self.subscribers and self.configSub is not None: + self.configSub.cancel() + + def _fireChanged(self, changes): + for sub in self.subscribers: + try: + sub.onConfigChanged() + except Exception: + logger.exception("Error while notifying Direwolf subscribers") + + def getPort(self): + # direwolf has some strange hardcoded port ranges + while self.port is None: + try: + port = random.randrange(1024, 49151) + # test if port is available for use + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + s.bind(("localhost", port)) + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.close() + self.port = port + except OSError: + pass + return self.port + + def getConfig(self, is_service): + pm = Config.get() + + config = """ +ACHANNELS 1 +ADEVICE stdin null + +CHANNEL 0 +MYCALL {callsign} +MODEM 1200 + +KISSPORT {port} +AGWPORT off + """.format( + port=self.getPort(), callsign=pm["aprs_callsign"] + ) + + if is_service and pm["aprs_igate_enabled"]: + pbeacon = "" + + if pm["aprs_igate_beacon"]: + # Format beacon lat/lon + lat = pm["receiver_gps"]["lat"] + lon = pm["receiver_gps"]["lon"] + direction_ns = "N" if lat > 0 else "S" + direction_we = "E" if lon > 0 else "W" + lat = abs(lat) + lon = abs(lon) + lat = "{0:02d}^{1:05.2f}{2}".format(int(lat), (lat - int(lat)) * 60, direction_ns) + lon = "{0:03d}^{1:05.2f}{2}".format(int(lon), (lon - int(lon)) * 60, direction_we) + + # Convert height from meters to feet if specified + height = "" + if "aprs_igate_height" in pm: + try: + height_m = float(pm["aprs_igate_height"]) + height_ft = round(height_m * FEET_PER_METER) + height = "HEIGHT=" + str(height_ft) + except: + logger.error( + "Cannot parse 'aprs_igate_height', expected float: " + str(pm["aprs_igate_height"]) + ) + + pbeacon = 'PBEACON sendto=IG delay=0:30 every=60:00 symbol={symbol} lat={lat} long={lon} {height} {gain} {adir} comment="{comment}"'.format( + symbol=pm["aprs_igate_symbol"], + lat=lat, + lon=lon, + height=height, + gain="GAIN=" + str(pm["aprs_igate_gain"]) if "aprs_igate_gain" in pm else "", + adir="DIR=" + str(pm["aprs_igate_dir"]) if "aprs_igate_dir" in pm else "", + comment=pm["aprs_igate_comment"], + ) + + logger.info("APRS PBEACON String: " + pbeacon) + + config += """ +IGSERVER {server} +IGLOGIN {callsign} {password} +{pbeacon} + """.format( + server=pm["aprs_igate_server"], + callsign=pm["aprs_callsign"], + password=pm["aprs_igate_password"], + pbeacon=pbeacon, + ) + + return config + + +class DirewolfModule(ExecModule, DirewolfConfigSubscriber): + def __init__(self, service: bool = False): + self.tcpSource = None + self.writer = None + self.service = service + self.direwolfConfigPath = "{tmp_dir}/openwebrx_direwolf_{myid}.conf".format( + tmp_dir=CoreConfig().get_temporary_directory(), myid=id(self) + ) + + self.direwolfConfig = DirewolfConfig() + self.direwolfConfig.wire(self) + self.__writeConfig() + + super().__init__(Format.SHORT, Format.CHAR, ["direwolf", "-c", self.direwolfConfigPath, "-r", "48000", "-t", "0", "-q", "d", "-q", "h"]) + # direwolf supplies the data via a socket which we tap into in start() + # the output on its STDOUT is informative, but we still want to log it + buffer = Buffer(Format.CHAR) + self.logReader = LogReader(__name__, buffer) + super().setWriter(buffer) + self.start() + + def __writeConfig(self): + file = open(self.direwolfConfigPath, "w") + file.write(self.direwolfConfig.getConfig(self.service)) + file.close() + + def setWriter(self, writer: Writer) -> None: + self.writer = writer + if self.tcpSource is not None: + self.tcpSource.setWriter(writer) + + def start(self): + delay = 0.5 + retries = 0 + while True: + try: + self.tcpSource = TcpSource(self.direwolfConfig.getPort(), Format.CHAR) + if self.writer: + self.tcpSource.setWriter(self.writer) + break + except ConnectionError: + if retries > 20: + logger.error("maximum number of connection attempts reached. did direwolf start up correctly?") + raise + retries += 1 + time.sleep(delay) + + def restart(self): + self.__writeConfig() + super().restart() + self.start() + + def onConfigChanged(self): + self.restart() + + def stop(self) -> None: + super().stop() + self.logReader.stop() + self.logReader = None + os.unlink(self.direwolfConfigPath) + self.direwolfConfig.unwire(self) + self.direwolfConfig = None diff --git a/owrx/aprs/kiss.py b/owrx/aprs/kiss.py new file mode 100644 index 000000000..70c2e5372 --- /dev/null +++ b/owrx/aprs/kiss.py @@ -0,0 +1,54 @@ +from pycsdr.types import Format +from csdr.module import ThreadModule +import pickle + +import logging + +logger = logging.getLogger(__name__) + +FEND = 0xC0 +FESC = 0xDB +TFEND = 0xDC +TFESC = 0xDD + + +class KissDeframer(ThreadModule): + def __init__(self): + self.escaped = False + self.buf = bytearray() + super().__init__() + + def getInputFormat(self) -> Format: + return Format.CHAR + + def getOutputFormat(self) -> Format: + return Format.CHAR + + def run(self): + while self.doRun: + data = self.reader.read() + if data is None: + self.doRun = False + else: + for frame in self.parse(data): + self.writer.write(pickle.dumps(frame)) + + def parse(self, input): + for b in input: + if b == FESC: + self.escaped = True + elif self.escaped: + if b == TFEND: + self.buf.append(FEND) + elif b == TFESC: + self.buf.append(FESC) + else: + logger.warning("invalid escape char: %s", str(input[0])) + self.escaped = False + elif b == FEND: + # data frames start with 0x00 + if len(self.buf) > 1 and self.buf[0] == 0x00: + yield self.buf[1:] + self.buf = bytearray() + else: + self.buf.append(b) diff --git a/owrx/audio/__init__.py b/owrx/audio/__init__.py new file mode 100644 index 000000000..170bde383 --- /dev/null +++ b/owrx/audio/__init__.py @@ -0,0 +1,86 @@ +from owrx.config import Config +from abc import ABC, ABCMeta, abstractmethod +from typing import List + +import logging + +logger = logging.getLogger(__name__) + + +class AudioChopperProfile(ABC): + @abstractmethod + def getInterval(self): + pass + + @abstractmethod + def getFileTimestampFormat(self): + pass + + @abstractmethod + def decoder_commandline(self, file): + pass + + +class ProfileSourceSubscriber(ABC): + @abstractmethod + def onProfilesChanged(self): + pass + + +class ProfileSource(ABC): + def __init__(self): + self.subscribers = [] + + @abstractmethod + def getProfiles(self) -> List[AudioChopperProfile]: + pass + + def subscribe(self, subscriber: ProfileSourceSubscriber): + if subscriber in self.subscribers: + return + self.subscribers.append(subscriber) + + def unsubscribe(self, subscriber: ProfileSourceSubscriber): + if subscriber not in self.subscribers: + return + self.subscribers.remove(subscriber) + + def fireProfilesChanged(self): + for sub in self.subscribers.copy(): + try: + sub.onProfilesChanged() + except Exception: + logger.exception("Error while notifying profile subscriptions") + + +class ConfigWiredProfileSource(ProfileSource, metaclass=ABCMeta): + def __init__(self): + super().__init__() + self.configSub = None + + @abstractmethod + def getPropertiesToWire(self) -> List[str]: + pass + + def subscribe(self, subscriber: ProfileSourceSubscriber): + super().subscribe(subscriber) + if self.subscribers and self.configSub is None: + self.configSub = Config.get().filter(*self.getPropertiesToWire()).wire(self.fireProfilesChanged) + + def unsubscribe(self, subscriber: ProfileSourceSubscriber): + super().unsubscribe(subscriber) + if not self.subscribers and self.configSub is not None: + self.configSub.cancel() + self.configSub = None + + def fireProfilesChanged(self, *args): + super().fireProfilesChanged() + + +class StaticProfileSource(ProfileSource): + def __init__(self, profiles: List[AudioChopperProfile]): + super().__init__() + self.profiles = profiles + + def getProfiles(self) -> List[AudioChopperProfile]: + return self.profiles diff --git a/owrx/audio/chopper.py b/owrx/audio/chopper.py new file mode 100644 index 000000000..92ccbb860 --- /dev/null +++ b/owrx/audio/chopper.py @@ -0,0 +1,91 @@ +from owrx.modes import Modes, AudioChopperMode +from owrx.audio import AudioChopperProfile +from itertools import groupby +from owrx.audio import ProfileSourceSubscriber +from owrx.audio.wav import AudioWriter +from owrx.audio.queue import QueueJob +from csdr.module import ThreadModule +from pycsdr.types import Format +from abc import ABC, abstractmethod +import pickle + +import logging + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class AudioChopperParser(ABC): + @abstractmethod + def parse(self, profile: AudioChopperProfile, frequency: int, line: bytes): + pass + + +class AudioChopper(ThreadModule, ProfileSourceSubscriber): + def __init__(self, mode_str: str, parser: AudioChopperParser): + self.parser = parser + self.dialFrequency = None + self.doRun = True + self.writers = [] + mode = Modes.findByModulation(mode_str) + if mode is None or not isinstance(mode, AudioChopperMode): + raise ValueError("Mode {} is not an audio chopper mode".format(mode_str)) + self.profile_source = mode.get_profile_source() + super().__init__() + + def getInputFormat(self) -> Format: + return Format.SHORT + + def getOutputFormat(self) -> Format: + return Format.CHAR + + def stop_writers(self): + while self.writers: + self.writers.pop().stop() + + def setup_writers(self): + self.stop_writers() + sorted_profiles = sorted(self.profile_source.getProfiles(), key=lambda p: p.getInterval()) + groups = {interval: list(group) for interval, group in groupby(sorted_profiles, key=lambda p: p.getInterval())} + writers = [ + AudioWriter(self, interval, profiles) for interval, profiles in groups.items() + ] + for w in writers: + w.start() + self.writers = writers + + def run(self) -> None: + logger.debug("Audio chopper starting up") + self.setup_writers() + self.profile_source.subscribe(self) + while self.doRun: + data = None + try: + data = self.reader.read() + except ValueError: + pass + if data is None: + self.doRun = False + else: + for w in self.writers: + w.write(data.tobytes()) + + logger.debug("Audio chopper shutting down") + self.profile_source.unsubscribe(self) + self.stop_writers() + + def onProfilesChanged(self): + logger.debug("profile change received, resetting writers...") + self.setup_writers() + + def setDialFrequency(self, frequency: int) -> None: + self.dialFrequency = frequency + + def createJob(self, profile, filename): + return QueueJob(profile, self.dialFrequency, self, filename) + + def sendResult(self, result): + for line in result.lines: + data = self.parser.parse(result.profile, result.frequency, line) + if data is not None and self.writer is not None: + self.writer.write(pickle.dumps(data)) diff --git a/owrx/audio/queue.py b/owrx/audio/queue.py new file mode 100644 index 000000000..8b5078fb3 --- /dev/null +++ b/owrx/audio/queue.py @@ -0,0 +1,184 @@ +from owrx.config import Config +from owrx.config.core import CoreConfig +from owrx.metrics import Metrics, CounterMetric, DirectMetric +from queue import Queue, Full, Empty +import subprocess +import os +import threading + +import logging + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class QueueJobResult: + def __init__(self, profile, frequency, lines): + self.profile = profile + self.frequency = frequency + self.lines = lines + + +class QueueJob(object): + def __init__(self, profile, frequency, writer, file): + self.profile = profile + self.frequency = frequency + self.writer = writer + self.file = file + + def run(self): + logger.debug("processing file %s", self.file) + tmp_dir = CoreConfig().get_temporary_directory() + decoder = subprocess.Popen( + ["nice", "-n", "10"] + self.profile.decoder_commandline(self.file), + stdout=subprocess.PIPE, + cwd=tmp_dir, + close_fds=True, + ) + lines = None + try: + lines = [l for l in decoder.stdout] + except OSError: + decoder.stdout.flush() + # TODO uncouple parsing from the output so that decodes can still go to the map and the spotters + logger.debug("output has gone away while decoding job.") + + # keep this out of the try/except + if lines is not None: + self.writer.sendResult(QueueJobResult(self.profile, self.frequency, lines)) + + try: + rc = decoder.wait(timeout=10) + if rc != 0: + raise RuntimeError("decoder return code: {0}".format(rc)) + except subprocess.TimeoutExpired: + logger.warning("subprocess (pid=%i}) did not terminate correctly; sending kill signal.", decoder.pid) + decoder.kill() + raise + + def unlink(self): + try: + os.unlink(self.file) + except FileNotFoundError: + pass + + +PoisonPill = object() + + +class QueueWorker(threading.Thread): + def __init__(self, queue): + self.queue = queue + self.doRun = True + super().__init__() + + def run(self) -> None: + while self.doRun: + job = self.queue.get() + if job is PoisonPill: + self.stop() + else: + try: + job.run() + except Exception: + logger.exception("failed to decode job") + self.queue.onError() + finally: + job.unlink() + + self.queue.task_done() + + def stop(self): + self.doRun = False + + +class DecoderQueue(Queue): + sharedInstance = None + creationLock = threading.Lock() + + @staticmethod + def getSharedInstance(): + with DecoderQueue.creationLock: + if DecoderQueue.sharedInstance is None: + DecoderQueue.sharedInstance = DecoderQueue() + return DecoderQueue.sharedInstance + + @staticmethod + def stopAll(): + with DecoderQueue.creationLock: + if DecoderQueue.sharedInstance is not None: + DecoderQueue.sharedInstance.stop() + DecoderQueue.sharedInstance = None + + def __init__(self): + pm = Config.get() + super().__init__(pm["decoding_queue_length"]) + self.workers = [] + self._setWorkers(pm["decoding_queue_workers"]) + self.subscriptions = [ + pm.wireProperty("decoding_queue_length", self._setMaxSize), + pm.wireProperty("decoding_queue_workers", self._setWorkers), + ] + metrics = Metrics.getSharedInstance() + metrics.addMetric("decoding.queue.length", DirectMetric(self.qsize)) + self.inCounter = CounterMetric() + metrics.addMetric("decoding.queue.in", self.inCounter) + self.outCounter = CounterMetric() + metrics.addMetric("decoding.queue.out", self.outCounter) + self.overflowCounter = CounterMetric() + metrics.addMetric("decoding.queue.overflow", self.overflowCounter) + self.errorCounter = CounterMetric() + metrics.addMetric("decoding.queue.error", self.errorCounter) + + def _setMaxSize(self, size): + if self.maxsize == size: + return + self.maxsize = size + + def _setWorkers(self, workers): + while len(self.workers) > workers: + logger.debug("stopping one worker") + self.workers.pop().stop() + while len(self.workers) < workers: + logger.debug("starting one worker") + self.workers.append(self.newWorker()) + + def stop(self): + logger.debug("shutting down the queue") + while self.subscriptions: + self.subscriptions.pop().cancel() + try: + # purge all remaining jobs + while not self.empty(): + job = self.get() + job.unlink() + self.task_done() + except Empty: + pass + # put() a PoisonPill for all active workers to shut them down + for w in self.workers: + if w.is_alive(): + self.put(PoisonPill) + self.join() + + def put(self, item, **kwargs): + self.inCounter.inc() + try: + super(DecoderQueue, self).put(item, block=False) + except Full: + self.overflowCounter.inc() + raise + + def get(self, **kwargs): + # super.get() is blocking, so it would mess up the stats to inc() first + out = super(DecoderQueue, self).get(**kwargs) + self.outCounter.inc() + return out + + def newWorker(self): + worker = QueueWorker(self) + worker.start() + return worker + + def onError(self): + self.errorCounter.inc() diff --git a/owrx/audio/wav.py b/owrx/audio/wav.py new file mode 100644 index 000000000..f801128af --- /dev/null +++ b/owrx/audio/wav.py @@ -0,0 +1,148 @@ +from owrx.config.core import CoreConfig +from owrx.audio import AudioChopperProfile +from owrx.audio.queue import DecoderQueue +import threading +import wave +import os +from datetime import datetime, timedelta +from queue import Full +from typing import List + +import logging + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +class WaveFile(object): + def __init__(self, writer_id): + self.timestamp = datetime.utcnow() + self.writer_id = writer_id + tmp_dir = CoreConfig().get_temporary_directory() + self.filename = "{tmp_dir}/openwebrx-audiochopper-master-{id}-{timestamp}.wav".format( + tmp_dir=tmp_dir, + id=self.writer_id, + timestamp=self.timestamp.strftime("%y%m%d_%H%M%S"), + ) + self.waveFile = wave.open(self.filename, "wb") + self.waveFile.setnchannels(1) + self.waveFile.setsampwidth(2) + self.waveFile.setframerate(12000) + + def __del__(self): + if self.waveFile is not None: + logger.warning("WaveFile going out of scope but not unlinked!") + + def close(self): + self.waveFile.close() + + def getFileName(self): + return self.filename + + def getTimestamp(self): + return self.timestamp + + def writeframes(self, data): + return self.waveFile.writeframes(data) + + def unlink(self): + os.unlink(self.filename) + self.waveFile = None + + +class AudioWriter(object): + def __init__(self, chopper, interval, profiles: List[AudioChopperProfile]): + self.chopper = chopper + self.interval = interval + self.profiles = profiles + self.wavefile = None + self.switchingLock = threading.Lock() + self.timer = None + + def getWaveFile(self): + return WaveFile(id(self)) + + def getNextDecodingTime(self): + # add one second to have the intervals tick over one second earlier + # this avoids filename collisions, but also avoids decoding wave files with less than one second of audio + t = datetime.utcnow() + timedelta(seconds=1) + zeroed = t.replace(minute=0, second=0, microsecond=0) + delta = t - zeroed + seconds = (int(delta.total_seconds() / self.interval) + 1) * self.interval + t = zeroed + timedelta(seconds=seconds) + logger.debug("scheduling: {0}".format(t)) + return t + + def cancelTimer(self): + if self.timer: + self.timer.cancel() + self.timer = None + + def _scheduleNextSwitch(self): + self.cancelTimer() + delta = self.getNextDecodingTime() - datetime.utcnow() + self.timer = threading.Timer(delta.total_seconds(), self._switchFiles) + self.timer.start() + + def _switchFiles(self): + with self.switchingLock: + file = self.wavefile + self.wavefile = self.getWaveFile() + + if file is None: + logger.warning("switchfiles() with no wave file. sequencing problem?") + return + + file.close() + tmp_dir = CoreConfig().get_temporary_directory() + + for profile in self.profiles: + # create hardlinks for the individual profiles + filename = "{tmp_dir}/openwebrx-audiochopper-{pid}-{timestamp}.wav".format( + tmp_dir=tmp_dir, + pid=id(profile), + timestamp=file.getTimestamp().strftime(profile.getFileTimestampFormat()), + ) + try: + os.link(file.getFileName(), filename) + except OSError: + logger.exception("Error while linking job files") + continue + + job = self.chopper.createJob(profile, filename) + try: + DecoderQueue.getSharedInstance().put(job) + except Full: + logger.warning("decoding queue overflow; dropping one file") + job.unlink() + + try: + # our master can be deleted now, the profiles will delete their hardlinked copies after processing + file.unlink() + except OSError: + logger.exception("Error while unlinking job files") + + self._scheduleNextSwitch() + + def start(self): + if self.wavefile is not None: + logger.warning("wavefile is not none on startup, sequencing problem?") + self.wavefile = self.getWaveFile() + self._scheduleNextSwitch() + + def write(self, data): + with self.switchingLock: + self.wavefile.writeframes(data) + + def stop(self): + self.cancelTimer() + try: + self.wavefile.close() + except Exception: + logger.exception("error closing wave file") + try: + with self.switchingLock: + self.wavefile.unlink() + except Exception: + logger.exception("error removing undecoded file") + self.wavefile = None diff --git a/owrx/bands.py b/owrx/bands.py new file mode 100644 index 000000000..ef4d0e918 --- /dev/null +++ b/owrx/bands.py @@ -0,0 +1,127 @@ +from owrx.modes import Modes, DigitalMode +from datetime import datetime, timezone +import json +import os + +import logging + +logger = logging.getLogger(__name__) + + +class Band(object): + def __init__(self, b_dict): + self.name = b_dict["name"] + self.lower_bound = b_dict["lower_bound"] + self.upper_bound = b_dict["upper_bound"] + self.frequencies = [] + if "frequencies" in b_dict: + availableModes = [mode.modulation for mode in Modes.getAvailableModes()] + for (mode, freqs) in b_dict["frequencies"].items(): + if mode not in availableModes: + logger.info( + 'Modulation "{mode}" is not available, bandplan bookmark will not be displayed'.format( + mode=mode + ) + ) + continue + if not isinstance(freqs, list): + freqs = [freqs] + for f in freqs: + f_dict = {"frequency": f} if not isinstance(f, dict) else f + f_dict["mode"] = mode + + if not self.inBand(f_dict["frequency"]): + logger.warning( + "Frequency for {mode} on {band} is not within band limits: {frequency}".format( + mode=mode, frequency=f_dict["frequency"], band=self.name + ) + ) + continue + + if "underlying" in f_dict: + m = Modes.findByModulation(mode) + if not isinstance(m, DigitalMode): + logger.warning("%s is not a digital mode, cannot be used with \"underlying\" config", mode) + continue + if f_dict["underlying"] not in m.underlying: + logger.warning( + "%s is not a valid underlying mode for %s; skipping", + f_dict["underlying"], + mode + ) + + self.frequencies.append(f_dict) + + def inBand(self, freq): + return self.lower_bound <= freq <= self.upper_bound + + def getName(self): + return self.name + + def getDialFrequencies(self, range): + (low, hi) = range + return [e for e in self.frequencies if low <= e["frequency"] <= hi] + + +class Bandplan(object): + sharedInstance = None + + @staticmethod + def getSharedInstance(): + if Bandplan.sharedInstance is None: + Bandplan.sharedInstance = Bandplan() + return Bandplan.sharedInstance + + def __init__(self): + self.bands = [] + self.file_modified = None + self.fileList = ["/etc/openwebrx/bands.json", "bands.json"] + + def _refresh(self): + modified = self._getFileModifiedTimestamp() + if self.file_modified is None or modified > self.file_modified: + logger.debug("reloading bands from disk due to file modification") + self.bands = self._loadBands() + self.file_modified = modified + + def _getFileModifiedTimestamp(self): + timestamp = 0 + for file in self.fileList: + try: + timestamp = os.path.getmtime(file) + break + except FileNotFoundError: + pass + return datetime.fromtimestamp(timestamp, timezone.utc) + + def _loadBands(self): + for file in self.fileList: + try: + f = open(file, "r") + bands_json = json.load(f) + f.close() + return [Band(d) for d in bands_json] + except FileNotFoundError: + pass + except json.JSONDecodeError: + logger.exception("error while parsing bandplan file %s", file) + return [] + except Exception: + logger.exception("error while processing bandplan from %s", file) + return [] + return [] + + def findBands(self, freq): + self._refresh() + return [band for band in self.bands if band.inBand(freq)] + + def findBand(self, freq): + bands = self.findBands(freq) + if bands: + return bands[0] + else: + return None + + def collectDialFrequencies(self, range): + self._refresh() + return [e for b in self.bands for e in b.getDialFrequencies(range)] diff --git a/owrx/bookmarks.py b/owrx/bookmarks.py new file mode 100644 index 000000000..242b237c2 --- /dev/null +++ b/owrx/bookmarks.py @@ -0,0 +1,147 @@ +from datetime import datetime, timezone +from owrx.config.core import CoreConfig +import json +import os + +import logging + +logger = logging.getLogger(__name__) + + +class Bookmark(object): + def __init__(self, j): + self.name = j["name"] + self.frequency = j["frequency"] + self.modulation = j["modulation"] + + def getName(self): + return self.name + + def getFrequency(self): + return self.frequency + + def getModulation(self): + return self.modulation + + def __dict__(self): + return { + "name": self.getName(), + "frequency": self.getFrequency(), + "modulation": self.getModulation(), + } + + +class BookmarkSubscription(object): + def __init__(self, subscriptee, range, subscriber: callable): + self.subscriptee = subscriptee + self.range = range + self.subscriber = subscriber + + def inRange(self, bookmark: Bookmark): + low, high = self.range + return low <= bookmark.getFrequency() <= high + + def call(self, *args, **kwargs): + self.subscriber(*args, **kwargs) + + def cancel(self): + self.subscriptee.unsubscribe(self) + + +class Bookmarks(object): + sharedInstance = None + + @staticmethod + def getSharedInstance(): + if Bookmarks.sharedInstance is None: + Bookmarks.sharedInstance = Bookmarks() + return Bookmarks.sharedInstance + + def __init__(self): + self.file_modified = None + self.bookmarks = [] + self.subscriptions = [] + self.fileList = [Bookmarks._getBookmarksFile(), "/etc/openwebrx/bookmarks.json", "bookmarks.json"] + + def _refresh(self): + modified = self._getFileModifiedTimestamp() + if self.file_modified is None or modified > self.file_modified: + logger.debug("reloading bookmarks from disk due to file modification") + self.bookmarks = self._loadBookmarks() + self.file_modified = modified + + def _getFileModifiedTimestamp(self): + timestamp = 0 + for file in self.fileList: + try: + timestamp = os.path.getmtime(file) + break + except FileNotFoundError: + pass + return datetime.fromtimestamp(timestamp, timezone.utc) + + def _loadBookmarks(self): + for file in self.fileList: + try: + with open(file, "r") as f: + content = f.read() + if content: + bookmarks_json = json.loads(content) + return [Bookmark(d) for d in bookmarks_json] + except FileNotFoundError: + pass + except json.JSONDecodeError: + logger.exception("error while parsing bookmarks file %s", file) + return [] + except Exception: + logger.exception("error while processing bookmarks from %s", file) + return [] + return [] + + def getBookmarks(self, range=None): + self._refresh() + if range is None: + return self.bookmarks + else: + (lo, hi) = range + return [b for b in self.bookmarks if lo <= b.getFrequency() <= hi] + + @staticmethod + def _getBookmarksFile(): + coreConfig = CoreConfig() + return "{data_directory}/bookmarks.json".format(data_directory=coreConfig.get_data_directory()) + + def store(self): + # don't write directly to file to avoid corruption on exceptions + jsonContent = json.dumps([b.__dict__() for b in self.bookmarks], indent=4) + with open(Bookmarks._getBookmarksFile(), "w") as file: + file.write(jsonContent) + self.file_modified = self._getFileModifiedTimestamp() + + def addBookmark(self, bookmark: Bookmark): + self.bookmarks.append(bookmark) + self.notifySubscriptions(bookmark) + + def removeBookmark(self, bookmark: Bookmark): + if bookmark not in self.bookmarks: + return + self.bookmarks.remove(bookmark) + self.notifySubscriptions(bookmark) + + def notifySubscriptions(self, bookmark: Bookmark): + for sub in self.subscriptions: + if sub.inRange(bookmark): + try: + sub.call() + except Exception: + logger.exception("Error while calling bookmark subscriptions") + + def subscribe(self, range, callback): + sub = BookmarkSubscription(self, range, callback) + self.subscriptions.append(BookmarkSubscription(self, range, callback)) + return sub + + def unsubscribe(self, subscription: BookmarkSubscription): + if subscription not in self.subscriptions: + return + self.subscriptions.remove(subscription) diff --git a/owrx/breadcrumb.py b/owrx/breadcrumb.py new file mode 100644 index 000000000..1a7d4f316 --- /dev/null +++ b/owrx/breadcrumb.py @@ -0,0 +1,44 @@ +from typing import List +from abc import ABC, abstractmethod + + +class BreadcrumbItem(object): + def __init__(self, title, href): + self.title = title + self.href = href + + def render(self, documentRoot, active=False): + return ''.format( + documentRoot=documentRoot, href=self.href, title=self.title, active="active" if active else "" + ) + + +class Breadcrumb(object): + def __init__(self, breadcrumbs: List[BreadcrumbItem]): + self.items = breadcrumbs + + def render(self, documentRoot): + return """ + + """.format( + crumbs="".join(item.render(documentRoot) for item in self.items[:-1]), + last_crumb="".join(item.render(documentRoot, True) for item in self.items[-1:]), + ) + + def append(self, crumb: BreadcrumbItem): + self.items.append(crumb) + return self + + +class BreadcrumbMixin(ABC): + def template_variables(self): + variables = super().template_variables() + variables["breadcrumb"] = self.get_breadcrumb().render(self.get_document_root()) + return variables + + @abstractmethod + def get_breadcrumb(self) -> Breadcrumb: + pass diff --git a/owrx/client.py b/owrx/client.py new file mode 100644 index 000000000..8ec7f4da3 --- /dev/null +++ b/owrx/client.py @@ -0,0 +1,54 @@ +from owrx.config import Config +import threading + +import logging + +logger = logging.getLogger(__name__) + + +class TooManyClientsException(Exception): + pass + + +class ClientRegistry(object): + sharedInstance = None + creationLock = threading.Lock() + + @staticmethod + def getSharedInstance(): + with ClientRegistry.creationLock: + if ClientRegistry.sharedInstance is None: + ClientRegistry.sharedInstance = ClientRegistry() + return ClientRegistry.sharedInstance + + def __init__(self): + self.clients = [] + Config.get().wireProperty("max_clients", self._checkClientCount) + super().__init__() + + def broadcast(self): + n = self.clientCount() + for c in self.clients: + c.write_clients(n) + + def addClient(self, client): + pm = Config.get() + if len(self.clients) >= pm["max_clients"]: + raise TooManyClientsException() + self.clients.append(client) + self.broadcast() + + def clientCount(self): + return len(self.clients) + + def removeClient(self, client): + try: + self.clients.remove(client) + except ValueError: + pass + self.broadcast() + + def _checkClientCount(self, new_count): + for client in self.clients[new_count:]: + logger.debug("closing one connection...") + client.close() diff --git a/owrx/command.py b/owrx/command.py new file mode 100644 index 000000000..0559b72c3 --- /dev/null +++ b/owrx/command.py @@ -0,0 +1,79 @@ +from abc import ABC, abstractmethod + + +class CommandMapper(object): + def __init__(self, base=None, mappings=None, static=None): + self.base = base + self.mappings = {} if mappings is None else mappings + self.static = static + + def map(self, values): + args = [self.mappings[k].map(v) for k, v in values.items() if k in self.mappings] + args = [a for a in args if a != ""] + options = " ".join(args) + command = "{0} {1}".format(self.base, options) + if self.static is not None: + command += " " + self.static + return command + + def setMapping(self, key, mapping): + self.mappings[key] = mapping + return self + + def setMappings(self, mappings): + for k, v in mappings.items(): + self.setMapping(k, v) + return self + + def setBase(self, base): + self.base = base + return self + + def setStatic(self, static): + self.static = static + return self + + def keys(self): + return self.mappings.keys() + + +class CommandMapping(ABC): + @abstractmethod + def map(self, value): + pass + + +class Flag(CommandMapping): + def __init__(self, flag): + self.flag = flag + + def map(self, value): + if value is not None and value: + return self.flag + else: + return "" + + +class Option(CommandMapping): + def __init__(self, option): + self.option = option + self.spacer = " " + + def map(self, value): + if value is not None: + if isinstance(value, str) and " " in value: + template = '{option}{spacer}"{value}"' + else: + template = "{option}{spacer}{value}" + return template.format(option=self.option, spacer=self.spacer, value=value) + else: + return "" + + def setSpacer(self, spacer): + self.spacer = spacer + return self + + +class Argument(CommandMapping): + def map(self, value): + return str(value) diff --git a/owrx/config/__init__.py b/owrx/config/__init__.py new file mode 100644 index 000000000..bbd0d5711 --- /dev/null +++ b/owrx/config/__init__.py @@ -0,0 +1,43 @@ +from owrx.property import PropertyStack +from owrx.config.error import ConfigError +from owrx.config.defaults import defaultConfig +from owrx.config.dynamic import DynamicConfig +from owrx.config.classic import ClassicConfig + + +class Config(PropertyStack): + sharedConfig = None + + def __init__(self): + super().__init__() + self.storableConfig = DynamicConfig() + layers = [ + self.storableConfig, + ClassicConfig(), + defaultConfig, + ] + for i, l in enumerate(layers): + self.addLayer(i, l) + + @staticmethod + def get(): + if Config.sharedConfig is None: + Config.sharedConfig = Config() + return Config.sharedConfig + + def store(self): + self.storableConfig.store() + + @staticmethod + def validateConfig(): + # no config checks atm + # just basic loading verification + Config.get() + + def __setitem__(self, key, value): + # in the config, all writes go to the json layer + return self.storableConfig.__setitem__(key, value) + + def __delitem__(self, key): + # all deletes go to the json layer, too + return self.storableConfig.__delitem__(key) diff --git a/owrx/config/classic.py b/owrx/config/classic.py new file mode 100644 index 000000000..a91d57b3a --- /dev/null +++ b/owrx/config/classic.py @@ -0,0 +1,36 @@ +from owrx.property import PropertyReadOnly, PropertyLayer +from owrx.config.migration import Migrator +import importlib.util + + +class ClassicConfig(PropertyReadOnly): + def __init__(self): + pm = ClassicConfig._loadConfig() + Migrator.migrate(pm) + super().__init__(pm) + + @staticmethod + def _loadConfig(): + for file in ["/etc/openwebrx/config_webrx.py", "./config_webrx.py"]: + try: + return ClassicConfig._loadPythonFile(file) + except FileNotFoundError: + pass + return PropertyLayer() + + @staticmethod + def _toLayer(dictionary: dict): + layer = PropertyLayer() + for k, v in dictionary.items(): + if isinstance(v, dict): + layer[k] = ClassicConfig._toLayer(v) + else: + layer[k] = v + return layer + + @staticmethod + def _loadPythonFile(file): + spec = importlib.util.spec_from_file_location("config_webrx", file) + cfg = importlib.util.module_from_spec(spec) + spec.loader.exec_module(cfg) + return ClassicConfig._toLayer({k: v for k, v in cfg.__dict__.items() if not k.startswith("__")}) diff --git a/owrx/config/commands.py b/owrx/config/commands.py new file mode 100644 index 000000000..153ca78fc --- /dev/null +++ b/owrx/config/commands.py @@ -0,0 +1,30 @@ +from owrx.admin.commands import Command +from owrx.config import Config +from owrx.bookmarks import Bookmarks + + +class MigrateCommand(Command): + # these keys have been moved to openwebrx.conf + blacklisted_keys = [ + "temporary_directory", + "web_port", + "aprs_symbols_path", + ] + + def run(self, args): + print("Migrating configuration...") + + config = Config.get() + # a key that is set will end up in the DynamicConfig, so this will transfer everything there + for key, value in config.items(): + if key not in MigrateCommand.blacklisted_keys: + config[key] = value + config.store() + + print("Migrating bookmarks...") + # bookmarks just need to be saved + b = Bookmarks.getSharedInstance() + b.getBookmarks() + b.store() + + print("Migration complete!") diff --git a/owrx/config/core.py b/owrx/config/core.py new file mode 100644 index 000000000..f17143364 --- /dev/null +++ b/owrx/config/core.py @@ -0,0 +1,101 @@ +from owrx.config import ConfigError +from configparser import ConfigParser +from pathlib import Path +from typing import Optional +import os + + +class CoreConfig(object): + defaultSearchLocations = ["./openwebrx.conf", "/etc/openwebrx/openwebrx.conf"] + + defaults = { + "core": { + "data_directory": "/var/lib/openwebrx", + "temporary_directory": "/tmp", + "log_level": "INFO", + }, + "web": { + "port": 8073, + "ipv6": True, + # won't work this way because values must be strings, but this is effectively the way it behaves. + #"bind_address": None, + }, + "aprs": { + "symbols_path": "/usr/share/aprs-symbols/png" + } + } + + sharedConfig = None + + @staticmethod + def load(file: Path = None): + + def expand_base(base: Path): + # check if config exists + if not base.exists() or not base.is_file(): + return [] + # every location can additionally have a directory containing config overrides + # this directory must have the same name, with the ".d" suffix + override_dir = Path(str(base) + ".d") + # check if override dir exists + if not override_dir.exists() or not override_dir.is_dir(): + return [base] + # load all .conf files from the override dir + overrides = override_dir.glob("*.conf") + return [base] + [o for o in overrides if o.is_file()] + + if file is None: + bases = [Path(b) for b in CoreConfig.defaultSearchLocations] + else: + bases = [file] + configFiles = [o for b in bases for o in expand_base(b)] + + config = ConfigParser() + # set up config defaults + config.read_dict(CoreConfig.defaults) + # read the allocated files + config.read(configFiles) + + CoreConfig.sharedConfig = config + + def __init__(self): + config = CoreConfig.sharedConfig + self.data_directory = config.get("core", "data_directory") + CoreConfig.checkDirectory(self.data_directory, "data_directory") + self.temporary_directory = config.get("core", "temporary_directory") + CoreConfig.checkDirectory(self.temporary_directory, "temporary_directory") + self.log_level = config.get("core", "log_level") + self.web_port = config.getint("web", "port") + self.web_ipv6 = config.getboolean("web", "ipv6") + self.web_bind_address = config.get("web", "bind_address", fallback=None) + self.aprs_symbols_path = config.get("aprs", "symbols_path") + + @staticmethod + def checkDirectory(dir, key): + if not os.path.exists(dir): + raise ConfigError(key, "{dir} doesn't exist".format(dir=dir)) + if not os.path.isdir(dir): + raise ConfigError(key, "{dir} is not a directory".format(dir=dir)) + if not os.access(dir, os.W_OK): + raise ConfigError(key, "{dir} is not writable".format(dir=dir)) + + def get_web_port(self) -> int: + return self.web_port + + def get_web_ipv6(self) -> bool: + return self.web_ipv6 + + def get_web_bind_address(self) -> Optional[str]: + return self.web_bind_address + + def get_data_directory(self) -> str: + return self.data_directory + + def get_temporary_directory(self) -> str: + return self.temporary_directory + + def get_aprs_symbols_path(self) -> str: + return self.aprs_symbols_path + + def get_log_level(self) -> str: + return self.log_level diff --git a/owrx/config/defaults.py b/owrx/config/defaults.py new file mode 100644 index 000000000..3f9be5d05 --- /dev/null +++ b/owrx/config/defaults.py @@ -0,0 +1,181 @@ +from owrx.property import PropertyLayer + + +defaultConfig = PropertyLayer( + version=8, + max_clients=20, + receiver_name="[Callsign]", + receiver_location="Budapest, Hungary", + receiver_asl=200, + receiver_admin="example@example.com", + receiver_gps=PropertyLayer(lat=47.0, lon=19.0), + photo_title="Panorama of Budapest from Schönherz Zoltán Dormitory", + photo_desc="", + fft_fps=9, + fft_size=4096, + fft_voverlap_factor=0.3, + audio_compression="adpcm", + fft_compression="adpcm", + wfm_deemphasis_tau=50e-6, + wfm_rds_rbds=False, + digimodes_fft_size=2048, + digital_voice_dmr_id_lookup=True, + digital_voice_nxdn_id_lookup=True, + sdrs=PropertyLayer( + rtlsdr=PropertyLayer( + name="RTL-SDR USB Stick", + type="rtl_sdr", + profiles=PropertyLayer( + **{ + "70cm": PropertyLayer( + name="70cm Repeaters", + center_freq=438800000, + rf_gain=29, + samp_rate=2400000, + start_freq=439275000, + start_mod="nfm", + ), + "2m": PropertyLayer( + name="2m", + center_freq=145000000, + rf_gain=29, + samp_rate=2048000, + start_freq=145725000, + start_mod="nfm", + ), + } + ), + ), + airspy=PropertyLayer( + name="Airspy HF+", + type="airspyhf", + rf_gain="auto", + profiles=PropertyLayer( + **{ + "20m": PropertyLayer( + name="20m", + center_freq=14150000, + samp_rate=384000, + start_freq=14070000, + start_mod="usb", + ), + "30m": PropertyLayer( + name="30m", + center_freq=10125000, + samp_rate=192000, + start_freq=10142000, + start_mod="usb", + ), + "40m": PropertyLayer( + name="40m", + center_freq=7100000, + samp_rate=256000, + start_freq=7070000, + start_mod="lsb", + ), + "80m": PropertyLayer( + name="80m", + center_freq=3650000, + samp_rate=384000, + start_freq=3570000, + start_mod="lsb", + ), + "49m": PropertyLayer( + name="49m Broadcast", + center_freq=6050000, + samp_rate=384000, + start_freq=6070000, + start_mod="am", + ), + } + ), + ), + sdrplay=PropertyLayer( + name="SDRPlay device", + type="sdrplay", + antenna="Antenna A", + profiles=PropertyLayer( + **{ + "20m": PropertyLayer( + name="20m", + center_freq=14150000, + rf_gain=0, + samp_rate=500000, + start_freq=14070000, + start_mod="usb", + ), + "30m": PropertyLayer( + name="30m", + center_freq=10125000, + rf_gain=0, + samp_rate=250000, + start_freq=10142000, + start_mod="usb", + ), + "40m": PropertyLayer( + name="40m", + center_freq=7100000, + rf_gain=0, + samp_rate=500000, + start_freq=7070000, + start_mod="lsb", + ), + "80m": PropertyLayer( + name="80m", + center_freq=3650000, + rf_gain=0, + samp_rate=500000, + start_freq=3570000, + start_mod="lsb", + ), + "49m": PropertyLayer( + name="49m Broadcast", + center_freq=6000000, + rf_gain=0, + samp_rate=500000, + start_freq=6070000, + start_mod="am", + ), + } + ), + ), + ), + waterfall_scheme="GoogleTurboWaterfall", + waterfall_levels=PropertyLayer(min=-88, max=-20), + waterfall_auto_levels=PropertyLayer(min=3, max=10), + waterfall_auto_level_default_mode=False, + waterfall_auto_min_range=50, + tuning_precision=2, + squelch_auto_margin=10, + google_maps_api_key="", + map_position_retention_time=2 * 60 * 60, + decoding_queue_workers=2, + decoding_queue_length=10, + wsjt_decoding_depth=3, + wsjt_decoding_depths=PropertyLayer(jt65=1), + fst4_enabled_intervals=[15, 30], + fst4w_enabled_intervals=[120, 300], + q65_enabled_combinations=["A30", "E120", "C60"], + js8_enabled_profiles=["normal", "slow"], + js8_decoding_depth=3, + services_enabled=False, + services_decoders=["ft8", "ft4", "wspr", "packet"], + aprs_callsign="N0CALL", + aprs_igate_enabled=False, + aprs_igate_server="euro.aprs2.net", + aprs_igate_password="", + aprs_igate_beacon=False, + aprs_igate_symbol="R&", + aprs_igate_comment="OpenWebRX APRS gateway", + # aprs_igate_height=None, + # aprs_igate_gain=None, + # aprs_igate_dir=None, + pskreporter_enabled=False, + pskreporter_callsign="N0CALL", + # pskreporter_antenna_information=None, + wsprnet_enabled=False, + wsprnet_callsign="N0CALL", + mqtt_enabled=False, + mqtt_host="localhost", + mqtt_use_ssl=False, +).readonly() diff --git a/owrx/config/dynamic.py b/owrx/config/dynamic.py new file mode 100644 index 000000000..9357e05aa --- /dev/null +++ b/owrx/config/dynamic.py @@ -0,0 +1,62 @@ +from owrx.config.core import CoreConfig +from owrx.config.migration import Migrator +from owrx.property import PropertyLayer, PropertyDeleted +from owrx.jsons import Encoder +import json + + +class DynamicConfig(PropertyLayer): + def __init__(self): + super().__init__() + try: + with open(DynamicConfig._getSettingsFile(), "r") as f: + for k, v in json.load(f).items(): + if isinstance(v, dict): + self[k] = DynamicConfig._toLayer(v) + else: + self[k] = v + except FileNotFoundError: + pass + Migrator.migrate(self) + + @staticmethod + def _toLayer(dictionary: dict): + layer = PropertyLayer() + for k, v in dictionary.items(): + if isinstance(v, dict): + layer[k] = DynamicConfig._toLayer(v) + else: + layer[k] = v + return layer + + @staticmethod + def _getSettingsFile(): + coreConfig = CoreConfig() + return "{data_directory}/settings.json".format(data_directory=coreConfig.get_data_directory()) + + def store(self): + # don't write directly to file to avoid corruption on exceptions + jsonContent = json.dumps(self.__dict__(), indent=4, cls=Encoder) + with open(DynamicConfig._getSettingsFile(), "w") as file: + file.write(jsonContent) + + def __delitem__(self, key): + self.__setitem__(key, PropertyDeleted) + + def __contains__(self, item): + if not super().__contains__(item): + return False + if super().__getitem__(item) is PropertyDeleted: + return False + return True + + def __getitem__(self, item): + if self.__contains__(item): + return super().__getitem__(item) + raise KeyError('Key "{key}" does not exist'.format(key=item)) + + def __dict__(self): + return {k: v for k, v in super().__dict__().items() if v is not PropertyDeleted} + + def keys(self): + return [k for k in super().keys() if self.__contains__(k)] diff --git a/owrx/config/error.py b/owrx/config/error.py new file mode 100644 index 000000000..19e11197d --- /dev/null +++ b/owrx/config/error.py @@ -0,0 +1,3 @@ +class ConfigError(Exception): + def __init__(self, key, message): + super().__init__("Configuration Error (key: {0}): {1}".format(key, message)) diff --git a/owrx/config/migration.py b/owrx/config/migration.py new file mode 100644 index 000000000..856d2709f --- /dev/null +++ b/owrx/config/migration.py @@ -0,0 +1,152 @@ +from abc import ABC, abstractmethod +from owrx.property import PropertyLayer + +import logging + +logger = logging.getLogger(__name__) + + +class ConfigMigrator(ABC): + @abstractmethod + def migrate(self, config): + pass + + def renameKey(self, config, old, new): + if old in config and new not in config: + config[new] = config[old] + del config[old] + + +class ConfigMigratorVersion1(ConfigMigrator): + def migrate(self, config): + if "receiver_gps" in config: + gps = config["receiver_gps"] + config["receiver_gps"] = {"lat": gps[0], "lon": gps[1]} + + if "waterfall_auto_level_margin" in config: + levels = config["waterfall_auto_level_margin"] + config["waterfall_auto_level_margin"] = {"min": levels[0], "max": levels[1]} + + self.renameKey(config, "wsjt_queue_workers", "decoding_queue_workers") + self.renameKey(config, "wsjt_queue_length", "decoding_queue_length") + + config["version"] = 2 + + +class ConfigMigratorVersion2(ConfigMigrator): + def migrate(self, config): + if "waterfall_colors" in config and any(v > 0xFFFFFF for v in config["waterfall_colors"]): + config["waterfall_colors"] = [v >> 8 for v in config["waterfall_colors"]] + + config["version"] = 3 + + +class ConfigMigratorVersion3(ConfigMigrator): + def migrate(self, config): + # inline import due to circular dependencies + from owrx.waterfall import WaterfallOptions + + if "waterfall_scheme" in config: + scheme = WaterfallOptions(config["waterfall_scheme"]) + if scheme is not WaterfallOptions.CUSTOM and "waterfall_colors" in config: + del config["waterfall_colors"] + elif "waterfall_colors" in config: + scheme = WaterfallOptions.findByColors(config["waterfall_colors"]) + if scheme is not WaterfallOptions.CUSTOM: + logger.debug("detected waterfall option: %s", scheme.value) + if "waterfall_colors" in config: + del config["waterfall_colors"] + config["waterfall_scheme"] = scheme.value + + config["version"] = 4 + + +class ConfigMigratorVersion4(ConfigMigrator): + def _replaceWaterfallLevels(self, instance): + if ( + "waterfall_min_level" in instance + and "waterfall_max_level" in instance + and not "waterfall_levels" in instance + ): + instance["waterfall_levels"] = { + "min": instance["waterfall_min_level"], + "max": instance["waterfall_max_level"], + } + del instance["waterfall_min_level"] + del instance["waterfall_max_level"] + + def migrate(self, config): + # migrate root level + self._replaceWaterfallLevels(config) + if "sdrs" in config: + for device in config["sdrs"].__dict__().values(): + # migrate device level + self._replaceWaterfallLevels(device) + if "profiles" in device: + for profile in device["profiles"].__dict__().values(): + # migrate profile level + self._replaceWaterfallLevels(profile) + + config["version"] = 5 + + +class ConfigMigratorVersion5(ConfigMigrator): + def migrate(self, config): + if "frequency_display_precision" in config: + # old config was always in relation to the display in MHz (1e6 Hz, hence the 6) + config["tuning_precision"] = 6 - config["frequency_display_precision"] + del config["frequency_display_precision"] + config["version"] = 6 + + +class ConfigMigratorVersion6(ConfigMigrator): + def migrate(self, config): + if "waterfall_auto_level_margin" in config: + walm_config = config["waterfall_auto_level_margin"] + if "min_range" in walm_config: + config["waterfall_auto_min_range"] = walm_config["min_range"] + wal = {k: v for k, v in walm_config.items() if k in ["min", "max"]} + config["waterfall_auto_levels"] = PropertyLayer(**wal) + del config["waterfall_auto_level_margin"] + config["version"] = 7 + + +class ConfigMigratorVersion7(ConfigMigrator): + def migrate(self, config): + if "callsign_url" in config: + if "qrzcq.com" in config["callsign_url"]: + config["callsign_service"] = "qrzcq" + elif "qrz.com" in config["callsign_url"]: + config["callsign_service"] = "qrz" + else: + logger.warning("unable to migrate callsign_url! please check settings!") + del config["callsign_url"] + config["version"] = 8 + + +class Migrator(object): + currentVersion = 8 + migrators = { + 1: ConfigMigratorVersion1(), + 2: ConfigMigratorVersion2(), + 3: ConfigMigratorVersion3(), + 4: ConfigMigratorVersion4(), + 5: ConfigMigratorVersion5(), + 6: ConfigMigratorVersion6(), + 7: ConfigMigratorVersion7(), + } + + @staticmethod + def migrate(config): + version = config["version"] if "version" in config else 1 + if version == Migrator.currentVersion: + return + elif version > Migrator.currentVersion: + raise ValueError( + "Configuration version is too high (current: {}, found: {})".format(Migrator.currentVersion, version) + ) + + logger.debug("migrating config from version %i", version) + migrators = [Migrator.migrators[i] for i in range(version, Migrator.currentVersion)] + for migrator in migrators: + migrator.migrate(config) diff --git a/owrx/connection.py b/owrx/connection.py new file mode 100644 index 000000000..0350e1559 --- /dev/null +++ b/owrx/connection.py @@ -0,0 +1,523 @@ +from owrx.details import ReceiverDetails +from owrx.dsp import DspManager +from owrx.cpu import CpuUsageThread +from owrx.sdr import SdrService +from owrx.source import SdrSourceState, SdrClientClass, SdrSourceEventClient +from owrx.client import ClientRegistry, TooManyClientsException +from owrx.feature import FeatureDetector +from owrx.version import openwebrx_version +from owrx.bands import Bandplan +from owrx.bookmarks import Bookmarks +from owrx.map import Map +from owrx.property import PropertyStack, PropertyDeleted +from owrx.modes import Modes, DigitalMode +from owrx.config import Config +from owrx.waterfall import WaterfallOptions +from owrx.websocket import Handler +from queue import Queue, Full, Empty +from abc import ABCMeta, abstractmethod +import json +import threading +import struct + +import logging + +logger = logging.getLogger(__name__) + +PoisonPill = object() + + +class Client(Handler, metaclass=ABCMeta): + def __init__(self, conn): + self.conn = conn + self.multithreadingQueue = Queue(100) + + def mp_passthru(): + run = True + while run: + try: + data = self.multithreadingQueue.get() + if data is PoisonPill: + run = False + else: + self.send(data) + self.multithreadingQueue.task_done() + except (EOFError, OSError, ValueError): + run = False + except Exception: + logger.exception("Exception on client multithreading queue") + + # unset the queue object to free shared memory file descriptors + self.multithreadingQueue = None + + threading.Thread(target=mp_passthru, name="connection_mp_passthru").start() + + def send(self, data): + try: + self.conn.send(data) + except IOError: + logger.exception("error in Client::send()") + self.close(error=True) + + def close(self, error: bool = False): + if self.multithreadingQueue is not None: + while True: + try: + self.multithreadingQueue.get(block=False) + except Empty: + break + try: + self.multithreadingQueue.put(PoisonPill, block=False) + except Full: + # this shouldn't happen, we just emptied the queue, but it's not worth risking the exception + logger.exception("impossible queue state: Full after Empty") + self.conn.close(socketError=error) + + def mp_send(self, data): + if self.multithreadingQueue is None: + return + try: + self.multithreadingQueue.put(data, block=False) + except Full: + self.close(error=True) + + @abstractmethod + def handleTextMessage(self, conn, message): + pass + + def handleBinaryMessage(self, conn, data): + logger.error("unsupported binary message, discarding") + + def handleClose(self): + self.close() + + +class OpenWebRxClient(Client, metaclass=ABCMeta): + def __init__(self, conn): + super().__init__(conn) + + receiver_details = ReceiverDetails() + + def send_receiver_info(*args): + receiver_info = receiver_details.__dict__() + self.write_receiver_details(receiver_info) + + self._detailsSubscription = receiver_details.wire(send_receiver_info) + send_receiver_info() + + def write_receiver_details(self, details): + self.send({"type": "receiver_details", "value": details}) + + def close(self, error: bool = False): + self._detailsSubscription.cancel() + super().close(error) + + +class OpenWebRxReceiverClient(OpenWebRxClient, SdrSourceEventClient): + sdr_config_keys = [ + "waterfall_levels", + "waterfall_auto_level_default_mode", + "samp_rate", + "start_mod", + "start_freq", + "center_freq", + "initial_squelch_level", + "sdr_id", + "profile_id", + "squelch_auto_margin", + ] + + global_config_keys = [ + "waterfall_scheme", + "waterfall_colors", + "waterfall_auto_levels", + "waterfall_auto_min_range", + "fft_size", + "audio_compression", + "fft_compression", + "max_clients", + "tuning_precision", + "aircraft_tracking_service", + ] + + def __init__(self, conn): + super().__init__(conn) + + self.dsp = None + self.dspLock = threading.Lock() + self.sdr = None + self.configSubs = [] + self.bookmarkSub = None + self.connectionProperties = {} + + try: + ClientRegistry.getSharedInstance().addClient(self) + except TooManyClientsException: + self.write_backoff_message("Too many clients") + self.close() + raise + + self.setupGlobalConfig() + self.stack = self.setupStack() + + self.setSdr() + + features = FeatureDetector().feature_availability() + self.write_features(features) + + modes = Modes.getModes() + self.write_modes(modes) + + self.configSubs.append(SdrService.getActiveSources().wire(self._onSdrDeviceChanges)) + self.configSubs.append(SdrService.getAvailableProfiles().wire(self._sendProfiles)) + self._sendProfiles() + + CpuUsageThread.getSharedInstance().add_client(self) + + def setupStack(self): + stack = PropertyStack() + # stack layer 0 reserved for sdr properties + # stack.addLayer(0, self.sdr.getProps()) + stack.addLayer(1, Config.get()) + configProps = stack.filter(*OpenWebRxReceiverClient.sdr_config_keys) + + def sendConfig(changes=None): + if changes is None: + config = configProps.__dict__() + else: + # transform deletions into Nones + config = {k: v if v is not PropertyDeleted else None for k, v in changes.items()} + if ( + (changes is None or "start_freq" in changes or "center_freq" in changes) + and "start_freq" in configProps + and "center_freq" in configProps + ): + config["start_offset_freq"] = configProps["start_freq"] - configProps["center_freq"] + if (changes is None or "profile_id" in changes) and self.sdr is not None: + config["sdr_id"] = self.sdr.getId() + self.write_config(config) + + def sendBookmarks(*args): + cf = configProps["center_freq"] + srh = configProps["samp_rate"] / 2 + dial_frequencies = [] + bookmarks = [] + if "center_freq" in configProps and "samp_rate" in configProps: + frequencyRange = (cf - srh, cf + srh) + dial_frequencies = Bandplan.getSharedInstance().collectDialFrequencies(frequencyRange) + bookmarks = [b.__dict__() for b in Bookmarks.getSharedInstance().getBookmarks(frequencyRange)] + self.write_dial_frequencies(dial_frequencies) + self.write_bookmarks(bookmarks) + + def updateBookmarkSubscription(*args): + if self.bookmarkSub is not None: + self.bookmarkSub.cancel() + if "center_freq" in configProps and "samp_rate" in configProps: + cf = configProps["center_freq"] + srh = configProps["samp_rate"] / 2 + frequencyRange = (cf - srh, cf + srh) + self.bookmarkSub = Bookmarks.getSharedInstance().subscribe(frequencyRange, sendBookmarks) + sendBookmarks() + + self.configSubs.append(configProps.wire(sendConfig)) + self.configSubs.append(stack.filter("center_freq", "samp_rate").wire(updateBookmarkSubscription)) + + # send initial config + sendConfig() + return stack + + def setupGlobalConfig(self): + def writeConfig(changes): + # TODO it would be nicer to have all options available and switchable in the client + # this restores the existing functionality for now, but there is lots of potential + if "waterfall_scheme" in changes or "waterfall_colors" in changes: + scheme = WaterfallOptions(globalConfig["waterfall_scheme"]).instantiate() + changes["waterfall_colors"] = scheme.getColors() + self.write_config(changes) + + globalConfig = Config.get().filter(*OpenWebRxReceiverClient.global_config_keys) + self.configSubs.append(globalConfig.wire(writeConfig)) + writeConfig(globalConfig.__dict__()) + + def onStateChange(self, state: SdrSourceState): + if state is SdrSourceState.RUNNING: + self.handleSdrAvailable() + + def onFail(self): + logger.warning('SDR device "%s" has failed, selecting new device', self.sdr.getName()) + self.write_log_message('SDR device "{0}" has failed, selecting new device'.format(self.sdr.getName())) + self.setSdr() + + def onDisable(self): + logger.warning('SDR device "%s" was disabled, selecting new device', self.sdr.getName()) + self.write_log_message('SDR device "{0}" was disabled, selecting new device'.format(self.sdr.getName())) + self.setSdr() + + def onShutdown(self): + logger.warning('SDR device "%s" is shutting down, selecting new device', self.sdr.getName()) + self.write_log_message('SDR device "{0}" is shutting down, selecting new device'.format(self.sdr.getName())) + self.setSdr() + + def getClientClass(self) -> SdrClientClass: + return SdrClientClass.USER + + def _onSdrDeviceChanges(self, changes): + # restart the client if an sdr has become available + if self.sdr is None and any(s is not PropertyDeleted for s in changes.values()): + self.setSdr() + + def _sendProfiles(self, *args): + profiles = [{"id": pid, "name": name} for pid, name in SdrService.getAvailableProfiles().items()] + self.write_profiles(profiles) + + def handleTextMessage(self, conn, message): + try: + message = json.loads(message) + if "type" in message: + if message["type"] == "dspcontrol": + dsp = self.getDsp() + if dsp is None: + logger.warning("DSP not available; discarding client dspcontrol message") + else: + if "action" in message and message["action"] == "start": + dsp.start() + + if "params" in message: + params = message["params"] + dsp.setProperties(params) + + elif message["type"] == "setsdr": + if "params" in message: + self.setSdr(message["params"]["sdr"]) + elif message["type"] == "selectprofile": + if "params" in message and "profile" in message["params"]: + profile = message["params"]["profile"].split("|") + self.setSdr(profile[0]) + self.sdr.activateProfile(profile[1]) + elif message["type"] == "connectionproperties": + if "params" in message: + self.connectionProperties = message["params"] + if self.dsp: + self.getDsp().setProperties(self.connectionProperties) + + else: + logger.warning("received message without type: {0}".format(message)) + + except json.JSONDecodeError: + logger.warning("message is not json: {0}".format(message)) + + def setSdr(self, id=None): + next = None + if id is not None: + next = SdrService.getSource(id) + if next is None: + next = SdrService.getFirstSource() + + # exit condition: no change + if next == self.sdr and next is not None: + return + + self.stopDsp() + self.stack.removeLayerByPriority(0) + + if self.sdr is not None: + self.sdr.removeClient(self) + + self.sdr = next + + if next is None: + # exit condition: no sdrs available + logger.warning("no more SDR devices available") + self.handleNoSdrsAvailable() + return + + self.sdr.addClient(self) + + def handleSdrAvailable(self): + self.getDsp().setProperties(self.connectionProperties) + self.stack.replaceLayer(0, self.sdr.getProps()) + + self.sdr.addSpectrumClient(self) + + def handleNoSdrsAvailable(self): + self.write_sdr_error("No SDR Devices available") + + def close(self, error: bool = False): + if self.sdr is not None: + self.sdr.removeClient(self) + self.stopDsp() + CpuUsageThread.getSharedInstance().remove_client(self) + ClientRegistry.getSharedInstance().removeClient(self) + while self.configSubs: + self.configSubs.pop().cancel() + if self.bookmarkSub is not None: + self.bookmarkSub.cancel() + self.bookmarkSub = None + super().close(error) + + def stopDsp(self): + with self.dspLock: + if self.dsp is not None: + self.dsp.stop() + self.dsp = None + if self.sdr is not None: + self.sdr.removeSpectrumClient(self) + + def getDsp(self): + with self.dspLock: + if self.dsp is None and self.sdr is not None: + self.dsp = DspManager(self, self.sdr) + return self.dsp + + def write_spectrum_data(self, data): + self.mp_send(bytes([0x01]) + data) + + def write_dsp_data(self, data): + self.send(bytes([0x02]) + data) + + def write_hd_audio(self, data): + self.send(bytes([0x04]) + data) + + def write_s_meter_level(self, level): + # may contain more than one sample, so only take the last 4 bytes = 1 float + level, = struct.unpack('f', level[-4:]) + try: + self.send({"type": "smeter", "value": level}) + except ValueError: + logger.warning("unable to send smeter value: %s", str(level)) + + def write_cpu_usage(self, usage): + self.mp_send({"type": "cpuusage", "value": usage}) + + def write_clients(self, clients): + self.mp_send({"type": "clients", "value": clients}) + + def write_secondary_fft(self, data): + self.send(bytes([0x03]) + data) + + def write_secondary_demod(self, message): + self.send({"type": "secondary_demod", "value": message}) + + def write_secondary_dsp_config(self, cfg): + self.send({"type": "secondary_config", "value": cfg}) + + def write_config(self, cfg): + self.send({"type": "config", "value": cfg}) + + def write_profiles(self, profiles): + self.send({"type": "profiles", "value": profiles}) + + def write_features(self, features): + self.send({"type": "features", "value": features}) + + def write_metadata(self, metadata): + self.send({"type": "metadata", "value": metadata}) + + def write_dial_frequencies(self, frequencies): + self.send({"type": "dial_frequencies", "value": frequencies}) + + def write_bookmarks(self, bookmarks): + self.send({"type": "bookmarks", "value": bookmarks}) + + def write_log_message(self, message): + self.send({"type": "log_message", "value": message}) + + def write_sdr_error(self, message): + self.send({"type": "sdr_error", "value": message}) + + def write_demodulator_error(self, message): + self.send({"type": "demodulator_error", "value": message}) + + def write_backoff_message(self, reason): + self.send({"type": "backoff", "reason": reason}) + + def write_modes(self, modes): + def to_json(m): + res = { + "modulation": m.modulation, + "name": m.name, + "type": "digimode" if isinstance(m, DigitalMode) else "analog", + "requirements": m.requirements, + "squelch": m.squelch, + } + if m.bandpass is not None: + res["bandpass"] = {"low_cut": m.bandpass.low_cut, "high_cut": m.bandpass.high_cut} + if m.ifRate is not None: + res["ifRate"] = m.ifRate + if isinstance(m, DigitalMode): + res["underlying"] = m.underlying + res["secondaryFft"] = m.secondaryFft + return res + + self.send({"type": "modes", "value": [to_json(m) for m in modes]}) + + +class MapConnection(OpenWebRxClient): + def __init__(self, conn): + super().__init__(conn) + + pm = Config.get() + filtered_config = pm.filter( + "google_maps_api_key", + "receiver_gps", + "callsign_service", + "aircraft_tracking_service", + "receiver_name", + ) + self.configSub = filtered_config.wire(self.write_config) + + self.write_config(filtered_config.__dict__()) + + Map.getSharedInstance().addClient(self) + + def handleTextMessage(self, conn, message): + pass + + def close(self, error: bool = False): + Map.getSharedInstance().removeClient(self) + self.configSub.cancel() + super().close(error) + + def write_config(self, cfg): + self.send({"type": "config", "value": cfg}) + + def write_update(self, update): + self.mp_send({"type": "update", "value": update}) + + +class HandshakeMessageHandler(Handler): + """ + This handler receives text messages, but will only respond to the second handshake string. + As soon as a valid handshake is received, the handler replaces itself with the corresponding handler type. + """ + def handleTextMessage(self, conn, message): + if message[:16] == "SERVER DE CLIENT": + meta = message[17:].split(" ") + handshake = {v[0]: "=".join(v[1:]) for v in map(lambda x: x.split("="), meta)} + + logger.debug("client connection initialized") + + client = None + if "type" in handshake: + if handshake["type"] == "receiver": + client = OpenWebRxReceiverClient + elif handshake["type"] == "map": + client = MapConnection + else: + logger.warning("invalid connection type: %s", handshake["type"]) + + if client is not None: + logger.debug("handshake complete, handing off to %s", client.__name__) + # hand off all further communication to the correspondig connection + conn.send("CLIENT DE SERVER server=openwebrx version={version}".format(version=openwebrx_version)) + conn.setMessageHandler(client(conn)) + else: + logger.warning('invalid handshake received') + else: + logger.warning("not answering client request since handshake is not complete") + + def handleBinaryMessage(self, conn, data): + pass + + def handleClose(self): + pass diff --git a/owrx/controllers/__init__.py b/owrx/controllers/__init__.py new file mode 100644 index 000000000..0dd4e3c61 --- /dev/null +++ b/owrx/controllers/__init__.py @@ -0,0 +1,62 @@ +from datetime import datetime, timezone + + +class BodySizeError(Exception): + pass + + +class Controller(object): + def __init__(self, handler, request, options): + self.handler = handler + self.request = request + self.options = options + self.responseCookies = None + + def send_response( + self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None, headers=None + ): + self.handler.send_response(code) + if headers is None: + headers = {} + if content_type is not None: + headers["Content-Type"] = content_type + if content_type.startswith("text/"): + headers["Content-Type"] += "; charset=utf-8" + if last_modified is not None: + headers["Last-Modified"] = last_modified.astimezone(tz=timezone.utc).strftime("%a, %d %b %Y %H:%M:%S GMT") + if max_age is not None: + headers["Cache-Control"] = "max-age={0}".format(max_age) + for key, value in headers.items(): + self.handler.send_header(key, value) + if self.responseCookies is not None: + self.handler.send_header("Set-Cookie", self.responseCookies.output(header="")) + self.handler.end_headers() + if type(content) == str: + content = content.encode() + while len(content): + w = self.handler.wfile.write(content) + content = content[w:] + + def send_redirect(self, location, code=303): + self.handler.send_response(code) + if self.responseCookies is not None: + self.handler.send_header("Set-Cookie", self.responseCookies.output(header="")) + self.handler.send_header("Location", location) + self.handler.end_headers() + + def set_response_cookies(self, cookies): + self.responseCookies = cookies + + def get_body(self, max_size=None): + if "Content-Length" not in self.handler.headers: + return None + length = int(self.handler.headers["Content-Length"]) + if max_size is not None and length > max_size: + raise BodySizeError("HTTP body exceeds maximum allowed size") + return self.handler.rfile.read(length) + + def handle_request(self): + action = "indexAction" + if "action" in self.options: + action = self.options["action"] + getattr(self, action)() diff --git a/owrx/controllers/admin.py b/owrx/controllers/admin.py new file mode 100644 index 000000000..803eeb8b0 --- /dev/null +++ b/owrx/controllers/admin.py @@ -0,0 +1,56 @@ +from owrx.controllers.session import SessionStorage +from owrx.users import UserList +from urllib import parse +from http.cookies import SimpleCookie + +import logging + +logger = logging.getLogger(__name__) + + +class Authentication(object): + def getUser(self, request): + if "owrx-session" not in request.cookies: + return None + session_id = request.cookies["owrx-session"].value + storage = SessionStorage.getSharedInstance() + session = storage.getSession(session_id) + if session is None: + return None + if "user" not in session: + return None + userList = UserList.getSharedInstance() + user = None + try: + user = userList[session["user"]] + storage.prolongSession(session_id) + except KeyError: + pass + return user + + +class AuthorizationMixin(object): + def __init__(self, handler, request, options): + self.authentication = Authentication() + self.user = self.authentication.getUser(request) + super().__init__(handler, request, options) + + def isAuthorized(self): + return self.user is not None and self.user.is_enabled() and not self.user.must_change_password + + def handle_request(self): + if self.isAuthorized(): + super().handle_request() + else: + cookie = SimpleCookie() + cookie["owrx-session"] = "" + cookie["owrx-session"]["expires"] = "Thu, 01 Jan 1970 00:00:00 GMT" + self.set_response_cookies(cookie) + if ( + "x-requested-with" in self.request.headers + and self.request.headers["x-requested-with"] == "XMLHttpRequest" + ): + self.send_response("{}", code=403) + else: + target = "{}login?{}".format(self.get_document_root(), parse.urlencode({"ref": self.request.path[1:]})) + self.send_redirect(target) diff --git a/owrx/controllers/api.py b/owrx/controllers/api.py new file mode 100644 index 000000000..4e7a966be --- /dev/null +++ b/owrx/controllers/api.py @@ -0,0 +1,9 @@ +from . import Controller +from owrx.feature import FeatureDetector +import json + + +class ApiController(Controller): + def indexAction(self): + data = json.dumps(FeatureDetector().feature_report()) + self.send_response(data, content_type="application/json") diff --git a/owrx/controllers/assets.py b/owrx/controllers/assets.py new file mode 100644 index 000000000..d9a273fa9 --- /dev/null +++ b/owrx/controllers/assets.py @@ -0,0 +1,193 @@ +from . import Controller +from owrx.config.core import CoreConfig +from datetime import datetime, timezone +import mimetypes +import os +import pkg_resources +from abc import ABCMeta, abstractmethod +import gzip + +import logging + +logger = logging.getLogger(__name__) + + +class GzipMixin(object): + def send_response(self, content, code=200, headers=None, content_type="text/html", *args, **kwargs): + if self.zipable(content_type) and "accept-encoding" in self.request.headers: + accepted = [s.strip().lower() for s in self.request.headers["accept-encoding"].split(",")] + if "gzip" in accepted: + if type(content) == str: + content = content.encode() + content = self.gzip(content) + if headers is None: + headers = {} + headers["Content-Encoding"] = "gzip" + super().send_response(content, code, headers=headers, content_type=content_type, *args, **kwargs) + + def zipable(self, content_type): + types = ["application/javascript", "text/css", "text/html", "image/svg+xml"] + return content_type in types + + def gzip(self, content): + return gzip.compress(content) + + +class ModificationAwareController(Controller, metaclass=ABCMeta): + @abstractmethod + def getModified(self, file): + pass + + def wasModified(self, file): + try: + modified = self.getModified(file).replace(microsecond=0) + + if modified is not None and "If-Modified-Since" in self.handler.headers: + client_modified = datetime.strptime( + self.handler.headers["If-Modified-Since"], "%a, %d %b %Y %H:%M:%S %Z" + ).replace(tzinfo=timezone.utc) + if modified <= client_modified: + return False + except FileNotFoundError: + pass + + return True + + +class AssetsController(GzipMixin, ModificationAwareController, metaclass=ABCMeta): + def getModified(self, file): + return datetime.fromtimestamp(os.path.getmtime(self.getFilePath(file)), timezone.utc) + + def openFile(self, file): + return open(self.getFilePath(file), "rb") + + @abstractmethod + def getFilePath(self, file): + pass + + def serve_file(self, file, content_type=None): + try: + modified = self.getModified(file) + + if not self.wasModified(file): + self.send_response("", code=304) + return + + f = self.openFile(file) + data = f.read() + f.close() + + if content_type is None: + (content_type, encoding) = mimetypes.guess_type(self.getFilePath(file)) + self.send_response(data, content_type=content_type, last_modified=modified, max_age=3600) + except FileNotFoundError: + self.send_response("file not found", code=404) + + def indexAction(self): + filename = self.request.matches.group(1) + self.serve_file(filename) + + +class OwrxAssetsController(AssetsController): + def getFilePath(self, file): + mappedFiles = { + "gfx/openwebrx-avatar.png": "receiver_avatar", + "gfx/openwebrx-top-photo.jpg": "receiver_top_photo", + } + if file in mappedFiles and ("mapped" not in self.request.query or self.request.query["mapped"][0] != "false"): + config = CoreConfig() + for ext in ["png", "jpg", "webp"]: + user_file = "{}/{}.{}".format(config.get_data_directory(), mappedFiles[file], ext) + if os.path.exists(user_file) and os.path.isfile(user_file): + return user_file + return pkg_resources.resource_filename("htdocs", file) + + +class AprsSymbolsController(AssetsController): + def __init__(self, handler, request, options): + path = CoreConfig().get_aprs_symbols_path() + if not path.endswith("/"): + path += "/" + self.path = path + super().__init__(handler, request, options) + + def getFilePath(self, file): + return self.path + file + + +class CompiledAssetsController(GzipMixin, ModificationAwareController): + profiles = { + "receiver.js": [ + "lib/chroma.min.js", + "lib/wheelDelta.js", + "openwebrx.js", + "lib/jquery-3.2.1.min.js", + "lib/jquery.nanoscroller.min.js", + "lib/Header.js", + "lib/Demodulator.js", + "lib/DemodulatorPanel.js", + "lib/BookmarkLocalStorage.js", + "lib/BookmarkBar.js", + "lib/BookmarkDialog.js", + "lib/AudioEngine.js", + "lib/ProgressBar.js", + "lib/Measurement.js", + "lib/FrequencyDisplay.js", + "lib/MessagePanel.js", + "lib/Js8Threads.js", + "lib/Modes.js", + "lib/MetaPanel.js", + ], + "map.js": [ + "lib/jquery-3.2.1.min.js", + "lib/chroma.min.js", + "lib/Header.js", + "map.js", + ], + "settings.js": [ + "lib/jquery-3.2.1.min.js", + "lib/bootstrap.bundle.min.js", + "lib/location-picker.min.js", + "lib/Header.js", + "lib/settings/MapInput.js", + "lib/settings/ImageUpload.js", + "lib/BookmarkLocalStorage.js", + "lib/settings/BookmarkTable.js", + "lib/settings/WsjtDecodingDepthsInput.js", + "lib/settings/WaterfallDropdown.js", + "lib/settings/GainInput.js", + "lib/settings/OptionalSection.js", + "lib/settings/SchedulerInput.js", + "lib/settings/ExponentialInput.js", + "lib/settings/LogMessages.js", + "settings.js", + ], + } + + def indexAction(self): + profileName = self.request.matches.group(1) + if profileName not in CompiledAssetsController.profiles: + self.send_response("profile not found", code=404) + return + + files = CompiledAssetsController.profiles[profileName] + files = [pkg_resources.resource_filename("htdocs", f) for f in files] + + modified = self.getModified(files) + + if not self.wasModified(files): + self.send_response("", code=304) + return + + contents = [self.getContents(f) for f in files] + + (content_type, encoding) = mimetypes.guess_type(profileName) + self.send_response("\n".join(contents), content_type=content_type, last_modified=modified, max_age=3600) + + def getContents(self, file): + with open(file) as f: + return f.read() + + def getModified(self, files): + modified = [os.path.getmtime(f) for f in files] + return datetime.fromtimestamp(max(*modified), timezone.utc) diff --git a/owrx/controllers/feature.py b/owrx/controllers/feature.py new file mode 100644 index 000000000..06e262d3e --- /dev/null +++ b/owrx/controllers/feature.py @@ -0,0 +1,11 @@ +from owrx.controllers.template import WebpageController +from owrx.breadcrumb import Breadcrumb, BreadcrumbItem, BreadcrumbMixin +from owrx.controllers.settings import SettingsBreadcrumb + + +class FeatureController(BreadcrumbMixin, WebpageController): + def get_breadcrumb(self) -> Breadcrumb: + return SettingsBreadcrumb().append(BreadcrumbItem("Feature report", "features")) + + def indexAction(self): + self.serve_template("features.html", **self.template_variables()) diff --git a/owrx/controllers/imageupload.py b/owrx/controllers/imageupload.py new file mode 100644 index 000000000..76866593d --- /dev/null +++ b/owrx/controllers/imageupload.py @@ -0,0 +1,79 @@ +from owrx.controllers import BodySizeError +from owrx.controllers.assets import AssetsController +from owrx.controllers.admin import AuthorizationMixin +from owrx.config.core import CoreConfig +from owrx.form.input.gfx import AvatarInput, TopPhotoInput +import uuid +import json + + +class ImageUploadController(AuthorizationMixin, AssetsController): + # max upload filesizes + max_sizes = { + # not the best idea to instantiate inputs, but i didn't want to duplicate the sizes here + "receiver_avatar": AvatarInput("id", "label").getMaxSize(), + "receiver_top_photo": TopPhotoInput("id", "label").getMaxSize(), + } + + def __init__(self, handler, request, options): + super().__init__(handler, request, options) + self.file = request.query["file"][0] if "file" in request.query else None + + def getFilePath(self, file=None): + if self.file is None: + raise FileNotFoundError("missing filename") + return "{tmp}/{file}".format( + tmp=CoreConfig().get_temporary_directory(), + file=self.file + ) + + def indexAction(self): + self.serve_file(None) + + def _is_png(self, contents): + return contents[0:8] == bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]) + + def _is_jpg(self, contents): + return contents[0:3] == bytes([0xFF, 0xD8, 0xFF]) + + def _is_webp(self, contents): + return contents[0:4] == bytes([0x52, 0x49, 0x46, 0x46]) and contents[8:12] == bytes([0x57, 0x45, 0x42, 0x50]) + + def processImage(self): + if "id" not in self.request.query: + self.send_json_response({"error": "missing id"}, code=400) + return + file_id = self.request.query["id"][0] + + if file_id not in ImageUploadController.max_sizes: + self.send_json_response({"error": "unexpected image id"}, code=400) + return + + try: + contents = self.get_body(ImageUploadController.max_sizes[file_id]) + except BodySizeError: + self.send_json_response({"error": "file size too large"}, code=400) + return + + filetype = None + if self._is_png(contents): + filetype = "png" + elif self._is_jpg(contents): + filetype = "jpg" + elif self._is_webp(contents): + filetype = "webp" + if filetype is None: + self.send_json_response({"error": "unsupported file type"}, code=400) + return + + self.file = "{id}-{uuid}.{ext}".format( + id=file_id, + uuid=uuid.uuid4().hex, + ext=filetype, + ) + with open(self.getFilePath(), "wb") as f: + f.write(contents) + self.send_json_response({"file": self.file}, code=200) + + def send_json_response(self, obj, code): + self.send_response(json.dumps(obj), code=code, content_type="application/json") diff --git a/owrx/controllers/metrics.py b/owrx/controllers/metrics.py new file mode 100644 index 000000000..83a3faa2b --- /dev/null +++ b/owrx/controllers/metrics.py @@ -0,0 +1,32 @@ +from . import Controller +from owrx.metrics import CounterMetric, DirectMetric, Metrics +import json +import re + + + +class MetricsController(Controller): + def indexAction(self): + data = json.dumps(Metrics.getSharedInstance().getHierarchicalMetrics()) + self.send_response(data, content_type="application/json") + + def prometheusAction(self): + metrics = Metrics.getSharedInstance().getFlatMetrics() + + def prometheusFormat(key, metric): + value = metric.getValue() + if isinstance(metric, CounterMetric): + key += "_total" + value = value["count"] + elif isinstance(metric, DirectMetric): + pass + else: + raise ValueError("Unexpected metric type for metric {}".format(repr(metric))) + + return "{key} {value}".format(key=re.sub('[^a-zA-Z0-9:_]', '_', key), value=value) + + data = ["# https://prometheus.io/docs/instrumenting/exposition_formats/"] + [ + prometheusFormat(k, v) for k, v in metrics.items() + ] + + self.send_response("\n".join(data), content_type="text/plain; version=0.0.4") diff --git a/owrx/controllers/profile.py b/owrx/controllers/profile.py new file mode 100644 index 000000000..6fd3aafcd --- /dev/null +++ b/owrx/controllers/profile.py @@ -0,0 +1,24 @@ +from owrx.controllers.template import WebpageController +from owrx.controllers.admin import AuthorizationMixin +from owrx.users import UserList, DefaultPasswordClass +from urllib.parse import parse_qs + + +class ProfileController(AuthorizationMixin, WebpageController): + def isAuthorized(self): + return self.user is not None and self.user.is_enabled() and self.user.must_change_password + + def indexAction(self): + self.serve_template("pwchange.html", **self.template_variables()) + + def processPwChange(self): + data = parse_qs(self.get_body().decode("utf-8")) + data = {k: v[0] for k, v in data.items()} + userlist = UserList.getSharedInstance() + if "password" in data and "confirm" in data and data["password"] == data["confirm"]: + self.user.setPassword(DefaultPasswordClass(data["password"]), must_change_password=False) + userlist.store() + target = self.request.query["ref"][0] if "ref" in self.request.query else "/settings" + else: + target = "/pwchange" + self.send_redirect(target) diff --git a/owrx/controllers/receiverid.py b/owrx/controllers/receiverid.py new file mode 100644 index 000000000..10c736115 --- /dev/null +++ b/owrx/controllers/receiverid.py @@ -0,0 +1,26 @@ +from owrx.controllers import Controller +from owrx.receiverid import ReceiverId +from datetime import datetime + + +class ReceiverIdController(Controller): + def __init__(self, handler, request, options): + super().__init__(handler, request, options) + self.authHeader = None + + def send_response( + self, content, code=200, content_type="text/html", last_modified: datetime = None, max_age=None, headers=None + ): + if self.authHeader is not None: + if headers is None: + headers = {} + headers["Authorization"] = self.authHeader + super().send_response( + content, code=code, content_type=content_type, last_modified=last_modified, max_age=max_age, headers=headers + ) + pass + + def handle_request(self): + if "Authorization" in self.request.headers: + self.authHeader = ReceiverId.getResponseHeader(self.request.headers["Authorization"]) + super().handle_request() diff --git a/owrx/controllers/robots.py b/owrx/controllers/robots.py new file mode 100644 index 000000000..3d00a1e2e --- /dev/null +++ b/owrx/controllers/robots.py @@ -0,0 +1,16 @@ +from owrx.controllers import Controller + + +class RobotsController(Controller): + def indexAction(self): + # search engines should not be crawling internal / API routes + self.send_response( + """User-agent: * +Disallow: /login +Disallow: /logout +Disallow: /pwchange +Disallow: /settings +Disallow: /imageupload +""", + content_type="text/plain", + ) diff --git a/owrx/controllers/session.py b/owrx/controllers/session.py new file mode 100644 index 000000000..5dd3c8837 --- /dev/null +++ b/owrx/controllers/session.py @@ -0,0 +1,79 @@ +from owrx.controllers.template import WebpageController +from urllib.parse import parse_qs, urlencode +from uuid import uuid4 +from http.cookies import SimpleCookie +from owrx.users import UserList +from datetime import datetime, timedelta + +import logging + +logger = logging.getLogger(__name__) + + +class SessionStorage(object): + sharedInstance = None + sessionLifetime = timedelta(hours=6) + + @staticmethod + def getSharedInstance(): + if SessionStorage.sharedInstance is None: + SessionStorage.sharedInstance = SessionStorage() + return SessionStorage.sharedInstance + + def __init__(self): + self.sessions = {} + + def generateKey(self): + return str(uuid4()) + + def startSession(self, data): + key = self.generateKey() + self.updateSession(key, data) + return key + + def getSession(self, key): + if key not in self.sessions: + return None + expires, data = self.sessions[key] + if expires < datetime.utcnow(): + del self.sessions[key] + return None + return data + + def updateSession(self, key, data): + expires = datetime.utcnow() + SessionStorage.sessionLifetime + self.sessions[key] = expires, data + + def prolongSession(self, key): + data = self.getSession(key) + if data is None: + raise KeyError("Invalid session key") + self.updateSession(key, data) + + +class SessionController(WebpageController): + def loginAction(self): + self.serve_template("login.html", **self.template_variables()) + + def processLoginAction(self): + data = parse_qs(self.get_body().decode("utf-8")) + data = {k: v[0] for k, v in data.items()} + userlist = UserList.getSharedInstance() + if "user" in data and "password" in data: + if data["user"] in userlist: + user = userlist[data["user"]] + if user.is_enabled() and user.password.is_valid(data["password"]): + key = SessionStorage.getSharedInstance().startSession({"user": user.name}) + cookie = SimpleCookie() + cookie["owrx-session"] = key + target = self.request.query["ref"][0] if "ref" in self.request.query else "/settings" + if user.must_change_password: + target = "/pwchange?{0}".format(urlencode({"ref": target})) + self.set_response_cookies(cookie) + self.send_redirect(target) + return + target = "{}login?{}".format(self.get_document_root(), urlencode({"ref": self.request.query["ref"][0]})) + self.send_redirect(target) + + def logoutAction(self): + self.send_redirect("logout happening here") diff --git a/owrx/controllers/settings/__init__.py b/owrx/controllers/settings/__init__.py new file mode 100644 index 000000000..0ac3c5de3 --- /dev/null +++ b/owrx/controllers/settings/__init__.py @@ -0,0 +1,162 @@ +from owrx.config import Config +from owrx.controllers.admin import AuthorizationMixin +from owrx.controllers.template import WebpageController +from owrx.breadcrumb import Breadcrumb, BreadcrumbItem, BreadcrumbMixin +from abc import ABCMeta, abstractmethod +from urllib.parse import parse_qs + +import logging + +logger = logging.getLogger(__name__) + + +class SettingsController(AuthorizationMixin, WebpageController): + def indexAction(self): + self.serve_template("settings.html", **self.template_variables()) + + +class SettingsFormController(AuthorizationMixin, BreadcrumbMixin, WebpageController, metaclass=ABCMeta): + def __init__(self, handler, request, options): + super().__init__(handler, request, options) + self.errors = {} + self.globalError = None + self.formData = None + + @abstractmethod + def getSections(self): + pass + + @abstractmethod + def getTitle(self): + pass + + def getData(self): + return Config.get() + + def getErrors(self): + return self.errors + + def buildRenderData(self): + # this basially builds an intermediate result to be rendered + # relevant when the form has to be displayed again due to errors + # in this specific scenario, we mix the config with the data the user already submitted + # we use a copy of the config so that whatever we apply here does not get accidentally stored + res = self.getData().__dict__() + if self.formData is not None: + self._applyConfigData(res, self.formData) + return res + + def render_sections(self): + sections = "".join(section.render(self.buildRenderData(), self.getErrors()) for section in self.getSections()) + buttons = self.render_buttons() + return """ +
    + {sections} +
    + {buttons} +
    + + """.format( + sections=sections, + buttons=buttons, + ) + + def render_buttons(self): + return """ + + """ + + def indexAction(self): + self.serve_template("settings/general.html", **self.template_variables()) + + def template_variables(self): + variables = super().template_variables() + variables["content"] = self.render_sections() + variables["title"] = self.getTitle() + variables["modal"] = self.buildModal() + variables["error"] = self.renderGlobalError() + return variables + + def parseFormData(self): + data = parse_qs(self.get_body().decode("utf-8"), keep_blank_values=True) + result = {} + errors = [] + for section in self.getSections(): + section_data, section_errors = section.parse(data) + result.update(section_data) + errors += section_errors + return result, errors + + def getSuccessfulRedirect(self): + return self.get_document_root() + self.request.path[1:] + + def _mergeErrors(self, errors): + result = {} + for e in errors: + if e.getKey() not in result: + result[e.getKey()] = [] + result[e.getKey()].append(e.getMessage()) + return result + + def processFormData(self): + data = None + errors = None + try: + data, errors = self.parseFormData() + except Exception as e: + logger.exception("Error while parsing form data") + self.globalError = str(e) + return self.indexAction() + + self.formData = data + if errors: + self.errors = self._mergeErrors(errors) + return self.indexAction() + try: + self.processData(data) + self.store() + self.send_redirect(self.getSuccessfulRedirect()) + except Exception as e: + logger.exception("Error while processing form data") + self.globalError = str(e) + return self.indexAction() + + def _applyConfigData(self, dest, data): + for k, v in data.items(): + if v is None: + if k in dest: + del dest[k] + else: + dest[k] = v + + def processData(self, data): + config = self.getData() + self._applyConfigData(config, data) + + def store(self): + Config.get().store() + + def buildModal(self): + return "" + + def renderGlobalError(self): + if self.globalError is None: + return "" + + return """ +
    +
    Error
    +
    +
    Your settings could not be saved due to an error:
    +
    {error}
    +
    +
    + """.format( + error=self.globalError + ) + + +class SettingsBreadcrumb(Breadcrumb): + def __init__(self): + super().__init__([]) + self.append(BreadcrumbItem("Settings", "settings")) diff --git a/owrx/controllers/settings/backgrounddecoding.py b/owrx/controllers/settings/backgrounddecoding.py new file mode 100644 index 000000000..902c37f5d --- /dev/null +++ b/owrx/controllers/settings/backgrounddecoding.py @@ -0,0 +1,25 @@ +from owrx.controllers.settings import SettingsFormController +from owrx.form.section import Section +from owrx.form.input import CheckboxInput, ServicesCheckboxInput +from owrx.breadcrumb import Breadcrumb, BreadcrumbItem +from owrx.controllers.settings import SettingsBreadcrumb + + +class BackgroundDecodingController(SettingsFormController): + def getTitle(self): + return "Background decoding" + + def get_breadcrumb(self) -> Breadcrumb: + return SettingsBreadcrumb().append(BreadcrumbItem("Background decoding", "settings/backgrounddecoding")) + + def getSections(self): + return [ + Section( + "Background decoding", + CheckboxInput( + "services_enabled", + "Enable background decoding services", + ), + ServicesCheckboxInput("services_decoders", "Enabled services"), + ), + ] diff --git a/owrx/controllers/settings/bookmarks.py b/owrx/controllers/settings/bookmarks.py new file mode 100644 index 000000000..2704bb6d4 --- /dev/null +++ b/owrx/controllers/settings/bookmarks.py @@ -0,0 +1,148 @@ +from owrx.controllers.template import WebpageController +from owrx.controllers.admin import AuthorizationMixin +from owrx.controllers.settings import SettingsBreadcrumb +from owrx.bookmarks import Bookmark, Bookmarks +from owrx.modes import Modes +from owrx.breadcrumb import Breadcrumb, BreadcrumbItem, BreadcrumbMixin +import json +import math + +import logging + +logger = logging.getLogger(__name__) + + +class BookmarksController(AuthorizationMixin, BreadcrumbMixin, WebpageController): + def get_breadcrumb(self) -> Breadcrumb: + return SettingsBreadcrumb().append(BreadcrumbItem("Bookmark editor", "settings/bookmarks")) + + def template_variables(self): + variables = super().template_variables() + variables["bookmarks"] = self.render_table() + return variables + + def render_table(self): + bookmarks = Bookmarks.getSharedInstance().getBookmarks() + emptyText = """ +
    + """ + + return """ +
    + No bookmarks in storage. You can add new bookmarks using the buttons below. +
    + + + + + + + {bookmarks} +
    NameFrequencyModulationActions
    + """.format( + bookmarks="".join(self.render_bookmark(b) for b in bookmarks) if bookmarks else emptyText, + modes=json.dumps({m.modulation: m.name for m in Modes.getAvailableModes()}), + ) + + def render_bookmark(self, bookmark: Bookmark): + def render_frequency(freq): + suffixes = { + 0: "", + 3: "k", + 6: "M", + 9: "G", + 12: "T", + } + exp = 0 + if freq > 0: + exp = int(math.log10(freq) / 3) * 3 + num = freq + suffix = "" + if exp in suffixes: + num = freq / 10 ** exp + suffix = suffixes[exp] + return "{num:g} {suffix}Hz".format(num=num, suffix=suffix) + + mode = Modes.findByModulation(bookmark.getModulation()) + return """ +
    {name}{rendered_frequency}{modulation_name} + +